From 16af9ab57a442d261747f0868bbd6546ac3dbce7 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 11:21:24 +0800 Subject: [PATCH 001/183] Fix broken cross references --- docs/guides/commands/commands.md | 12 ++++++------ docs/guides/concepts/connections.md | 5 +---- docs/guides/getting_started/intro.md | 4 ++-- docs/guides/voice/sending-voice.md | 4 ++-- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index 2b012af0e..821a1bfa7 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -153,7 +153,7 @@ 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_ +[CommandService.AddModulesAsync]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModulesAsync_Assembly_System_IServiceProvider_ #### Loading Modules Manually @@ -161,7 +161,7 @@ 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 +[CommandService.AddModuleAsync]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModuleAsync__1_System_IServiceProvider_ ### Module Constructors @@ -268,7 +268,7 @@ either [PreconditionAttribute] or [ParameterPreconditionAttribute] depending on your use. In order for your Precondition to function, you will need to override -the [CheckPermissions] method. +the [CheckPermissionsAsync] method. Your IDE should provide an option to fill this in for you. @@ -279,7 +279,7 @@ 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_ +[CheckPermissionsAsync]: xref:Discord.Commands.PreconditionAttribute#Discord_Commands_PreconditionAttribute_CheckPermissionsAsync_Discord_Commands_ICommandContext_Discord_Commands_CommandInfo_System_IServiceProvider_ [PreconditionResult.FromSuccess]: xref:Discord.Commands.PreconditionResult#Discord_Commands_PreconditionResult_FromSuccess [PreconditionResult.FromError]: xref:Discord.Commands.PreconditionResult#Discord_Commands_PreconditionResult_FromError_System_String_ @@ -310,7 +310,7 @@ 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. +Next, satisfy the `TypeReader` class by overriding the [ReadAsync] method. >[!NOTE] >In many cases, Visual Studio can fill this in for you, using the @@ -327,7 +327,7 @@ 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_ +[ReadAsync]: xref:Discord.Commands.TypeReader#Discord_Commands_TypeReader_ReadAsync_Discord_Commands_ICommandContext_System_String_System_IServiceProvider_ #### Sample diff --git a/docs/guides/concepts/connections.md b/docs/guides/concepts/connections.md index 30e5e55cd..b36774d60 100644 --- a/docs/guides/concepts/connections.md +++ b/docs/guides/concepts/connections.md @@ -11,9 +11,6 @@ stopped. 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. @@ -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]: events.md diff --git a/docs/guides/getting_started/intro.md b/docs/guides/getting_started/intro.md index db086df21..361dd7f29 100644 --- a/docs/guides/getting_started/intro.md +++ b/docs/guides/getting_started/intro.md @@ -212,7 +212,7 @@ shown below. For your reference, you may view the [completed program]. -[MessageReceived]: xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived +[MessageReceived]: xref:Discord.WebSocket.BaseSocketClient#Discord_WebSocket_BaseSocketClient_MessageReceived [SocketMessage]: xref:Discord.WebSocket.SocketMessage [SocketMessageChannel]: xref:Discord.WebSocket.ISocketMessageChannel [completed program]: samples/intro/complete.cs @@ -234,4 +234,4 @@ 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 +**todo:** diagram of bot structure diff --git a/docs/guides/voice/sending-voice.md b/docs/guides/voice/sending-voice.md index 024a98b95..30be558b1 100644 --- a/docs/guides/voice/sending-voice.md +++ b/docs/guides/voice/sending-voice.md @@ -44,7 +44,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#Discord_IAudioChannel_ConnectAsync_System_Action_IAudioClient__ ## Transmitting Audio @@ -98,4 +98,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)] From c157dc62fdfd2d39631fe0df98de2b39a7a563af Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 11:48:29 +0800 Subject: [PATCH 002/183] Add FAQ section --- docs/docfx.json | 6 ++- docs/faq/0-GettingStarted.md | 36 ++++++++++++++ docs/faq/1-Basics.md | 18 +++++++ docs/faq/2-BasicOperations.md | 75 +++++++++++++++++++++++++++++ docs/faq/3-AdvancedOperations.md | 5 ++ docs/faq/4-Commands.md | 81 ++++++++++++++++++++++++++++++++ docs/faq/5-Legacy.md | 11 +++++ docs/faq/toc.yml | 10 ++++ docs/toc.yml | 3 +- 9 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 docs/faq/0-GettingStarted.md create mode 100644 docs/faq/1-Basics.md create mode 100644 docs/faq/2-BasicOperations.md create mode 100644 docs/faq/3-AdvancedOperations.md create mode 100644 docs/faq/4-Commands.md create mode 100644 docs/faq/5-Legacy.md create mode 100644 docs/faq/toc.yml diff --git a/docs/docfx.json b/docs/docfx.json index 3c0b0611e..d0e1c65cd 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -30,6 +30,8 @@ "files": [ "guides/**.md", "guides/**/toc.yml", + "faq/**.md", + "faq/**/toc.yml", "toc.yml", "*.md" ], @@ -67,8 +69,8 @@ "default" ], "globalMetadata": { - "_appFooter": "Discord.Net (c) 2015-2017" + "_appFooter": "Discord.Net (c) 2015-2018" }, "noLangKeyword": false } -} \ No newline at end of file +} diff --git a/docs/faq/0-GettingStarted.md b/docs/faq/0-GettingStarted.md new file mode 100644 index 000000000..0a3c0d575 --- /dev/null +++ b/docs/faq/0-GettingStarted.md @@ -0,0 +1,36 @@ +# Basic Concepts / Getting Started + +## How do I get started? Is there a really simple example I can follow? + First of all, welcome! 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 TAP (Task-based Asynchronous Pattern), 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](https://github.com/RogueException/Discord.Net/blob/dev/docs/guides/getting_started/samples/intro/structure.cs) + 2. [Official template](https://github.com/foxbot/DiscordBotBase/tree/csharp/src/DiscordBot) + + Please note that you should *not* try to blindly copy paste the code. It is meant to be a template or a guide. It is not meant to be something that will work out of the box. + +## How do I add my bot to my server/guild? + + The [OAuth2 URL](https://discordapp.com/developers/tools/oauth2-url-generator) can be generated via the Discord developer page. This allows you to set the permissions that the bot will be added with. With this method, bots will also be assigned their own special roles that normal users cannot use, which is what we call a `Managed` role. + +## What is a Client/User/Object ID? Is it the token? + + Each user and object on Discord has its own snowflake ID generated based on various conditions, see [here](https://this.is-a-professional-domain.com/7da0e4.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 library require an ID to retrieve the said object. + + There are 2 ways to obtain the said ID. + 1. Using the built-in [dev mode](https://catch-me-outside.how-about-th.at/d083ab.png). With dev mode enabled, you can - as an example - right click on a guild and copy the guild id (please note that this does not apply to Role IDs, see below). + 2. Escape the object using `\` in front the object. For example, when you do `\@Example#1234`, it will return the user ID of the aforementioned user. + + 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. + +## How do I get the role ID? + + Few 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. + + Please note that right-clicking on the role and copying the ID will **not** work. It will only copy the message ID. + +## I have more questions! + +Please visit us at #dotnet_discord-net at the Discord API server. Describe the problem in details to us, and preferably with the problematic code uploaded onto [Hastebin](https://hastebin.com). \ No newline at end of file diff --git a/docs/faq/1-Basics.md b/docs/faq/1-Basics.md new file mode 100644 index 000000000..a30e1c252 --- /dev/null +++ b/docs/faq/1-Basics.md @@ -0,0 +1,18 @@ +# Client Basics Questions + +## My client keeps returning 401 upon logging in! + + 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 start with `Mj*`. If it starts with any other characters, chances are, you are using the *client secret*, which has nothing to do with the login token. + +## 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. + +## 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](https://discord.foxbot.me/docs/guides/concepts/events.html#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. \ No newline at end of file diff --git a/docs/faq/2-BasicOperations.md b/docs/faq/2-BasicOperations.md new file mode 100644 index 000000000..e8424448d --- /dev/null +++ b/docs/faq/2-BasicOperations.md @@ -0,0 +1,75 @@ +# Basic Operations Questions + +## How should I safely check a type? +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. There are several ways to cast, with direct casting `(Type)type` being the the least recommended, as it *can* throw an `InvalidCastException` when the object isn't the desired type. Please refer to [this post](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/types/how-to-safely-cast-by-using-as-and-is-operators) for more details. + +A good and safe casting example: +```cs +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 + } +} +``` + +## How do I send a message? + + Any implementation of **IMessageChannel** has a **SendMessageAsync** method. Using the client, you can get an appropriate channel (**GetChannel(id)**) to send a message to. 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. + +## How can I tell if a message is from X, Y, Z? + + You may check message channel type. + + * 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 a group). + * A **Private channel** (`IPrivateChannel`) is a DM or a Group. + * A **Message channel** (`IMessageChannel`) is all of the above. + +## How do I add hyperlink text to an embed? + + Embeds can use standard [markdown](https://support.discordapp.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline-) in the Description as well as in field values. With that in mind, links can be added using the following format \[text](link). + + +## How do I add reactions to a message? + + Any entities that implement `IUserMessage` has an **AddReactionAsync** method. This method expects an `IEmote` as a parameter. In Discord.Net, an Emote represents a server custom emote, while an Emoji is a Unicode emoji (standard emoji). Both `Emoji` and `Emote` implement `IEmote` and are valid options. + ```cs + // 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("\u23F8"); + + // 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); + ``` + +## Why am I getting so many preemptive rate limits when I try to add more than one reactions? + + This is due to how .NET parses the HTML header, mistreating 0.25sec/action to 1sec. This casues 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). \ No newline at end of file diff --git a/docs/faq/3-AdvancedOperations.md b/docs/faq/3-AdvancedOperations.md new file mode 100644 index 000000000..70bcbf04d --- /dev/null +++ b/docs/faq/3-AdvancedOperations.md @@ -0,0 +1,5 @@ +# Advanced Operations + +## I want to create a service that sends a message or does things at a regular interval! + +You will need to create a separate service alongside the program. Here's [an example](https://gist.github.com/Joe4evr/967949a477ed0c6c841407f0f25fa730) to get you started. \ No newline at end of file diff --git a/docs/faq/4-Commands.md b/docs/faq/4-Commands.md new file mode 100644 index 000000000..96d6cb053 --- /dev/null +++ b/docs/faq/4-Commands.md @@ -0,0 +1,81 @@ +# 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 eithe create your own custom precondition or use Joe4evr's [Preconditions Addons](https://github.com/Joe4evr/Discord.Addons/tree/master/src/Discord.Addons.Preconditions) 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. + + +## I'm getting an error about `Assembly#GetEntryAssembly`. What now? + + 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. + + +## 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. + ```cs + // !echo repeat this message in chat + [Command("echo")] + [Summary("Replies whatever the user adds")] + [Remarks("The entire message is considered one String")] + public Task EchoAsync([Remainder]String text) => ReplyAsync(text); + + // !echo repeat this message in chat + [Command("echo")] + [Summary("Replies whatever the user adds")] + [Remarks("This command will error for having too many arguments. + The message would be seen as having 5 parameters while the method only accepts one. + Wrapping the message in quotes solves this - '!echo repeat this message in chat' - + this way, the system knows the entire message is to be parsed as a single String")] + public Task EchoAsync(String text) => ReplyAsync(text); + ``` + +## 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 before proceeding. You can learn more about it [here](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection), and how it works in Discord.NET [here](https://discord.foxbot.me/latest/guides/commands/commands.html#usage-in-modules). A brief example of service and dependency injection can be seen below, + +```cs +public class MyService +{ + public string MyCoolString {get; set;} +} +public class SetupOrWhatever +{ + 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("setorprintstring")] + public Task GetOrSetStringAsync() + { + if (_myService.MyCoolString == null) _myService.MyCoolString = "ya boi"; + return ReplyAsync(_myService.MyCoolString); + } +} +``` + +## 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 long-running task, 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. One being `RunMode.Sync`, which is the default; another being `RunMode.Async`. `RunMode.Async` essentially calls an unawaited Task and continues with the execution without waiting for the command task to finish. You should use `RunMode.Async` in either the `CommandAttribute` or the `DefaultRunMode` flag in `CommandServiceConfig`. Further details regarding `RunMode.Async` can be found below. + +## Okay, that's great and all, but how does `RunMode.Async` work, and if it's so great, why is the lib *not* using it by default? + + As with any async operation, `RunMode.Async` also comes at a cost. The following are the caveats with RunMode.Async, + 1) You introduce race condition. + 2) Unnecessary overhead caused by async state machine (learn more about it [here](https://www.red-gate.com/simple-talk/dotnet/net-tools/c-async-what-is-it-and-how-does-it-work/)). + 3) `CommandService#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 #3 and #4. + + For #3, in Discord.NET 2.0, the library introduces a new event called `CommandExecuted`, which is raised whenever the command is finished. This event will be called regardless of the `RunMode` type and will return the appropriate execution result + + For #4, exceptions are caught in `CommandService#Log` under `(CommandException)LogMessage.Exception`. \ No newline at end of file diff --git a/docs/faq/5-Legacy.md b/docs/faq/5-Legacy.md new file mode 100644 index 000000000..9f10fd400 --- /dev/null +++ b/docs/faq/5-Legacy.md @@ -0,0 +1,11 @@ +# Legacy Questions + +## X, Y, Z does not work! It doesn't return a valid value anymore. + If you're currently using 1.0.0, please upgrade to the latest 2.0 beta to ensure maximum compatibility. Several methods or props may be broken in 1.0.x and will not be fixed in the 1.0 branch due to their breaking nature. + Notable breaking changes are as follows, + * `IChannel#IsNsfw` has been replaced with `ITextChannel#IsNsfw` and now returns valid value in 2.0. + * Bulk message removal (`DeletedMessagesAsync`) has been moved from `IMessageChannel` to `ITextChannel`. + * `IAsyncEnumerable#Flatten` has been renamed to `FlattenAsync`. + +## 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](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) Abstractions. An example usage can be seen [here](https://github.com/foxbot/DiscordBotBase/blob/csharp/src/DiscordBot/Program.cs#L36). \ No newline at end of file diff --git a/docs/faq/toc.yml b/docs/faq/toc.yml new file mode 100644 index 000000000..446ddbf54 --- /dev/null +++ b/docs/faq/toc.yml @@ -0,0 +1,10 @@ +- name: Getting Started + href: 0-GettingStarted.md +- name: Basics + href: 1-Basics.md +- name: Basic Operations + href: 2-BasicOperations.md +- name: Commands + href: 4-Commands.md +- name: Legacy or Upgrade + href: 5-Legacy.md diff --git a/docs/toc.yml b/docs/toc.yml index c08e708bf..7fd96916e 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,4 +1,5 @@ - +- name: FAQ + href: faq/ - name: Guides href: guides/ - name: API Documentation From 0041de4f2993d343080c7f7944c24f65b8023452 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 11:57:40 +0800 Subject: [PATCH 003/183] Add dev mode image --- docs/faq/0-GettingStarted.md | 23 ++++++++++++----------- docs/faq/images/dev-mode.png | Bin 0 -> 80742 bytes 2 files changed, 12 insertions(+), 11 deletions(-) create mode 100644 docs/faq/images/dev-mode.png diff --git a/docs/faq/0-GettingStarted.md b/docs/faq/0-GettingStarted.md index 0a3c0d575..4b0c5cc36 100644 --- a/docs/faq/0-GettingStarted.md +++ b/docs/faq/0-GettingStarted.md @@ -1,36 +1,37 @@ # Basic Concepts / Getting Started -## How do I get started? Is there a really simple example I can follow? - First of all, welcome! 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 TAP (Task-based Asynchronous Pattern), polymorphism, interface and many more advanced topics extensively. Please make sure that you understand these topics to some extent before proceeding. +## How do I get started? +First of all, welcome! 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 TAP (Task-based Asynchronous Pattern), 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](https://github.com/RogueException/Discord.Net/blob/dev/docs/guides/getting_started/samples/intro/structure.cs) 2. [Official template](https://github.com/foxbot/DiscordBotBase/tree/csharp/src/DiscordBot) - Please note that you should *not* try to blindly copy paste the code. It is meant to be a template or a guide. It is not meant to be something that will work out of the box. +Please note that you should *not* try to blindly copy paste the code. It is meant to be a template or a guide. It is not meant to be something that will work out of the box. ## How do I add my bot to my server/guild? - The [OAuth2 URL](https://discordapp.com/developers/tools/oauth2-url-generator) can be generated via the Discord developer page. This allows you to set the permissions that the bot will be added with. With this method, bots will also be assigned their own special roles that normal users cannot use, which is what we call a `Managed` role. +The [OAuth2 URL](https://discordapp.com/developers/tools/oauth2-url-generator) can be generated via the Discord developer page. This allows you to set the permissions that the bot will be added with. With this method, bots will also be assigned their own special roles that normal users cannot use, which is what we call a `Managed` role. ## What is a Client/User/Object ID? Is it the token? - Each user and object on Discord has its own snowflake ID generated based on various conditions, see [here](https://this.is-a-professional-domain.com/7da0e4.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 library require an ID to retrieve the said object. +Each user and object on Discord has its own snowflake ID generated based on various conditions, see [here](https://this.is-a-professional-domain.com/7da0e4.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 library require an ID to retrieve the said object. There are 2 ways to obtain the said ID. - 1. Using the built-in [dev mode](https://catch-me-outside.how-about-th.at/d083ab.png). With dev mode enabled, you can - as an example - right click on a guild and copy the guild id (please note that this does not apply to Role IDs, see below). + 1. Enable Discord's developer mode. With developer mode enabled, you can - as an example - right click on a guild and copy the guild id (please note that this does not apply to Role IDs, see below). + ![Developer Mode](images/dev-mode.png) 2. Escape the object using `\` in front the object. For example, when you do `\@Example#1234`, it will return the user ID of the aforementioned user. - 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. - +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. + ## How do I get the role ID? - Few common ways to do this: +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. - Please note that right-clicking on the role and copying the ID will **not** work. It will only copy the message ID. +Please note that right-clicking on the role and copying the ID will **not** work. It will only copy the message ID. ## I have more questions! -Please visit us at #dotnet_discord-net at the Discord API server. Describe the problem in details to us, and preferably with the problematic code uploaded onto [Hastebin](https://hastebin.com). \ No newline at end of file +Please visit us at #dotnet_discord-net at the Discord API server. Describe the problem in details to us, and preferably with the problematic code uploaded onto [Hastebin](https://hastebin.com). diff --git a/docs/faq/images/dev-mode.png b/docs/faq/images/dev-mode.png new file mode 100644 index 0000000000000000000000000000000000000000..fd20b95d188751a2f3b4268e7ba7294b71afaf06 GIT binary patch literal 80742 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 B0lxqM literal 0 HcmV?d00001 From c86e199a9dcd75be5c06ad56e876c8681417b132 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 12:45:13 +0800 Subject: [PATCH 004/183] Remove migration docs - The link never worked in the first place and is rarely used. --- docs/guides/migrating/migrating.md | 61 --------------------- docs/guides/migrating/samples/event.cs | 4 -- docs/guides/migrating/samples/sync_event.cs | 5 -- docs/guides/toc.yml | 1 - 4 files changed, 71 deletions(-) delete mode 100644 docs/guides/migrating/migrating.md delete mode 100644 docs/guides/migrating/samples/event.cs delete mode 100644 docs/guides/migrating/samples/sync_event.cs 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..5a13a234c 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -24,4 +24,3 @@ items: - name: Voice Guide href: voice/sending-voice.md -- name: Migrating from 0.9 \ No newline at end of file From 6a5cf07e0a619ab3aa7f457bf48e3f31fc271719 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 14:09:50 +0800 Subject: [PATCH 005/183] Comply with CONTRIBUTING.md - As well as making minor adjustments here and there --- docs/faq/0-GettingStarted.md | 37 ------ docs/faq/1-Basics.md | 18 --- docs/faq/2-BasicOperations.md | 75 ------------ docs/faq/4-Commands.md | 81 ------------- docs/faq/5-Legacy.md | 11 -- ...cedOperations.md => AdvancedOperations.md} | 0 docs/faq/BasicOperations.md | 74 ++++++++++++ docs/faq/ClientBasics.md | 47 ++++++++ docs/faq/Commands.md | 114 ++++++++++++++++++ docs/faq/GettingStarted.md | 71 +++++++++++ docs/faq/Glossary.md | 18 +++ docs/faq/Legacy.md | 19 +++ docs/faq/images/snowflake.png | Bin 0 -> 73062 bytes docs/faq/samples/basics/cast.cs | 15 +++ docs/faq/samples/basics/emoji.cs | 18 +++ docs/faq/samples/commands/DI.cs | 27 +++++ docs/faq/samples/commands/Remainder.cs | 19 +++ docs/faq/toc.yml | 14 ++- 18 files changed, 430 insertions(+), 228 deletions(-) delete mode 100644 docs/faq/0-GettingStarted.md delete mode 100644 docs/faq/1-Basics.md delete mode 100644 docs/faq/2-BasicOperations.md delete mode 100644 docs/faq/4-Commands.md delete mode 100644 docs/faq/5-Legacy.md rename docs/faq/{3-AdvancedOperations.md => AdvancedOperations.md} (100%) create mode 100644 docs/faq/BasicOperations.md create mode 100644 docs/faq/ClientBasics.md create mode 100644 docs/faq/Commands.md create mode 100644 docs/faq/GettingStarted.md create mode 100644 docs/faq/Glossary.md create mode 100644 docs/faq/Legacy.md create mode 100644 docs/faq/images/snowflake.png create mode 100644 docs/faq/samples/basics/cast.cs create mode 100644 docs/faq/samples/basics/emoji.cs create mode 100644 docs/faq/samples/commands/DI.cs create mode 100644 docs/faq/samples/commands/Remainder.cs diff --git a/docs/faq/0-GettingStarted.md b/docs/faq/0-GettingStarted.md deleted file mode 100644 index 4b0c5cc36..000000000 --- a/docs/faq/0-GettingStarted.md +++ /dev/null @@ -1,37 +0,0 @@ -# Basic Concepts / Getting Started - -## How do I get started? -First of all, welcome! 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 TAP (Task-based Asynchronous Pattern), 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](https://github.com/RogueException/Discord.Net/blob/dev/docs/guides/getting_started/samples/intro/structure.cs) - 2. [Official template](https://github.com/foxbot/DiscordBotBase/tree/csharp/src/DiscordBot) - -Please note that you should *not* try to blindly copy paste the code. It is meant to be a template or a guide. It is not meant to be something that will work out of the box. - -## How do I add my bot to my server/guild? - -The [OAuth2 URL](https://discordapp.com/developers/tools/oauth2-url-generator) can be generated via the Discord developer page. This allows you to set the permissions that the bot will be added with. With this method, bots will also be assigned their own special roles that normal users cannot use, which is what we call a `Managed` role. - -## What is a Client/User/Object ID? Is it the token? - -Each user and object on Discord has its own snowflake ID generated based on various conditions, see [here](https://this.is-a-professional-domain.com/7da0e4.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 library require an ID to retrieve the said object. - - There are 2 ways to obtain the said ID. - 1. Enable Discord's developer mode. With developer mode enabled, you can - as an example - right click on a guild and copy the guild id (please note that this does not apply to Role IDs, see below). - ![Developer Mode](images/dev-mode.png) - 2. Escape the object using `\` in front the object. For example, when you do `\@Example#1234`, it will return the user ID of the aforementioned user. - -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. - -## How do I get the role 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. - -Please note that right-clicking on the role and copying the ID will **not** work. It will only copy the message ID. - -## I have more questions! - -Please visit us at #dotnet_discord-net at the Discord API server. Describe the problem in details to us, and preferably with the problematic code uploaded onto [Hastebin](https://hastebin.com). diff --git a/docs/faq/1-Basics.md b/docs/faq/1-Basics.md deleted file mode 100644 index a30e1c252..000000000 --- a/docs/faq/1-Basics.md +++ /dev/null @@ -1,18 +0,0 @@ -# Client Basics Questions - -## My client keeps returning 401 upon logging in! - - 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 start with `Mj*`. If it starts with any other characters, chances are, you are using the *client secret*, which has nothing to do with the login token. - -## 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. - -## 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](https://discord.foxbot.me/docs/guides/concepts/events.html#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. \ No newline at end of file diff --git a/docs/faq/2-BasicOperations.md b/docs/faq/2-BasicOperations.md deleted file mode 100644 index e8424448d..000000000 --- a/docs/faq/2-BasicOperations.md +++ /dev/null @@ -1,75 +0,0 @@ -# Basic Operations Questions - -## How should I safely check a type? -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. There are several ways to cast, with direct casting `(Type)type` being the the least recommended, as it *can* throw an `InvalidCastException` when the object isn't the desired type. Please refer to [this post](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/types/how-to-safely-cast-by-using-as-and-is-operators) for more details. - -A good and safe casting example: -```cs -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 - } -} -``` - -## How do I send a message? - - Any implementation of **IMessageChannel** has a **SendMessageAsync** method. Using the client, you can get an appropriate channel (**GetChannel(id)**) to send a message to. 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. - -## How can I tell if a message is from X, Y, Z? - - You may check message channel type. - - * 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 a group). - * A **Private channel** (`IPrivateChannel`) is a DM or a Group. - * A **Message channel** (`IMessageChannel`) is all of the above. - -## How do I add hyperlink text to an embed? - - Embeds can use standard [markdown](https://support.discordapp.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline-) in the Description as well as in field values. With that in mind, links can be added using the following format \[text](link). - - -## How do I add reactions to a message? - - Any entities that implement `IUserMessage` has an **AddReactionAsync** method. This method expects an `IEmote` as a parameter. In Discord.Net, an Emote represents a server custom emote, while an Emoji is a Unicode emoji (standard emoji). Both `Emoji` and `Emote` implement `IEmote` and are valid options. - ```cs - // 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("\u23F8"); - - // 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); - ``` - -## Why am I getting so many preemptive rate limits when I try to add more than one reactions? - - This is due to how .NET parses the HTML header, mistreating 0.25sec/action to 1sec. This casues 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). \ No newline at end of file diff --git a/docs/faq/4-Commands.md b/docs/faq/4-Commands.md deleted file mode 100644 index 96d6cb053..000000000 --- a/docs/faq/4-Commands.md +++ /dev/null @@ -1,81 +0,0 @@ -# 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 eithe create your own custom precondition or use Joe4evr's [Preconditions Addons](https://github.com/Joe4evr/Discord.Addons/tree/master/src/Discord.Addons.Preconditions) 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. - - -## I'm getting an error about `Assembly#GetEntryAssembly`. What now? - - 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. - - -## 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. - ```cs - // !echo repeat this message in chat - [Command("echo")] - [Summary("Replies whatever the user adds")] - [Remarks("The entire message is considered one String")] - public Task EchoAsync([Remainder]String text) => ReplyAsync(text); - - // !echo repeat this message in chat - [Command("echo")] - [Summary("Replies whatever the user adds")] - [Remarks("This command will error for having too many arguments. - The message would be seen as having 5 parameters while the method only accepts one. - Wrapping the message in quotes solves this - '!echo repeat this message in chat' - - this way, the system knows the entire message is to be parsed as a single String")] - public Task EchoAsync(String text) => ReplyAsync(text); - ``` - -## 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 before proceeding. You can learn more about it [here](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection), and how it works in Discord.NET [here](https://discord.foxbot.me/latest/guides/commands/commands.html#usage-in-modules). A brief example of service and dependency injection can be seen below, - -```cs -public class MyService -{ - public string MyCoolString {get; set;} -} -public class SetupOrWhatever -{ - 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("setorprintstring")] - public Task GetOrSetStringAsync() - { - if (_myService.MyCoolString == null) _myService.MyCoolString = "ya boi"; - return ReplyAsync(_myService.MyCoolString); - } -} -``` - -## 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 long-running task, 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. One being `RunMode.Sync`, which is the default; another being `RunMode.Async`. `RunMode.Async` essentially calls an unawaited Task and continues with the execution without waiting for the command task to finish. You should use `RunMode.Async` in either the `CommandAttribute` or the `DefaultRunMode` flag in `CommandServiceConfig`. Further details regarding `RunMode.Async` can be found below. - -## Okay, that's great and all, but how does `RunMode.Async` work, and if it's so great, why is the lib *not* using it by default? - - As with any async operation, `RunMode.Async` also comes at a cost. The following are the caveats with RunMode.Async, - 1) You introduce race condition. - 2) Unnecessary overhead caused by async state machine (learn more about it [here](https://www.red-gate.com/simple-talk/dotnet/net-tools/c-async-what-is-it-and-how-does-it-work/)). - 3) `CommandService#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 #3 and #4. - - For #3, in Discord.NET 2.0, the library introduces a new event called `CommandExecuted`, which is raised whenever the command is finished. This event will be called regardless of the `RunMode` type and will return the appropriate execution result - - For #4, exceptions are caught in `CommandService#Log` under `(CommandException)LogMessage.Exception`. \ No newline at end of file diff --git a/docs/faq/5-Legacy.md b/docs/faq/5-Legacy.md deleted file mode 100644 index 9f10fd400..000000000 --- a/docs/faq/5-Legacy.md +++ /dev/null @@ -1,11 +0,0 @@ -# Legacy Questions - -## X, Y, Z does not work! It doesn't return a valid value anymore. - If you're currently using 1.0.0, please upgrade to the latest 2.0 beta to ensure maximum compatibility. Several methods or props may be broken in 1.0.x and will not be fixed in the 1.0 branch due to their breaking nature. - Notable breaking changes are as follows, - * `IChannel#IsNsfw` has been replaced with `ITextChannel#IsNsfw` and now returns valid value in 2.0. - * Bulk message removal (`DeletedMessagesAsync`) has been moved from `IMessageChannel` to `ITextChannel`. - * `IAsyncEnumerable#Flatten` has been renamed to `FlattenAsync`. - -## 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](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) Abstractions. An example usage can be seen [here](https://github.com/foxbot/DiscordBotBase/blob/csharp/src/DiscordBot/Program.cs#L36). \ No newline at end of file diff --git a/docs/faq/3-AdvancedOperations.md b/docs/faq/AdvancedOperations.md similarity index 100% rename from docs/faq/3-AdvancedOperations.md rename to docs/faq/AdvancedOperations.md diff --git a/docs/faq/BasicOperations.md b/docs/faq/BasicOperations.md new file mode 100644 index 000000000..c9e1d7d6b --- /dev/null +++ b/docs/faq/BasicOperations.md @@ -0,0 +1,74 @@ +# Basic Operations Questions + +## How should I safely check a type? +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. There are several ways to cast, with direct casting +`(Type)type` being the the least recommended, as it *can* throw an +[InvalidCastException] when the object isn't the desired type. +Please refer to [this post] for more details. + +A good and safe casting example: + +[!code-csharp[Casting](samples/basics/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? + +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#Discord_IMessageChannel_SendMessageAsync_System_String_System_Boolean_Discord_Embed_Discord_RequestOptions_ +[GetChannel]: xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_GetChannel_System_UInt64_ + +## How can I tell if a message is from X, Y, Z? + +You may check the message channel type. Visit [Glossary] to see the +various types of channels. + +[Glossary]: Glossary.md + +## 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 using the + following format \[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 entities that implement [IUserMessage] has an [AddReactionAsync] +method. This method expects an [IEmote] as a parameter. +In Discord.Net, an Emote represents a server custom emote, while an +Emoji is a Unicode emoji (standard emoji). Both [Emoji] and [Emote] +implement [IEmote] and are valid options. + +[!code-csharp[Emoji](samples/basics/emoji.cs)] + +[AddReactionAsync]: xref:Discord.IUserMessage#Discord_IUserMessage_AddReactionAsync_Discord_IEmote_Discord_RequestOptions_ + +## Why am I getting so many preemptive rate limits when I try to add more than one reactions? + +This is due to how .NET parses the HTML header, mistreating +0.25sec/action to 1sec. This casues 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). + + +[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/ClientBasics.md b/docs/faq/ClientBasics.md new file mode 100644 index 000000000..8ea5394b4 --- /dev/null +++ b/docs/faq/ClientBasics.md @@ -0,0 +1,47 @@ +# Client Basics Questions + +## My client keeps returning 401 upon logging in! + +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 start with `Mj*`. + If it starts with any other characters, chances are, you might be + using the *client secret*, which has nothing to do with the login + token. + +[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#Discord_WebSocket_DiscordSocketClient_Ready +[GuildAvailable]: xref:Discord.WebSocket.BaseSocketClient#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](../guides/concepts/events.md#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#Discord_WebSocket_DiscordSocketConfig_MessageCacheSize +[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig +[MessageUpdated]: xref:Discord.WebSocket.BaseSocketClient#Discord_WebSocket_BaseSocketClient_MessageUpdated \ No newline at end of file diff --git a/docs/faq/Commands.md b/docs/faq/Commands.md new file mode 100644 index 000000000..948b7639d --- /dev/null +++ b/docs/faq/Commands.md @@ -0,0 +1,114 @@ +# 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`. What now? + +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#Discord_Commands_CommandService_AddModulesAsync_Assembly_System_IServiceProvider_ +[CommandService#AddModuleAsync]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModuleAsync__1_System_IServiceProvider_ + +## 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/commands/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] before proceeding, as well +as how it works in [Discord.NET](../guides/commands/commands.md#usage-in-modules). + +A brief example of service and dependency injection can be seen below. + +[!code-csharp[DI](samples/commands/DI.cs)] + +[Dependency Injection]: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection + +## 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 long-running task, +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. One being `RunMode.Sync`, which is the +default; another being `RunMode.Async`. `RunMode.Async` essentially +calls an unawaited Task and continues with the execution without +waiting for the command task to finish. You should use +`RunMode.Async` in either the [CommandAttribute] or the +[DefaultRunMode] flag in `CommandServiceConfig`. +Further details regarding `RunMode.Async` can be found below. + +[RunMode]: xref:Discord.Commands.RunMode +[CommandAttribute]: xref:Discord.Commands.CommandAttribute +[DefaultRunMode]: xref:Discord.Commands.CommandServiceConfig#Discord_Commands_CommandServiceConfig_DefaultRunMode + +## Okay, that's great and all, but how does `RunMode.Async` work, and if it's so great, why is the lib *not* using it by default? + +As with any async operation, `RunMode.Async` also comes at a cost. +The following are the caveats with RunMode.Async, +1) You 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 #3 and #4. + +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]. + +[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#Discord_Commands_CommandService_ExecuteAsync_Discord_Commands_ICommandContext_System_Int32_System_IServiceProvider_Discord_Commands_MultiMatchHandling_ +[ExecuteResult]: xref:Discord.Commands.ExecuteResult +[RuntimeResult]: xref:Discord.Commands.RuntimeResult +[CommandExecuted]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_CommandExecuted +[CommandService#Log]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_Log +[LogMessage.Exception]: xref:Discord.LogMessage#Discord_LogMessage_Exception +[CommandException]: xref:Discord.Commands.CommandException \ No newline at end of file diff --git a/docs/faq/GettingStarted.md b/docs/faq/GettingStarted.md new file mode 100644 index 000000000..1e4276b80 --- /dev/null +++ b/docs/faq/GettingStarted.md @@ -0,0 +1,71 @@ +# Basic Concepts / Getting Started + +## How do I get started? +First of all, welcome! 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], [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](https://github.com/RogueException/Discord.Net/blob/dev/docs/guides/getting_started/samples/intro/structure.cs) + 2. [Official template](https://github.com/foxbot/DiscordBotBase/tree/csharp/src/DiscordBot) + +Please note that you should *not* try to blindly copy paste the code. It is meant to be a template or a guide. It is not meant to be something that will work out of the box. + +[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/ + +## How do I add my bot to my server/guild? + +The [OAuth2 URL](https://discordapp.com/developers/tools/oauth2-url-generator) +can be generated via the Discord developer page. This allows you to +set the permissions that the bot will be added with. With this method, + bots will also be assigned their own special roles that normal users + cannot use, which is what we call a `Managed` role. + +## What is a Client/User/Object ID? Is it the token? + +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 +library require an ID to retrieve the said object. + +There are 2 ways to obtain the said ID. + 1. Enable Discord's developer mode. With developer mode enabled, + you can - as an example - right click on a guild and copy the guild + id (please note that this does not apply to Role IDs, see below). + ![Developer Mode](images/dev-mode.png) + 2. Escape the object using `\` in front the object. For example, + when you do `\@Example#1234` in chat, it will return the user ID of + the aforementioned user. + +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. + +## How do I get the role 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. + +Please note that right-clicking on the role and copying the ID will +**not** work. It will only copy the message ID. + +## I have more questions! + +Please visit us at #dotnet_discord-net at [Discord API]. +Describe the problem in details to us, and preferably with 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/faq/Glossary.md b/docs/faq/Glossary.md new file mode 100644 index 000000000..84e95d251 --- /dev/null +++ b/docs/faq/Glossary.md @@ -0,0 +1,18 @@ +# Glossary + +## Channel types + +* 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]) is all of the above. + + +[IMessageChannel]: xref:Discord.IMessageChannel +[ITextChannel]: xref:Discord.ITextChannel +[IGroupChannel]: xref:Discord.IGroupChannel +[IDMChannel]: xref:Discord.IDMChannel +[IPrivateChannel]: xref:Discord.IPrivateChannel \ No newline at end of file diff --git a/docs/faq/Legacy.md b/docs/faq/Legacy.md new file mode 100644 index 000000000..83495ef79 --- /dev/null +++ b/docs/faq/Legacy.md @@ -0,0 +1,19 @@ +# 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 methods or props 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/images/snowflake.png b/docs/faq/images/snowflake.png new file mode 100644 index 0000000000000000000000000000000000000000..816a10eee07e156ec4e27ee90a000512b1f12463 GIT binary patch literal 73062 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 XrU2O)WYlV=ATMnV1NF+Mj{p7##ChBc literal 0 HcmV?d00001 diff --git a/docs/faq/samples/basics/cast.cs b/docs/faq/samples/basics/cast.cs new file mode 100644 index 000000000..73ef5237f --- /dev/null +++ b/docs/faq/samples/basics/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/samples/basics/emoji.cs b/docs/faq/samples/basics/emoji.cs new file mode 100644 index 000000000..dd3e6317f --- /dev/null +++ b/docs/faq/samples/basics/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/samples/commands/DI.cs b/docs/faq/samples/commands/DI.cs new file mode 100644 index 000000000..59f098ff9 --- /dev/null +++ b/docs/faq/samples/commands/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/samples/commands/Remainder.cs b/docs/faq/samples/commands/Remainder.cs new file mode 100644 index 000000000..9a4ac4d78 --- /dev/null +++ b/docs/faq/samples/commands/Remainder.cs @@ -0,0 +1,19 @@ +// 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 5 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/toc.yml b/docs/faq/toc.yml index 446ddbf54..08ba404c1 100644 --- a/docs/faq/toc.yml +++ b/docs/faq/toc.yml @@ -1,10 +1,12 @@ - name: Getting Started - href: 0-GettingStarted.md -- name: Basics - href: 1-Basics.md + href: GettingStarted.md +- name: Client Basics + href: ClientBasics.md - name: Basic Operations - href: 2-BasicOperations.md + href: BasicOperations.md - name: Commands - href: 4-Commands.md + href: Commands.md +- name: Glossary + href: Glossary.md - name: Legacy or Upgrade - href: 5-Legacy.md + href: Legacy.md \ No newline at end of file From 962faf820da76006901ba07e831fdb0cd73e5799 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 14:14:12 +0800 Subject: [PATCH 006/183] Swap Guides and FAQ position --- docs/toc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/toc.yml b/docs/toc.yml index 7fd96916e..d869eed59 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,7 +1,7 @@ -- name: FAQ - href: faq/ - name: Guides href: guides/ +- name: FAQ + href: faq/ - name: API Documentation href: api/ homepage: api/index.md From a2df7c8d8ed065ab61cd1c7629295280025a4fbb Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 14:47:55 +0800 Subject: [PATCH 007/183] Add more glossary types --- docs/faq/Glossary.md | 54 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/faq/Glossary.md b/docs/faq/Glossary.md index 84e95d251..4390edf99 100644 --- a/docs/faq/Glossary.md +++ b/docs/faq/Glossary.md @@ -1,18 +1,64 @@ # Glossary -## Channel types +## 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 either a voice or text channel. + - Example: #dotnet_discord-net + +[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). +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]) is all of the above. +* A **Message channel** ([IMessageChannel]) can be any of the above. +### Misc Channels +* 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. [IMessageChannel]: xref:Discord.IMessageChannel [ITextChannel]: xref:Discord.ITextChannel [IGroupChannel]: xref:Discord.IGroupChannel [IDMChannel]: xref:Discord.IDMChannel -[IPrivateChannel]: xref:Discord.IPrivateChannel \ No newline at end of file +[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 From 886c4119dfe41b76f7db623ab94d8bebcdd3233c Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 14:49:37 +0800 Subject: [PATCH 008/183] Add channel type anchor --- docs/faq/BasicOperations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq/BasicOperations.md b/docs/faq/BasicOperations.md index c9e1d7d6b..3569415f6 100644 --- a/docs/faq/BasicOperations.md +++ b/docs/faq/BasicOperations.md @@ -33,7 +33,7 @@ able to message. You may check the message channel type. Visit [Glossary] to see the various types of channels. -[Glossary]: Glossary.md +[Glossary]: Glossary.md#message-channels ## How do I add hyperlink text to an embed? From 0cd59fb5baf7f7909e83688756c4b443bdc93c2f Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 14:53:11 +0800 Subject: [PATCH 009/183] Rename filenames and remove adv op --- docs/faq/AdvancedOperations.md | 5 ----- docs/faq/{BasicOperations.md => basic-operations.md} | 0 docs/faq/{ClientBasics.md => client-basics.md} | 0 docs/faq/{GettingStarted.md => getting-started.md} | 0 docs/faq/toc.yml | 12 ++++++------ 5 files changed, 6 insertions(+), 11 deletions(-) delete mode 100644 docs/faq/AdvancedOperations.md rename docs/faq/{BasicOperations.md => basic-operations.md} (100%) rename docs/faq/{ClientBasics.md => client-basics.md} (100%) rename docs/faq/{GettingStarted.md => getting-started.md} (100%) diff --git a/docs/faq/AdvancedOperations.md b/docs/faq/AdvancedOperations.md deleted file mode 100644 index 70bcbf04d..000000000 --- a/docs/faq/AdvancedOperations.md +++ /dev/null @@ -1,5 +0,0 @@ -# Advanced Operations - -## I want to create a service that sends a message or does things at a regular interval! - -You will need to create a separate service alongside the program. Here's [an example](https://gist.github.com/Joe4evr/967949a477ed0c6c841407f0f25fa730) to get you started. \ No newline at end of file diff --git a/docs/faq/BasicOperations.md b/docs/faq/basic-operations.md similarity index 100% rename from docs/faq/BasicOperations.md rename to docs/faq/basic-operations.md diff --git a/docs/faq/ClientBasics.md b/docs/faq/client-basics.md similarity index 100% rename from docs/faq/ClientBasics.md rename to docs/faq/client-basics.md diff --git a/docs/faq/GettingStarted.md b/docs/faq/getting-started.md similarity index 100% rename from docs/faq/GettingStarted.md rename to docs/faq/getting-started.md diff --git a/docs/faq/toc.yml b/docs/faq/toc.yml index 08ba404c1..a1b4faf10 100644 --- a/docs/faq/toc.yml +++ b/docs/faq/toc.yml @@ -1,12 +1,12 @@ - name: Getting Started - href: GettingStarted.md + href: getting-started.md - name: Client Basics - href: ClientBasics.md + href: client-basics.md - name: Basic Operations - href: BasicOperations.md + href: basic-operations.md - name: Commands - href: Commands.md + href: commands.md - name: Glossary - href: Glossary.md + href: glossary.md - name: Legacy or Upgrade - href: Legacy.md \ No newline at end of file + href: legacy.md From c98ed1238bcbf0e5c9ee80fd6bdad549ef212484 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 14:54:01 +0800 Subject: [PATCH 010/183] Sort order --- docs/faq/toc.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/faq/toc.yml b/docs/faq/toc.yml index a1b4faf10..7630051b9 100644 --- a/docs/faq/toc.yml +++ b/docs/faq/toc.yml @@ -1,9 +1,9 @@ - name: Getting Started href: getting-started.md -- name: Client Basics - href: client-basics.md - name: Basic Operations href: basic-operations.md +- name: Client Basics + href: client-basics.md - name: Commands href: commands.md - name: Glossary From b7cb798a05dc9e78f008a3fc216696ea8e801baa Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 15:05:46 +0800 Subject: [PATCH 011/183] Add xmldocs for CommandExecuted and various methods --- src/Discord.Net.Commands/CommandService.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index f4fbcf8b2..451b783dd 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -17,6 +17,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. + /// public event Func CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } } internal readonly AsyncEvent> _commandExecutedEvent = new AsyncEvent>(); @@ -34,8 +37,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()) { } From e6b00979f9d8dbafb119140049778e7ad8e767f1 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 15:14:55 +0800 Subject: [PATCH 012/183] Add RunMode documentation --- src/Discord.Net.Commands/RunMode.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/RunMode.cs b/src/Discord.Net.Commands/RunMode.cs index ecb6a4b58..43ced3f72 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 { + /// + /// Default behaviour set in . + /// Default, + /// + /// Executes the command on the same thread as gateway. + /// Sync, + /// + /// Executes the command on a different thread from the gateway one. + /// Async } } From 7c431b52bb084aef1e671bba4d78d19257f51112 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 15:15:54 +0800 Subject: [PATCH 013/183] Grammar fix --- src/Discord.Net.Commands/RunMode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/RunMode.cs b/src/Discord.Net.Commands/RunMode.cs index 43ced3f72..d3c50d9ed 100644 --- a/src/Discord.Net.Commands/RunMode.cs +++ b/src/Discord.Net.Commands/RunMode.cs @@ -7,7 +7,7 @@ namespace Discord.Commands /// Default, /// - /// Executes the command on the same thread as gateway. + /// Executes the command on the same thread as gateway one. /// Sync, /// From bf84af35b73215a95d9eb7fd82952fa02e852ea6 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 15:16:12 +0800 Subject: [PATCH 014/183] Remove unnecessary RunMode reference --- src/Discord.Net.Commands/RunMode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/RunMode.cs b/src/Discord.Net.Commands/RunMode.cs index d3c50d9ed..751af1ebf 100644 --- a/src/Discord.Net.Commands/RunMode.cs +++ b/src/Discord.Net.Commands/RunMode.cs @@ -3,7 +3,7 @@ namespace Discord.Commands public enum RunMode { /// - /// Default behaviour set in . + /// Default behaviour set in . /// Default, /// From c6d4494047756a5f2c08b25836200a69dfcb7106 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 19 Mar 2018 15:21:31 +0800 Subject: [PATCH 015/183] Add CommandError xmldocs --- src/Discord.Net.Commands/CommandError.cs | 26 +++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/CommandError.cs b/src/Discord.Net.Commands/CommandError.cs index abfc14e1d..9a78dcc8c 100644 --- a/src/Discord.Net.Commands/CommandError.cs +++ b/src/Discord.Net.Commands/CommandError.cs @@ -1,26 +1,50 @@ -namespace Discord.Commands +namespace Discord.Commands { 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 parameters. + /// 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 } } From 878de473ff5074eeaeab64722e7f008cbe74ebb4 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 09:53:24 +0800 Subject: [PATCH 016/183] Add ModuleBase and EmbedBuilder docfx --- src/Discord.Net.Commands/ModuleBase.cs | 9 +++++++-- src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs | 3 +++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index 3e6fbbd9b..b1445e0d3 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -21,11 +21,16 @@ namespace Discord.Commands { 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) { } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index f5663cea3..baabe50c6 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -4,6 +4,9 @@ using System.Collections.Immutable; namespace Discord { + /// + /// Builder for creating an to be sent. + /// public class EmbedBuilder { private readonly Embed _embed; From 6fdeae1ffc6d8d6a795533e6deaf69382da41ea5 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 14:18:30 +0800 Subject: [PATCH 017/183] Add "How can I get the guild from a message?" --- docs/faq/basic-operations.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/faq/basic-operations.md b/docs/faq/basic-operations.md index 3569415f6..dbd82c6d9 100644 --- a/docs/faq/basic-operations.md +++ b/docs/faq/basic-operations.md @@ -28,13 +28,20 @@ able to message. [SendMessageAsync]: xref:Discord.IMessageChannel#Discord_IMessageChannel_SendMessageAsync_System_String_System_Boolean_Discord_Embed_Discord_RequestOptions_ [GetChannel]: xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_GetChannel_System_UInt64_ -## How can I tell if a message is from X, Y, Z? +## 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]: Glossary.md#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 [ITextChannel]/[IVoiceChannel] 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 @@ -67,6 +74,10 @@ reactions. Unfortunately, not at the moment. See [#401](https://github.com/RogueException/Discord.Net/issues/401). +[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 From c3a31400324e02829315de449e4a7be5c0be4014 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 14:19:56 +0800 Subject: [PATCH 018/183] Change wording with hyperlink section --- docs/faq/basic-operations.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/faq/basic-operations.md b/docs/faq/basic-operations.md index dbd82c6d9..fbbc41feb 100644 --- a/docs/faq/basic-operations.md +++ b/docs/faq/basic-operations.md @@ -44,9 +44,9 @@ There are 2 ways to do this. You can do either of the following, ## 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 using the - following format \[text](link). +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- From f0b3a3439c03f8dee6643993bdc3264dce775cea Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 14:21:22 +0800 Subject: [PATCH 019/183] Fix typo & improve styling in Basic Operations --- docs/faq/basic-operations.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/faq/basic-operations.md b/docs/faq/basic-operations.md index fbbc41feb..13d482779 100644 --- a/docs/faq/basic-operations.md +++ b/docs/faq/basic-operations.md @@ -3,9 +3,12 @@ ## How should I safely check a type? 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. There are several ways to cast, with direct casting -`(Type)type` being the the least recommended, as it *can* throw an +action. + +There are several ways to cast, with direct casting +`(Type)type` being **the least recommended**, as it *can* throw an [InvalidCastException] when the object isn't the desired type. + Please refer to [this post] for more details. A good and safe casting example: From 1821ea15bbda167434d5c1c58fb958bf2a6359d3 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 15:02:38 +0800 Subject: [PATCH 020/183] Add examples & tidy commands section + Modified the headings to fit the formal aspect of docs. + Added more details regarding RunMode --- docs/faq/Commands.md | 70 +++++++++++++------ .../faq/samples/commands/runmode-cmdattrib.cs | 7 ++ .../faq/samples/commands/runmode-cmdconfig.cs | 9 +++ 3 files changed, 65 insertions(+), 21 deletions(-) create mode 100644 docs/faq/samples/commands/runmode-cmdattrib.cs create mode 100644 docs/faq/samples/commands/runmode-cmdconfig.cs diff --git a/docs/faq/Commands.md b/docs/faq/Commands.md index 948b7639d..27e00d43a 100644 --- a/docs/faq/Commands.md +++ b/docs/faq/Commands.md @@ -18,7 +18,7 @@ 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`. What now? +## I'm getting an error about `Assembly#GetEntryAssembly`. You may be confusing [CommandService#AddModulesAsync] with [CommandService#AddModuleAsync]. The former is used to add modules @@ -64,37 +64,64 @@ A brief example of service and dependency injection can be seen below. 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 long-running task, +your client to Discord alive. By default, 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. +persists for **more than 3 seconds**. To resolve this, the library has designed a flag called [RunMode]. -There are 2 main `RunMode`s. One being `RunMode.Sync`, which is the -default; another being `RunMode.Async`. `RunMode.Async` essentially -calls an unawaited Task and continues with the execution without -waiting for the command task to finish. You should use -`RunMode.Async` in either the [CommandAttribute] or the -[DefaultRunMode] flag in `CommandServiceConfig`. -Further details regarding `RunMode.Async` can be found below. + +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/commands/runmode-cmdattrib.cs)] + +# [CommandServiceConfig](#tab/cmdconfig) + +[!code-csharp[Command Service Config](samples/commands/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#Discord_Commands_CommandServiceConfig_DefaultRunMode -## Okay, that's great and all, but how does `RunMode.Async` work, and if it's so great, why is the lib *not* using it by default? +## 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 execution. -As with any async operation, `RunMode.Async` also comes at a cost. -The following are the caveats with RunMode.Async, -1) You 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. +The following are the known caveats with `RunMode.Async`, + 1. You 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 #3 and #4. +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 @@ -104,7 +131,8 @@ the `RunMode` type and will return the appropriate execution result. For #4, exceptions are caught in [CommandService#Log] event under [LogMessage.Exception] as [CommandException]. -[async state machine]: https://www.red-gate.com/simple-talk/dotnet/net-tools/c-async-what-is-it-and-how-does-it-work/)) +[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#Discord_Commands_CommandService_ExecuteAsync_Discord_Commands_ICommandContext_System_Int32_System_IServiceProvider_Discord_Commands_MultiMatchHandling_ [ExecuteResult]: xref:Discord.Commands.ExecuteResult [RuntimeResult]: xref:Discord.Commands.RuntimeResult diff --git a/docs/faq/samples/commands/runmode-cmdattrib.cs b/docs/faq/samples/commands/runmode-cmdattrib.cs new file mode 100644 index 000000000..253acc4a9 --- /dev/null +++ b/docs/faq/samples/commands/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/samples/commands/runmode-cmdconfig.cs b/docs/faq/samples/commands/runmode-cmdconfig.cs new file mode 100644 index 000000000..22b356aa3 --- /dev/null +++ b/docs/faq/samples/commands/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 From 21413d3c192b6808b8c349d75b758029ef87d37c Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 15:09:34 +0800 Subject: [PATCH 021/183] Add self-bot warning & clean-up --- docs/faq/client-basics.md | 17 ++++++++++++----- docs/faq/getting-started.md | 4 +++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/faq/client-basics.md b/docs/faq/client-basics.md index 8ea5394b4..2aaf789a2 100644 --- a/docs/faq/client-basics.md +++ b/docs/faq/client-basics.md @@ -2,6 +2,13 @@ ## 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 @@ -16,11 +23,11 @@ There are few possible reasons why this may occur. ## 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. +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 diff --git a/docs/faq/getting-started.md b/docs/faq/getting-started.md index 1e4276b80..25dd81c7a 100644 --- a/docs/faq/getting-started.md +++ b/docs/faq/getting-started.md @@ -12,7 +12,9 @@ understand these topics to some extent before proceeding. 1. [Official quick start guide](https://github.com/RogueException/Discord.Net/blob/dev/docs/guides/getting_started/samples/intro/structure.cs) 2. [Official template](https://github.com/foxbot/DiscordBotBase/tree/csharp/src/DiscordBot) -Please note that you should *not* try to blindly copy paste the code. It is meant to be a template or a guide. It is not meant to be something that will work out of the box. +Please note that you should *not* try to blindly copy paste the code. +It is meant to be a template or a guide. It is not meant to be +something that will work out of the box. [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 From a04d178db2168efc33f009ea7e03152dd6c20547 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 16:03:13 +0800 Subject: [PATCH 022/183] Add permission calc & minor wording changes --- docs/faq/getting-started.md | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/faq/getting-started.md b/docs/faq/getting-started.md index 25dd81c7a..9a1060ae8 100644 --- a/docs/faq/getting-started.md +++ b/docs/faq/getting-started.md @@ -1,7 +1,8 @@ # Basic Concepts / Getting Started ## How do I get started? -First of all, welcome! Before you delve into using the library; +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], [polymorphism], [interface] and @@ -11,10 +12,11 @@ understand these topics to some extent before proceeding. Here are some examples: 1. [Official quick start guide](https://github.com/RogueException/Discord.Net/blob/dev/docs/guides/getting_started/samples/intro/structure.cs) 2. [Official template](https://github.com/foxbot/DiscordBotBase/tree/csharp/src/DiscordBot) - -Please note that you should *not* try to blindly copy paste the code. -It is meant to be a template or a guide. It is not meant to be -something that will work out of the box. + +> [!TIP] +> Please note that you should *not* try to blindly copy paste +> the code. It is meant to be a template or a guide. It is not +> meant to be something that will work out of the box. [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 @@ -22,11 +24,16 @@ something that will work out of the box. ## How do I add my bot to my server/guild? -The [OAuth2 URL](https://discordapp.com/developers/tools/oauth2-url-generator) -can be generated via the Discord developer page. This allows you to -set the permissions that the bot will be added with. With this method, - bots will also be assigned their own special roles that normal users - cannot use, which is what we call a `Managed` role. +You can do so by using the [permissions 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 Client/User/Object ID? Is it the token? From edb64bde1379f932667a4876c0573dec7316c492 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 16:51:57 +0800 Subject: [PATCH 023/183] Add changes according to comments from PR --- docs/faq/Commands.md | 2 +- docs/faq/Glossary.md | 3 +++ docs/faq/basic-operations.md | 11 +++++++++-- docs/faq/getting-started.md | 7 ++++--- docs/faq/samples/commands/Remainder.cs | 5 +++-- docs/guides/commands/commands.md | 10 +++++++--- src/Discord.Net.Commands/CommandError.cs | 2 +- 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/docs/faq/Commands.md b/docs/faq/Commands.md index 27e00d43a..fdb5d8f79 100644 --- a/docs/faq/Commands.md +++ b/docs/faq/Commands.md @@ -114,7 +114,7 @@ different thread. This means that [ExecuteAsync] will be forced to return a successful [ExecuteResult] regardless of the execution. The following are the known caveats with `RunMode.Async`, - 1. You introduce race condition. + 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 diff --git a/docs/faq/Glossary.md b/docs/faq/Glossary.md index 4390edf99..3a5198bb1 100644 --- a/docs/faq/Glossary.md +++ b/docs/faq/Glossary.md @@ -24,10 +24,13 @@ 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 diff --git a/docs/faq/basic-operations.md b/docs/faq/basic-operations.md index 13d482779..289ebc22d 100644 --- a/docs/faq/basic-operations.md +++ b/docs/faq/basic-operations.md @@ -20,6 +20,12 @@ A good and safe casting example: ## 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 @@ -42,7 +48,7 @@ various types of channels. 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 [ITextChannel]/[IVoiceChannel] and use + 2. Cast the channel as an [IGuildChannel] and use its [IGuild] property. ## How do I add hyperlink text to an embed? @@ -67,7 +73,7 @@ implement [IEmote] and are valid options. ## Why am I getting so many preemptive rate limits when I try to add more than one reactions? -This is due to how .NET parses the HTML header, mistreating +This is due to how HTML header works, mistreating 0.25sec/action to 1sec. This casues the lib to throw preemptive rate limit more frequently than it should for methods such as adding reactions. @@ -77,6 +83,7 @@ reactions. Unfortunately, not at the moment. See [#401](https://github.com/RogueException/Discord.Net/issues/401). +[IGuildChannel]: xref:Discord.IGuildChannel [ITextChannel]: xref:Discord.ITextChannel [IGuild]: xref:Discord.IGuild [IVoiceChannel]: xref:Discord.IVoiceChannel diff --git a/docs/faq/getting-started.md b/docs/faq/getting-started.md index 9a1060ae8..cd44a8c63 100644 --- a/docs/faq/getting-started.md +++ b/docs/faq/getting-started.md @@ -5,8 +5,8 @@ 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], [polymorphism], [interface] and -many more advanced topics extensively. Please make sure that you +[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: @@ -47,7 +47,8 @@ library require an ID to retrieve the said object. There are 2 ways to obtain the said ID. 1. Enable Discord's developer mode. With developer mode enabled, you can - as an example - right click on a guild and copy the guild - id (please note that this does not apply to Role IDs, see below). + id (please note that this does not apply to all objects, such as + Role IDs \[see below], or DM channel IDs). ![Developer Mode](images/dev-mode.png) 2. Escape the object using `\` in front the object. For example, when you do `\@Example#1234` in chat, it will return the user ID of diff --git a/docs/faq/samples/commands/Remainder.cs b/docs/faq/samples/commands/Remainder.cs index 9a4ac4d78..728c7438c 100644 --- a/docs/faq/samples/commands/Remainder.cs +++ b/docs/faq/samples/commands/Remainder.cs @@ -11,8 +11,9 @@ public Task EchoRemainderAsync([Remainder]string text) => ReplyAsync(text); [Command("echo-hassle")] public Task EchoAsync(string text) => ReplyAsync(text); -// The message would be seen as having 5 parameters, while the method -// only accepts one. Wrapping the message in quotes solves this. +// 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. diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index 821a1bfa7..8eaaa3818 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -159,7 +159,7 @@ install them. To manually load a module, invoke [CommandService.AddModuleAsync] by passing in the generic type of your module and optionally, a -dependency map. +service provider. [CommandService.AddModuleAsync]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModuleAsync__1_System_IServiceProvider_ @@ -167,8 +167,12 @@ dependency map. 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. +@System.IServiceProvider first. + +> [!TIP] +> Alternatively, you may accept an +> `IServiceProvider` as an argument and extract services yourself, +> although this is discouraged. ### Module Properties diff --git a/src/Discord.Net.Commands/CommandError.cs b/src/Discord.Net.Commands/CommandError.cs index 9a78dcc8c..2ef4c7496 100644 --- a/src/Discord.Net.Commands/CommandError.cs +++ b/src/Discord.Net.Commands/CommandError.cs @@ -14,7 +14,7 @@ namespace Discord.Commands /// ParseFailed, /// - /// Thrown when the input text has too few parameters. + /// Thrown when the input text has too few or too many arguments. /// BadArgCount, From 179cd43a7f36467a19f2e5eca85889e849cccdc8 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 16:57:40 +0800 Subject: [PATCH 024/183] Add DI video link --- docs/faq/Commands.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/faq/Commands.md b/docs/faq/Commands.md index fdb5d8f79..0e8263e89 100644 --- a/docs/faq/Commands.md +++ b/docs/faq/Commands.md @@ -51,7 +51,7 @@ 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] before proceeding, as well +implementation of [Dependency Injection] ([video]) before proceeding, as well as how it works in [Discord.NET](../guides/commands/commands.md#usage-in-modules). A brief example of service and dependency injection can be seen below. @@ -59,6 +59,7 @@ A brief example of service and dependency injection can be seen below. [!code-csharp[DI](samples/commands/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? From 98b4d30431d038d06763053431e635b2667fea6d Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 17:01:45 +0800 Subject: [PATCH 025/183] Fix broken markdown links --- docs/faq/Commands.md | 2 +- docs/faq/getting-started.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/faq/Commands.md b/docs/faq/Commands.md index 0e8263e89..8f42c4d09 100644 --- a/docs/faq/Commands.md +++ b/docs/faq/Commands.md @@ -51,7 +51,7 @@ 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 +implementation of [Dependency Injection] \([video]) before proceeding, as well as how it works in [Discord.NET](../guides/commands/commands.md#usage-in-modules). A brief example of service and dependency injection can be seen below. diff --git a/docs/faq/getting-started.md b/docs/faq/getting-started.md index cd44a8c63..0140d3666 100644 --- a/docs/faq/getting-started.md +++ b/docs/faq/getting-started.md @@ -5,7 +5,7 @@ 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] +[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. @@ -15,8 +15,8 @@ understand these topics to some extent before proceeding. > [!TIP] > Please note that you should *not* try to blindly copy paste -> the code. It is meant to be a template or a guide. It is not -> meant to be something that will work out of the box. +> 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. [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 From f80e4247a0411ed583cf825fecdd5395a9c5a092 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 17:03:40 +0800 Subject: [PATCH 026/183] Fix perm calc link --- docs/faq/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq/getting-started.md b/docs/faq/getting-started.md index 0140d3666..c9fd7a738 100644 --- a/docs/faq/getting-started.md +++ b/docs/faq/getting-started.md @@ -24,7 +24,7 @@ understand these topics to some extent before proceeding. ## How do I add my bot to my server/guild? -You can do so by using the [permissions calculator] provided +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, From 34e6b2c9052f76e01cd5ba625a309922c2506a31 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 17:07:03 +0800 Subject: [PATCH 027/183] Remove references to 1.0 & tidy links --- docs/guides/getting_started/installing.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/guides/getting_started/installing.md b/docs/guides/getting_started/installing.md index 5d4c85d81..589225c76 100644 --- a/docs/guides/getting_started/installing.md +++ b/docs/guides/getting_started/installing.md @@ -24,10 +24,10 @@ required. When using .NET Framework, it is suggested to target # 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 +Development builds of Discord.Net, as well as addons *(TODO)* are published to our development [MyGet feed]. Direct feed link: `https://www.myget.org/F/discord-net/api/v3/index.json` @@ -91,14 +91,16 @@ In order to compile Discord.Net, you require 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] + +[.NET Core SDK]: https://www.microsoft.com/net/download/ # Additional Information From 8a4c06acb6e20ee68901d74812833c943f882dc3 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 17:12:27 +0800 Subject: [PATCH 028/183] Add notification of potential difference in sample csproj --- docs/guides/getting_started/samples/project.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/guides/getting_started/samples/project.csproj b/docs/guides/getting_started/samples/project.csproj index feb0b0c40..085d81c03 100644 --- a/docs/guides/getting_started/samples/project.csproj +++ b/docs/guides/getting_started/samples/project.csproj @@ -1,5 +1,8 @@ + // The following may differ depending on the latest version of + // .NET Core Framework or Discord.NET. + Exe netcoreapp1.1 From 2642fd78287e4aaa2f3a219a9100a897fff34799 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 17:18:32 +0800 Subject: [PATCH 029/183] Remove 1.0 ref --- docs/guides/getting_started/installing.md | 4 ++-- docs/guides/getting_started/samples/project.csproj | 7 ++++--- docs/guides/getting_started/terminology.md | 8 ++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/guides/getting_started/installing.md b/docs/guides/getting_started/installing.md index 589225c76..372009e90 100644 --- a/docs/guides/getting_started/installing.md +++ b/docs/guides/getting_started/installing.md @@ -80,7 +80,7 @@ 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.csproj)] [NuGet.Config file]: #configuring-nuget-without-visual-studio @@ -145,4 +145,4 @@ your application, where the project solution is located. Paste the following snippets into this configuration file, adding any additional feeds as necessary. -[!code-xml[NuGet Configuration](samples/nuget.config)] +[!code[NuGet Configuration](samples/nuget.config)] diff --git a/docs/guides/getting_started/samples/project.csproj b/docs/guides/getting_started/samples/project.csproj index 085d81c03..a964bbc26 100644 --- a/docs/guides/getting_started/samples/project.csproj +++ b/docs/guides/getting_started/samples/project.csproj @@ -1,8 +1,9 @@ - // The following may differ depending on the latest version of - // .NET Core Framework or Discord.NET. - + Exe netcoreapp1.1 diff --git a/docs/guides/getting_started/terminology.md b/docs/guides/getting_started/terminology.md index 74f7a6259..5685477dd 100644 --- a/docs/guides/getting_started/terminology.md +++ b/docs/guides/getting_started/terminology.md @@ -7,13 +7,13 @@ 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 +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`. From 2ef5d8ce09ce1cb854c20a2a8a39babd19a3be00 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 17:25:11 +0800 Subject: [PATCH 030/183] Add warning against direct cast & fix broken MD link --- docs/faq/basic-operations.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/faq/basic-operations.md b/docs/faq/basic-operations.md index 289ebc22d..f806e59de 100644 --- a/docs/faq/basic-operations.md +++ b/docs/faq/basic-operations.md @@ -1,16 +1,18 @@ # 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. -There are several ways to cast, with direct casting -`(Type)type` being **the least recommended**, as it *can* throw an -[InvalidCastException] when the object isn't the desired type. - -Please refer to [this post] for more details. - A good and safe casting example: [!code-csharp[Casting](samples/basics/cast.cs)] @@ -83,6 +85,8 @@ reactions. 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 From 1d6220187d237d6e76be3bbfa18ee7d76b3317e5 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 17:27:47 +0800 Subject: [PATCH 031/183] Fix broken indent --- docs/faq/samples/commands/Remainder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq/samples/commands/Remainder.cs b/docs/faq/samples/commands/Remainder.cs index 728c7438c..a28c782e0 100644 --- a/docs/faq/samples/commands/Remainder.cs +++ b/docs/faq/samples/commands/Remainder.cs @@ -12,7 +12,7 @@ public Task EchoRemainderAsync([Remainder]string text) => ReplyAsync(text); public Task EchoAsync(string text) => ReplyAsync(text); // The message would be seen as having multiple parameters, -// while the method only accepts one. +// 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. From c49869fd1d7d9a577bc42272428c682de7cdc250 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 17:29:32 +0800 Subject: [PATCH 032/183] Reword legacy versions --- docs/faq/Legacy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq/Legacy.md b/docs/faq/Legacy.md index 83495ef79..61d1b3d5a 100644 --- a/docs/faq/Legacy.md +++ b/docs/faq/Legacy.md @@ -3,7 +3,7 @@ ## 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 methods or props may be broken in older +compatibility. Several features may be broken in older versions and will likely not be fixed in the version branch due to their breaking nature. From ee91c606fe75e5ea87e0c259d7447f9ac28423f9 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 22:37:42 +0800 Subject: [PATCH 033/183] Change FAQ structure to tree --- docs/faq/toc.yml | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/faq/toc.yml b/docs/faq/toc.yml index 7630051b9..bce0aaf87 100644 --- a/docs/faq/toc.yml +++ b/docs/faq/toc.yml @@ -1,12 +1,18 @@ -- name: Getting Started - href: getting-started.md -- name: Basic Operations - href: basic-operations.md -- name: Client Basics - href: client-basics.md -- name: Commands - href: commands.md -- name: Glossary - href: glossary.md -- name: Legacy or Upgrade - href: legacy.md +- name: Basic Concepts + items: + - name: Getting Started + href: getting-started.md + - name: Basic Operations + href: basic-operations.md + - name: Client Basics + href: client-basics.md +- name: Command + items: + - name: Commands + href: commands.md +- name: Misc + items: + - name: Glossary + href: glossary.md + - name: Legacy or Upgrade + href: legacy.md From ba1d61b109e94dbaec9b02952ec118682e2bf249 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 22:38:42 +0800 Subject: [PATCH 034/183] Remove overwrite from docfx.json --- docs/docfx.json | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docs/docfx.json b/docs/docfx.json index d0e1c65cd..a2785b8e8 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -53,17 +53,6 @@ ] } ], - "overwrite": [ - { - "files": [ - "apidoc/**.md" - ], - "exclude": [ - "obj/**", - "_site/**" - ] - } - ], "dest": "_site", "template": [ "default" From 68be9d3dfc7e7ff83ed89c20206054f63e6e95cf Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 22:42:46 +0800 Subject: [PATCH 035/183] Enable docs search --- docs/docfx.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/docfx.json b/docs/docfx.json index a2785b8e8..46c6ad984 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -58,7 +58,8 @@ "default" ], "globalMetadata": { - "_appFooter": "Discord.Net (c) 2015-2018" + "_appFooter": "Discord.Net (c) 2015-2018", + "_enableSearch": true }, "noLangKeyword": false } From 291ad1cf5e08829d209c4252650531c009ab8a30 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 20 Mar 2018 23:24:19 +0800 Subject: [PATCH 036/183] Vastly simplified xref redirection --- docs/faq/Commands.md | 14 +++++++------- docs/faq/basic-operations.md | 6 +++--- docs/faq/client-basics.md | 8 ++++---- docs/guides/commands/commands.md | 24 ++++++++++++------------ docs/guides/concepts/logging.md | 4 ++-- docs/guides/getting_started/intro.md | 4 ++-- docs/guides/voice/sending-voice.md | 2 +- 7 files changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/faq/Commands.md b/docs/faq/Commands.md index 8f42c4d09..7e8b23d7a 100644 --- a/docs/faq/Commands.md +++ b/docs/faq/Commands.md @@ -24,8 +24,8 @@ 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#Discord_Commands_CommandService_AddModulesAsync_Assembly_System_IServiceProvider_ -[CommandService#AddModuleAsync]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModuleAsync__1_System_IServiceProvider_ +[CommandService#AddModulesAsync]: xref:Discord.Commands.CommandService.AddModulesAsync* +[CommandService#AddModuleAsync]: xref:Discord.Commands.CommandService.AddModuleAsync* ## What does [Remainder] do in the command signature? @@ -104,7 +104,7 @@ the [DefaultRunMode] flag under `CommandServiceConfig`. [RunMode]: xref:Discord.Commands.RunMode [CommandAttribute]: xref:Discord.Commands.CommandAttribute -[DefaultRunMode]: xref:Discord.Commands.CommandServiceConfig#Discord_Commands_CommandServiceConfig_DefaultRunMode +[DefaultRunMode]: xref:Discord.Commands.CommandServiceConfig.DefaultRunMode ## How does `RunMode.Async` work, and why is Discord.NET *not* using it by default? @@ -134,10 +134,10 @@ For #4, exceptions are caught in [CommandService#Log] event under [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#Discord_Commands_CommandService_ExecuteAsync_Discord_Commands_ICommandContext_System_Int32_System_IServiceProvider_Discord_Commands_MultiMatchHandling_ +[ExecuteAsync]: xref:Discord.Commands.CommandService.ExecuteAsync* [ExecuteResult]: xref:Discord.Commands.ExecuteResult [RuntimeResult]: xref:Discord.Commands.RuntimeResult -[CommandExecuted]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_CommandExecuted -[CommandService#Log]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_Log -[LogMessage.Exception]: xref:Discord.LogMessage#Discord_LogMessage_Exception +[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/basic-operations.md b/docs/faq/basic-operations.md index f806e59de..e7490c8bb 100644 --- a/docs/faq/basic-operations.md +++ b/docs/faq/basic-operations.md @@ -36,8 +36,8 @@ 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#Discord_IMessageChannel_SendMessageAsync_System_String_System_Boolean_Discord_Embed_Discord_RequestOptions_ -[GetChannel]: xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_GetChannel_System_UInt64_ +[SendMessageAsync]: xref:Discord.IMessageChannel.SendMessageAsync* +[GetChannel]: xref:Discord.WebSocket.DiscordSocketClient.GetChannel* ## How can I tell if a message is from X, Y, Z channel? @@ -71,7 +71,7 @@ implement [IEmote] and are valid options. [!code-csharp[Emoji](samples/basics/emoji.cs)] -[AddReactionAsync]: xref:Discord.IUserMessage#Discord_IUserMessage_AddReactionAsync_Discord_IEmote_Discord_RequestOptions_ +[AddReactionAsync]: xref:Discord.IUserMessage.AddReactionAsync* ## Why am I getting so many preemptive rate limits when I try to add more than one reactions? diff --git a/docs/faq/client-basics.md b/docs/faq/client-basics.md index 2aaf789a2..a8de2a552 100644 --- a/docs/faq/client-basics.md +++ b/docs/faq/client-basics.md @@ -34,8 +34,8 @@ 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#Discord_WebSocket_DiscordSocketClient_Ready -[GuildAvailable]: xref:Discord.WebSocket.BaseSocketClient#Discord_WebSocket_BaseSocketClient_GuildAvailable +[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? @@ -49,6 +49,6 @@ use the cached message entity. Read more about it [here](../guides/concepts/even 3. Only messages received *after* the bot comes online will be available in the cache. -[MessageCacheSize]: xref:Discord.WebSocket.DiscordSocketConfig#Discord_WebSocket_DiscordSocketConfig_MessageCacheSize +[MessageCacheSize]: xref:Discord.WebSocket.DiscordSocketConfig.MessageCacheSize [DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig -[MessageUpdated]: xref:Discord.WebSocket.BaseSocketClient#Discord_WebSocket_BaseSocketClient_MessageUpdated \ No newline at end of file +[MessageUpdated]: xref:Discord.WebSocket.BaseSocketClient.MessageUpdated \ No newline at end of file diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index 8eaaa3818..73dc46d92 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -134,7 +134,7 @@ accessing the channel through the [Context] and sending a message. [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_ +[ReplyAsync]: xref:Discord.Commands.ModuleBase`1.ReplyAsync* ### Example Module @@ -153,7 +153,7 @@ 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_System_IServiceProvider_ +[CommandService.AddModulesAsync]: xref:Discord.Commands.CommandService.AddModulesAsync* #### Loading Modules Manually @@ -161,7 +161,7 @@ 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#Discord_Commands_CommandService_AddModuleAsync__1_System_IServiceProvider_ +[CommandService.AddModuleAsync]: xref:Discord.Commands.CommandService.AddModuleAsync* ### Module Constructors @@ -228,8 +228,8 @@ 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. +> Annotating a property with a [DontInjectAttribute] attribute will +> prevent the property from being injected. >[!NOTE] >If you accept `CommandService` or `IServiceProvider` as a parameter @@ -283,9 +283,9 @@ necessary. [!code-csharp[Custom Precondition](samples/require_owner.cs)] -[CheckPermissionsAsync]: xref:Discord.Commands.PreconditionAttribute#Discord_Commands_PreconditionAttribute_CheckPermissionsAsync_Discord_Commands_ICommandContext_Discord_Commands_CommandInfo_System_IServiceProvider_ -[PreconditionResult.FromSuccess]: xref:Discord.Commands.PreconditionResult#Discord_Commands_PreconditionResult_FromSuccess -[PreconditionResult.FromError]: xref:Discord.Commands.PreconditionResult#Discord_Commands_PreconditionResult_FromError_System_String_ +[CheckPermissionsAsync]: xref:Discord.Commands.PreconditionAttribute.CheckPermissionsAsync* +[PreconditionResult.FromSuccess]: xref:Discord.Commands.PreconditionResult.FromSuccess* +[PreconditionResult.FromError]: xref:Discord.Commands.PreconditionResult.FromError* # Type Readers @@ -329,9 +329,9 @@ If you are able to successfully parse the input, return 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_ -[ReadAsync]: xref:Discord.Commands.TypeReader#Discord_Commands_TypeReader_ReadAsync_Discord_Commands_ICommandContext_System_String_System_IServiceProvider_ +[TypeReaderResult.FromSuccess]: xref:Discord.Commands.TypeReaderResult.FromSuccess* +[TypeReaderResult.FromError]: xref:Discord.Commands.TypeReaderResult.FromError* +[ReadAsync]: xref:Discord.Commands.TypeReader.ReadAsync* #### Sample @@ -344,4 +344,4 @@ 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_ +[CommandService.AddTypeReader]: xref:Discord.Commands.CommandService.AddTypeReader* diff --git a/docs/guides/concepts/logging.md b/docs/guides/concepts/logging.md index 50d2e9546..ae2acdf0f 100644 --- a/docs/guides/concepts/logging.md +++ b/docs/guides/concepts/logging.md @@ -3,12 +3,12 @@ title: Logging --- Discord.Net's clients provide a [Log] event that all messages will be -disbatched over. +dispatched over. For more information about events in Discord.Net, see the [Events] section. -[Log]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log +[Log]: xref:Discord.Rest.BaseDiscordClient.Log [Events]: events.md ### Usage diff --git a/docs/guides/getting_started/intro.md b/docs/guides/getting_started/intro.md index 361dd7f29..c0c26e3f1 100644 --- a/docs/guides/getting_started/intro.md +++ b/docs/guides/getting_started/intro.md @@ -105,7 +105,7 @@ the Console. [!code-csharp[Async Context](samples/intro/logging.cs)] -[API Documentation]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log +[API Documentation]: xref:Discord.Rest.BaseDiscordClient.Log ### Creating a Discord Client @@ -212,7 +212,7 @@ shown below. For your reference, you may view the [completed program]. -[MessageReceived]: xref:Discord.WebSocket.BaseSocketClient#Discord_WebSocket_BaseSocketClient_MessageReceived +[MessageReceived]: xref:Discord.WebSocket.BaseSocketClient.MessageReceived [SocketMessage]: xref:Discord.WebSocket.SocketMessage [SocketMessageChannel]: xref:Discord.WebSocket.ISocketMessageChannel [completed program]: samples/intro/complete.cs diff --git a/docs/guides/voice/sending-voice.md b/docs/guides/voice/sending-voice.md index 30be558b1..1b47dc2f8 100644 --- a/docs/guides/voice/sending-voice.md +++ b/docs/guides/voice/sending-voice.md @@ -44,7 +44,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_System_Action_IAudioClient__ +[ConnectAsync]: xref:Discord.IAudioChannel.ConnectAsync* ## Transmitting Audio From 0979a7cac4f7b432adb56a0b8763f6ad8603dee1 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Wed, 21 Mar 2018 00:24:04 +0800 Subject: [PATCH 037/183] Add XMLDocs for common entities + DiscordConfig + CDN + MentionUtils + LogMessage + LogSeverity --- src/Discord.Net.Core/CDN.cs | 8 ++++++++ src/Discord.Net.Core/DiscordConfig.cs | 17 ++++++++++++++--- src/Discord.Net.Core/Logging/LogMessage.cs | 7 ++++++- src/Discord.Net.Core/Logging/LogSeverity.cs | 21 ++++++++++++++++++++- src/Discord.Net.Core/Utils/MentionUtils.cs | 7 +++++-- 5 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index f23f55238..f18adbd7c 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -4,8 +4,10 @@ namespace Discord { 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 size and . public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, ImageFormat format) { if (avatarId == null) @@ -13,21 +15,27 @@ namespace Discord string extension = FormatToExtension(format, avatarId); return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}"; } + /// Returns the guild icon URL based on the guild and icon ID. public static string GetGuildIconUrl(ulong guildId, string iconId) => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; + /// Returns the guild splash URL based on the guild and icon ID. public static string GetGuildSplashUrl(ulong guildId, string splashId) => splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; + /// Returns the channel icon URL based on the 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}"; diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index fd2fe92e8..6b50adc5c 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -1,24 +1,35 @@ -using System.Reflection; +using System.Reflection; namespace Discord { public class DiscordConfig { - public const int APIVersion = 6; + /// Returns the gateway version Discord.NET uses. + public const int APIVersion = 6; + /// Returns the Discord.NET verion, including the build number. public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute()?.InformationalVersion ?? typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ?? "Unknown"; - + + /// Returns 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. diff --git a/src/Discord.Net.Core/Logging/LogMessage.cs b/src/Discord.Net.Core/Logging/LogMessage.cs index d1b3782be..7c382f95f 100644 --- a/src/Discord.Net.Core/Logging/LogMessage.cs +++ b/src/Discord.Net.Core/Logging/LogMessage.cs @@ -1,13 +1,18 @@ -using System; +using System; using System.Text; namespace Discord { + /// The message object for logging purposes. public struct LogMessage { + /// The severity of the log message. public LogSeverity Severity { get; } + /// The source of the log message. public string Source { get; } + /// The message of the log message. public string Message { get; } + /// The exception of the log message. public Exception Exception { get; } public LogMessage(LogSeverity severity, string source, string message, Exception exception = null) diff --git a/src/Discord.Net.Core/Logging/LogSeverity.cs b/src/Discord.Net.Core/Logging/LogSeverity.cs index 785b0ef46..98a822425 100644 --- a/src/Discord.Net.Core/Logging/LogSeverity.cs +++ b/src/Discord.Net.Core/Logging/LogSeverity.cs @@ -1,12 +1,31 @@ -namespace Discord +namespace Discord { 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/Utils/MentionUtils.cs b/src/Discord.Net.Core/Utils/MentionUtils.cs index 6c69827b4..bdc4a80ae 100644 --- a/src/Discord.Net.Core/Utils/MentionUtils.cs +++ b/src/Discord.Net.Core/Utils/MentionUtils.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.Text; @@ -10,10 +10,13 @@ namespace Discord //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. From faaf3397dab24bd13003a81195f08fa7b0060237 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Wed, 21 Mar 2018 00:43:12 +0800 Subject: [PATCH 038/183] Add XMLDocs + EmbedBuilderExtensions + CacheMode + ImageFormat --- src/Discord.Net.Core/Entities/CacheMode.cs | 4 +++- src/Discord.Net.Core/Entities/ImageFormat.cs | 3 ++- src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs | 7 +++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Entities/CacheMode.cs b/src/Discord.Net.Core/Entities/CacheMode.cs index a047bd616..c4342eea2 100644 --- a/src/Discord.Net.Core/Entities/CacheMode.cs +++ b/src/Discord.Net.Core/Entities/CacheMode.cs @@ -1,8 +1,10 @@ -namespace Discord +namespace Discord { 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/ImageFormat.cs b/src/Discord.Net.Core/Entities/ImageFormat.cs index 302da79c8..0b74a443e 100644 --- a/src/Discord.Net.Core/Entities/ImageFormat.cs +++ b/src/Discord.Net.Core/Entities/ImageFormat.cs @@ -1,5 +1,6 @@ -namespace Discord +namespace Discord { + /// The type of format for the image to return. public enum ImageFormat { Auto, diff --git a/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs index 2eb4ed473..5ad8d8b1d 100644 --- a/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs +++ b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs @@ -4,24 +4,31 @@ namespace Discord { 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) From 1663450b9f8f4a9cbd629a29b6b32632d4c08ef8 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Wed, 21 Mar 2018 01:22:13 +0800 Subject: [PATCH 039/183] Add XMLDocs + ActivityType + IAttachment + ChannelPermission + ChannelPermissions + ChannelType + Attachment --- .../Entities/Activities/ActivityType.cs | 9 ++- .../Entities/Messages/IAttachment.cs | 9 ++- .../Entities/Permissions/ChannelPermission.cs | 63 ++++++++++++------- .../Permissions/ChannelPermissions.cs | 22 +++---- .../Entities/Channels/ChannelType.cs | 8 ++- .../Entities/Messages/Attachment.cs | 9 ++- 6 files changed, 84 insertions(+), 36 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs index c7db7b247..4cc71b119 100644 --- a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs +++ b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs @@ -1,10 +1,17 @@ -namespace Discord +namespace Discord { + /// + /// Defines user's activity type. + /// public enum ActivityType { + /// Activity that represents a user that is playing a game. Playing = 0, + /// Activity that represents a user that is streaming online. Streaming = 1, + /// Activity that represents a user that is listening to a song. Listening = 2, + /// Activity that represents a user that is watching a media. Watching = 3 } } diff --git a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs index 225e9cf2e..d0f99ba4a 100644 --- a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs +++ b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs @@ -1,14 +1,21 @@ -namespace Discord +namespace Discord { public interface IAttachment { + /// The snowflake ID of the attachment. ulong Id { get; } + /// The filename of the attachment. string Filename { get; } + /// The URL of the attachment. string Url { get; } + /// The proxied URL of the attachment. string ProxyUrl { get; } + /// The file size of the attachment. int Size { get; } + /// The height of the attachment if it is an image, or return when it is not. int? Height { get; } + /// 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/Permissions/ChannelPermission.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs index 740b6c30b..3db506487 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Discord { @@ -6,33 +6,54 @@ namespace Discord public enum ChannelPermission : ulong { // General + /// Allows creation of instant invites. CreateInstantInvite = 0x00_00_00_01, - ManageChannels = 0x00_00_00_10, + /// 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, - AttachFiles = 0x00_00_80_00, - ReadMessageHistory = 0x00_01_00_00, - MentionEveryone = 0x00_02_00_00, - UseExternalEmojis = 0x00_04_00_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, + /// 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, // More General - ManageRoles = 0x10_00_00_00, - ManageWebhooks = 0x20_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, } } diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index 1a8aad53c..92cc90c09 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,17 +7,17 @@ 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 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) @@ -30,7 +30,7 @@ namespace Discord } } - /// 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. @@ -117,7 +117,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 +129,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 +157,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.Rest/Entities/Channels/ChannelType.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs index e9f069a50..c1fdc5af5 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 { + /// Represents a text channel. Text = 0, + /// Represents a Direct Message channel. DM = 1, + /// Represents a voice channel. Voice = 2, + /// Represents a group channel. Group = 3, + /// Represents a category channel. Category = 4 } } diff --git a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs index e185234ac..9d3912bfc 100644 --- a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs +++ b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using Model = Discord.API.Attachment; namespace Discord @@ -6,12 +6,19 @@ namespace Discord [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) From 7c60b0293b287e8eb0f2497bd007a6f764418957 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Wed, 21 Mar 2018 15:13:37 +0800 Subject: [PATCH 040/183] Add XMLDocs * Fix comments to comply with third-person commenting style + Various attributes + Various Command-related objects + Many more --- .../Attributes/CommandAttribute.cs | 7 +++++ .../RequireBotPermissionAttribute.cs | 8 +++--- .../Preconditions/RequireContextAttribute.cs | 4 +-- .../Preconditions/RequireNsfwAttribute.cs | 2 +- .../Preconditions/RequireOwnerAttribute.cs | 2 +- .../RequireUserPermissionAttribute.cs | 6 ++--- src/Discord.Net.Commands/CommandContext.cs | 8 +++++- src/Discord.Net.Commands/CommandException.cs | 2 ++ src/Discord.Net.Commands/CommandMatch.cs | 4 ++- src/Discord.Net.Commands/Info/CommandInfo.cs | 26 +++++++++++++++++++ .../MultiMatchHandling.cs | 4 ++- src/Discord.Net.Commands/Results/IResult.cs | 5 +++- .../Results/ParseResult.cs | 5 +++- .../Results/PreconditionResult.cs | 5 +++- .../Results/RuntimeResult.cs | 6 ++++- .../Results/SearchResult.cs | 5 +++- .../Results/TypeReaderResult.cs | 5 +++- src/Discord.Net.Commands/RunMode.cs | 2 +- src/Discord.Net.Core/CDN.cs | 1 + .../Commands/ICommandContext.cs | 7 ++++- .../Channels/GuildChannelProperties.cs | 10 +++---- src/Discord.Net.Core/Entities/Emotes/Emoji.cs | 4 +-- src/Discord.Net.Core/Entities/Emotes/Emote.cs | 10 +++---- .../Entities/Emotes/GuildEmote.cs | 4 +-- .../Entities/Guilds/GuildEmbedProperties.cs | 4 +-- .../Entities/Guilds/GuildProperties.cs | 18 ++++++------- .../Entities/Messages/IAttachment.cs | 1 + .../Entities/Messages/MessageProperties.cs | 8 +++--- .../Entities/Users/GuildUserProperties.cs | 8 +++--- src/Discord.Net.Core/Utils/MentionUtils.cs | 1 + .../Entities/Messages/Attachment.cs | 2 ++ 31 files changed, 129 insertions(+), 55 deletions(-) diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs index 5f8e9ceaf..e1fc59a73 100644 --- a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -2,10 +2,17 @@ using System; namespace Discord.Commands { + /// Provides the execution information for a command. [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class CommandAttribute : Attribute { + /// + /// Specifies the text required 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() diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs index 104252799..1afe82685 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -1,10 +1,10 @@ -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. + /// This attribute 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 @@ -13,7 +13,7 @@ namespace Discord.Commands public ChannelPermission? ChannelPermission { get; } /// - /// Require that the bot account has a specified GuildPermission + /// Requires that 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. @@ -23,7 +23,7 @@ namespace Discord.Commands 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. /// diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs index 90af035e4..252d8b5f2 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -13,7 +13,7 @@ namespace Discord.Commands } /// - /// Require that the command be invoked in a specified context. + /// This attribute requires that the command be invoked in a specified context. (e.g. in guild, DM) /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireContextAttribute : PreconditionAttribute @@ -21,7 +21,7 @@ namespace Discord.Commands public ContextType Contexts { get; } /// - /// Require that the command be invoked in a specified context. + /// Requires that the command 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..baff01d8c 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord.Commands { /// - /// Require that the command is invoked in a channel marked NSFW + /// This attribute requires that the command to be invoked in a channel marked NSFW. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class RequireNsfwAttribute : PreconditionAttribute diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs index 93e3cbe18..bf6a3352e 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord.Commands { /// - /// Require that the command is invoked by the owner of the bot. + /// This attribute requires that 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)] diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 14121f35b..7aaeab064 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Commands @@ -13,7 +13,7 @@ namespace Discord.Commands 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. @@ -23,7 +23,7 @@ namespace Discord.Commands 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. /// diff --git a/src/Discord.Net.Commands/CommandContext.cs b/src/Discord.Net.Commands/CommandContext.cs index 05bde56b1..d884d8b29 100644 --- a/src/Discord.Net.Commands/CommandContext.cs +++ b/src/Discord.Net.Commands/CommandContext.cs @@ -1,13 +1,19 @@ -namespace Discord.Commands +namespace Discord.Commands { 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/CommandException.cs b/src/Discord.Net.Commands/CommandException.cs index d5300841a..1584641b3 100644 --- a/src/Discord.Net.Commands/CommandException.cs +++ b/src/Discord.Net.Commands/CommandException.cs @@ -4,7 +4,9 @@ namespace Discord.Commands { public class CommandException : Exception { + /// The command that caused the exception. public CommandInfo Command { get; } + /// The command context of the exception. public ICommandContext Context { get; } public CommandException(CommandInfo command, ICommandContext context, Exception ex) diff --git a/src/Discord.Net.Commands/CommandMatch.cs b/src/Discord.Net.Commands/CommandMatch.cs index d922a2229..6411ef084 100644 --- a/src/Discord.Net.Commands/CommandMatch.cs +++ b/src/Discord.Net.Commands/CommandMatch.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -7,7 +7,9 @@ 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/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index f0d406e8d..75f3f56eb 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -12,6 +12,11 @@ using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { + /// 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 +26,36 @@ namespace Discord.Commands private readonly CommandService _commandService; private readonly Func _action; + /// The module that the command belongs in. public ModuleInfo Module { get; } + /// Name of the command. If none is set, the first alias is used. public string Name { get; } + /// Summary of the command. + /// + /// This field returns the summary of the command. + /// Summary and remarks can be useful in help commands and various implementation that fetches details of the command for the user. + /// public string Summary { get; } + /// Remarks of the command. + /// + /// This field returns the remarks of the command. + /// Summary and remarks can be useful in help commands and various implementation that fetches details of the command for the user. + /// public string Remarks { get; } + /// 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; } + /// Indicates the that is being used for the command. public RunMode RunMode { get; } + /// List of aliases defined by the of the command. public IReadOnlyList Aliases { get; } + /// List of information about the parameters of the command. public IReadOnlyList Parameters { get; } + /// List of preconditions defined by the of the command. public IReadOnlyList Preconditions { get; } + /// List of attributes of the command. public IReadOnlyList Attributes { get; } internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) @@ -122,6 +146,7 @@ namespace Discord.Commands return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0).ConfigureAwait(false); } + /// Executes the command with the provided context, parsed value, and service provider. public Task ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) { if (!parseResult.IsSuccess) @@ -145,6 +170,7 @@ namespace Discord.Commands return ExecuteAsync(context, argList, paramList, services); } + /// Executes the command with the provided context, argument and parameter list, and service provider. public async Task ExecuteAsync(ICommandContext context, IEnumerable argList, IEnumerable paramList, IServiceProvider services) { services = services ?? EmptyServiceProvider.Instance; 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/Results/IResult.cs b/src/Discord.Net.Commands/Results/IResult.cs index 928d1139e..cc5d4c3be 100644 --- a/src/Discord.Net.Commands/Results/IResult.cs +++ b/src/Discord.Net.Commands/Results/IResult.cs @@ -1,9 +1,12 @@ -namespace Discord.Commands +namespace Discord.Commands { 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..f128ab7a9 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) diff --git a/src/Discord.Net.Commands/Results/PreconditionResult.cs b/src/Discord.Net.Commands/Results/PreconditionResult.cs index ca65a373e..beb156b33 100644 --- a/src/Discord.Net.Commands/Results/PreconditionResult.cs +++ b/src/Discord.Net.Commands/Results/PreconditionResult.cs @@ -1,13 +1,16 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Discord.Commands { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class PreconditionResult : IResult { + /// public CommandError? Error { get; } + /// public string ErrorReason { get; } + /// public bool IsSuccess => !Error.HasValue; protected PreconditionResult(CommandError? error, string errorReason) diff --git a/src/Discord.Net.Commands/Results/RuntimeResult.cs b/src/Discord.Net.Commands/Results/RuntimeResult.cs index 2a326a7a3..73c8b9066 100644 --- a/src/Discord.Net.Commands/Results/RuntimeResult.cs +++ b/src/Discord.Net.Commands/Results/RuntimeResult.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; @@ -14,11 +14,15 @@ namespace Discord.Commands 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 751af1ebf..b5ab1116b 100644 --- a/src/Discord.Net.Commands/RunMode.cs +++ b/src/Discord.Net.Commands/RunMode.cs @@ -3,7 +3,7 @@ namespace Discord.Commands public enum RunMode { /// - /// Default behaviour set in . + /// The default behaviour set in . /// Default, /// diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index f18adbd7c..b8aa2084f 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -2,6 +2,7 @@ using System; namespace Discord { + /// Contains the strings related to various Content Delievery Networks (CDNs). public static class CDN { /// Returns the Discord developer application icon. diff --git a/src/Discord.Net.Core/Commands/ICommandContext.cs b/src/Discord.Net.Core/Commands/ICommandContext.cs index ac1424339..e0ea18ca8 100644 --- a/src/Discord.Net.Core/Commands/ICommandContext.cs +++ b/src/Discord.Net.Core/Commands/ICommandContext.cs @@ -1,11 +1,16 @@ -namespace Discord.Commands +namespace Discord.Commands { public interface ICommandContext { + /// The Discord client of which the command is executed with. IDiscordClient Client { get; } + /// The guild of which the command is executed in. IGuild Guild { get; } + /// The channel of which the command is executed in. IMessageChannel Channel { get; } + /// The user who executed the command. IUser User { get; } + /// The message of which the command is interpreted from. IUserMessage Message { get; } } } diff --git a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs index 2ac6c8d52..09703bd65 100644 --- a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs @@ -1,7 +1,7 @@ -namespace Discord +namespace Discord { /// - /// Modify an IGuildChannel with the specified changes. + /// Properties that are used to modify an with the specified changes. /// /// /// @@ -14,7 +14,7 @@ public class GuildChannelProperties { /// - /// Set the channel to this name + /// Sets the channel to this name. /// /// /// When modifying an ITextChannel, the Name MUST be alphanumeric with dashes. @@ -23,11 +23,11 @@ /// 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 + /// Sets the category for this channel. /// public Optional CategoryId { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs index c2dfc31ad..1b2d7beb2 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs @@ -1,7 +1,7 @@ -namespace Discord +namespace Discord { /// - /// A unicode emoji + /// A unicode emoji. /// public class Emoji : IEmote { diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs index e3a228c83..6488e320d 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -1,19 +1,19 @@ -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 + /// The display name (tooltip) of this emote. /// public string Name { get; } /// - /// The ID of this emote + /// The ID of this emote. /// public ulong Id { get; } /// @@ -50,7 +50,7 @@ namespace Discord } /// - /// Parse an Emote from its raw format + /// Parses an Emote from its raw format. /// /// The raw encoding of an emote; for example, <:dab:277855270321782784> /// An emote diff --git a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs index 95b062bd2..fec5519e5 100644 --- a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs @@ -1,10 +1,10 @@ -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 diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs index a2b2ec4fc..62bb8dfa9 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs @@ -1,7 +1,7 @@ -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 parameters. /// public class GuildEmbedProperties { diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs index 1b406ef7f..d98d7815a 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs @@ -1,7 +1,7 @@ -namespace Discord +namespace Discord { /// - /// Modify an IGuild with the specified changes + /// Properties that are used to modify an with the specified changes. /// /// /// @@ -17,23 +17,23 @@ { public Optional Username { get; set; } /// - /// The name of the Guild + /// The name of the Guild. /// public Optional Name { get; set; } /// - /// The region for the Guild's voice connections + /// The region for the Guild's voice connections. /// public Optional Region { get; set; } /// - /// The ID of the region for the Guild's voice connections + /// 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 + /// What verification level new users need to achieve before speaking. /// public Optional VerificationLevel { get; set; } /// - /// The default message notification state for the guild + /// The default message notification state for the guild. /// public Optional DefaultMessageNotifications { get; set; } /// @@ -41,11 +41,11 @@ /// public Optional AfkTimeout { get; set; } /// - /// The icon of the guild + /// The icon of the guild. /// public Optional Icon { get; set; } /// - /// The guild's splash image + /// The guild's splash image. /// /// /// The guild must be partnered for this value to have any effect. diff --git a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs index d0f99ba4a..90b3f28ba 100644 --- a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs +++ b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs @@ -1,5 +1,6 @@ namespace Discord { + /// The interface that defines an attachment object. public interface IAttachment { /// The snowflake ID of the attachment. diff --git a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs index b3f3a9c89..bb6dcbe0f 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs @@ -1,7 +1,7 @@ -namespace Discord +namespace Discord { /// - /// Modify a message with the specified parameters. + /// Properties that are used to modify a message with the specified parameters. /// /// /// The content of a message can be cleared with String.Empty; if and only if an Embed is present. @@ -23,14 +23,14 @@ public class MessageProperties { /// - /// The content of the message + /// The content of the message. /// /// /// This must be less than 2000 characters. /// public Optional Content { get; set; } /// - /// The embed the message should display + /// The embed the message should display. /// public Optional Embed { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs index 1c5e5482c..32d892e96 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -1,9 +1,9 @@ -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. /// /// /// @@ -54,14 +54,14 @@ namespace Discord /// 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. /// 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. diff --git a/src/Discord.Net.Core/Utils/MentionUtils.cs b/src/Discord.Net.Core/Utils/MentionUtils.cs index bdc4a80ae..c510c7b3f 100644 --- a/src/Discord.Net.Core/Utils/MentionUtils.cs +++ b/src/Discord.Net.Core/Utils/MentionUtils.cs @@ -4,6 +4,7 @@ using System.Text; namespace Discord { + /// A helper class for mention-related parsing. public static class MentionUtils { private const char SanitizeChar = '\x200b'; diff --git a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs index 9d3912bfc..f33380ce3 100644 --- a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs +++ b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs @@ -3,6 +3,7 @@ using Model = Discord.API.Attachment; namespace Discord { + /// A Discord attachment. [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Attachment : IAttachment { @@ -38,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)"; } From b4f986031959172b4c66b9a3bc1d0b19ccc47bc5 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Wed, 21 Mar 2018 15:27:22 +0800 Subject: [PATCH 041/183] Add XMLDocs + GameAsset + GameTimestamps + Format --- src/Discord.Net.Core/Entities/Activities/GameAsset.cs | 8 ++++++-- .../Entities/Activities/GameTimestamps.cs | 5 +++-- src/Discord.Net.Core/Format.cs | 3 ++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs index 02c29ba41..ee084235d 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs @@ -1,14 +1,18 @@ namespace Discord { + /// The asset for a object. public class GameAsset { internal GameAsset() { } internal ulong? ApplicationId { get; set; } - + + /// The description of the asset. public string Text { get; internal set; } + /// The image ID of the asset. public string ImageId { get; internal set; } - + + /// The image URL of the asset; may return null when the application ID does not exist. public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => ApplicationId.HasValue ? CDN.GetRichAssetUrl(ApplicationId.Value, ImageId, size, format) : null; } diff --git a/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs index 8c8c992fa..846832796 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs @@ -1,7 +1,8 @@ -using System; +using System; namespace Discord { + /// The timestamps for a object. public class GameTimestamps { public DateTimeOffset? Start { get; } @@ -13,4 +14,4 @@ namespace Discord End = end; } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Format.cs b/src/Discord.Net.Core/Format.cs index aa822f99e..beb13b6d8 100644 --- a/src/Discord.Net.Core/Format.cs +++ b/src/Discord.Net.Core/Format.cs @@ -1,5 +1,6 @@ -namespace Discord +namespace Discord { + /// A helper class for formatting characters. public static class Format { // Characters which need escaping From 759720bd318104b998c3ae2ca357863c6c16b738 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 23 Mar 2018 09:36:36 +0800 Subject: [PATCH 042/183] Inital post-execution draft --- docs/guides/commands/post-execution.md | 41 +++++++++++++++++++ .../commands/samples/post-execution_basic.cs | 11 +++++ docs/guides/toc.yml | 2 + 3 files changed, 54 insertions(+) create mode 100644 docs/guides/commands/post-execution.md create mode 100644 docs/guides/commands/samples/post-execution_basic.cs diff --git a/docs/guides/commands/post-execution.md b/docs/guides/commands/post-execution.md new file mode 100644 index 000000000..b9f0427ef --- /dev/null +++ b/docs/guides/commands/post-execution.md @@ -0,0 +1,41 @@ +# Preface + +When developing a command system or modules, you may want to consider +building a post-execution handling system so you can have a finer +control over commands. Discord.NET offers several different +post-execution workflow 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)] + +**But!** 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 has another potential +issue, working with `RunMode.Async`. + +If your command is marked with `RunMode.Async`, [ExecuteAsync] will +return a successful [ExecuteResult] instead of whatever results +the actual command may return. Because of the way `RunMode.Async` +[works](../../faq/commands.md), handling within the command handler +may not always achieve the desired effect. + +## CommandExecuted Event + +Enter [CommandExecuted], an event that was introduced in +Discord.NET 2.0. This event is raised **when the command is +sucessfully executed** and is not prone to `RunMode.Async`'s +[ExecuteAsync] drawbacks. + +[CommandExecuted]: xref:Discord.Commands.CommandService.CommandExecuted +[ExecuteAsync]: xref:Discord.Commands.CommandService.ExecuteAsync* +[ExecuteResult]: xref:Discord.Commands.ExecuteResult +[Command Guide]: Commands.md \ No newline at end of file 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/toc.yml b/docs/guides/toc.yml index 5a13a234c..7e34a047b 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -20,6 +20,8 @@ items: - name: Command Guide href: commands/commands.md + - name: Post-execution Handling + href: commands/post-execution.md - name: Voice items: - name: Voice Guide From 5b5603c00ff77a4a7169843979d270251d2613eb Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 23 Mar 2018 13:39:23 +0800 Subject: [PATCH 043/183] Add Nsfw precondition into bundle --- docs/guides/commands/commands.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index 73dc46d92..2723afefb 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -132,7 +132,7 @@ accessing the channel through the [Context] and sending a message. >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 +[Context]: xref:Discord.Commands.ModuleBase`1.Context [SocketCommandContext]: xref:Discord.Commands.SocketCommandContext [ReplyAsync]: xref:Discord.Commands.ModuleBase`1.ReplyAsync* @@ -264,6 +264,7 @@ usages on their respective API pages. - @Discord.Commands.RequireOwnerAttribute - @Discord.Commands.RequireBotPermissionAttribute - @Discord.Commands.RequireUserPermissionAttribute +- @Discord.Commands.RequireNsfwAttribute ## Custom Preconditions From 0f24db04c86ca119bcd97061820f65e0bfe0e6b3 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 23 Mar 2018 13:43:05 +0800 Subject: [PATCH 044/183] Warning-ify long running code part in commands --- docs/guides/commands/commands.md | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index 2723afefb..d87f7eb24 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -24,8 +24,8 @@ values. ## With Attributes -In 1.0, Commands can be defined ahead of time with attributes, or at -runtime with builders. +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. @@ -41,21 +41,22 @@ 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`. +> [!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]. >[!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. +To begin, create a new class somewhere in your project and inherit the +class from [ModuleBase]. This class **must** be `public`. + By now, your module should look like this: [!code-csharp[Empty Module](samples/empty-module.cs)] From e37652bd55fa972b778b92b68475fd7a3b2dd142 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 23 Mar 2018 13:44:38 +0800 Subject: [PATCH 045/183] Add Task in command section --- docs/guides/commands/commands.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index d87f7eb24..b48a19b69 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -69,7 +69,8 @@ By now, your module should look like this: The next step to creating Commands is actually creating the Commands. -To create a Command, add a method to your module of type `Task`. +To create a Command, add a method to your module of type `Task` or +`Task` depending on your use. Typically, you will want to mark this method as `async`, although it is not required. From 8a7a066c8eff61439a74387d7d66b416e3924bb3 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 23 Mar 2018 13:51:39 +0800 Subject: [PATCH 046/183] Linkify Type Readers in Commands --- docs/guides/commands/commands.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index b48a19b69..896843621 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -79,9 +79,9 @@ 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_. +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 +the below section on [Type Readers](#type-readers). Parameters, by default, are always required. To make a parameter optional, give it a default value. To accept a comma-separated list, From ae6c0645913bd375d936f839b2cb292747664071 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 23 Mar 2018 13:53:09 +0800 Subject: [PATCH 047/183] Fix CheckPermission in docs --- docs/guides/commands/samples/require_owner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From 98f63a5b5ef8c7c6e82fc9d44f6ff739f6d8d280 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 23 Mar 2018 13:55:07 +0800 Subject: [PATCH 048/183] Add missing variable name in module example --- docs/guides/commands/samples/dependency_module.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/commands/samples/dependency_module.cs b/docs/guides/commands/samples/dependency_module.cs index 561b0f6ac..92a5d55e7 100644 --- a/docs/guides/commands/samples/dependency_module.cs +++ b/docs/guides/commands/samples/dependency_module.cs @@ -23,7 +23,7 @@ public class ModuleB { // Public settable properties will be injected - public AnnounceService { get; set; } + public AnnounceService Announce { get; set; } // Public properties without setters will not public CommandService Commands { get; } From d06d67517a8c3f4017bd70c0f05f24d9e004bdd8 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 23 Mar 2018 14:17:21 +0800 Subject: [PATCH 049/183] Improve readability & general fixes Notable changes include the following, + Add _services into examples. + Rename Commands back to commands. - This was changed in my previous PR; however, this made readability really weird. + Fix broken examples. + Directly referencing anchors instead of saying 'see this below' --- docs/guides/commands/commands.md | 76 +++++++++---------- .../commands/samples/command_handler.cs | 2 +- .../commands/samples/dependency_map_setup.cs | 2 +- 3 files changed, 38 insertions(+), 42 deletions(-) diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index 896843621..4999e2b9d 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -1,15 +1,15 @@ # The Command Service -[Discord.Commands](xref:Discord.Commands) provides an Attribute-based +[Discord.Commands](xref:Discord.Commands) provides an attribute-based command parser. ## Setup -To use Commands, you must create a [Command Service] and a Command +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 +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], @@ -39,7 +39,7 @@ 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. +module instance is only as long as the command is being invoked. > [!WARNING] > **Avoid using long-running code** in your modules wherever possible. @@ -67,37 +67,36 @@ By now, your module should look like this: ### Adding Commands -The next step to creating Commands is actually creating the 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` or +To create a command, add a method to your module of type `Task` or `Task` depending on your use. 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`. -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 -the below section on [Type Readers](#type-readers). +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`. 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 the below +section on [Type Readers](#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. +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. -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) +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](#module-groups). [RemainderAttribute]: xref:Discord.Commands.RemainderAttribute [CommandAttribute]: xref:Discord.Commands.CommandAttribute @@ -116,11 +115,10 @@ priority will be called first. ### Command Context -Every Command can access the execution context through the [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. +message, channel, guild, user, and 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 @@ -146,14 +144,13 @@ At this point, your module should look comparable to this example: #### Loading Modules Automatically The Command Service can automatically discover all classes in an -Assembly that inherit [ModuleBase] and load them. +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]. -Invoke [CommandService.AddModulesAsync] to discover modules and -install them. - [DontAutoLoadAttribute]: xref:Discord.Commands.DontAutoLoadAttribute [CommandService.AddModulesAsync]: xref:Discord.Commands.CommandService.AddModulesAsync* @@ -189,7 +186,7 @@ prefixed. To create a group, flag a module with the 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. +command will inherit the name of the group it belongs to. ### Submodules @@ -211,20 +208,19 @@ DI when writing your modules. ### Setup -First, you need to create an @System.IServiceProvider; you may create -your own one if you wish. +First, you need to create an @System.IServiceProvider. -Next, add the dependencies that your modules will use to the map. +Next, add the dependencies to the service collection that you wish +to use in the Modules. -Finally, pass the map into the `LoadAssembly` method. Your modules -will be automatically loaded with this dependency map. +Finally, pass the service collection into `AddModulesAsync`. [!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`. +the @System.IServiceProvider that you've passed. Any publicly settable properties will also be filled in the same manner. diff --git a/docs/guides/commands/samples/command_handler.cs b/docs/guides/commands/samples/command_handler.cs index da2453aa8..1e68d70ec 100644 --- a/docs/guides/commands/samples/command_handler.cs +++ b/docs/guides/commands/samples/command_handler.cs @@ -40,7 +40,7 @@ public class Program // 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/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 From 1d087c391fafcdda9ad34cb5876d472aa19213cb Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 23 Mar 2018 14:22:32 +0800 Subject: [PATCH 050/183] Improve readability for module example --- docs/guides/commands/samples/module.cs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/guides/commands/samples/module.cs b/docs/guides/commands/samples/module.cs index 1e3555501..ad96b6051 100644 --- a/docs/guides/commands/samples/module.cs +++ b/docs/guides/commands/samples/module.cs @@ -1,14 +1,13 @@ // Create a module with no prefix public class Info : 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 @@ -18,7 +17,9 @@ public class Sample : 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 for")] + SocketUser user = null) { var userInfo = user ?? Context.Client.CurrentUser; await ReplyAsync($"{userInfo.Username}#{userInfo.Discriminator}"); From 2cf94bab3552db0787e22abcf2f0d13ab222f64f Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 23 Mar 2018 14:23:48 +0800 Subject: [PATCH 051/183] Fix ReadAsync for TypeReader example --- docs/guides/commands/samples/typereader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)) From 8be767b67466e97601c067ef6fdafd15d8260c23 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 23 Mar 2018 14:30:27 +0800 Subject: [PATCH 052/183] Fix xrefing for foreign objects --- docs/docfx.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/docfx.json b/docs/docfx.json index 46c6ad984..901357ae3 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -61,6 +61,7 @@ "_appFooter": "Discord.Net (c) 2015-2018", "_enableSearch": true }, - "noLangKeyword": false + "noLangKeyword": false, + "xrefService": [ "https://xref.docs.microsoft.com/query?uid={uid}" ] } } From bc671b19ca98b4190ee527a8847ee40a708954b6 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 23 Mar 2018 14:49:49 +0800 Subject: [PATCH 053/183] Add missing ModuleBase in DI example --- docs/guides/commands/samples/dependency_module.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/commands/samples/dependency_module.cs b/docs/guides/commands/samples/dependency_module.cs index 92a5d55e7..24009cc41 100644 --- a/docs/guides/commands/samples/dependency_module.cs +++ b/docs/guides/commands/samples/dependency_module.cs @@ -2,7 +2,7 @@ using Discord; using Discord.Commands; using Discord.WebSocket; -public class ModuleA : ModuleBase +public class ModuleA : ModuleBase { private readonly DatabaseService _database; @@ -19,7 +19,7 @@ public class ModuleA : ModuleBase } } -public class ModuleB +public class ModuleB : ModuleBase { // Public settable properties will be injected From 81ad8abf595dc6304c85abd37e06c28491f8546c Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 23 Mar 2018 16:44:26 +0800 Subject: [PATCH 054/183] Add XMLDocs To many files. --- .../Attributes/CommandAttribute.cs | 2 +- .../Attributes/DontAutoLoadAttribute.cs | 1 + .../Attributes/DontInjectAttribute.cs | 1 + .../RequireBotPermissionAttribute.cs | 4 ++-- .../Preconditions/RequireContextAttribute.cs | 2 +- .../Preconditions/RequireNsfwAttribute.cs | 2 +- .../Preconditions/RequireOwnerAttribute.cs | 2 +- .../RequireUserPermissionAttribute.cs | 2 +- .../Attributes/PriorityAttribute.cs | 4 ++-- src/Discord.Net.Core/ConnectionState.cs | 7 ++++++- .../Entities/Activities/ActivityType.cs | 12 +++++------- src/Discord.Net.Core/Entities/Activities/Game.cs | 3 +++ .../Entities/Activities/GameAsset.cs | 2 +- .../Entities/Activities/GameTimestamps.cs | 2 +- .../Entities/Activities/IActivity.cs | 5 ++++- .../Entities/Activities/SpotifyGame.cs | 9 +++++++++ .../Entities/Activities/StreamingGame.cs | 7 +++++-- src/Discord.Net.Core/Entities/CacheMode.cs | 1 + .../Entities/Channels/Direction.cs | 6 +++++- .../Guilds/DefaultMessageNotifications.cs | 3 ++- .../Entities/Guilds/GuildEmbedProperties.cs | 2 +- .../Guilds/GuildIntegrationProperties.cs | 6 +++++- src/Discord.Net.Core/Entities/Guilds/MfaLevel.cs | 3 ++- .../Entities/Guilds/PermissionTarget.cs | 5 ++++- .../Entities/Guilds/VerificationLevel.cs | 3 ++- src/Discord.Net.Core/Entities/IDeletable.cs | 3 ++- src/Discord.Net.Core/Entities/IMentionable.cs | 3 ++- .../Entities/ISnowflakeEntity.cs | 1 + src/Discord.Net.Core/Entities/IUpdateable.cs | 3 ++- src/Discord.Net.Core/Entities/ImageFormat.cs | 2 +- src/Discord.Net.Core/Entities/Messages/Embed.cs | 15 ++++++++++++++- .../Entities/Messages/EmbedAuthor.cs | 7 ++++++- .../Entities/Messages/EmbedBuilder.cs | 6 ++---- .../Entities/Messages/EmbedField.cs | 6 +++++- .../Entities/Messages/EmbedFooter.cs | 6 +++++- .../Entities/Messages/EmbedImage.cs | 7 ++++++- .../Entities/Messages/EmbedProvider.cs | 5 ++++- .../Entities/Messages/EmbedThumbnail.cs | 7 ++++++- .../Entities/Messages/EmbedType.cs | 10 ++++++++++ .../Entities/Messages/EmbedVideo.cs | 6 +++++- .../Entities/Messages/IAttachment.cs | 16 ++++++++-------- src/Discord.Net.Core/Entities/Messages/IEmbed.cs | 16 +++++++++++++++- .../Entities/Messages/IMessage.cs | 3 ++- .../Entities/Messages/IReaction.cs | 4 +++- .../Entities/Messages/IUserMessage.cs | 3 ++- .../Entities/Messages/MessageProperties.cs | 2 +- .../Entities/Messages/MessageSource.cs | 5 +++++ .../Entities/Messages/MessageType.cs | 10 +++++++++- .../Entities/Messages/ReactionMetadata.cs | 7 ++++--- .../Entities/Messages/TagHandling.cs | 12 ++++++++++-- .../Entities/Messages/TagType.cs | 9 ++++++++- .../Entities/Permissions/ChannelPermission.cs | 1 + .../Entities/Permissions/GuildPermission.cs | 3 ++- .../Entities/Permissions/PermValue.cs | 6 +++++- src/Discord.Net.Core/Entities/Roles/Color.cs | 1 + .../Entities/Roles/ReorderRoleProperties.cs | 7 ++++--- .../Entities/Roles/RoleProperties.cs | 16 ++++++++-------- .../Entities/Users/IGroupUser.cs | 3 ++- .../Entities/Users/IGuildUser.cs | 4 ++-- src/Discord.Net.Core/Entities/Users/IPresence.cs | 5 +++-- src/Discord.Net.Core/Entities/Users/ISelfUser.cs | 3 ++- src/Discord.Net.Core/Entities/Users/IUser.cs | 1 + .../Entities/Users/IWebhookUser.cs | 3 ++- .../Entities/Users/SelfUserProperties.cs | 4 ++-- .../Entities/Users/UserStatus.cs | 9 ++++++++- .../Entities/Webhooks/WebhookProperties.cs | 12 ++++++------ src/Discord.Net.Core/Logging/LogSeverity.cs | 1 + src/Discord.Net.Core/LoginState.cs | 7 ++++++- src/Discord.Net.Core/TokenType.cs | 1 + 69 files changed, 262 insertions(+), 95 deletions(-) diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs index e1fc59a73..eeb43bad3 100644 --- a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -7,7 +7,7 @@ namespace Discord.Commands public class CommandAttribute : Attribute { /// - /// Specifies the text required to be recognized as a command. + /// Gets the text that has been set to be recognized as a command. /// public string Text { get; } /// diff --git a/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs b/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs index cc23a6d15..22664afd1 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 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..354b87364 100644 --- a/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs @@ -2,6 +2,7 @@ using System; namespace Discord.Commands { + /// Prevents the 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/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs index 1afe82685..152376819 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord.Commands { /// - /// This attribute requires the bot to have a specific 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 @@ -13,7 +13,7 @@ namespace Discord.Commands public ChannelPermission? ChannelPermission { get; } /// - /// Requires that the bot account to have a specific . + /// 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. diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs index 252d8b5f2..bf5d081ce 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -13,7 +13,7 @@ namespace Discord.Commands } /// - /// This attribute requires that the command be invoked in a specified context. (e.g. in guild, DM) + /// 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 diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs index baff01d8c..ec3931dbe 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord.Commands { /// - /// This attribute requires that the command to be 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 diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs index bf6a3352e..bade39bc2 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord.Commands { /// - /// This attribute requires that the command to be 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)] diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 7aaeab064..6343ca190 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -4,7 +4,7 @@ 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 diff --git a/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs b/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs index 353e96e41..ad49ebb06 100644 --- a/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PriorityAttribute.cs @@ -2,11 +2,11 @@ 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. diff --git a/src/Discord.Net.Core/ConnectionState.cs b/src/Discord.Net.Core/ConnectionState.cs index 42c505ccd..b240eb134 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 { + /// Represents that the client has disconnected from the WebSocket. Disconnected, + /// Represents that the client is connecting to the WebSocket. Connecting, + /// Represents that the client has established a connection to the WebSocket. Connected, + /// Represents that the client is disconnecting from the WebSocket. Disconnecting } } diff --git a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs index 4cc71b119..14281a329 100644 --- a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs +++ b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs @@ -1,17 +1,15 @@ namespace Discord { - /// - /// Defines user's activity type. - /// + /// Specifies a Discord user's activity type. public enum ActivityType { - /// Activity that represents a user that is playing a game. + /// Represents that the user is playing a game. Playing = 0, - /// Activity that represents a user that is streaming online. + /// Represents that the user is streaming online. Streaming = 1, - /// Activity that represents a user that is listening to a song. + /// Represents that the user is listening to a song. Listening = 2, - /// Activity that represents a user that is watching a media. + /// Represents that 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 179ad4eaa..5c5bd98b5 100644 --- a/src/Discord.Net.Core/Entities/Activities/Game.cs +++ b/src/Discord.Net.Core/Entities/Activities/Game.cs @@ -2,10 +2,13 @@ using System.Diagnostics; namespace Discord { + /// A user's game activity. [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Game : IActivity { + /// public string Name { get; internal set; } + /// public ActivityType Type { get; internal set; } internal Game() { } diff --git a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs index ee084235d..78560ebe3 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs @@ -1,6 +1,6 @@ namespace Discord { - /// The asset for a object. + /// An asset for a object. public class GameAsset { internal GameAsset() { } diff --git a/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs index 846832796..572547d6e 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs @@ -2,7 +2,7 @@ using System; namespace Discord { - /// The timestamps for a object. + /// Timestamps for a object. public class GameTimestamps { public DateTimeOffset? Start { get; } diff --git a/src/Discord.Net.Core/Entities/Activities/IActivity.cs b/src/Discord.Net.Core/Entities/Activities/IActivity.cs index 1f158217d..1b2ebc540 100644 --- a/src/Discord.Net.Core/Entities/Activities/IActivity.cs +++ b/src/Discord.Net.Core/Entities/Activities/IActivity.cs @@ -1,8 +1,11 @@ -namespace Discord +namespace Discord { + /// A Discord activity. 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/SpotifyGame.cs b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs index a20384242..1db816037 100644 --- a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs @@ -4,19 +4,28 @@ 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 for the song. public string AlbumArt { get; internal set; } + /// Gets the Spotify album title for the song. public string AlbumTitle { get; internal set; } + /// Gets the track title for the song. public string TrackTitle { get; internal set; } + /// Gets the synchronization ID for the song. public string SyncId { get; internal set; } + /// Gets the session ID for the song. public string SessionId { get; internal set; } + /// Gets the duration for 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..a50e4686b 100644 --- a/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs @@ -1,10 +1,12 @@ -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; } public StreamingGame(string name, string url) @@ -14,7 +16,8 @@ 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 c4342eea2..a68a89174 100644 --- a/src/Discord.Net.Core/Entities/CacheMode.cs +++ b/src/Discord.Net.Core/Entities/CacheMode.cs @@ -1,5 +1,6 @@ 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. diff --git a/src/Discord.Net.Core/Entities/Channels/Direction.cs b/src/Discord.Net.Core/Entities/Channels/Direction.cs index 5d8d5e621..750d928e5 100644 --- a/src/Discord.Net.Core/Entities/Channels/Direction.cs +++ b/src/Discord.Net.Core/Entities/Channels/Direction.cs @@ -1,9 +1,13 @@ -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/Guilds/DefaultMessageNotifications.cs b/src/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs index a5cabc117..4cefa0e0a 100644 --- a/src/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs +++ b/src/Discord.Net.Core/Entities/Guilds/DefaultMessageNotifications.cs @@ -1,5 +1,6 @@ -namespace Discord +namespace Discord { + /// Specifies the default message notification behavior the guild uses. public enum DefaultMessageNotifications { /// By default, all messages will trigger notifications. diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs index 62bb8dfa9..5c1deb91e 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs @@ -1,7 +1,7 @@ namespace Discord { /// - /// Properties that are used to modify the widget of an with the specified parameters. + /// Properties that are used to modify the widget of an with the specified changes. /// public class GuildEmbedProperties { diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs index f329e78e6..588a99eaf 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs @@ -1,9 +1,13 @@ -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/MfaLevel.cs b/src/Discord.Net.Core/Entities/Guilds/MfaLevel.cs index 1dfef17d5..0f590a9cc 100644 --- a/src/Discord.Net.Core/Entities/Guilds/MfaLevel.cs +++ b/src/Discord.Net.Core/Entities/Guilds/MfaLevel.cs @@ -1,5 +1,6 @@ -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. diff --git a/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs b/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs index 96595fb69..448abaf86 100644 --- a/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs +++ b/src/Discord.Net.Core/Entities/Guilds/PermissionTarget.cs @@ -1,8 +1,11 @@ -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..22e8305c3 100644 --- a/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs +++ b/src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs @@ -1,5 +1,6 @@ -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. diff --git a/src/Discord.Net.Core/Entities/IDeletable.cs b/src/Discord.Net.Core/Entities/IDeletable.cs index ba22a537a..0269b19f5 100644 --- a/src/Discord.Net.Core/Entities/IDeletable.cs +++ b/src/Discord.Net.Core/Entities/IDeletable.cs @@ -1,7 +1,8 @@ -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. diff --git a/src/Discord.Net.Core/Entities/IMentionable.cs b/src/Discord.Net.Core/Entities/IMentionable.cs index abccc4480..87235fb6e 100644 --- a/src/Discord.Net.Core/Entities/IMentionable.cs +++ b/src/Discord.Net.Core/Entities/IMentionable.cs @@ -1,5 +1,6 @@ -namespace Discord +namespace Discord { + /// Represents whether the object is mentionable or not. public interface IMentionable { /// Returns a special string used to mention this object. diff --git a/src/Discord.Net.Core/Entities/ISnowflakeEntity.cs b/src/Discord.Net.Core/Entities/ISnowflakeEntity.cs index 5b099b5ac..2287c56f3 100644 --- a/src/Discord.Net.Core/Entities/ISnowflakeEntity.cs +++ b/src/Discord.Net.Core/Entities/ISnowflakeEntity.cs @@ -2,6 +2,7 @@ using System; namespace Discord { + /// Represents a Discord snowflake entity. public interface ISnowflakeEntity : IEntity { DateTimeOffset CreatedAt { get; } diff --git a/src/Discord.Net.Core/Entities/IUpdateable.cs b/src/Discord.Net.Core/Entities/IUpdateable.cs index b0f51aee7..303db25c0 100644 --- a/src/Discord.Net.Core/Entities/IUpdateable.cs +++ b/src/Discord.Net.Core/Entities/IUpdateable.cs @@ -1,7 +1,8 @@ -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. diff --git a/src/Discord.Net.Core/Entities/ImageFormat.cs b/src/Discord.Net.Core/Entities/ImageFormat.cs index 0b74a443e..e6f80110d 100644 --- a/src/Discord.Net.Core/Entities/ImageFormat.cs +++ b/src/Discord.Net.Core/Entities/ImageFormat.cs @@ -1,6 +1,6 @@ namespace Discord { - /// The type of format for the image to return. + /// Specifies the type of format the image should return in. public enum ImageFormat { Auto, diff --git a/src/Discord.Net.Core/Entities/Messages/Embed.cs b/src/Discord.Net.Core/Entities/Messages/Embed.cs index 5fae7acde..6ea7c0bd5 100644 --- a/src/Discord.Net.Core/Entities/Messages/Embed.cs +++ b/src/Discord.Net.Core/Entities/Messages/Embed.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; @@ -8,19 +8,32 @@ namespace Discord [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) diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs index c59473704..9d7cfdf18 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs @@ -1,14 +1,19 @@ -using System; +using System; using System.Diagnostics; namespace Discord { + /// An author field for 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) diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index baabe50c6..bab72fe1b 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -1,12 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; namespace Discord { - /// - /// Builder for creating an to be sent. - /// + /// A builder for creating an to be sent. public class EmbedBuilder { private readonly Embed _embed; diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs index f7c1f8348..2bc4ae436 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs @@ -1,12 +1,16 @@ -using System.Diagnostics; +using System.Diagnostics; namespace Discord { + /// 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 is inline inside an or not. public bool Inline { get; internal set; } internal EmbedField(string name, string value, bool inline) diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs index 29d85cd90..591c90642 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs @@ -1,13 +1,17 @@ -using System; +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..0c59f6407 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs @@ -1,14 +1,19 @@ -using System; +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..f536224b0 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs @@ -1,12 +1,15 @@ -using System; +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..6196b1342 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs @@ -1,14 +1,19 @@ -using System; +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..7bfc9ec3d 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedType.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedType.cs @@ -1,15 +1,25 @@ 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..1ee03b70c 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs @@ -1,13 +1,17 @@ -using System; +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 if there is any. public int? Height { get; } + /// Gets the weight of the video if there is any. public int? Width { get; } internal EmbedVideo(string url, int? height, int? width) diff --git a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs index 90b3f28ba..b1eb67b5b 100644 --- a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs +++ b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs @@ -1,22 +1,22 @@ namespace Discord { - /// The interface that defines an attachment object. + /// Represents a Discord attachment object. public interface IAttachment { - /// The snowflake ID of the attachment. + /// Gets the snowflake ID of the attachment. ulong Id { get; } - /// The filename of the attachment. + /// Gets the filename of the attachment. string Filename { get; } - /// The URL of the attachment. + /// Gets the URL of the attachment. string Url { get; } - /// The proxied URL of the attachment. + /// Gets the proxied URL of the attachment. string ProxyUrl { get; } - /// The file size of the attachment. + /// Gets the file size of the attachment. int Size { get; } - /// The height of the attachment if it is an image, or return when it is not. + /// Gets the height of the attachment if it is an image, or return when it is not. int? Height { get; } - /// The width of the attachment if it is an image, or return when it is not. + /// 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..77b43c25d 100644 --- a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs +++ b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs @@ -1,22 +1,36 @@ -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. DateTimeOffset? Timestamp { get; } + /// Gets the sidebar color of the embed. Color? Color { get; } + /// Gets the image of the embed. EmbedImage? Image { get; } + /// Gets the video of the embed. EmbedVideo? Video { get; } + /// Gets the author field of the embed. EmbedAuthor? Author { get; } + /// Gets the footer field of the embed. EmbedFooter? Footer { get; } + /// Gets the provider of the embed. EmbedProvider? Provider { get; } + /// Gets the thumbnail featured in the embed. 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..7d4a749f7 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -1,8 +1,9 @@ -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. diff --git a/src/Discord.Net.Core/Entities/Messages/IReaction.cs b/src/Discord.Net.Core/Entities/Messages/IReaction.cs index 37ead42ae..6325901f8 100644 --- a/src/Discord.Net.Core/Entities/Messages/IReaction.cs +++ b/src/Discord.Net.Core/Entities/Messages/IReaction.cs @@ -1,7 +1,9 @@ -namespace Discord +namespace Discord { + /// Represents a Discord reaction object. public interface IReaction { + /// The used in the reaction. IEmote Emote { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index 52df187f8..ab58ca008 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Discord { + /// A Discord message object. public interface IUserMessage : IMessage { /// Modifies this message. diff --git a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs index bb6dcbe0f..788ea25a5 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs @@ -1,7 +1,7 @@ namespace Discord { /// - /// Properties that are used to 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. diff --git a/src/Discord.Net.Core/Entities/Messages/MessageSource.cs b/src/Discord.Net.Core/Entities/Messages/MessageSource.cs index 1cb2f8b94..7dff168c4 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageSource.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageSource.cs @@ -1,10 +1,15 @@ 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..167f9fed3 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageType.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageType.cs @@ -1,13 +1,21 @@ -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..c34a34729 100644 --- a/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs +++ b/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs @@ -1,11 +1,12 @@ -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 true 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 3db506487..498aa5ef8 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs @@ -2,6 +2,7 @@ using System; namespace Discord { + /// Defines the available permissions for a channel. [FlagsAttribute] public enum ChannelPermission : ulong { diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs index 8469fd304..f94ff121e 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermission.cs @@ -1,7 +1,8 @@ -using System; +using System; namespace Discord { + /// Defines the available permissions for a channel. [FlagsAttribute] public enum GuildPermission : ulong { 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..ffd391e5a 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -3,6 +3,7 @@ using System.Diagnostics; namespace Discord { + /// A color object that Discord uses. [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct Color { diff --git a/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs index 0c8afa24c..852651beb 100644 --- a/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs +++ b/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs @@ -1,10 +1,11 @@ -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; } public ReorderRoleProperties(ulong id, int pos) diff --git a/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs index 8950a2634..82d57ffa9 100644 --- a/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs +++ b/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs @@ -1,7 +1,7 @@ -namespace Discord +namespace Discord { /// - /// Modify an IRole with the specified parameters + /// Properties that are used to modify an with the specified changes. /// /// /// @@ -16,39 +16,39 @@ 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. /// 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. /// 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. /// 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 userlist. /// /// /// 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. diff --git a/src/Discord.Net.Core/Entities/Users/IGroupUser.cs b/src/Discord.Net.Core/Entities/Users/IGroupUser.cs index dd046a5a8..f3d44b583 100644 --- a/src/Discord.Net.Core/Entities/Users/IGroupUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGroupUser.cs @@ -1,5 +1,6 @@ -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..97ef7d5c1 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -1,10 +1,10 @@ -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. diff --git a/src/Discord.Net.Core/Entities/Users/IPresence.cs b/src/Discord.Net.Core/Entities/Users/IPresence.cs index 25adcc9c4..9d3a5ecb9 100644 --- a/src/Discord.Net.Core/Entities/Users/IPresence.cs +++ b/src/Discord.Net.Core/Entities/Users/IPresence.cs @@ -1,5 +1,6 @@ -namespace Discord +namespace Discord { + /// Represents a Discord user's presence status. public interface IPresence { /// Gets the activity this user is currently doing. @@ -7,4 +8,4 @@ /// 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..a1078dd35 100644 --- a/src/Discord.Net.Core/Entities/Users/ISelfUser.cs +++ b/src/Discord.Net.Core/Entities/Users/ISelfUser.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; namespace Discord { + /// Represents a logged-in Discord user. public interface ISelfUser : IUser { /// Gets the email associated with this user. @@ -14,4 +15,4 @@ namespace Discord 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..6d4ddf6af 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; namespace Discord { + /// Represents a Discord user. public interface IUser : ISnowflakeEntity, IMentionable, IPresence { /// Gets the id of this user's avatar. diff --git a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs index be769b944..a29debf0f 100644 --- a/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IWebhookUser.cs @@ -1,5 +1,6 @@ -namespace Discord +namespace Discord { + /// Represents a Webhook Discord user. public interface IWebhookUser : IGuildUser { 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..9245a3616 100644 --- a/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs @@ -1,7 +1,7 @@ -namespace Discord +namespace Discord { /// - /// Modify the current user with the specified arguments + /// Properties that are used to modify the with the specified changes. /// /// /// diff --git a/src/Discord.Net.Core/Entities/Users/UserStatus.cs b/src/Discord.Net.Core/Entities/Users/UserStatus.cs index 74a52a0fa..f999bf068 100644 --- a/src/Discord.Net.Core/Entities/Users/UserStatus.cs +++ b/src/Discord.Net.Core/Entities/Users/UserStatus.cs @@ -1,12 +1,19 @@ -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/WebhookProperties.cs b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs index 8759a1729..8edec02cd 100644 --- a/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs +++ b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs @@ -1,7 +1,7 @@ -namespace Discord +namespace Discord { /// - /// Modify an with the specified parameters. + /// Properties used to modify an with the specified changes. /// /// /// @@ -16,22 +16,22 @@ 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 . /// 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 . diff --git a/src/Discord.Net.Core/Logging/LogSeverity.cs b/src/Discord.Net.Core/Logging/LogSeverity.cs index 98a822425..98677dbde 100644 --- a/src/Discord.Net.Core/Logging/LogSeverity.cs +++ b/src/Discord.Net.Core/Logging/LogSeverity.cs @@ -1,5 +1,6 @@ namespace Discord { + /// Specifies the severity of the log message. public enum LogSeverity { /// 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/TokenType.cs b/src/Discord.Net.Core/TokenType.cs index 62181420a..dc30c418a 100644 --- a/src/Discord.Net.Core/TokenType.cs +++ b/src/Discord.Net.Core/TokenType.cs @@ -2,6 +2,7 @@ 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)] From ded513c77b40b650236ec60dc657200366c9621b Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Sat, 24 Mar 2018 11:23:10 +0800 Subject: [PATCH 055/183] Add extension comments --- .../Extensions/DiscordClientExtensions.cs | 9 ++++++++- .../Extensions/EmbedBuilderExtensions.cs | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs b/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs index ff3c7caf7..03d736c6e 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 { + /// Extensions for . 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.Core/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs index 5ad8d8b1d..90eaf90ad 100644 --- a/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs +++ b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs @@ -2,6 +2,7 @@ using System; namespace Discord { + /// Extensions for building an embed. public static class EmbedBuilderExtensions { /// Adds embed color based on the provided raw value. From 6f4a4e069b1184cc879301a848fad9d1ef5a476b Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 26 Mar 2018 11:48:50 +0800 Subject: [PATCH 056/183] Add 'View Source' button + This change requires us to target the solution. + Due to docfx issues, we must specify a upon metadata compilation. - Because of this, docfx may complain about missing targets. - If we don't target a specific TargetFramework, docfx will fail to recognize documents with multiple framework targets. - Further investigation may be required. --- docs/docfx.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/docfx.json b/docs/docfx.json index 901357ae3..a41db4fb6 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -5,17 +5,19 @@ { "src": "..", "files": [ - "src/**/*.cs" + "*.sln" ], "exclude": [ "**/obj/**", - "**/bin/**", - "_site/**" + "**/bin/**" ] } ], "dest": "api", - "filter": "filterConfig.yml" + "filter": "filterConfig.yml", + "properties":{ + "TargetFramework": "netstandard1.1" + } } ], "build": { From d67f96d03b026ed1ef8f58aa3cc34abc64af9c82 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 26 Mar 2018 14:28:53 +0800 Subject: [PATCH 057/183] Target individual projects instead of solution + This gets rid of unnecessary compilation for the test project. --- docs/docfx.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/docfx.json b/docs/docfx.json index a41db4fb6..03de34ee6 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -3,13 +3,13 @@ { "src": [ { - "src": "..", + "src": "../src", "files": [ - "*.sln" + "**.csproj" ], "exclude": [ - "**/obj/**", - "**/bin/**" + "**/bin/**", + "**/obj/**" ] } ], From 70712969e78c5d75fb60e860d3b80daf56c8d0bd Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Mon, 26 Mar 2018 16:30:18 +0800 Subject: [PATCH 058/183] Add workaround for merging framework targets --- docs/docfx.json | 184 +++++++++++++++++++++++++++++++----------------- 1 file changed, 121 insertions(+), 63 deletions(-) diff --git a/docs/docfx.json b/docs/docfx.json index 03de34ee6..d76ee7613 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -1,69 +1,127 @@ { - "metadata": [ - { - "src": [ - { - "src": "../src", - "files": [ - "**.csproj" - ], - "exclude": [ - "**/bin/**", - "**/obj/**" - ] - } - ], - "dest": "api", - "filter": "filterConfig.yml", - "properties":{ - "TargetFramework": "netstandard1.1" - } - } - ], - "build": { - "content": [ + "metadata":[ { - "files": [ - "api/**.yml", - "api/index.md" - ] + "src":[ + { + "src":"../src", + "files":[ + "**.csproj" + ] + } + ], + "dest":"temp/api/netstandard1.1", + "filter":"filterConfig.yml", + "properties":{ + "TargetFramework":"netstandard1.1" + } }, { - "files": [ - "guides/**.md", - "guides/**/toc.yml", - "faq/**.md", - "faq/**/toc.yml", - "toc.yml", - "*.md" - ], - "exclude": [ - "obj/**", - "_site/**" - ] - } - ], - "resource": [ + "src":[ + { + "src":"../src", + "files":[ + "**.csproj" + ] + } + ], + "dest":"temp/api/netstandard1.3", + "filter":"filterConfig.yml", + "properties":{ + "TargetFramework":"netstandard1.3" + } + }, { - "files": [ - "**/images/**", - "**/samples/**" - ], - "exclude": [ - "obj/**", - "_site/**" - ] + "src":[ + { + "src":"../src", + "files":[ + "**.csproj" + ] + } + ], + "dest":"temp/api/net45", + "filter":"filterConfig.yml", + "properties":{ + "TargetFramework":"net45" + } } - ], - "dest": "_site", - "template": [ - "default" - ], - "globalMetadata": { - "_appFooter": "Discord.Net (c) 2015-2018", - "_enableSearch": true - }, - "noLangKeyword": false, - "xrefService": [ "https://xref.docs.microsoft.com/query?uid={uid}" ] - } -} + ], + "merge":{ + "content":[ + { + "files":"*.yml", + "src":"temp/api/netstandard1.1" + }, + { + "files":"*.yml", + "src":"temp/api/netstandard1.3" + }, + { + "files":"*.yml", + "src":"temp/api/net45" + } + ], + "fileMetadata":{ + "platform":{ + "temp/api/netstandard1.3/*.yml":[ + "netstandard1.3" + ], + "temp/api/netstandard1.1/*.yml":[ + "netstandard1.1" + ], + "temp/api/net46/*.yml":[ + "net46" + ] + } + }, + "dest":"api" + }, + "build":{ + "content":[ + { + "files":[ + "api/**.yml", + "api/index.md" + ] + }, + { + "files":[ + "guides/**.md", + "guides/**/toc.yml", + "faq/**.md", + "faq/**/toc.yml", + "toc.yml", + "*.md" + ], + "exclude":[ + "obj/**", + "_site/**" + ] + } + ], + "resource":[ + { + "files":[ + "**/images/**", + "**/samples/**" + ], + "exclude":[ + "obj/**", + "_site/**" + ] + } + ], + "dest":"_site", + "template":[ + "default" + ], + "globalMetadata":{ + "_appFooter":"Discord.Net (c) 2015-2018", + "_enableSearch":true + }, + "noLangKeyword":false, + "xrefService":[ + "https://xref.docs.microsoft.com/query?uid={uid}" + ] + } +} \ No newline at end of file From 5300534b201121185e4f7a178524f1367927d1a9 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 27 Mar 2018 06:07:00 +0800 Subject: [PATCH 059/183] Fix typo --- docs/docfx.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docfx.json b/docs/docfx.json index d76ee7613..922e8a8cc 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -69,8 +69,8 @@ "temp/api/netstandard1.1/*.yml":[ "netstandard1.1" ], - "temp/api/net46/*.yml":[ - "net46" + "temp/api/net45/*.yml":[ + "net45" ] } }, From a7a4efb70843999cb0d7ea490acbb6538b255679 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 27 Mar 2018 06:17:59 +0800 Subject: [PATCH 060/183] Rollback to target NS1.1 only --- docs/docfx.json | 62 +------------------------------------------------ 1 file changed, 1 insertion(+), 61 deletions(-) diff --git a/docs/docfx.json b/docs/docfx.json index 922e8a8cc..8219d8bd6 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -9,73 +9,13 @@ ] } ], - "dest":"temp/api/netstandard1.1", + "dest":"api", "filter":"filterConfig.yml", "properties":{ "TargetFramework":"netstandard1.1" } - }, - { - "src":[ - { - "src":"../src", - "files":[ - "**.csproj" - ] - } - ], - "dest":"temp/api/netstandard1.3", - "filter":"filterConfig.yml", - "properties":{ - "TargetFramework":"netstandard1.3" - } - }, - { - "src":[ - { - "src":"../src", - "files":[ - "**.csproj" - ] - } - ], - "dest":"temp/api/net45", - "filter":"filterConfig.yml", - "properties":{ - "TargetFramework":"net45" - } } ], - "merge":{ - "content":[ - { - "files":"*.yml", - "src":"temp/api/netstandard1.1" - }, - { - "files":"*.yml", - "src":"temp/api/netstandard1.3" - }, - { - "files":"*.yml", - "src":"temp/api/net45" - } - ], - "fileMetadata":{ - "platform":{ - "temp/api/netstandard1.3/*.yml":[ - "netstandard1.3" - ], - "temp/api/netstandard1.1/*.yml":[ - "netstandard1.1" - ], - "temp/api/net45/*.yml":[ - "net45" - ] - } - }, - "dest":"api" - }, "build":{ "content":[ { From 9483bc8eeaf593e3a6eeddaf32a58252ebc8ea07 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 27 Mar 2018 06:30:10 +0800 Subject: [PATCH 061/183] Target NS1.3 --- docs/docfx.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docfx.json b/docs/docfx.json index 8219d8bd6..8dc0abd38 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -12,7 +12,7 @@ "dest":"api", "filter":"filterConfig.yml", "properties":{ - "TargetFramework":"netstandard1.1" + "TargetFramework":"netstandard1.3" } } ], From 897676c5b0034ee97beab719a31d0ddc2d5e40ab Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 27 Mar 2018 06:30:45 +0800 Subject: [PATCH 062/183] Add XMLDocs for some channel entities --- src/Discord.Net.Core/Entities/Channels/IChannel.cs | 3 ++- src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs | 5 +++-- .../Entities/Channels/SocketTextChannel.cs | 8 ++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IChannel.cs b/src/Discord.Net.Core/Entities/Channels/IChannel.cs index ea930e112..ce1d859a9 100644 --- a/src/Discord.Net.Core/Entities/Channels/IChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IChannel.cs @@ -1,8 +1,9 @@ -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. diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index c9841cb15..e502d2287 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; namespace Discord { + /// Represents a guild channel. This includes a text channel, voice channel, and category channel. public interface IGuildChannel : IChannel, IDeletable { /// Gets the position of this channel in the guild's channel list, relative to others of the same type. @@ -49,4 +50,4 @@ namespace Discord /// 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.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index ec7615b55..8b5e9bb17 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) @@ -100,6 +107,7 @@ namespace Discord.WebSocket => _messages?.Remove(id); //Users + /// public override SocketGuildUser GetUser(ulong id) { var user = Guild.GetUser(id); From 0c11481ac32f97e05bd18a6e4ea5a39832b5bcc3 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 27 Mar 2018 08:40:09 +0800 Subject: [PATCH 063/183] Replace True keyword with langword xmldocs --- .../Permissions/ChannelPermissions.cs | 44 ++++++------ .../Entities/Permissions/GuildPermissions.cs | 72 +++++++++---------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index b80498307..9ee1e0860 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -36,55 +36,55 @@ namespace Discord /// 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, 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, From 3a10820c844a41675bede4d7bd47fada389eb683 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 27 Mar 2018 17:30:32 +0800 Subject: [PATCH 064/183] Add XMLDocs + This commit also adds overwrites for CommandContexts; this allows for additional remarks for the class. --- .../Commands/CommandContext.Overwrite.md | 9 +++++++++ .../Commands/SocketCommandContext.Overwrite.md | 9 +++++++++ docs/docfx.json | 1 + .../Attributes/AliasAttribute.cs | 2 +- .../Attributes/CommandAttribute.cs | 2 +- .../Attributes/DontAutoLoadAttribute.cs | 2 +- .../Attributes/DontInjectAttribute.cs | 2 +- .../Attributes/GroupAttribute.cs | 4 ++++ .../Attributes/NameAttribute.cs | 1 + .../Attributes/OverrideTypeReaderAttribute.cs | 4 ++++ .../Attributes/ParameterPreconditionAttribute.cs | 1 + .../Attributes/PreconditionAttribute.cs | 14 ++++++++------ .../Attributes/RemainderAttribute.cs | 1 + src/Discord.Net.Commands/CommandContext.cs | 1 + src/Discord.Net.Commands/CommandException.cs | 1 + src/Discord.Net.Core/Commands/ICommandContext.cs | 11 ++++++----- .../Commands/SocketCommandContext.cs | 15 ++++++++++++++- 17 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 docs/_overwrites/Commands/CommandContext.Overwrite.md create mode 100644 docs/_overwrites/Commands/SocketCommandContext.Overwrite.md diff --git a/docs/_overwrites/Commands/CommandContext.Overwrite.md b/docs/_overwrites/Commands/CommandContext.Overwrite.md new file mode 100644 index 000000000..fdde3b2e5 --- /dev/null +++ b/docs/_overwrites/Commands/CommandContext.Overwrite.md @@ -0,0 +1,9 @@ +--- +uid: Discord.Commands.CommandContext +--- + +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/SocketCommandContext.Overwrite.md b/docs/_overwrites/Commands/SocketCommandContext.Overwrite.md new file mode 100644 index 000000000..233418201 --- /dev/null +++ b/docs/_overwrites/Commands/SocketCommandContext.Overwrite.md @@ -0,0 +1,9 @@ +--- +uid: Discord.Commands.SocketCommandContext +--- + +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/docfx.json b/docs/docfx.json index a1c1ef2f6..cc126b0e1 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -55,6 +55,7 @@ "template":[ "default" ], + "overwrite": "_overwrites/**/**.md", "globalMetadata":{ "_appFooter":"Discord.Net (c) 2015-2018 2.0.0-beta", "_enableSearch":true diff --git a/src/Discord.Net.Commands/Attributes/AliasAttribute.cs b/src/Discord.Net.Commands/Attributes/AliasAttribute.cs index 6cd0abbb7..085a1f307 100644 --- a/src/Discord.Net.Commands/Attributes/AliasAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/AliasAttribute.cs @@ -2,7 +2,7 @@ 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 { diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs index eeb43bad3..db1f3067b 100644 --- a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -2,7 +2,7 @@ using System; namespace Discord.Commands { - /// Provides the execution information for a command. + /// Marks the execution information for a command. [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class CommandAttribute : Attribute { diff --git a/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs b/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs index 22664afd1..451b87f5e 100644 --- a/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs @@ -2,7 +2,7 @@ using System; namespace Discord.Commands { - /// Prevents the module from being loaded automatically. + /// 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 354b87364..b0823693e 100644 --- a/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs @@ -2,7 +2,7 @@ using System; namespace Discord.Commands { - /// Prevents the property from being injected into a module. + /// 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..d2cc6165f 100644 --- a/src/Discord.Net.Commands/Attributes/GroupAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs @@ -2,15 +2,19 @@ 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; } + /// Creates a 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..b52a2fe71 100644 --- a/src/Discord.Net.Commands/Attributes/NameAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/NameAttribute.cs @@ -3,6 +3,7 @@ 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 { diff --git a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs index 44ab6d214..67d61c8d3 100644 --- a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs @@ -4,13 +4,17 @@ 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(); + /// Gets the specified of the parameter. public Type TypeReader { get; } + /// Marks the parameter to be read with the specified . + /// The to be used with the parameter. public OverrideTypeReaderAttribute(Type overridenTypeReader) { if (!_typeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo())) diff --git a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs index 3c5e8cf92..e6d244b8f 100644 --- a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs @@ -3,6 +3,7 @@ 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 { diff --git a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs index 367adebf0..6b6bd8b6e 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Commands @@ -6,11 +6,13 @@ namespace Discord.Commands [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. + /// + /// 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). + /// public string Group { get; set; } = null; public abstract Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services); diff --git a/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs b/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs index 56938f167..fd1d93d9f 100644 --- a/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/RemainderAttribute.cs @@ -2,6 +2,7 @@ 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/CommandContext.cs b/src/Discord.Net.Commands/CommandContext.cs index d884d8b29..9e0766a68 100644 --- a/src/Discord.Net.Commands/CommandContext.cs +++ b/src/Discord.Net.Commands/CommandContext.cs @@ -1,5 +1,6 @@ namespace Discord.Commands { + /// The context of a command which may contain the client, user, guild, channel, and message. public class CommandContext : ICommandContext { /// diff --git a/src/Discord.Net.Commands/CommandException.cs b/src/Discord.Net.Commands/CommandException.cs index 1584641b3..335033258 100644 --- a/src/Discord.Net.Commands/CommandException.cs +++ b/src/Discord.Net.Commands/CommandException.cs @@ -2,6 +2,7 @@ using System; namespace Discord.Commands { + /// An exception thrown when a command fails to execute. public class CommandException : Exception { /// The command that caused the exception. diff --git a/src/Discord.Net.Core/Commands/ICommandContext.cs b/src/Discord.Net.Core/Commands/ICommandContext.cs index e0ea18ca8..2df18c748 100644 --- a/src/Discord.Net.Core/Commands/ICommandContext.cs +++ b/src/Discord.Net.Core/Commands/ICommandContext.cs @@ -1,16 +1,17 @@ namespace Discord.Commands { + /// Represents the context of a command. This may include the client, guild, channel, user, and message. public interface ICommandContext { - /// The Discord client of which the command is executed with. + /// Gets the that the command is executed with. IDiscordClient Client { get; } - /// The guild of which the command is executed in. + /// Gets the that the command is executed in. IGuild Guild { get; } - /// The channel of which the command is executed in. + /// Gets the that the command is executed in. IMessageChannel Channel { get; } - /// The user who executed the command. + /// Gets the who executed the command. IUser User { get; } - /// The message of which the command is interpreted from. + /// Gets the that the command is interpreted from. IUserMessage Message { get; } } } diff --git a/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs index c8b0747e7..bac62e81b 100644 --- a/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs +++ b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs @@ -1,15 +1,23 @@ -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 +30,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; } } From 1d2db3a61cae77318bf5abd1dbcf005a15fb9c80 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Wed, 28 Mar 2018 02:47:38 +0800 Subject: [PATCH 065/183] Add XMLDocs --- .../Preconditions/RequireContextAttribute.cs | 11 ++-- src/Discord.Net.Commands/Results/IResult.cs | 1 + .../Entities/Activities/Game.cs | 4 ++ .../Entities/Messages/EmbedBuilder.cs | 55 +++++++++++++++++++ .../Entities/Messages/IAttachment.cs | 2 +- .../Entities/Messages/Attachment.cs | 14 ++--- .../Commands/ShardedCommandContext.cs | 6 +- .../Commands/SocketCommandContext.cs | 1 - 8 files changed, 80 insertions(+), 14 deletions(-) diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs index bf5d081ce..c66cd7c12 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -4,25 +4,28 @@ using Microsoft.Extensions.DependencyInjection; namespace Discord.Commands { + /// Defines the type of command context. [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 } /// - /// Requires the command to be invoked in a specified context. (e.g. in guild, DM) + /// 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 { + /// Gets the context required to execute the command. public ContextType Contexts { get; } - /// - /// Requires that the command be invoked in the specified context. - /// + /// Requires that the command 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/Results/IResult.cs b/src/Discord.Net.Commands/Results/IResult.cs index cc5d4c3be..d5b9ffe0f 100644 --- a/src/Discord.Net.Commands/Results/IResult.cs +++ b/src/Discord.Net.Commands/Results/IResult.cs @@ -1,5 +1,6 @@ namespace Discord.Commands { + /// Represents the information of command execution result. public interface IResult { /// Describes the error type that may have occurred during the operation. diff --git a/src/Discord.Net.Core/Entities/Activities/Game.cs b/src/Discord.Net.Core/Entities/Activities/Game.cs index 5c5bd98b5..c75d96969 100644 --- a/src/Discord.Net.Core/Entities/Activities/Game.cs +++ b/src/Discord.Net.Core/Entities/Activities/Game.cs @@ -12,12 +12,16 @@ namespace Discord public ActivityType Type { get; internal set; } internal Game() { } + /// Creates a with the provided name 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/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index bab72fe1b..1fa8c85f9 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -9,17 +9,23 @@ namespace Discord { private readonly Embed _embed; + /// The maximum number of fields allowed by Discord. public const int MaxFieldCount = 25; + /// The maximum length of title allowed by Discord. public const int MaxTitleLength = 256; + /// The maximum length of description allowed by Discord. public const int MaxDescriptionLength = 2048; + /// The maximum length of total characters allowed by Discord. public const int MaxEmbedLength = 6000; // user bot limit is 2000, but we don't validate that here. + /// Creates a new . public EmbedBuilder() { _embed = new Embed(EmbedType.Rich); Fields = new List(); } + /// Gets or sets the title of an . public string Title { get => _embed.Title; @@ -30,6 +36,7 @@ namespace Discord } } + /// Gets or sets the description of an . public string Description { get => _embed.Description; @@ -40,6 +47,7 @@ namespace Discord } } + /// Gets or sets the URL of an . public string Url { get => _embed.Url; @@ -49,6 +57,7 @@ namespace Discord _embed.Url = value; } } + /// Gets or sets the thumbnail URL of an . public string ThumbnailUrl { get => _embed.Thumbnail?.Url; @@ -58,6 +67,7 @@ namespace Discord _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); } } + /// Gets or sets the image URL of an . public string ImageUrl { get => _embed.Image?.Url; @@ -67,12 +77,17 @@ namespace Discord _embed.Image = new EmbedImage(value, null, null, null); } } + /// Gets or sets the timestamp of an . public DateTimeOffset? Timestamp { get => _embed.Timestamp; set { _embed.Timestamp = value; } } + /// Gets or sets the sidebar color of an . public Color? Color { get => _embed.Color; set { _embed.Color = value; } } + /// Gets or sets the of an . public EmbedAuthorBuilder Author { get; set; } + /// Gets or sets the of an . public EmbedFooterBuilder Footer { get; set; } private List _fields; + /// Gets or sets the list of of an . public List Fields { get => _fields; @@ -85,52 +100,71 @@ namespace Discord } } + /// 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 delegate containing the author field properties. public EmbedBuilder WithAuthor(Action action) { var author = new EmbedAuthorBuilder(); @@ -138,6 +172,10 @@ namespace Discord 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 @@ -149,11 +187,15 @@ namespace Discord 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 delegate containing the footer field properties. public EmbedBuilder WithFooter(Action action) { var footer = new EmbedFooterBuilder(); @@ -161,6 +203,9 @@ namespace Discord 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 @@ -172,6 +217,10 @@ namespace Discord 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() @@ -182,6 +231,8 @@ namespace Discord 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) @@ -192,6 +243,8 @@ namespace Discord Fields.Add(field); return this; } + /// Adds an field with the provided properties. + /// The delegate containing the field properties. public EmbedBuilder AddField(Action action) { var field = new EmbedFieldBuilder(); @@ -200,6 +253,8 @@ namespace Discord return this; } + /// Builds the into a Rich Embed format. + /// The built embed object. public Embed Build() { _embed.Footer = Footer?.Build(); diff --git a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs index b1eb67b5b..408153df1 100644 --- a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs +++ b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs @@ -1,6 +1,6 @@ namespace Discord { - /// Represents a Discord attachment object. + /// Represents a Discord attachment. public interface IAttachment { /// Gets the snowflake ID of the attachment. diff --git a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs index f33380ce3..a51ac8e09 100644 --- a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs +++ b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs @@ -7,19 +7,19 @@ namespace Discord [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) 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 bac62e81b..29d78ab45 100644 --- a/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs +++ b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs @@ -3,7 +3,6 @@ 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. From 91ff88e2eec5ba92ca3d82dbc0bad75978e8ef94 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Wed, 28 Mar 2018 02:48:46 +0800 Subject: [PATCH 066/183] Fix DI module example --- docs/guides/commands/samples/dependency_module.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guides/commands/samples/dependency_module.cs b/docs/guides/commands/samples/dependency_module.cs index 24009cc41..2014ed881 100644 --- a/docs/guides/commands/samples/dependency_module.cs +++ b/docs/guides/commands/samples/dependency_module.cs @@ -21,16 +21,16 @@ public class ModuleA : ModuleBase public class ModuleB : ModuleBase { - - // Public settable properties will be injected + // Public settable properties will be injected. public AnnounceService Announce { get; set; } - // Public properties without setters will not + // Public properties without setters will not be injected. public CommandService Commands { get; } // Public properties annotated with [DontInject] will not + // be injected. [DontInject] - public NotificationService { get; set; } + public NotificationService NotificationService { get; set; } public ModuleB(CommandService commands) { From f81af0bdd14f4eec9f92116bbbcb373665a34527 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Wed, 28 Mar 2018 02:51:16 +0800 Subject: [PATCH 067/183] Add Overwrites files for some classes + Starting from this commit, there will be overwrite files added to provide further details about certain APIs. --- .../Commands/CommandException.Overwrite.md | 27 ++++++++++++++++++ .../DontAutoLoadAttribute.Overwrite.md | 20 +++++++++++++ .../Commands/DontInjectAttribute.Overwrite.md | 26 +++++++++++++++++ .../ShardedCommandContext.Overwrite.md | 10 +++++++ .../Common/Discord.EmbedBuilder.Overwrites.md | 28 +++++++++++++++++++ 5 files changed, 111 insertions(+) create mode 100644 docs/_overwrites/Commands/CommandException.Overwrite.md create mode 100644 docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md create mode 100644 docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md create mode 100644 docs/_overwrites/Commands/ShardedCommandContext.Overwrite.md create mode 100644 docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md diff --git a/docs/_overwrites/Commands/CommandException.Overwrite.md b/docs/_overwrites/Commands/CommandException.Overwrite.md new file mode 100644 index 000000000..a5a713bcb --- /dev/null +++ b/docs/_overwrites/Commands/CommandException.Overwrite.md @@ -0,0 +1,27 @@ +--- +uid: Discord.Commands.CommandException +--- + +### Remarks + +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. + +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..857e05cf8 --- /dev/null +++ b/docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md @@ -0,0 +1,20 @@ +--- +uid: Discord.Commands.DontAutoLoadAttribute +--- + +### Remarks + +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. + +### Example + +```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..ed59c6ced --- /dev/null +++ b/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md @@ -0,0 +1,26 @@ +--- +uid: Discord.Commands.DontInjectAttribute +--- + +### Remarks + +The attribute can be applied to a public settable property inside a +@Discord.Commands.ModuleBase based class. By applying this property, +the marked property will not be automatically injected of the +dependency. See [Dependency Injection](../../guides/commands/commands.md#dependency-injection) +to learn more. + +### Example + +```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/ShardedCommandContext.Overwrite.md b/docs/_overwrites/Commands/ShardedCommandContext.Overwrite.md new file mode 100644 index 000000000..f3c5bacbc --- /dev/null +++ b/docs/_overwrites/Commands/ShardedCommandContext.Overwrite.md @@ -0,0 +1,10 @@ +--- +uid: Discord.Commands.ShardedCommandContext +--- + +### Example +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/Common/Discord.EmbedBuilder.Overwrites.md b/docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md new file mode 100644 index 000000000..1a7fcd9bb --- /dev/null +++ b/docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md @@ -0,0 +1,28 @@ +--- +uid: Discord.EmbedBuilder +--- + +### Remarks + +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. + +### Example + +```cs +public async Task SendRichEmbedAsync() +{ + var embed = new EmbedBuilder + { + // Embed property can be set within object initializer + Title = "Hello world!" + } + // Or with the method + .WithTitle("I overwrote the title.") + .WithDescription("I am a description.") + .WithUrl("https://example.com") + .Build(); + await _channel.SendMessageAsync(string.Empty, embed: embed); +} +``` \ No newline at end of file From 9f536177fa2d18f93c10b0b32e150ae0b0712b23 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Wed, 28 Mar 2018 14:58:41 +0800 Subject: [PATCH 068/183] Add XMLDocs --- .../Attributes/GroupAttribute.cs | 2 +- .../Attributes/NameAttribute.cs | 3 + .../Attributes/PreconditionAttribute.cs | 1 + src/Discord.Net.Commands/CommandException.cs | 2 +- .../Entities/Guilds/IGuild.cs | 74 ++++++++++++++----- .../Entities/Guilds/SocketGuild.cs | 72 ++++++++++++++++++ 6 files changed, 132 insertions(+), 22 deletions(-) diff --git a/src/Discord.Net.Commands/Attributes/GroupAttribute.cs b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs index d2cc6165f..41bf3e261 100644 --- a/src/Discord.Net.Commands/Attributes/GroupAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/GroupAttribute.cs @@ -14,7 +14,7 @@ namespace Discord.Commands Prefix = null; } /// Creates a with the provided prefix. - /// The prefix of the module group. + /// 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 b52a2fe71..aba44370b 100644 --- a/src/Discord.Net.Commands/Attributes/NameAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/NameAttribute.cs @@ -7,8 +7,11 @@ namespace Discord.Commands [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/PreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs index 6b6bd8b6e..772027f55 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -3,6 +3,7 @@ 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 { diff --git a/src/Discord.Net.Commands/CommandException.cs b/src/Discord.Net.Commands/CommandException.cs index 335033258..c44bb2a01 100644 --- a/src/Discord.Net.Commands/CommandException.cs +++ b/src/Discord.Net.Commands/CommandException.cs @@ -2,7 +2,7 @@ using System; namespace Discord.Commands { - /// An exception thrown when a command fails to execute. + /// Thrown when a command fails to execute. public class CommandException : Exception { /// The command that caused the exception. diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 2f0599d76..8ac539ef7 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -1,4 +1,4 @@ -using Discord.Audio; +using Discord.Audio; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -15,34 +15,34 @@ namespace Discord bool IsEmbeddable { get; } /// 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. 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 null if one is not set. string IconId { get; } /// Returns the url to this guild's icon, or null 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 null if one is not set. string SplashId { get; } /// Returns the url to this guild's splash image, or null 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. 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 null 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 null 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, or null 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. IRole EveryoneRole { get; } @@ -67,34 +67,53 @@ namespace Discord /// 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] + /// 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(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. + /// 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. 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. 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 null 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 null 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 null 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 null 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 null 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 null 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. + /// The new name for the text channel. Task CreateTextChannelAsync(string name, RequestOptions options = null); /// Creates a new voice channel. + /// The new name for the voice channel. Task CreateVoiceChannelAsync(string name, RequestOptions options = null); /// Creates a new channel category. + /// The new name for the category. Task CreateCategoryAsync(string name, RequestOptions options = null); Task> GetIntegrationsAsync(RequestOptions options = null); @@ -103,14 +122,20 @@ namespace Discord /// 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 null 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. 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 null if not found. + /// The user ID. Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// Gets the current user for this guild. Task GetCurrentUserAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); @@ -119,20 +144,29 @@ namespace Discord /// 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. + /// 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 null if not found. + /// The webhook ID. Task GetWebhookAsync(ulong id, RequestOptions options = null); /// Gets a collection of all webhooks for this guild. Task> GetWebhooksAsync(RequestOptions options = null); /// 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. + /// 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. Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null); /// Deletes an existing emote 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.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); } From 3cf3811bce1a7846234f9a844089f2d9bfa7e581 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Thu, 29 Mar 2018 01:12:44 +0800 Subject: [PATCH 069/183] Add XMLDocs --- .../Entities/Channels/IGuildChannel.cs | 4 +-- .../Channels/ReorderChannelProperties.cs | 10 +++++-- .../Entities/Users/IGuildUser.cs | 6 ++++ .../Entities/Users/IVoiceState.cs | 12 ++++---- .../Extensions/DiscordClientExtensions.cs | 2 +- .../Extensions/EmbedBuilderExtensions.cs | 2 +- .../Extensions/UserExtensions.cs | 18 +++++------ .../Entities/Users/RestGroupUser.cs | 11 +++++-- .../Entities/Users/RestGuildUser.cs | 13 +++++++- .../Entities/Users/RestWebhookUser.cs | 26 ++++++++++++++-- .../Entities/Channels/SocketGuildChannel.cs | 24 ++++++++++++++- .../Entities/Users/SocketGroupUser.cs | 14 ++++++++- .../Entities/Users/SocketGuildUser.cs | 30 ++++++++++++++++--- .../Entities/Users/SocketUser.cs | 18 +++++++++-- 14 files changed, 154 insertions(+), 36 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index e502d2287..c66662ef1 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -4,13 +4,13 @@ using System.Threading.Tasks; namespace Discord { - /// Represents a guild channel. This includes a text channel, voice channel, and category channel. + /// 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. 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. Task GetCategoryAsync(); diff --git a/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs index 31f814334..62d35a15f 100644 --- a/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs @@ -1,12 +1,16 @@ -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/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 97ef7d5c1..c6c9fabd5 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -22,20 +22,26 @@ namespace Discord IReadOnlyCollection RoleIds { get; } /// 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. + /// 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. Task ModifyAsync(Action func, RequestOptions options = null); /// Adds a role 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. + /// The roles to be added to the user. Task AddRolesAsync(IEnumerable roles, RequestOptions options = null); /// Removes a role 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. + /// The roles to be removed from the user. Task RemoveRolesAsync(IEnumerable roles, 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..fc7855975 100644 --- a/src/Discord.Net.Core/Entities/Users/IVoiceState.cs +++ b/src/Discord.Net.Core/Entities/Users/IVoiceState.cs @@ -1,16 +1,16 @@ -namespace Discord +namespace Discord { 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. IVoiceChannel VoiceChannel { get; } diff --git a/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs b/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs index 03d736c6e..c38fa8e00 100644 --- a/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs +++ b/src/Discord.Net.Core/Extensions/DiscordClientExtensions.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord { - /// Extensions for . + /// An extension class for the Discord client. public static class DiscordClientExtensions { /// Gets the private channel with the provided ID. diff --git a/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs index 90eaf90ad..b650aa401 100644 --- a/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs +++ b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs @@ -2,7 +2,7 @@ using System; namespace Discord { - /// Extensions for building an embed. + /// An extension class for building an embed. public static class EmbedBuilderExtensions { /// Adds embed color based on the provided raw value. diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index d3e968e39..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,7 +41,10 @@ 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.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/RestWebhookUser.cs b/src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs index bb44f2777..5070d1b6e 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,63 @@ 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) { throw new NotSupportedException("Webhook users cannot be kicked."); } + /// Task IGuildUser.ModifyAsync(Action func, RequestOptions options) { throw new NotSupportedException("Webhook users cannot be modified."); } + /// Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) { throw new NotSupportedException("Roles are not supported on webhook users."); } + /// Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) { throw new NotSupportedException("Roles are not supported on webhook users."); } + /// Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) { throw new NotSupportedException("Roles are not supported on webhook users."); } + /// 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/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/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..5c255f3a0 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) @@ -53,14 +64,17 @@ namespace Discord.WebSocket 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); + /// Gets the username and the discriminator. public override string ToString() => $"{Username}#{Discriminator}"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; internal SocketUser Clone() => MemberwiseClone() as SocketUser; From 0dcb16119cac693c0fb9daace0c4c6064f21baef Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Thu, 29 Mar 2018 01:15:49 +0800 Subject: [PATCH 070/183] Cleanup FAQ trailing spaces --- docs/faq/Commands.md | 100 +++++++++++++++++------------------ docs/faq/Glossary.md | 18 +++---- docs/faq/Legacy.md | 12 ++--- docs/faq/basic-operations.md | 48 ++++++++--------- docs/faq/client-basics.md | 47 ++++++++-------- docs/faq/getting-started.md | 85 +++++++++++++++-------------- 6 files changed, 157 insertions(+), 153 deletions(-) diff --git a/docs/faq/Commands.md b/docs/faq/Commands.md index 7e8b23d7a..bc902811b 100644 --- a/docs/faq/Commands.md +++ b/docs/faq/Commands.md @@ -2,17 +2,17 @@ ## 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 +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 @@ -20,8 +20,8 @@ custom 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 +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* @@ -29,10 +29,10 @@ via the assembly, while the latter is used to add a single module. ## 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 [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/commands/Remainder.cs)] @@ -41,17 +41,17 @@ the last parameter. ## 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 +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](../guides/commands/commands.md#usage-in-modules). A brief example of service and dependency injection can be seen below. @@ -63,22 +63,22 @@ A brief example of service and dependency injection can be seen below. ## 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. By default, 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**. +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. By default, 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]. +To resolve this, the library has designed a flag called [RunMode]. There are 2 main `RunMode`s. - 1. `RunMode.Sync` (default) + 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 `CommandAttribute`, or by setting the global default with the [DefaultRunMode] flag under `CommandServiceConfig`. # [CommandAttribute](#tab/cmdattrib) @@ -94,10 +94,10 @@ the [DefaultRunMode] flag under `CommandServiceConfig`. *** > [!IMPORTANT] -> While specifying `RunMode.Async` allows the command to be spun off +> 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 +> 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. @@ -108,28 +108,28 @@ the [DefaultRunMode] flag under `CommandServiceConfig`. ## 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 +`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 execution. 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 + 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 +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 +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 diff --git a/docs/faq/Glossary.md b/docs/faq/Glossary.md index 3a5198bb1..096bcc0ab 100644 --- a/docs/faq/Glossary.md +++ b/docs/faq/Glossary.md @@ -2,7 +2,7 @@ ## Common Types -* A **Guild** ([IGuild]) is an isolated collection of users and +* 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 either a voice or text channel. @@ -14,11 +14,11 @@ channels, and are often referred to as "servers". ## Channel Types ### Message Channels -* A **Text channel** ([ITextChannel]) is a message channel from a +* 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. +* 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. @@ -27,7 +27,7 @@ Group. * 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 +* A **Category channel** ([ICategoryChannel]) (2.0+) is a category that holds one or more sub-channels. [IGuildChannel]: xref:Discord.IGuildChannel @@ -52,12 +52,12 @@ holds one or more sub-channels. ## Activity Types * A **Game** ([Game]) refers to a user's game activity. -* A **Rich Presence** ([RichGame]) refers to a user's detailed -gameplay status. +* 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 +* 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 +* A **Spotify Status** ([SpotifyGame]) (2.0+) refers to a user's activity for listening to a song on Spotify. [Game]: xref:Discord.Game diff --git a/docs/faq/Legacy.md b/docs/faq/Legacy.md index 61d1b3d5a..925ee45b4 100644 --- a/docs/faq/Legacy.md +++ b/docs/faq/Legacy.md @@ -1,10 +1,10 @@ # 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 +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. @@ -12,8 +12,8 @@ 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 +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/basic-operations.md b/docs/faq/basic-operations.md index e7490c8bb..c91f155e6 100644 --- a/docs/faq/basic-operations.md +++ b/docs/faq/basic-operations.md @@ -3,15 +3,15 @@ ## 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] +> 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. +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: @@ -25,15 +25,15 @@ A good and safe casting example: > [!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 +> 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 +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* @@ -41,7 +41,7 @@ able to message. ## 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 +You may check the message channel type. Visit [Glossary] to see the various types of channels. [Glossary]: Glossary.md#message-channels @@ -50,13 +50,13 @@ various types of channels. 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 + 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 +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- @@ -64,26 +64,26 @@ as in field values. With that in mind, links can be added with ## How do I add reactions to a message? Any entities that implement [IUserMessage] has an [AddReactionAsync] -method. This method expects an [IEmote] as a parameter. -In Discord.Net, an Emote represents a server custom emote, while an -Emoji is a Unicode emoji (standard emoji). Both [Emoji] and [Emote] -implement [IEmote] and are valid options. +method. This method expects an [IEmote] as a parameter. +In Discord.Net, an Emote represents a server custom emote, while an +Emoji is a Unicode emoji (standard emoji). Both [Emoji] and [Emote] +implement [IEmote] and are valid options. [!code-csharp[Emoji](samples/basics/emoji.cs)] [AddReactionAsync]: xref:Discord.IUserMessage.AddReactionAsync* - + ## 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 casues the lib to throw preemptive rate -limit more frequently than it should for methods such as adding +This is due to how HTML header works, mistreating +0.25sec/action to 1sec. This casues 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 diff --git a/docs/faq/client-basics.md b/docs/faq/client-basics.md index a8de2a552..8c56e86fd 100644 --- a/docs/faq/client-basics.md +++ b/docs/faq/client-basics.md @@ -3,35 +3,35 @@ ## My client keeps returning 401 upon logging in! > [!WARNING] -> Userbot/selfbot (logging in with a user token) is not +> 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 +> 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 start with `Mj*`. - If it starts with any other characters, chances are, you might be - using the *client secret*, which has nothing to do with the login + 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 start with `Mj*`. + If it starts with any other characters, chances are, you might be + using the *client secret*, which has nothing to do with the login token. [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] +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 @@ -39,14 +39,15 @@ event is triggered, then you can proceed to do whatever you like. ## 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 +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](../guides/concepts/events.md#cacheable). + 1. Message Cache must be enabled. -2. Hook the MessageUpdated event. This event provides a *before* and +2. Hook the MessageUpdated event. This event provides a *before* and *after* object. -3. Only messages received *after* the bot comes online will be +3. Only messages received *after* the bot comes online will be available in the cache. [MessageCacheSize]: xref:Discord.WebSocket.DiscordSocketConfig.MessageCacheSize diff --git a/docs/faq/getting-started.md b/docs/faq/getting-started.md index c9fd7a738..1b1057bcf 100644 --- a/docs/faq/getting-started.md +++ b/docs/faq/getting-started.md @@ -1,81 +1,84 @@ # Basic Concepts / Getting Started ## How do I 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 + +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](https://github.com/RogueException/Discord.Net/blob/dev/docs/guides/getting_started/samples/intro/structure.cs) 2. [Official template](https://github.com/foxbot/DiscordBotBase/tree/csharp/src/DiscordBot) -> [!TIP] -> Please note that you should *not* try to blindly copy paste -> the code. The examples are meant to be a template or a guide. +> [!TIP] +> 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. - + [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/ - + ## How do I add my bot to my server/guild? -You can do so by using the [permission calculator] provided +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 +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 +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 Client/User/Object ID? Is it the token? -Each user and object on Discord has its own snowflake ID generated +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 -library require an ID to retrieve the said object. - -There are 2 ways to obtain the said ID. - 1. Enable Discord's developer mode. With developer mode enabled, - you can - as an example - right click on a guild and copy the guild - id (please note that this does not apply to all objects, such as +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 +library require an ID to retrieve the said object. + +There are 2 ways to obtain the said ID. + + 1. Enable Discord's developer mode. With developer mode enabled, + you can - as an example - right click on a guild and copy the guild + id (please note that this does not apply to all objects, such as Role IDs \[see below], or DM channel IDs). ![Developer Mode](images/dev-mode.png) - 2. Escape the object using `\` in front the object. For example, - when you do `\@Example#1234` in chat, it will return the user ID of + 2. Escape the object using `\` in front the object. For example, + when you do `\@Example#1234` in chat, it will return the user ID of the aforementioned user. - -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 + +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. ## How do I get the role ID? Several common ways to do this: - 1. Make the role mentionable and mention the role, and escape it + + 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. - -Please note that right-clicking on the role and copying the ID will + +Please note that right-clicking on the role and copying the ID will **not** work. It will only copy the message ID. -## I have more questions! +## I have more questions! -Please visit us at #dotnet_discord-net at [Discord API]. -Describe the problem in details to us, and preferably with the +Please visit us at #dotnet_discord-net at [Discord API]. +Describe the problem in details to us, and preferably with the problematic code uploaded onto [Hastebin](https://hastebin.com). [Discord API]: https://discord.gg/jkrBmQR \ No newline at end of file From be5e3097136d69a9675999942bdadff854683e51 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Thu, 29 Mar 2018 01:16:22 +0800 Subject: [PATCH 071/183] Add more embed example details --- .../Common/Discord.EmbedBuilder.Overwrites.md | 35 ++++++++++++------ .../Common/images/embed-example.png | Bin 0 -> 15290 bytes 2 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 docs/_overwrites/Common/images/embed-example.png diff --git a/docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md b/docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md index 1a7fcd9bb..566e623a7 100644 --- a/docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md +++ b/docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md @@ -10,19 +10,32 @@ after @Discord.EmbedBuilder.Build* is called. ### Example +The example below builds an embed and sends it to the chat. + ```cs +[Command("embed")] public async Task SendRichEmbedAsync() { var embed = new EmbedBuilder - { - // Embed property can be set within object initializer - Title = "Hello world!" - } - // Or with the method - .WithTitle("I overwrote the title.") - .WithDescription("I am a description.") - .WithUrl("https://example.com") - .Build(); - await _channel.SendMessageAsync(string.Empty, embed: embed); + { + // 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); } -``` \ No newline at end of file +``` + +#### Result + +![Embed Example](images/embed-example.png) \ 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 0000000000000000000000000000000000000000..f23fb4d70d797dd65e40fae54e84cd1935dd8608 GIT binary patch literal 15290 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 Date: Fri, 30 Mar 2018 00:44:52 +0800 Subject: [PATCH 072/183] Initial dark theme support --- docs/_template/styles/docfx.css | 971 ++++++++++++++++++++++++++++++++ docs/docfx.json | 3 +- 2 files changed, 973 insertions(+), 1 deletion(-) create mode 100644 docs/_template/styles/docfx.css diff --git a/docs/_template/styles/docfx.css b/docs/_template/styles/docfx.css new file mode 100644 index 000000000..57a02b2fc --- /dev/null +++ b/docs/_template/styles/docfx.css @@ -0,0 +1,971 @@ +/* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. */ +@import url('https://fonts.googleapis.com/css?family=PT+Sans'); +html, +body { + font-family: 'PT Sans', 'Segoe UI', Tahoma, Helvetica, sans-serif; + height: 100%; + background: #212121; + color: #C0C0C0 +} +button, +a { + color: #64B5F6; + cursor: pointer; +} +button:hover, +button:focus, +a:hover, +a:focus { + color: #2196F3; + text-decoration: none; +} +a.disable, +a.disable:hover { + text-decoration: none; + cursor: default; + color: #EEEEEE; +} + +/* workaround for leave space for fixed navbar with # anchor url*/ + +h1:before, +h2:before, +h3:before, +h4:before { + content: ''; + display: block; + position: relative; + width: 0; + height: 100px; + margin-top: -100px; +} + +h1, h2, h3, h4, h5, h6, .text-break { + word-wrap: break-word; + word-break: break-word; +} + +h1 mark, +h2 mark, +h3 mark, +h4 mark, +h5 mark, +h6 mark { + padding: 0; +} + +.inheritance .level0:before, +.inheritance .level1:before, +.inheritance .level2:before, +.inheritance .level3:before, +.inheritance .level4:before, +.inheritance .level5:before { + content: '↳'; + margin-right: 5px; +} + +.inheritance .level0 { + margin-left: 0em; +} + +.inheritance .level1 { + margin-left: 1em; +} + +.inheritance .level2 { + margin-left: 2em; +} + +.inheritance .level3 { + margin-left: 3em; +} + +.inheritance .level4 { + margin-left: 4em; +} + +.inheritance .level5 { + margin-left: 5em; +} + +span.parametername, +span.paramref, +span.typeparamref { + font-style: italic; +} +span.languagekeyword{ + font-weight: bold; +} + +svg:hover path { + fill: #ffffff; +} + +.hljs { + display: inline; + background-color: inherit; + padding: 0; +} +/* additional spacing fixes */ +.btn + .btn { + margin-left: 10px; +} +.btn.pull-right { + margin-left: 10px; + margin-top: 5px; +} +.table { + margin-bottom: 10px; +} +table p { + margin-bottom: 0; +} +table a { + display: inline-block; +} + +/* Make hidden attribute compatible with old browser.*/ +[hidden] { + display: none !important; +} + +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 15px; + margin-bottom: 10px; + font-weight: 400; +} +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 5px; +} +.navbar { + margin-bottom: 0; +} +#wrapper { + min-height: 100%; + position: relative; +} +/* blends header footer and content together with gradient effect */ +.grad-top { + /* For Safari 5.1 to 6.0 */ + /* For Opera 11.1 to 12.0 */ + /* For Firefox 3.6 to 15 */ + background: linear-gradient(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0)); + /* Standard syntax */ + height: 5px; +} +.grad-bottom { + /* For Safari 5.1 to 6.0 */ + /* For Opera 11.1 to 12.0 */ + /* For Firefox 3.6 to 15 */ + background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.05)); + /* Standard syntax */ + height: 5px; +} +.divider { + margin: 0 5px; + color: #37474F; +} +hr { + border-color: #37474F; +} +header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; +} +header .navbar { + border-width: 0 0 0px; + border-radius: 0; +} +.navbar-brand { + font-size: inherit; + padding: 0; +} +.navbar-collapse { + margin: 0 -15px; +} +.subnav { + min-height: 40px; + background: #383838 +} + +.inheritance h5, .inheritedMembers h5{ + padding-bottom: 5px; + border-bottom: 1px solid #37474F; +} + +article h1, article h2, article h3, article h4{ + margin-top: 25px; +} + +article h4{ + border-bottom: 1px solid #37474F; +} + +article span.small.pull-right{ + margin-top: 20px; +} + +article section { + margin-left: 1em; +} + +/*.expand-all { + padding: 10px 0; +}*/ +.breadcrumb { + margin: 0; + padding: 10px 0; + background-color: inherit; + white-space: nowrap; +} +.breadcrumb > li + li:before { + content: "\00a0/"; +} +#autocollapse.collapsed .navbar-header { + float: none; +} +#autocollapse.collapsed .navbar-toggle { + display: block; +} +#autocollapse.collapsed .navbar-collapse { + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); +} +#autocollapse.collapsed .navbar-collapse.collapse { + display: none !important; +} +#autocollapse.collapsed .navbar-nav { + float: none !important; + margin: 7.5px -15px; +} +#autocollapse.collapsed .navbar-nav > li { + float: none; +} +#autocollapse.collapsed .navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; +} +#autocollapse.collapsed .collapse.in, +#autocollapse.collapsed .collapsing { + display: block !important; +} +#autocollapse.collapsed .collapse.in .navbar-right, +#autocollapse.collapsed .collapsing .navbar-right { + float: none !important; +} +#autocollapse .form-group { + width: 100%; +} +#autocollapse .form-control { + width: 100%; +} +#autocollapse .navbar-header { + margin-left: 0; + margin-right: 0; +} +#autocollapse .navbar-brand { + margin-left: 0; +} +.collapse.in, +.collapsing { + text-align: center; +} +.collapsing .navbar-form { + margin: 0 auto; + max-width: 400px; + padding: 10px 15px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); +} +.collapsed .collapse.in .navbar-form { + margin: 0 auto; + max-width: 400px; + padding: 10px 15px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); +} +.navbar .navbar-nav { + display: inline-block; +} +.docs-search { + background: #424242; + vertical-align: middle; +} +.docs-search > .search-query { + font-size: 14px; + border: 0; + width: 120%; +} +.docs-search > .search-query:focus { + outline: 0; +} +.search-results-frame { + clear: both; + display: table; + width: 100%; +} +.search-results.ng-hide { + display: none; +} +.search-results-container { + padding-bottom: 1em; + border-top: 1px solid #111; + background: rgba(25, 25, 25, 0.5); +} +.search-results-container .search-results-group { + padding-top: 50px !important; + padding: 10px; +} +.search-results-group-heading { + font-family: "Open Sans"; + padding-left: 10px; + color: #424242; +} +.search-close { + position: absolute; + left: 50%; + margin-left: -100px; + color: #424242; + text-align: center; + padding: 5px; + background: #333; + border-top-right-radius: 5px; + border-top-left-radius: 5px; + width: 200px; + box-shadow: 0 0 10px #111; +} +#search { + display: none; +} + +/* Search results display*/ +#search-results { + max-width: 960px !important; + margin-top: 120px; + margin-bottom: 115px; + margin-left: auto; + margin-right: auto; + line-height: 1.8; + display: none; +} + +#search-results>.search-list { + text-align: center; + font-size: 2.5rem; + margin-bottom: 50px; +} + +#search-results p { + text-align: center; +} + +#search-results .sr-items { + font-size: 24px; +} + +.sr-item { + margin-bottom: 25px; +} + +.sr-item>.item-href { + font-size: 14px; + color: #093; +} + +.sr-item>.item-brief { + font-size: 13px; +} + +.pagination>li>a { + color: #47A7A0 +} + +.pagination>.active>a { + background-color: #47A7A0; + border-color: #47A7A0; +} + +.fixed_header { + position: fixed; + width: 100%; + padding-bottom: 10px; + padding-top: 10px; + margin: 0px; + top: 0; + z-index: 9999; + left: 0; +} + +.fixed_header+.toc{ + margin-top: 50px; + margin-left: 0; +} + +.sidenav, .fixed_header, .toc { + background-color: #212121; +} + +.sidetoc { + position: fixed; + width: 260px; + top: 150px; + bottom: 0; + overflow-x: hidden; + overflow-y: auto; + background-color: #1b1b1b; + border-left: 0px solid #37474F; + border-right: 0px solid #37474F; + z-index: 1; +} + +.sidetoc.shiftup { + bottom: 70px; +} + +body .toc{ + background-color: inherit; + overflow-x: hidden; +} + +.sidetoggle.ng-hide { + display: block !important; +} +.sidetoc-expand > .caret { + margin-left: 0px; + margin-top: -2px; +} +.sidetoc-expand > .caret-side { + border-left: 4px solid; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + margin-left: 4px; + margin-top: -4px; +} +.sidetoc-heading { + font-weight: 500; +} + +.toc { + margin: 0px 0 0 10px; + padding: 0 10px; +} +.expand-stub { + position: absolute; + left: -10px; +} +.toc .nav > li > a.sidetoc-expand { + position: absolute; + top: 0; + left: 0; +} +.toc .nav > li > a { + color: rgb(218, 218, 218); + margin-left: 5px; + display: block; + padding: 0; +} +.toc .nav > li > a:hover, +.toc .nav > li > a:focus { + color: #E0E0E0; + background: none; + text-decoration: inherit; +} +.toc .nav > li.active > a { + color: #90CAF9; +} +.toc .nav > li.active > a:hover, +.toc .nav > li.active > a:focus { + color: #4FC3F7; +} + +.toc .nav > li> .expand-stub { + cursor: pointer; +} + +.toc .nav > li.active > .expand-stub::before, +.toc .nav > li.in > .expand-stub::before, +.toc .nav > li.in.active > .expand-stub::before, +.toc .nav > li.filtered > .expand-stub::before { + content: "-"; +} + +.toc .nav > li > .expand-stub::before, +.toc .nav > li.active > .expand-stub::before { + content: "+"; +} + +.toc .nav > li.filtered > ul, +.toc .nav > li.in > ul { + display: block; +} + +.toc .nav > li > ul { + display: none; +} + +.toc ul{ + font-size: 12px; + margin: 0 0 0 3px; +} + +.toc .level1 > li { + font-weight: bold; + margin-top: 10px; + position: relative; + font-size: 16px; +} +.toc .level2 { + font-weight: normal; + margin: 5px 0 0 15px; + font-size: 14px; +} +.toc-toggle { + display: none; + margin: 0 15px 0px 15px; +} +.sidefilter { + position: fixed; + top: 90px; + width: 260px; + background-color: #1b1b1b; + padding: 15px; + border-left: 0px solid #37474F; + border-right: 0px solid #37474F; + z-index: 1; +} +.toc-filter { + border-radius: 5px; + background: #fff; + color: #666666; + padding: 5px; + position: relative; + margin: 0 5px 0 5px; +} +.toc-filter > input { + border: 0; + color: #666666; + padding-left: 20px; + width: 100%; +} +.toc-filter > input:focus { + outline: 0; +} +.toc-filter > .filter-icon { + position: absolute; + top: 10px; + left: 5px; +} +.article { + margin-top: 120px; + margin-bottom: 115px; +} + +#_content>a{ + margin-top: 105px; +} + +.article.grid-right { + margin-left: 280px; +} + +.inheritance hr { + margin-top: 5px; + margin-bottom: 5px; +} +.article img { + max-width: 100%; +} +.sideaffix { + margin-top: 50px; + font-size: 12px; + max-height: 100%; + overflow: hidden; + top: 100px; + bottom: 10px; + position: fixed; +} +.sideaffix.shiftup { + bottom: 70px; +} +.affix { + position: relative; + height: 100%; +} +.sideaffix > div.contribution { + margin-bottom: 20px; +} +.sideaffix > div.contribution > ul > li > a.contribution-link { + padding: 6px 10px; + font-weight: bold; + font-size: 14px; +} +.sideaffix > div.contribution > ul > li > a.contribution-link:hover { + background-color: #ffffff; +} +.sideaffix ul.nav > li > a:focus { + background: none; +} +.affix h5 { + font-weight: bold; + text-transform: uppercase; + padding-left: 10px; + font-size: 12px; +} +.affix > ul.level1 { + overflow: hidden; + padding-bottom: 10px; + height: calc(100% - 100px); + margin-right: -20px; +} +.affix ul > li > a:before { + color: #cccccc; + position: absolute; +} +.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 { + padding: 5px 12px; + color: #EEEEEE; +} +.affix > ul > li.active:last-child { + margin-bottom: 50px; +} +.affix > ul > li > a:before { + content: "|"; + font-size: 16px; + top: 1px; + left: 0; +} +.affix > ul > li.active > a, +.affix > ul > li.active > a:before { + color: #B3E5FC; + font-weight: bold; +} +.affix ul ul > li > a { + padding: 2px 15px; +} +.affix ul ul > li > a:before { + content: ">"; + font-size: 14px; + top: -1px; + left: 5px; +} +.affix ul > li > a:before, +.affix ul ul { + display: none; +} +.affix ul > li.active > ul, +.affix ul > li.active > a:before, +.affix ul > li > a:hover:before { + display: block; + white-space: nowrap; +} +.codewrapper { + position: relative; +} +.trydiv { + height: 0px; +} +.tryspan { + position: absolute; + top: 0px; + right: 0px; + border-style: solid; + border-radius: 0px 4px; + box-sizing: border-box; + border-width: 1px; + border-color: #37474F; + text-align: center; + padding: 2px 8px; + background-color: white; + font-size: 12px; + cursor: pointer; + z-index: 100; + display: none; + color: #767676; +} +.tryspan:hover { + background-color: #3b8bd0; + color: white; + border-color: #3b8bd0; +} +.codewrapper:hover .tryspan { + display: block; +} +.sample-response .response-content{ + max-height: 200px; +} +footer { + position: absolute; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; +} +.footer { + border-top: 1px solid #5F5F5F; + background: #616161; + padding: 15px 0; +} +@media (min-width: 768px) { + #sidetoggle.collapse { + display: block; + } + .topnav .navbar-nav { + float: none; + white-space: nowrap; + } + .topnav .navbar-nav > li { + float: none; + display: inline-block; + } +} +@media only screen and (max-width: 768px) { + #mobile-indicator { + display: block; + } + /* TOC display for responsive */ + .article { + margin-top: 30px !important; + } + header { + position: static; + } + .topnav { + text-align: center; + } + .sidenav { + padding: 15px 0; + margin-left: -15px; + margin-right: -15px; + } + .sidefilter { + position: static; + width: auto; + float: none; + border: none; + } + .sidetoc { + position: static; + width: auto; + float: none; + padding-bottom: 0px; + border: none; + } + .toc .nav > li, .toc .nav > li >a { + display: inline-block; + } + .toc li:after { + margin-left: -3px; + margin-right: 5px; + content: ", "; + color: #666666; + } + .toc .level1 > li { + display: block; + } + + .toc .level1 > li:after { + display: none; + } + .article.grid-right { + margin-left: 0; + } + .grad-top, + .grad-bottom { + display: none; + } + .toc-toggle { + display: block; + } + .sidetoggle.ng-hide { + display: none !important; + } + /*.expand-all { + display: none; + }*/ + .sideaffix { + display: none; + } + .mobile-hide { + display: none; + } + .breadcrumb { + white-space: inherit; + } + + /* workaround for #hashtag url is no longer needed*/ + h1:before, + h2:before, + h3:before, + h4:before { + content: ''; + display: none; + } +} + +/* For toc iframe */ +@media (max-width: 260px) { + .toc .level2 > li { + display: block; + } + + .toc .level2 > li:after { + display: none; + } +} + +/* For code snippet line highlight */ +pre > code .line-highlight { + background-color: #ffffcc; +} + +/* Alerts */ +.alert h5 { + text-transform: uppercase; + font-weight: bold; + margin-top: 0; +} + +.alert h5:before { + position:relative; + top:1px; + display:inline-block; + font-family:'Glyphicons Halflings'; + line-height:1; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + margin-right: 5px; + font-weight: normal; +} + +.alert-info { + color: #d9edf7; + background: #004458; + border-color: #005873 +} + +.alert-info h5:before { + content:"\e086" +} + +.alert-warning h5:before { + content:"\e127" +} + +.alert-danger { + color: #fff2f2; + background: #4d0000; + border-color: #660000 +} + +.alert-danger h5:before { + content:"\e107" +} + +/* For Embedded Video */ +div.embeddedvideo { + padding-top: 56.25%; + position: relative; + width: 100%; +} + +div.embeddedvideo iframe { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; +} + +/* For printer */ +@media print{ + .article.grid-right { + margin-top: 0px; + margin-left: 0px; + } + .sideaffix { + display: none; + } + .mobile-hide { + display: none; + } + .footer { + display: none; + } +} + +/* 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 +} \ No newline at end of file diff --git a/docs/docfx.json b/docs/docfx.json index cc126b0e1..df45f9d98 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -53,7 +53,8 @@ ], "dest":"_site", "template":[ - "default" + "default", + "_template" ], "overwrite": "_overwrites/**/**.md", "globalMetadata":{ From 7cc10c843e9adbdd3152f2734aea78f9d7fb97bd Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 30 Mar 2018 00:46:17 +0800 Subject: [PATCH 073/183] Add dotnet CLI example --- docs/guides/getting_started/installing.md | 33 ++++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/guides/getting_started/installing.md b/docs/guides/getting_started/installing.md index 372009e90..be5b2494e 100644 --- a/docs/guides/getting_started/installing.md +++ b/docs/guides/getting_started/installing.md @@ -40,13 +40,13 @@ 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! +> 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 @@ -57,10 +57,10 @@ bot's project. 5. Install the `Discord.Net` package. ![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 @@ -71,11 +71,11 @@ Solution). 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. @@ -84,6 +84,19 @@ you're installing from the developer feed. [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 In order to compile Discord.Net, you require the following: From c0d5eda7f649866e80dd7d6e75c4faaad6fc7b89 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 30 Mar 2018 02:34:11 +0800 Subject: [PATCH 074/183] Update dark theme + Fix table color. + Change warning/tip/important tip color. + Change font to Titillium Web. + Other changes that's easier on eyes. --- docs/_template/styles/docfx.css | 35 +++++++++---- docs/_template/styles/dracula.css | 85 +++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 docs/_template/styles/dracula.css diff --git a/docs/_template/styles/docfx.css b/docs/_template/styles/docfx.css index 57a02b2fc..8ec20152a 100644 --- a/docs/_template/styles/docfx.css +++ b/docs/_template/styles/docfx.css @@ -1,8 +1,9 @@ /* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. */ -@import url('https://fonts.googleapis.com/css?family=PT+Sans'); +@import url('https://fonts.googleapis.com/css?family=Titillium+Web'); +@import url("dracula.css"); html, body { - font-family: 'PT Sans', 'Segoe UI', Tahoma, Helvetica, sans-serif; + font-family: 'Titillium Web', 'Segoe UI', Tahoma, Helvetica, sans-serif; height: 100%; background: #212121; color: #C0C0C0 @@ -413,10 +414,6 @@ article section { margin-left: 0; } -.sidenav, .fixed_header, .toc { - background-color: #212121; -} - .sidetoc { position: fixed; width: 260px; @@ -436,7 +433,6 @@ article section { body .toc{ background-color: inherit; - overflow-x: hidden; } .sidetoggle.ng-hide { @@ -591,7 +587,6 @@ body .toc{ margin-top: 50px; font-size: 12px; max-height: 100%; - overflow: hidden; top: 100px; bottom: 10px; position: fixed; @@ -867,6 +862,12 @@ pre > code .line-highlight { content:"\e086" } +.alert-warning { + color: #fffaf2; + background: #80551a; + border-color: #99661f +} + .alert-warning h5:before { content:"\e127" } @@ -968,4 +969,20 @@ code { color:white; background-color:#4a4c52; border-radius:4px -} \ No newline at end of file +} +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) +} diff --git a/docs/_template/styles/dracula.css b/docs/_template/styles/dracula.css new file mode 100644 index 000000000..78fbe45f3 --- /dev/null +++ b/docs/_template/styles/dracula.css @@ -0,0 +1,85 @@ +/* Dracula Theme v1.2.5 + * + * https://github.com/dracula/highlightjs + * + * Copyright 2016-present, All rights reserved + * + * Code licensed under the MIT license + * + * @author Denis Ciccale + * @author Zeno Rocha + */ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #282a36; +} + +.hljs-built_in, +.hljs-selector-tag, +.hljs-section, +.hljs-link, +.hljs-keyword { + color: rgb(86, 156, 214); +} + +/* +.hljs-keyword { + color: rgb(86, 156, 214); +} + +*/ + +.hljs, +.hljs-subst, +.hljs-tag { + color: #f8f8f2 !important; +} + +.hljs-string, +.hljs-meta, +.hljs-name, +.hljs-type, +.hljs-attr, +.hljs-symbol, +.hljs-bullet, +.hljs-addition, +.hljs-variable, +.hljs-template-tag, +.hljs-template-variable, +.hljs-title { + color: rgb(214, 157, 133); +} + +.hljs-title { + color: rgb(78, 201, 176); +} + +.hljs-comment, +.hljs-quote, +.hljs-deletion { + color: rgb(87, 166, 74); +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-title, +.hljs-section, +.hljs-doctag, +.hljs-type, +.hljs-name, +.hljs-strong { + font-weight: bold; +} + +.hljs-literal, +.hljs-number { + color: rgb(181, 206, 168); +} + +.hljs-emphasis { + font-style: italic; +} From 9c08d4f3604d758863fe2ef668302d836a0eedde Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 30 Mar 2018 02:47:37 +0800 Subject: [PATCH 075/183] Improve Overwrites styling --- docs/_overwrites/Commands/CommandContext.Overwrite.md | 1 + .../_overwrites/Commands/CommandException.Overwrite.md | 8 ++++++-- .../Commands/DontAutoLoadAttribute.Overwrite.md | 8 +++++--- .../Commands/DontInjectAttribute.Overwrite.md | 10 ++++++---- .../Commands/ShardedCommandContext.Overwrite.md | 2 +- .../Commands/SocketCommandContext.Overwrite.md | 1 + .../Common/Discord.EmbedBuilder.Overwrites.md | 10 ++++++---- 7 files changed, 26 insertions(+), 14 deletions(-) diff --git a/docs/_overwrites/Commands/CommandContext.Overwrite.md b/docs/_overwrites/Commands/CommandContext.Overwrite.md index fdde3b2e5..b94388324 100644 --- a/docs/_overwrites/Commands/CommandContext.Overwrite.md +++ b/docs/_overwrites/Commands/CommandContext.Overwrite.md @@ -1,5 +1,6 @@ --- uid: Discord.Commands.CommandContext +example: [*content] --- An example of how this class is used the command system can be seen diff --git a/docs/_overwrites/Commands/CommandException.Overwrite.md b/docs/_overwrites/Commands/CommandException.Overwrite.md index a5a713bcb..166a011de 100644 --- a/docs/_overwrites/Commands/CommandException.Overwrite.md +++ b/docs/_overwrites/Commands/CommandException.Overwrite.md @@ -1,9 +1,8 @@ --- uid: Discord.Commands.CommandException +remarks: *content --- -### Remarks - 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 @@ -11,6 +10,11 @@ thrown exception passed into 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: diff --git a/docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md b/docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md index 857e05cf8..d47980df7 100644 --- a/docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md +++ b/docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md @@ -1,15 +1,17 @@ --- uid: Discord.Commands.DontAutoLoadAttribute +remarks: *content --- -### Remarks - 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. -### Example +--- +uid: Discord.Commands.DontAutoLoadAttribute +example: [*content] +--- ```cs [DontAutoLoad] diff --git a/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md b/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md index ed59c6ced..2d9dd6e9a 100644 --- a/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md +++ b/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md @@ -1,16 +1,18 @@ --- uid: Discord.Commands.DontInjectAttribute +remarks: *content --- -### Remarks - The attribute can be applied to a public settable property inside a -@Discord.Commands.ModuleBase based class. By applying this property, +@Discord.Commands.ModuleBase based class. By applying this attribute, the marked property will not be automatically injected of the dependency. See [Dependency Injection](../../guides/commands/commands.md#dependency-injection) to learn more. -### Example +--- +uid: Discord.Commands.DontInjectAttribute +example: [*content] +--- ```cs public class MyModule : ModuleBase diff --git a/docs/_overwrites/Commands/ShardedCommandContext.Overwrite.md b/docs/_overwrites/Commands/ShardedCommandContext.Overwrite.md index f3c5bacbc..0a97dc47b 100644 --- a/docs/_overwrites/Commands/ShardedCommandContext.Overwrite.md +++ b/docs/_overwrites/Commands/ShardedCommandContext.Overwrite.md @@ -1,8 +1,8 @@ --- uid: Discord.Commands.ShardedCommandContext +example: [*content] --- -### Example An example of how this class is used the command system can be seen below: diff --git a/docs/_overwrites/Commands/SocketCommandContext.Overwrite.md b/docs/_overwrites/Commands/SocketCommandContext.Overwrite.md index 233418201..192ad1682 100644 --- a/docs/_overwrites/Commands/SocketCommandContext.Overwrite.md +++ b/docs/_overwrites/Commands/SocketCommandContext.Overwrite.md @@ -1,5 +1,6 @@ --- uid: Discord.Commands.SocketCommandContext +example: [*content] --- An example of how this class is used the command system can be seen diff --git a/docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md b/docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md index 566e623a7..2a0de73d5 100644 --- a/docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md +++ b/docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md @@ -1,14 +1,16 @@ --- uid: Discord.EmbedBuilder +remarks: *content --- -### Remarks - This builder class is used to build an @Discord.Embed (rich embed) -object that will be ready to be sent via @Discord.IMessageChannel.SendMessageAsync* +object that will be ready to be sent via @Discord.IMessageChannel.SendMessageAsync* after @Discord.EmbedBuilder.Build* is called. -### Example +--- +uid: Discord.EmbedBuilder +example: [*content] +--- The example below builds an embed and sends it to the chat. From 03dcdca91ea9b0aa5823151e842956643a41b94c Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 30 Mar 2018 03:23:29 +0800 Subject: [PATCH 076/183] Add lightbox & categorize templates --- .../plugins/DocFx.Plugin.ImageProcessor.dll | Bin 0 -> 10240 bytes docs/_template/{ => dark}/styles/docfx.css | 0 docs/_template/{ => dark}/styles/dracula.css | 0 .../partials/head.tmpl.partial | 20 ++++++++++++++++++ .../partials/scripts.tmpl.partial | 8 +++++++ .../styles/plugin-featherlight.js | 15 +++++++++++++ docs/docfx.json | 3 ++- 7 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 docs/_template/bootstrap-modal/plugins/DocFx.Plugin.ImageProcessor.dll rename docs/_template/{ => dark}/styles/docfx.css (100%) rename docs/_template/{ => dark}/styles/dracula.css (100%) create mode 100644 docs/_template/lightbox-featherlight/partials/head.tmpl.partial create mode 100644 docs/_template/lightbox-featherlight/partials/scripts.tmpl.partial create mode 100644 docs/_template/lightbox-featherlight/styles/plugin-featherlight.js diff --git a/docs/_template/bootstrap-modal/plugins/DocFx.Plugin.ImageProcessor.dll b/docs/_template/bootstrap-modal/plugins/DocFx.Plugin.ImageProcessor.dll new file mode 100644 index 0000000000000000000000000000000000000000..c608b941a6904789bc80b1ac646349c5de11df9f GIT binary patch literal 10240 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 EH + + + {{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}} + + + + {{#_description}}{{/_description}} + + + + + + + + {{#_enableSearch}}{{/_enableSearch}} + {{#_enableNewTab}}{{/_enableNewTab}} + \ No newline at end of file diff --git a/docs/_template/lightbox-featherlight/partials/scripts.tmpl.partial b/docs/_template/lightbox-featherlight/partials/scripts.tmpl.partial new file mode 100644 index 000000000..cd7d0074c --- /dev/null +++ b/docs/_template/lightbox-featherlight/partials/scripts.tmpl.partial @@ -0,0 +1,8 @@ +{{!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/lightbox-featherlight/styles/plugin-featherlight.js b/docs/_template/lightbox-featherlight/styles/plugin-featherlight.js new file mode 100644 index 000000000..846494cf3 --- /dev/null +++ b/docs/_template/lightbox-featherlight/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/docfx.json b/docs/docfx.json index df45f9d98..78232becd 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -54,7 +54,8 @@ "dest":"_site", "template":[ "default", - "_template" + "_template/dark", + "_template/lightbox-featherlight" ], "overwrite": "_overwrites/**/**.md", "globalMetadata":{ From 6612a03bee4f8e9d65c9ccd9a801ef301a045cdc Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 30 Mar 2018 15:59:26 +0800 Subject: [PATCH 077/183] Clean up pages --- docs/guides/concepts/entities.md | 38 ++++------ docs/guides/concepts/events.md | 21 +++--- docs/guides/concepts/logging.md | 30 ++++---- docs/guides/getting_started/installing.md | 6 +- docs/guides/getting_started/intro.md | 75 +++++++++---------- .../getting_started/samples/project.csproj | 6 +- 6 files changed, 80 insertions(+), 96 deletions(-) diff --git a/docs/guides/concepts/entities.md b/docs/guides/concepts/entities.md index 3a5d5496b..49139cecf 100644 --- a/docs/guides/concepts/entities.md +++ b/docs/guides/concepts/entities.md @@ -2,9 +2,11 @@ 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. @@ -15,9 +17,9 @@ 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 @@ -42,11 +44,11 @@ The most basic forms of entities, `SocketGuild`, `SocketUser`, and 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. More detailed versions of entities can be pulled from the basic entities, e.g. `SocketGuild.GetUser`, which returns a @@ -54,18 +56,6 @@ entities, e.g. `SocketGuild.GetUser`, 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..758e5e6d8 100644 --- a/docs/guides/concepts/events.md +++ b/docs/guides/concepts/events.md @@ -2,6 +2,8 @@ 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 @@ -61,7 +63,7 @@ This pattern is typically only found on `EntityUpdated` events. #### 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 +72,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 ae2acdf0f..52fccd02b 100644 --- a/docs/guides/concepts/logging.md +++ b/docs/guides/concepts/logging.md @@ -2,18 +2,26 @@ title: Logging --- -Discord.Net's clients provide a [Log] event that all messages will be +# 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.Log [Events]: events.md -### Usage +> [!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 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 @@ -24,8 +32,8 @@ to a logging function to write their own messages. ### 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 +42,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/getting_started/installing.md b/docs/guides/getting_started/installing.md index be5b2494e..e2cee0d5b 100644 --- a/docs/guides/getting_started/installing.md +++ b/docs/guides/getting_started/installing.md @@ -27,12 +27,12 @@ required. When using .NET Framework, it is suggested to target Release builds of Discord.Net will be published to the [official NuGet feed]. -Development builds of Discord.Net, as well as addons *(TODO)* are -published to our development [MyGet feed]. +Development builds of Discord.Net, as well as add-ons are published to +our development [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 diff --git a/docs/guides/getting_started/intro.md b/docs/guides/getting_started/intro.md index c0c26e3f1..7afc045ec 100644 --- a/docs/guides/getting_started/intro.md +++ b/docs/guides/getting_started/intro.md @@ -15,16 +15,15 @@ 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). +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) - + + ![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) - + + ![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!** @@ -33,26 +32,25 @@ other options!** ## Adding your bot to a server -Bots **cannot** use invite links, they must be explicitly invited +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) - + + ![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) + > [!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 @@ -75,7 +73,7 @@ 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 +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. @@ -123,7 +121,7 @@ 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` +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 @@ -131,11 +129,11 @@ 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. +> [!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 @@ -156,27 +154,26 @@ The following lines can now be added: 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. +> [!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. +> [!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!". +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]. @@ -201,7 +198,7 @@ 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: +You should have now added the following lines, [!code-csharp[Message](samples/intro/message.cs)] diff --git a/docs/guides/getting_started/samples/project.csproj b/docs/guides/getting_started/samples/project.csproj index a964bbc26..79307226d 100644 --- a/docs/guides/getting_started/samples/project.csproj +++ b/docs/guides/getting_started/samples/project.csproj @@ -1,17 +1,15 @@ - Exe - netcoreapp1.1 - true + netcoreapp2.0 - + From 32d5afe454b0d2b30f824f24d25c0e0d34367b5f Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 30 Mar 2018 15:59:39 +0800 Subject: [PATCH 078/183] Further styling improvement --- docs/_template/dark/styles/dracula.css | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/_template/dark/styles/dracula.css b/docs/_template/dark/styles/dracula.css index 78fbe45f3..b87e33b10 100644 --- a/docs/_template/dark/styles/dracula.css +++ b/docs/_template/dark/styles/dracula.css @@ -22,7 +22,7 @@ .hljs-section, .hljs-link, .hljs-keyword { - color: rgb(86, 156, 214); + color: rgb(86, 156, 214) !important; } /* @@ -41,26 +41,25 @@ .hljs-string, .hljs-meta, .hljs-name, -.hljs-type, .hljs-attr, .hljs-symbol, .hljs-bullet, .hljs-addition, .hljs-variable, .hljs-template-tag, -.hljs-template-variable, -.hljs-title { - color: rgb(214, 157, 133); +.hljs-template-variable { + color: rgb(214, 157, 133) !important; } -.hljs-title { - color: rgb(78, 201, 176); +.hljs-title, +.hljs-type { + color: rgb(78, 201, 176) !important; } .hljs-comment, .hljs-quote, .hljs-deletion { - color: rgb(87, 166, 74); + color: rgb(87, 166, 74) !important; } .hljs-keyword, @@ -77,7 +76,7 @@ .hljs-literal, .hljs-number { - color: rgb(181, 206, 168); + color: rgb(181, 206, 168) !important; } .hljs-emphasis { From 2ff6d677cb8570cbfddc6cc79c6f82ca1737e39b Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 30 Mar 2018 15:59:57 +0800 Subject: [PATCH 079/183] Add Xmldocs --- src/Discord.Net.Core/ConnectionState.cs | 8 ++--- .../Entities/Activities/ActivityType.cs | 8 ++--- .../Entities/Permissions/ChannelPermission.cs | 2 +- src/Discord.Net.Core/Utils/Cacheable.cs | 32 ++++++------------- .../Entities/Channels/ChannelType.cs | 10 +++--- 5 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/Discord.Net.Core/ConnectionState.cs b/src/Discord.Net.Core/ConnectionState.cs index b240eb134..fadbc4065 100644 --- a/src/Discord.Net.Core/ConnectionState.cs +++ b/src/Discord.Net.Core/ConnectionState.cs @@ -3,13 +3,13 @@ namespace Discord /// Specifies the connection state of a client. public enum ConnectionState : byte { - /// Represents that the client has disconnected from the WebSocket. + /// The client has disconnected from Discord. Disconnected, - /// Represents that the client is connecting to the WebSocket. + /// The client is connecting to Discord. Connecting, - /// Represents that the client has established a connection to the WebSocket. + /// The client has established a connection to Discord. Connected, - /// Represents that the client is disconnecting from the WebSocket. + /// The client is disconnecting from Discord. Disconnecting } } diff --git a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs index 14281a329..62831f165 100644 --- a/src/Discord.Net.Core/Entities/Activities/ActivityType.cs +++ b/src/Discord.Net.Core/Entities/Activities/ActivityType.cs @@ -3,13 +3,13 @@ namespace Discord /// Specifies a Discord user's activity type. public enum ActivityType { - /// Represents that the user is playing a game. + /// The user is playing a game. Playing = 0, - /// Represents that the user is streaming online. + /// The user is streaming online. Streaming = 1, - /// Represents that the user is listening to a song. + /// The user is listening to a song. Listening = 2, - /// Represents that the user is watching a media. + /// The user is watching a media. Watching = 3 } } diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs index 498aa5ef8..183945147 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermission.cs @@ -26,7 +26,7 @@ namespace Discord SendTTSMessages = 0x00_00_10_00, /// Allows for deletion of other users messages. ManageMessages = 0x00_00_20_00, - /// Links sent by users with this permission will be auto-embedded. + /// 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, diff --git a/src/Discord.Net.Core/Utils/Cacheable.cs b/src/Discord.Net.Core/Utils/Cacheable.cs index f17aa8699..c2d83b04d 100644 --- a/src/Discord.Net.Core/Utils/Cacheable.cs +++ b/src/Discord.Net.Core/Utils/Cacheable.cs @@ -1,28 +1,20 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord { - /// - /// Contains an entity that may be cached. - /// - /// The type of entity that is cached - /// The type of this entity's ID + /// Contains an entity that may be cached. + /// 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? - /// + /// Indicates 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. /// @@ -37,9 +29,7 @@ namespace Discord DownloadFunc = downloadFunc; } - /// - /// 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. @@ -48,12 +38,10 @@ namespace Discord return await DownloadFunc(); } - /// - /// 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(); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs index c1fdc5af5..7759622c2 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelType.cs @@ -3,15 +3,15 @@ namespace Discord /// Defines the types of channels. public enum ChannelType { - /// Represents a text channel. + /// The channel is a text channel. Text = 0, - /// Represents a Direct Message channel. + /// The channel is a Direct Message channel. DM = 1, - /// Represents a voice channel. + /// The channel is a voice channel. Voice = 2, - /// Represents a group channel. + /// The channel is a group channel. Group = 3, - /// Represents a category channel. + /// The channel is a category channel. Category = 4 } } From 08d6558975f7aeb9a7d1e3169bca6e646a77ce42 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Sat, 31 Mar 2018 00:29:18 +0800 Subject: [PATCH 080/183] Tidy up docs --- ...erwrite.md => CommandContext.Inclusion.md} | 5 ---- .../Commands/CommandContext.Overwrite.md | 18 ++++++++++--- .../ShardedCommandContext.Overwrite.md | 10 -------- .../Common/Discord.EmbedBuilder.Overwrites.md | 7 +++++- .../Discord.EmbedObjectBuilder.Inclusion.md | 25 +++++++++++++++++++ .../Discord.EmbedObjectBuilder.Overwrites.md | 20 +++++++++++++++ 6 files changed, 65 insertions(+), 20 deletions(-) rename docs/_overwrites/Commands/{SocketCommandContext.Overwrite.md => CommandContext.Inclusion.md} (72%) delete mode 100644 docs/_overwrites/Commands/ShardedCommandContext.Overwrite.md create mode 100644 docs/_overwrites/Common/Discord.EmbedObjectBuilder.Inclusion.md create mode 100644 docs/_overwrites/Common/Discord.EmbedObjectBuilder.Overwrites.md diff --git a/docs/_overwrites/Commands/SocketCommandContext.Overwrite.md b/docs/_overwrites/Commands/CommandContext.Inclusion.md similarity index 72% rename from docs/_overwrites/Commands/SocketCommandContext.Overwrite.md rename to docs/_overwrites/Commands/CommandContext.Inclusion.md index 192ad1682..c99148db7 100644 --- a/docs/_overwrites/Commands/SocketCommandContext.Overwrite.md +++ b/docs/_overwrites/Commands/CommandContext.Inclusion.md @@ -1,8 +1,3 @@ ---- -uid: Discord.Commands.SocketCommandContext -example: [*content] ---- - An example of how this class is used the command system can be seen below: diff --git a/docs/_overwrites/Commands/CommandContext.Overwrite.md b/docs/_overwrites/Commands/CommandContext.Overwrite.md index b94388324..2f8c0cd48 100644 --- a/docs/_overwrites/Commands/CommandContext.Overwrite.md +++ b/docs/_overwrites/Commands/CommandContext.Overwrite.md @@ -3,8 +3,18 @@ uid: Discord.Commands.CommandContext example: [*content] --- -An example of how this class is used the command system can be seen -below: +[!include[Example Section](CommandContext.Inclusion.md)] -[!code[Sample module](../../guides/commands/samples/empty-module.cs)] -[!code[Command handler](../../guides/commands/samples/command_handler.cs)] \ No newline at end of file +--- +uid: Discord.Commands.SocketCommandContext +example: [*content] +--- + +[!include[Example Section](CommandContext.Inclusion.md)] + +--- +uid: Discord.Commands.ShardCommandContext +example: [*content] +--- + +[!include[Example Section](CommandContext.Inclusion.md)] \ No newline at end of file diff --git a/docs/_overwrites/Commands/ShardedCommandContext.Overwrite.md b/docs/_overwrites/Commands/ShardedCommandContext.Overwrite.md deleted file mode 100644 index 0a97dc47b..000000000 --- a/docs/_overwrites/Commands/ShardedCommandContext.Overwrite.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -uid: Discord.Commands.ShardedCommandContext -example: [*content] ---- - -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/Common/Discord.EmbedBuilder.Overwrites.md b/docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md index 2a0de73d5..de7da8f5d 100644 --- a/docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md +++ b/docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md @@ -1,5 +1,9 @@ --- uid: Discord.EmbedBuilder +seealso: + - linkId: Discord.EmbedFooterBuilder + - linkId: Discord.EmbedAuthorBuilder + - linkId: Discord.EmbedFieldBuilder remarks: *content --- @@ -12,7 +16,8 @@ uid: Discord.EmbedBuilder example: [*content] --- -The example below builds an embed and sends it to the chat. +The example below builds an embed and sends it to the chat using the +command system. ```cs [Command("embed")] diff --git a/docs/_overwrites/Common/Discord.EmbedObjectBuilder.Inclusion.md b/docs/_overwrites/Common/Discord.EmbedObjectBuilder.Inclusion.md new file mode 100644 index 000000000..eac0d9ca5 --- /dev/null +++ b/docs/_overwrites/Common/Discord.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/Discord.EmbedObjectBuilder.Overwrites.md b/docs/_overwrites/Common/Discord.EmbedObjectBuilder.Overwrites.md new file mode 100644 index 000000000..85bf150b7 --- /dev/null +++ b/docs/_overwrites/Common/Discord.EmbedObjectBuilder.Overwrites.md @@ -0,0 +1,20 @@ +--- +uid: Discord.EmbedAuthorBuilder +example: [*content] +--- + +[!include[Embed Object Builder Sample](Discord.EmbedObjectBuilder.Inclusion.md)] + +--- +uid: Discord.EmbedFooterBuilder +example: [*content] +--- + +[!include[Embed Object Builder Sample](Discord.EmbedObjectBuilder.Inclusion.md)] + +--- +uid: Discord.EmbedFieldBuilder +example: [*content] +--- + +[!include[Embed Object Builder Sample](Discord.EmbedObjectBuilder.Inclusion.md)] \ No newline at end of file From 27e6e2bdbf2ae050b564bfa2246ece65178825d2 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Tue, 3 Apr 2018 08:27:04 +0800 Subject: [PATCH 081/183] Add PreconditionAttribute docs --- .../PreconditionAttribute.Overwrites.md | 30 +++++++++++++++++++ ...PreconditionAttribute.Remarks.Inclusion.md | 6 ++++ .../OverrideTypeReaderAttribute.Overwrites.md | 24 +++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md create mode 100644 docs/_overwrites/Commands/PreconditionAttribute.Remarks.Inclusion.md create mode 100644 docs/_overwrites/Common/OverrideTypeReaderAttribute.Overwrites.md diff --git a/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md b/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md new file mode 100644 index 000000000..343547b47 --- /dev/null +++ b/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md @@ -0,0 +1,30 @@ +--- +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[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[Remarks(PreconditionAttribute.Remarks.Inclusion.md)]] + +--- +uid: Discord.Commands.PreconditionAttribute +example: [*content] +--- + +// todo: add example \ 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/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 From 197875b7bd5d1fbab57e93fda9744dd35eacd4be Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Wed, 4 Apr 2018 11:16:38 +0800 Subject: [PATCH 082/183] Add more examples --- .../Commands/CommandContext.Overwrite.md | 20 ----- ...lusion.md => ICommandContext.Inclusion.md} | 0 .../Commands/ICommandContext.Overwrite.md | 27 ++++++ .../Discord.EmbedObjectBuilder.Overwrites.md | 20 ----- ...erwrites.md => EmbedBuilder.Overwrites.md} | 0 ...ion.md => EmbedObjectBuilder.Inclusion.md} | 0 .../Common/EmbedObjectBuilder.Overwrites.md | 20 +++++ docs/_overwrites/Common/IEmote.Inclusion.md | 26 ++++++ docs/_overwrites/Common/IEmote.Overwrites.md | 77 ++++++++++++++++++ .../Common/images/react-example.png | Bin 0 -> 8994 bytes 10 files changed, 150 insertions(+), 40 deletions(-) delete mode 100644 docs/_overwrites/Commands/CommandContext.Overwrite.md rename docs/_overwrites/Commands/{CommandContext.Inclusion.md => ICommandContext.Inclusion.md} (100%) create mode 100644 docs/_overwrites/Commands/ICommandContext.Overwrite.md delete mode 100644 docs/_overwrites/Common/Discord.EmbedObjectBuilder.Overwrites.md rename docs/_overwrites/Common/{Discord.EmbedBuilder.Overwrites.md => EmbedBuilder.Overwrites.md} (100%) rename docs/_overwrites/Common/{Discord.EmbedObjectBuilder.Inclusion.md => EmbedObjectBuilder.Inclusion.md} (100%) create mode 100644 docs/_overwrites/Common/EmbedObjectBuilder.Overwrites.md create mode 100644 docs/_overwrites/Common/IEmote.Inclusion.md create mode 100644 docs/_overwrites/Common/IEmote.Overwrites.md create mode 100644 docs/_overwrites/Common/images/react-example.png diff --git a/docs/_overwrites/Commands/CommandContext.Overwrite.md b/docs/_overwrites/Commands/CommandContext.Overwrite.md deleted file mode 100644 index 2f8c0cd48..000000000 --- a/docs/_overwrites/Commands/CommandContext.Overwrite.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -uid: Discord.Commands.CommandContext -example: [*content] ---- - -[!include[Example Section](CommandContext.Inclusion.md)] - ---- -uid: Discord.Commands.SocketCommandContext -example: [*content] ---- - -[!include[Example Section](CommandContext.Inclusion.md)] - ---- -uid: Discord.Commands.ShardCommandContext -example: [*content] ---- - -[!include[Example Section](CommandContext.Inclusion.md)] \ No newline at end of file diff --git a/docs/_overwrites/Commands/CommandContext.Inclusion.md b/docs/_overwrites/Commands/ICommandContext.Inclusion.md similarity index 100% rename from docs/_overwrites/Commands/CommandContext.Inclusion.md rename to docs/_overwrites/Commands/ICommandContext.Inclusion.md 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/Common/Discord.EmbedObjectBuilder.Overwrites.md b/docs/_overwrites/Common/Discord.EmbedObjectBuilder.Overwrites.md deleted file mode 100644 index 85bf150b7..000000000 --- a/docs/_overwrites/Common/Discord.EmbedObjectBuilder.Overwrites.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -uid: Discord.EmbedAuthorBuilder -example: [*content] ---- - -[!include[Embed Object Builder Sample](Discord.EmbedObjectBuilder.Inclusion.md)] - ---- -uid: Discord.EmbedFooterBuilder -example: [*content] ---- - -[!include[Embed Object Builder Sample](Discord.EmbedObjectBuilder.Inclusion.md)] - ---- -uid: Discord.EmbedFieldBuilder -example: [*content] ---- - -[!include[Embed Object Builder Sample](Discord.EmbedObjectBuilder.Inclusion.md)] \ No newline at end of file diff --git a/docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md b/docs/_overwrites/Common/EmbedBuilder.Overwrites.md similarity index 100% rename from docs/_overwrites/Common/Discord.EmbedBuilder.Overwrites.md rename to docs/_overwrites/Common/EmbedBuilder.Overwrites.md diff --git a/docs/_overwrites/Common/Discord.EmbedObjectBuilder.Inclusion.md b/docs/_overwrites/Common/EmbedObjectBuilder.Inclusion.md similarity index 100% rename from docs/_overwrites/Common/Discord.EmbedObjectBuilder.Inclusion.md rename to docs/_overwrites/Common/EmbedObjectBuilder.Inclusion.md 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..9e4824c33 --- /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 effctively 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..b05ae2b32 --- /dev/null +++ b/docs/_overwrites/Common/IEmote.Overwrites.md @@ -0,0 +1,77 @@ +--- +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:`. + +This class represents a standard Unicode-based emoji. This type of emoji +can be created by passing the unicode into its 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/images/react-example.png b/docs/_overwrites/Common/images/react-example.png new file mode 100644 index 0000000000000000000000000000000000000000..822857d3d108f151a2c7b3e75614f8830373aa68 GIT binary patch literal 8994 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 IWt)5d2bHp1(f|Me literal 0 HcmV?d00001 From a4b48277d3843afdf3bd708db0a4abc54ef904c1 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Thu, 5 Apr 2018 16:46:02 +0800 Subject: [PATCH 083/183] Target .NS1.3 for Commands and Webhook --- src/Discord.Net.Commands/Discord.Net.Commands.csproj | 2 +- src/Discord.Net.Webhook/Discord.Net.Webhook.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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.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 From 771157dfb6e746d416a90f97e68cb0bc8df3f271 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Thu, 5 Apr 2018 18:25:21 +0800 Subject: [PATCH 084/183] Increase font-size to 15px --- docs/_template/dark/styles/docfx.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/_template/dark/styles/docfx.css b/docs/_template/dark/styles/docfx.css index 8ec20152a..703a09a0f 100644 --- a/docs/_template/dark/styles/docfx.css +++ b/docs/_template/dark/styles/docfx.css @@ -6,7 +6,8 @@ body { font-family: 'Titillium Web', 'Segoe UI', Tahoma, Helvetica, sans-serif; height: 100%; background: #212121; - color: #C0C0C0 + color: #C0C0C0; + font-size: 15px; } button, a { From 11495f2c2a00e3f790c40b2fab4a71647f486838 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Thu, 5 Apr 2018 18:41:28 +0800 Subject: [PATCH 085/183] Improve landing page & add intro --- docs/faq/getting-started.md | 33 +------------- .../{intro.md => first-bot.md} | 0 docs/guides/introduction/intro.md | 44 +++++++++++++++++++ docs/guides/toc.yml | 8 ++-- docs/index.md | 27 ++++++++---- 5 files changed, 67 insertions(+), 45 deletions(-) rename docs/guides/getting_started/{intro.md => first-bot.md} (100%) create mode 100644 docs/guides/introduction/intro.md diff --git a/docs/faq/getting-started.md b/docs/faq/getting-started.md index 1b1057bcf..eb6ff20c7 100644 --- a/docs/faq/getting-started.md +++ b/docs/faq/getting-started.md @@ -1,28 +1,5 @@ # Basic Concepts / Getting Started -## How do I 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](https://github.com/RogueException/Discord.Net/blob/dev/docs/guides/getting_started/samples/intro/structure.cs) - 2. [Official template](https://github.com/foxbot/DiscordBotBase/tree/csharp/src/DiscordBot) - -> [!TIP] -> 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. - -[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/ - ## How do I add my bot to my server/guild? You can do so by using the [permission calculator] provided @@ -73,12 +50,4 @@ Several common ways to do this: 2. Inspect the roles collection within the guild via your debugger. Please note that right-clicking on the role and copying the ID will -**not** work. It will only copy the message ID. - -## I have more questions! - -Please visit us at #dotnet_discord-net at [Discord API]. -Describe the problem in details to us, and preferably with the -problematic code uploaded onto [Hastebin](https://hastebin.com). - -[Discord API]: https://discord.gg/jkrBmQR \ No newline at end of file +**not** work. It will only copy the message ID. \ No newline at end of file diff --git a/docs/guides/getting_started/intro.md b/docs/guides/getting_started/first-bot.md similarity index 100% rename from docs/guides/getting_started/intro.md rename to docs/guides/getting_started/first-bot.md diff --git a/docs/guides/introduction/intro.md b/docs/guides/introduction/intro.md new file mode 100644 index 000000000..83620f100 --- /dev/null +++ b/docs/guides/introduction/intro.md @@ -0,0 +1,44 @@ +# Introduction + +## How do I 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](https://github.com/RogueException/Discord.Net/blob/dev/docs/guides/getting_started/samples/intro/structure.cs) +2. [Official template](https://github.com/foxbot/DiscordBotBase/tree/csharp/src/DiscordBot) + +> [!TIP] +> 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. + +[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/toc.yml b/docs/guides/toc.yml index 7e34a047b..1f3c06d70 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -1,9 +1,11 @@ +- name: Introduction + href: introduction/intro.md - name: Getting Started items: - name: Installation href: getting_started/installing.md - name: Your First Bot - href: getting_started/intro.md + href: getting_started/first-bot.md - name: Terminology href: getting_started/terminology.md - name: Basic Concepts @@ -23,6 +25,4 @@ - name: Post-execution Handling href: commands/post-execution.md - name: Voice - items: - - name: Voice Guide - href: voice/sending-voice.md + href: voice/sending-voice.md diff --git a/docs/index.md b/docs/index.md index ef9ecdfdd..22b15c5e2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,13 +1,22 @@ - # 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? + +Discord.NET is an asynchronous, multi-platform .NET Library used to +interface with the [Discord API](https://discordapp.com/). + +## Where to begin? + +If this is your first time using Discord.Net, you should refer to the +[Intro](guides/introduction/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. -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. +## Additional Resources -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 +- [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 From e124b651663814b150f7503b77e94ca19602685d Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Thu, 5 Apr 2018 19:22:42 +0800 Subject: [PATCH 086/183] Improve readability --- docs/faq/getting-started.md | 59 +++++++++++++++++++------------ docs/guides/introduction/intro.md | 2 +- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/docs/faq/getting-started.md b/docs/faq/getting-started.md index eb6ff20c7..c79fdbdec 100644 --- a/docs/faq/getting-started.md +++ b/docs/faq/getting-started.md @@ -13,41 +13,54 @@ users can be assigned to. [permission calculator]: https://finitereality.github.io/permissions-calculator -## What is a Client/User/Object ID? Is it the token? +## 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 -library require an ID to retrieve the said object. +Discord ecosystem require an ID to retrieve or identify the said +object. -There are 2 ways to obtain the said ID. +There are 2 common ways to obtain the said ID. - 1. Enable Discord's developer mode. With developer mode enabled, - you can - as an example - right click on a guild and copy the guild - id (please note that this does not apply to all objects, such as - Role IDs \[see below], or DM channel IDs). - ![Developer Mode](images/dev-mode.png) - 2. Escape the object using `\` in front the object. For example, - when you do `\@Example#1234` in chat, it will return the user ID of - the aforementioned user. +### [Discord Developer Mode](#tab/dev-mode) -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. +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. + +*** ## 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 +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. - -Please note that right-clicking on the role and copying the ID will -**not** work. It will only copy the message ID. \ No newline at end of file +2. Inspect the roles collection within the guild via your debugger. \ No newline at end of file diff --git a/docs/guides/introduction/intro.md b/docs/guides/introduction/intro.md index 83620f100..3af3d66f6 100644 --- a/docs/guides/introduction/intro.md +++ b/docs/guides/introduction/intro.md @@ -1,6 +1,6 @@ # Introduction -## How do I get started? +## 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, From 3ed6402c8bdf5aef892c5908c4559f4139122de2 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Thu, 5 Apr 2018 19:23:33 +0800 Subject: [PATCH 087/183] Add 'preemptive rate limit' section --- docs/faq/basic-operations.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/faq/basic-operations.md b/docs/faq/basic-operations.md index c91f155e6..89a826ee0 100644 --- a/docs/faq/basic-operations.md +++ b/docs/faq/basic-operations.md @@ -49,9 +49,9 @@ various types of 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. + +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? @@ -73,6 +73,14 @@ implement [IEmote] and are valid options. [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 @@ -84,7 +92,6 @@ reactions. 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 From ecda318ae88275e22436a797bafdb227f4c09628 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Thu, 5 Apr 2018 23:50:22 +0800 Subject: [PATCH 088/183] Add titles and UIDs to each article --- .../Commands/DontInjectAttribute.Overwrite.md | 2 +- docs/api/index.md | 19 +++++++----- docs/faq/{ => basics}/basic-operations.md | 11 +++++-- docs/faq/{ => basics}/client-basics.md | 7 ++++- docs/faq/{ => basics}/getting-started.md | 5 ++++ docs/faq/{ => basics}/images/dev-mode.png | Bin docs/faq/{ => basics}/images/snowflake.png | Bin .../basics => basics/samples}/cast.cs | 0 .../basics => basics/samples}/emoji.cs | 0 docs/faq/{ => commands}/Commands.md | 15 ++++++---- .../commands => commands/samples}/DI.cs | 0 .../samples}/Remainder.cs | 0 .../samples}/runmode-cmdattrib.cs | 0 .../samples}/runmode-cmdconfig.cs | 0 docs/faq/{ => misc}/Glossary.md | 5 ++++ docs/faq/{ => misc}/Legacy.md | 5 ++++ docs/faq/toc.yml | 12 ++++---- docs/guides/commands/commands.md | 5 ++++ docs/guides/commands/post-execution.md | 9 ++++-- docs/guides/concepts/connections.md | 28 +++++++++--------- docs/guides/concepts/entities.md | 9 +++--- docs/guides/concepts/events.md | 13 ++++---- docs/guides/concepts/logging.md | 9 +++--- docs/guides/getting_started/first-bot.md | 17 ++++++----- docs/guides/getting_started/installing.md | 1 + docs/guides/getting_started/terminology.md | 2 +- docs/guides/introduction/intro.md | 5 ++++ docs/guides/toc.yml | 22 +++++++------- docs/guides/voice/sending-voice.md | 1 + docs/index.md | 12 ++++++-- docs/toc.yml | 4 ++- 31 files changed, 140 insertions(+), 78 deletions(-) rename docs/faq/{ => basics}/basic-operations.md (94%) rename docs/faq/{ => basics}/client-basics.md (92%) rename docs/faq/{ => basics}/getting-started.md (96%) rename docs/faq/{ => basics}/images/dev-mode.png (100%) rename docs/faq/{ => basics}/images/snowflake.png (100%) rename docs/faq/{samples/basics => basics/samples}/cast.cs (100%) rename docs/faq/{samples/basics => basics/samples}/emoji.cs (100%) rename docs/faq/{ => commands}/Commands.md (93%) rename docs/faq/{samples/commands => commands/samples}/DI.cs (100%) rename docs/faq/{samples/commands => commands/samples}/Remainder.cs (100%) rename docs/faq/{samples/commands => commands/samples}/runmode-cmdattrib.cs (100%) rename docs/faq/{samples/commands => commands/samples}/runmode-cmdconfig.cs (100%) rename docs/faq/{ => misc}/Glossary.md (96%) rename docs/faq/{ => misc}/Legacy.md (89%) diff --git a/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md b/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md index 2d9dd6e9a..e395f2058 100644 --- a/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md +++ b/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md @@ -6,7 +6,7 @@ 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 [Dependency Injection](../../guides/commands/commands.md#dependency-injection) +dependency. See [Dependency Injection](xref:Guides.Commands.Intro#dependency-injection) to learn more. --- 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/faq/basic-operations.md b/docs/faq/basics/basic-operations.md similarity index 94% rename from docs/faq/basic-operations.md rename to docs/faq/basics/basic-operations.md index 89a826ee0..0db4a577b 100644 --- a/docs/faq/basic-operations.md +++ b/docs/faq/basics/basic-operations.md @@ -1,3 +1,8 @@ +--- +uid: FAQ.Basics.BasicOp +title: Questions about Basic Operations +--- + # Basic Operations Questions ## How should I safely check a type? @@ -15,7 +20,7 @@ action. A good and safe casting example: -[!code-csharp[Casting](samples/basics/cast.cs)] +[!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 @@ -44,7 +49,7 @@ able to message. You may check the message channel type. Visit [Glossary] to see the various types of channels. -[Glossary]: Glossary.md#message-channels +[Glossary]: xref:FAQ.Misc.Glossary#message-channels ## How can I get the guild from a message? @@ -69,7 +74,7 @@ In Discord.Net, an Emote represents a server custom emote, while an Emoji is a Unicode emoji (standard emoji). Both [Emoji] and [Emote] implement [IEmote] and are valid options. -[!code-csharp[Emoji](samples/basics/emoji.cs)] +[!code-csharp[Emoji](samples/emoji.cs)] [AddReactionAsync]: xref:Discord.IUserMessage.AddReactionAsync* diff --git a/docs/faq/client-basics.md b/docs/faq/basics/client-basics.md similarity index 92% rename from docs/faq/client-basics.md rename to docs/faq/basics/client-basics.md index 8c56e86fd..9f088e8a7 100644 --- a/docs/faq/client-basics.md +++ b/docs/faq/basics/client-basics.md @@ -1,3 +1,8 @@ +--- +uid: FAQ.Basics.ClientBasics +title: Basic Questions about Client +--- + # Client Basics Questions ## My client keeps returning 401 upon logging in! @@ -42,7 +47,7 @@ event is triggered, then you can proceed to do whatever you like. 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](../guides/concepts/events.md#cacheable). +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 diff --git a/docs/faq/getting-started.md b/docs/faq/basics/getting-started.md similarity index 96% rename from docs/faq/getting-started.md rename to docs/faq/basics/getting-started.md index c79fdbdec..533adae04 100644 --- a/docs/faq/getting-started.md +++ b/docs/faq/basics/getting-started.md @@ -1,3 +1,8 @@ +--- +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? diff --git a/docs/faq/images/dev-mode.png b/docs/faq/basics/images/dev-mode.png similarity index 100% rename from docs/faq/images/dev-mode.png rename to docs/faq/basics/images/dev-mode.png diff --git a/docs/faq/images/snowflake.png b/docs/faq/basics/images/snowflake.png similarity index 100% rename from docs/faq/images/snowflake.png rename to docs/faq/basics/images/snowflake.png diff --git a/docs/faq/samples/basics/cast.cs b/docs/faq/basics/samples/cast.cs similarity index 100% rename from docs/faq/samples/basics/cast.cs rename to docs/faq/basics/samples/cast.cs diff --git a/docs/faq/samples/basics/emoji.cs b/docs/faq/basics/samples/emoji.cs similarity index 100% rename from docs/faq/samples/basics/emoji.cs rename to docs/faq/basics/samples/emoji.cs diff --git a/docs/faq/Commands.md b/docs/faq/commands/Commands.md similarity index 93% rename from docs/faq/Commands.md rename to docs/faq/commands/Commands.md index bc902811b..374372d6d 100644 --- a/docs/faq/Commands.md +++ b/docs/faq/commands/Commands.md @@ -1,3 +1,8 @@ +--- +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? @@ -35,7 +40,7 @@ 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/commands/Remainder.cs)] +[!code-csharp[Remainder](samples/Remainder.cs)] [RemainderAttribute]: xref:Discord.Commands.RemainderAttribute @@ -52,11 +57,11 @@ 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](../guides/commands/commands.md#usage-in-modules). +as how it works in [Discord.NET](xref:Guides.Commands.Intro#usage-in-modules). A brief example of service and dependency injection can be seen below. -[!code-csharp[DI](samples/commands/DI.cs)] +[!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 @@ -83,11 +88,11 @@ the [DefaultRunMode] flag under `CommandServiceConfig`. # [CommandAttribute](#tab/cmdattrib) -[!code-csharp[Command Attribute](samples/commands/runmode-cmdattrib.cs)] +[!code-csharp[Command Attribute](samples/runmode-cmdattrib.cs)] # [CommandServiceConfig](#tab/cmdconfig) -[!code-csharp[Command Service Config](samples/commands/runmode-cmdconfig.cs)] +[!code-csharp[Command Service Config](samples/runmode-cmdconfig.cs)] *** diff --git a/docs/faq/samples/commands/DI.cs b/docs/faq/commands/samples/DI.cs similarity index 100% rename from docs/faq/samples/commands/DI.cs rename to docs/faq/commands/samples/DI.cs diff --git a/docs/faq/samples/commands/Remainder.cs b/docs/faq/commands/samples/Remainder.cs similarity index 100% rename from docs/faq/samples/commands/Remainder.cs rename to docs/faq/commands/samples/Remainder.cs diff --git a/docs/faq/samples/commands/runmode-cmdattrib.cs b/docs/faq/commands/samples/runmode-cmdattrib.cs similarity index 100% rename from docs/faq/samples/commands/runmode-cmdattrib.cs rename to docs/faq/commands/samples/runmode-cmdattrib.cs diff --git a/docs/faq/samples/commands/runmode-cmdconfig.cs b/docs/faq/commands/samples/runmode-cmdconfig.cs similarity index 100% rename from docs/faq/samples/commands/runmode-cmdconfig.cs rename to docs/faq/commands/samples/runmode-cmdconfig.cs diff --git a/docs/faq/Glossary.md b/docs/faq/misc/Glossary.md similarity index 96% rename from docs/faq/Glossary.md rename to docs/faq/misc/Glossary.md index 096bcc0ab..32246886d 100644 --- a/docs/faq/Glossary.md +++ b/docs/faq/misc/Glossary.md @@ -1,3 +1,8 @@ +--- +uid: FAQ.Misc.Glossary +title: Common Terminologies / Glossary +--- + # Glossary ## Common Types diff --git a/docs/faq/Legacy.md b/docs/faq/misc/Legacy.md similarity index 89% rename from docs/faq/Legacy.md rename to docs/faq/misc/Legacy.md index 925ee45b4..63656654e 100644 --- a/docs/faq/Legacy.md +++ b/docs/faq/misc/Legacy.md @@ -1,3 +1,8 @@ +--- +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. diff --git a/docs/faq/toc.yml b/docs/faq/toc.yml index bce0aaf87..1a6cd21bb 100644 --- a/docs/faq/toc.yml +++ b/docs/faq/toc.yml @@ -1,18 +1,18 @@ - name: Basic Concepts items: - name: Getting Started - href: getting-started.md + topicUid: FAQ.Basics.GetStarted - name: Basic Operations - href: basic-operations.md + topicUid: FAQ.Basics.BasicOp - name: Client Basics - href: client-basics.md + topicUid: FAQ.Basics.ClientBasics - name: Command items: - name: Commands - href: commands.md + topicUid: FAQ.Commands - name: Misc items: - name: Glossary - href: glossary.md + topicUid: FAQ.Misc.Glossary - name: Legacy or Upgrade - href: legacy.md + topicUid: FAQ.Misc.Legacy diff --git a/docs/guides/commands/commands.md b/docs/guides/commands/commands.md index 4999e2b9d..6ee262289 100644 --- a/docs/guides/commands/commands.md +++ b/docs/guides/commands/commands.md @@ -1,3 +1,8 @@ +--- +uid: Guides.Commands.Intro +title: Introduction to the Command Service +--- + # The Command Service [Discord.Commands](xref:Discord.Commands) provides an attribute-based diff --git a/docs/guides/commands/post-execution.md b/docs/guides/commands/post-execution.md index b9f0427ef..86a97a320 100644 --- a/docs/guides/commands/post-execution.md +++ b/docs/guides/commands/post-execution.md @@ -1,3 +1,8 @@ +--- +uid: Guides.Commands.PostExecution +title: Post-command Execution Handling +--- + # Preface When developing a command system or modules, you may want to consider @@ -25,7 +30,7 @@ issue, working with `RunMode.Async`. If your command is marked with `RunMode.Async`, [ExecuteAsync] will return a successful [ExecuteResult] instead of whatever results the actual command may return. Because of the way `RunMode.Async` -[works](../../faq/commands.md), handling within the command handler +[works](xref:FAQ.Commands), handling within the command handler may not always achieve the desired effect. ## CommandExecuted Event @@ -38,4 +43,4 @@ sucessfully executed** and is not prone to `RunMode.Async`'s [CommandExecuted]: xref:Discord.Commands.CommandService.CommandExecuted [ExecuteAsync]: xref:Discord.Commands.CommandService.ExecuteAsync* [ExecuteResult]: xref:Discord.Commands.ExecuteResult -[Command Guide]: Commands.md \ No newline at end of file +[Command Guide]: xref:Guides.Commands.Intro \ No newline at end of file diff --git a/docs/guides/concepts/connections.md b/docs/guides/concepts/connections.md index b36774d60..324b67566 100644 --- a/docs/guides/concepts/connections.md +++ b/docs/guides/concepts/connections.md @@ -1,21 +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. - -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 @@ -24,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 @@ -52,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 +[events]: xref:Guides.Concepts.Events diff --git a/docs/guides/concepts/entities.md b/docs/guides/concepts/entities.md index 49139cecf..7747291c6 100644 --- a/docs/guides/concepts/entities.md +++ b/docs/guides/concepts/entities.md @@ -1,4 +1,5 @@ --- +uid: Guides.Concepts.Entities title: Entities --- @@ -11,7 +12,7 @@ title: 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`. @@ -30,14 +31,14 @@ 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 +## 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 @@ -56,6 +57,6 @@ entities, e.g. `SocketGuild.GetUser`, which returns a `SocketGuildChannel`. Again, you may need to cast these objects to get a variant of the type that you need. -### Sample +## Sample [!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 758e5e6d8..a6a65f439 100644 --- a/docs/guides/concepts/events.md +++ b/docs/guides/concepts/events.md @@ -1,4 +1,5 @@ --- +uid: Guides.Concepts.Events title: Working with Events --- @@ -12,7 +13,7 @@ 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. @@ -20,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 @@ -41,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, @@ -51,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 @@ -60,7 +61,7 @@ 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 @@ -78,6 +79,6 @@ object. > **not** enabled by default. Set the `MessageCacheSize` flag in > @Discord.WebSocket.DiscordSocketConfig to enable it. -### Sample +## Sample [!code-csharp[Event Sample](samples/events.cs)] diff --git a/docs/guides/concepts/logging.md b/docs/guides/concepts/logging.md index 52fccd02b..21565c828 100644 --- a/docs/guides/concepts/logging.md +++ b/docs/guides/concepts/logging.md @@ -1,4 +1,5 @@ --- +uid: Guides.Concepts.Logging title: Logging --- @@ -10,7 +11,7 @@ dispatched over. For more information about events in Discord.Net, see the [Events] section. -[Events]: events.md +[Events]: xref:Guides.Concepts.Events > [!WARNING] > Due to the nature of Discord.Net's event system, all log event @@ -19,7 +20,7 @@ section. > 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 in Client(s) +## Usage in Client(s) To receive log events, simply hook the Discord client's @Discord.Rest.BaseDiscordClient.Log to a `Task` with a single parameter of type [LogMessage]. @@ -30,7 +31,7 @@ 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 @Discord.Commands.CommandService.Log event, identical in signature to other log events. @@ -42,6 +43,6 @@ and error can be found and handled. [CommandService]: xref:Discord.Commands.CommandService [CommandException]: xref:Discord.Commands.CommandException -### Sample +## Sample [!code-csharp[Logging Sample](samples/logging.cs)] diff --git a/docs/guides/getting_started/first-bot.md b/docs/guides/getting_started/first-bot.md index 7afc045ec..19af2de0e 100644 --- a/docs/guides/getting_started/first-bot.md +++ b/docs/guides/getting_started/first-bot.md @@ -1,5 +1,6 @@ --- -title: Getting Started +uid: Guides.GettingStarted.FirstBot +title: Start making a bot --- # Making a Ping-Pong bot @@ -25,8 +26,8 @@ account on Discord. ![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!** +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 @@ -55,7 +56,7 @@ through the OAuth2 flow. ## Connecting to Discord If you have not already created a project and installed Discord.Net, -do that now. (see the [Installing](installing.md) section) +do that now. (see the [Installing](xref:Guides.GettingStarted.Installation) section) ### Async @@ -109,7 +110,7 @@ the Console. 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 +entities. See the [terminology](xref:Guides.GettingStarted.Terminology) if you're unsure of the differences. To do so, create an instance of [DiscordSocketClient] in your async @@ -161,7 +162,7 @@ online in Discord. > for how to fix this. [DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient -[installation guide]: installing.md#installing-on-net-standard-11 +[installation guide]: xref:Guides.GettingStarted.Installation#installing-on-net-standard-11 ### Handling a 'ping' @@ -213,12 +214,12 @@ For your reference, you may view the [completed program]. [SocketMessage]: xref:Discord.WebSocket.SocketMessage [SocketMessageChannel]: xref:Discord.WebSocket.ISocketMessageChannel [completed program]: samples/intro/complete.cs -[Command Guide]: ../commands/commands.md +[Command Guide]: xref:Guides.Commands.Intro # 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 +[Commands](xref:Guides.Commands.Intro). Note that we will not be explaining _how_ to write Commands or Services, it will only be covering the general structure. diff --git a/docs/guides/getting_started/installing.md b/docs/guides/getting_started/installing.md index e2cee0d5b..1e22f0199 100644 --- a/docs/guides/getting_started/installing.md +++ b/docs/guides/getting_started/installing.md @@ -1,4 +1,5 @@ --- +uid: Guides.GettingStarted.Installation title: Installing Discord.Net --- diff --git a/docs/guides/getting_started/terminology.md b/docs/guides/getting_started/terminology.md index 5685477dd..7bbec23b4 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 --- diff --git a/docs/guides/introduction/intro.md b/docs/guides/introduction/intro.md index 3af3d66f6..9350872ee 100644 --- a/docs/guides/introduction/intro.md +++ b/docs/guides/introduction/intro.md @@ -1,3 +1,8 @@ +--- +uid: Guides.Introduction +title: Introduction to Discord.NET +--- + # Introduction ## Looking to get started? diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 1f3c06d70..43f221c17 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -1,28 +1,28 @@ - name: Introduction - href: introduction/intro.md + 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/first-bot.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: The Command Service items: - name: Command Guide - href: commands/commands.md + topicUid: Guides.Commands.Intro - name: Post-execution Handling - href: commands/post-execution.md + topicUid: Guides.Commands.PostExecution - name: Voice - href: voice/sending-voice.md + topicUid: Guides.Voice.SendingVoice diff --git a/docs/guides/voice/sending-voice.md b/docs/guides/voice/sending-voice.md index 1b47dc2f8..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 --- diff --git a/docs/index.md b/docs/index.md index 22b15c5e2..5548afbb5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,8 @@ +--- +uid: Root.Landing +title: Home +--- + # Discord.Net Documentation ## What is Discord.NET? @@ -8,9 +13,10 @@ interface with the [Discord API](https://discordapp.com/). ## Where to begin? If this is your first time using Discord.Net, you should refer to the -[Intro](guides/introduction/intro.md) for tutorials. -More experienced users might refer to the -[API Documentation](api/index.md) for a breakdown of the individuals +[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 diff --git a/docs/toc.yml b/docs/toc.yml index d869eed59..337294b1a 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,7 +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 From 96b95796a16ec934072451a7e29d2f58cba79d41 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 6 Apr 2018 11:58:57 +0800 Subject: [PATCH 089/183] Add examples for precondition attributes --- .../PreconditionAttribute.Overwrites.md | 83 ++++++++++++++++++- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md b/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md index 343547b47..8117b2c3f 100644 --- a/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md +++ b/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md @@ -8,7 +8,7 @@ remarks: *content This precondition attribute can be applied on module-level or method-level for a command. -[!include[Remarks(PreconditionAttribute.Remarks.Inclusion.md)]] +[!include[Additional Remarks](PreconditionAttribute.Remarks.Inclusion.md)] --- uid: Discord.Commands.ParameterPreconditionAttribute @@ -20,11 +20,88 @@ remarks: *content This precondition attribute can be applied on parameter-level for a command. -[!include[Remarks(PreconditionAttribute.Remarks.Inclusion.md)]] +[!include[Additional Remarks](PreconditionAttribute.Remarks.Inclusion.md)] --- uid: Discord.Commands.PreconditionAttribute example: [*content] --- -// todo: add example \ No newline at end of file +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 From 50e830e826c7aabe7f94da313d2c218b9736c4a6 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 6 Apr 2018 14:06:38 +0800 Subject: [PATCH 090/183] Simplify CSS + Changes dark theme changes to a different css to respect future docfx.css changes + Merge featherlight plugin into the dark theme --- .../partials/head.tmpl.partial | 5 +- .../partials/scripts.tmpl.partial | 0 docs/_template/dark/styles/dark.css | 197 ++++ docs/_template/dark/styles/docfx.css | 989 ------------------ docs/_template/dark/styles/dracula.css | 1 - .../styles/plugin-featherlight.js | 0 docs/docfx.json | 3 +- docs/guides/getting_started/first-bot.md | 16 +- 8 files changed, 210 insertions(+), 1001 deletions(-) rename docs/_template/{lightbox-featherlight => dark}/partials/head.tmpl.partial (87%) rename docs/_template/{lightbox-featherlight => dark}/partials/scripts.tmpl.partial (100%) create mode 100644 docs/_template/dark/styles/dark.css delete mode 100644 docs/_template/dark/styles/docfx.css rename docs/_template/{lightbox-featherlight => dark}/styles/plugin-featherlight.js (100%) diff --git a/docs/_template/lightbox-featherlight/partials/head.tmpl.partial b/docs/_template/dark/partials/head.tmpl.partial similarity index 87% rename from docs/_template/lightbox-featherlight/partials/head.tmpl.partial rename to docs/_template/dark/partials/head.tmpl.partial index c330cda31..6cf00b897 100644 --- a/docs/_template/lightbox-featherlight/partials/head.tmpl.partial +++ b/docs/_template/dark/partials/head.tmpl.partial @@ -11,10 +11,13 @@ + + + {{#_noindex}}{{/_noindex}} {{#_enableSearch}}{{/_enableSearch}} {{#_enableNewTab}}{{/_enableNewTab}} - \ No newline at end of file + diff --git a/docs/_template/lightbox-featherlight/partials/scripts.tmpl.partial b/docs/_template/dark/partials/scripts.tmpl.partial similarity index 100% rename from docs/_template/lightbox-featherlight/partials/scripts.tmpl.partial rename to docs/_template/dark/partials/scripts.tmpl.partial diff --git a/docs/_template/dark/styles/dark.css b/docs/_template/dark/styles/dark.css new file mode 100644 index 000000000..c92d777bc --- /dev/null +++ b/docs/_template/dark/styles/dark.css @@ -0,0 +1,197 @@ +/* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. */ +@import url('https://fonts.googleapis.com/css?family=Titillium+Web'); +html, +body { + font-family: 'Titillium Web', 'Segoe UI', Tahoma, Helvetica, sans-serif; + height: 100%; + background: #212121; + color: #C0C0C0; + font-size: 15px; +} +button, +a { + color: #64B5F6; +} +button:hover, +button:focus, +a:hover, +a:focus { + color: #2196F3; +} +a.disable, +a.disable:hover { + color: #EEEEEE; +} +.divider { + color: #37474F; +} +hr { + border-color: #37474F; +} +header .navbar { + border-width: 0 0 0px; + border-radius: 0; +} +.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; +} + +body .toc{ + background-color: inherit; + overflow: visible; +} + +.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 { + 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 +} +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) +} diff --git a/docs/_template/dark/styles/docfx.css b/docs/_template/dark/styles/docfx.css deleted file mode 100644 index 703a09a0f..000000000 --- a/docs/_template/dark/styles/docfx.css +++ /dev/null @@ -1,989 +0,0 @@ -/* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. */ -@import url('https://fonts.googleapis.com/css?family=Titillium+Web'); -@import url("dracula.css"); -html, -body { - font-family: 'Titillium Web', 'Segoe UI', Tahoma, Helvetica, sans-serif; - height: 100%; - background: #212121; - color: #C0C0C0; - font-size: 15px; -} -button, -a { - color: #64B5F6; - cursor: pointer; -} -button:hover, -button:focus, -a:hover, -a:focus { - color: #2196F3; - text-decoration: none; -} -a.disable, -a.disable:hover { - text-decoration: none; - cursor: default; - color: #EEEEEE; -} - -/* workaround for leave space for fixed navbar with # anchor url*/ - -h1:before, -h2:before, -h3:before, -h4:before { - content: ''; - display: block; - position: relative; - width: 0; - height: 100px; - margin-top: -100px; -} - -h1, h2, h3, h4, h5, h6, .text-break { - word-wrap: break-word; - word-break: break-word; -} - -h1 mark, -h2 mark, -h3 mark, -h4 mark, -h5 mark, -h6 mark { - padding: 0; -} - -.inheritance .level0:before, -.inheritance .level1:before, -.inheritance .level2:before, -.inheritance .level3:before, -.inheritance .level4:before, -.inheritance .level5:before { - content: '↳'; - margin-right: 5px; -} - -.inheritance .level0 { - margin-left: 0em; -} - -.inheritance .level1 { - margin-left: 1em; -} - -.inheritance .level2 { - margin-left: 2em; -} - -.inheritance .level3 { - margin-left: 3em; -} - -.inheritance .level4 { - margin-left: 4em; -} - -.inheritance .level5 { - margin-left: 5em; -} - -span.parametername, -span.paramref, -span.typeparamref { - font-style: italic; -} -span.languagekeyword{ - font-weight: bold; -} - -svg:hover path { - fill: #ffffff; -} - -.hljs { - display: inline; - background-color: inherit; - padding: 0; -} -/* additional spacing fixes */ -.btn + .btn { - margin-left: 10px; -} -.btn.pull-right { - margin-left: 10px; - margin-top: 5px; -} -.table { - margin-bottom: 10px; -} -table p { - margin-bottom: 0; -} -table a { - display: inline-block; -} - -/* Make hidden attribute compatible with old browser.*/ -[hidden] { - display: none !important; -} - -h1, -.h1, -h2, -.h2, -h3, -.h3 { - margin-top: 15px; - margin-bottom: 10px; - font-weight: 400; -} -h4, -.h4, -h5, -.h5, -h6, -.h6 { - margin-top: 10px; - margin-bottom: 5px; -} -.navbar { - margin-bottom: 0; -} -#wrapper { - min-height: 100%; - position: relative; -} -/* blends header footer and content together with gradient effect */ -.grad-top { - /* For Safari 5.1 to 6.0 */ - /* For Opera 11.1 to 12.0 */ - /* For Firefox 3.6 to 15 */ - background: linear-gradient(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0)); - /* Standard syntax */ - height: 5px; -} -.grad-bottom { - /* For Safari 5.1 to 6.0 */ - /* For Opera 11.1 to 12.0 */ - /* For Firefox 3.6 to 15 */ - background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.05)); - /* Standard syntax */ - height: 5px; -} -.divider { - margin: 0 5px; - color: #37474F; -} -hr { - border-color: #37474F; -} -header { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 1000; -} -header .navbar { - border-width: 0 0 0px; - border-radius: 0; -} -.navbar-brand { - font-size: inherit; - padding: 0; -} -.navbar-collapse { - margin: 0 -15px; -} -.subnav { - min-height: 40px; - background: #383838 -} - -.inheritance h5, .inheritedMembers h5{ - padding-bottom: 5px; - border-bottom: 1px solid #37474F; -} - -article h1, article h2, article h3, article h4{ - margin-top: 25px; -} - -article h4{ - border-bottom: 1px solid #37474F; -} - -article span.small.pull-right{ - margin-top: 20px; -} - -article section { - margin-left: 1em; -} - -/*.expand-all { - padding: 10px 0; -}*/ -.breadcrumb { - margin: 0; - padding: 10px 0; - background-color: inherit; - white-space: nowrap; -} -.breadcrumb > li + li:before { - content: "\00a0/"; -} -#autocollapse.collapsed .navbar-header { - float: none; -} -#autocollapse.collapsed .navbar-toggle { - display: block; -} -#autocollapse.collapsed .navbar-collapse { - border-top: 1px solid transparent; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); -} -#autocollapse.collapsed .navbar-collapse.collapse { - display: none !important; -} -#autocollapse.collapsed .navbar-nav { - float: none !important; - margin: 7.5px -15px; -} -#autocollapse.collapsed .navbar-nav > li { - float: none; -} -#autocollapse.collapsed .navbar-nav > li > a { - padding-top: 10px; - padding-bottom: 10px; -} -#autocollapse.collapsed .collapse.in, -#autocollapse.collapsed .collapsing { - display: block !important; -} -#autocollapse.collapsed .collapse.in .navbar-right, -#autocollapse.collapsed .collapsing .navbar-right { - float: none !important; -} -#autocollapse .form-group { - width: 100%; -} -#autocollapse .form-control { - width: 100%; -} -#autocollapse .navbar-header { - margin-left: 0; - margin-right: 0; -} -#autocollapse .navbar-brand { - margin-left: 0; -} -.collapse.in, -.collapsing { - text-align: center; -} -.collapsing .navbar-form { - margin: 0 auto; - max-width: 400px; - padding: 10px 15px; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); -} -.collapsed .collapse.in .navbar-form { - margin: 0 auto; - max-width: 400px; - padding: 10px 15px; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); -} -.navbar .navbar-nav { - display: inline-block; -} -.docs-search { - background: #424242; - vertical-align: middle; -} -.docs-search > .search-query { - font-size: 14px; - border: 0; - width: 120%; -} -.docs-search > .search-query:focus { - outline: 0; -} -.search-results-frame { - clear: both; - display: table; - width: 100%; -} -.search-results.ng-hide { - display: none; -} -.search-results-container { - padding-bottom: 1em; - border-top: 1px solid #111; - background: rgba(25, 25, 25, 0.5); -} -.search-results-container .search-results-group { - padding-top: 50px !important; - padding: 10px; -} -.search-results-group-heading { - font-family: "Open Sans"; - padding-left: 10px; - color: #424242; -} -.search-close { - position: absolute; - left: 50%; - margin-left: -100px; - color: #424242; - text-align: center; - padding: 5px; - background: #333; - border-top-right-radius: 5px; - border-top-left-radius: 5px; - width: 200px; - box-shadow: 0 0 10px #111; -} -#search { - display: none; -} - -/* Search results display*/ -#search-results { - max-width: 960px !important; - margin-top: 120px; - margin-bottom: 115px; - margin-left: auto; - margin-right: auto; - line-height: 1.8; - display: none; -} - -#search-results>.search-list { - text-align: center; - font-size: 2.5rem; - margin-bottom: 50px; -} - -#search-results p { - text-align: center; -} - -#search-results .sr-items { - font-size: 24px; -} - -.sr-item { - margin-bottom: 25px; -} - -.sr-item>.item-href { - font-size: 14px; - color: #093; -} - -.sr-item>.item-brief { - font-size: 13px; -} - -.pagination>li>a { - color: #47A7A0 -} - -.pagination>.active>a { - background-color: #47A7A0; - border-color: #47A7A0; -} - -.fixed_header { - position: fixed; - width: 100%; - padding-bottom: 10px; - padding-top: 10px; - margin: 0px; - top: 0; - z-index: 9999; - left: 0; -} - -.fixed_header+.toc{ - margin-top: 50px; - margin-left: 0; -} - -.sidetoc { - position: fixed; - width: 260px; - top: 150px; - bottom: 0; - overflow-x: hidden; - overflow-y: auto; - background-color: #1b1b1b; - border-left: 0px solid #37474F; - border-right: 0px solid #37474F; - z-index: 1; -} - -.sidetoc.shiftup { - bottom: 70px; -} - -body .toc{ - background-color: inherit; -} - -.sidetoggle.ng-hide { - display: block !important; -} -.sidetoc-expand > .caret { - margin-left: 0px; - margin-top: -2px; -} -.sidetoc-expand > .caret-side { - border-left: 4px solid; - border-top: 4px solid transparent; - border-bottom: 4px solid transparent; - margin-left: 4px; - margin-top: -4px; -} -.sidetoc-heading { - font-weight: 500; -} - -.toc { - margin: 0px 0 0 10px; - padding: 0 10px; -} -.expand-stub { - position: absolute; - left: -10px; -} -.toc .nav > li > a.sidetoc-expand { - position: absolute; - top: 0; - left: 0; -} -.toc .nav > li > a { - color: rgb(218, 218, 218); - margin-left: 5px; - display: block; - padding: 0; -} -.toc .nav > li > a:hover, -.toc .nav > li > a:focus { - color: #E0E0E0; - background: none; - text-decoration: inherit; -} -.toc .nav > li.active > a { - color: #90CAF9; -} -.toc .nav > li.active > a:hover, -.toc .nav > li.active > a:focus { - color: #4FC3F7; -} - -.toc .nav > li> .expand-stub { - cursor: pointer; -} - -.toc .nav > li.active > .expand-stub::before, -.toc .nav > li.in > .expand-stub::before, -.toc .nav > li.in.active > .expand-stub::before, -.toc .nav > li.filtered > .expand-stub::before { - content: "-"; -} - -.toc .nav > li > .expand-stub::before, -.toc .nav > li.active > .expand-stub::before { - content: "+"; -} - -.toc .nav > li.filtered > ul, -.toc .nav > li.in > ul { - display: block; -} - -.toc .nav > li > ul { - display: none; -} - -.toc ul{ - font-size: 12px; - margin: 0 0 0 3px; -} - -.toc .level1 > li { - font-weight: bold; - margin-top: 10px; - position: relative; - font-size: 16px; -} -.toc .level2 { - font-weight: normal; - margin: 5px 0 0 15px; - font-size: 14px; -} -.toc-toggle { - display: none; - margin: 0 15px 0px 15px; -} -.sidefilter { - position: fixed; - top: 90px; - width: 260px; - background-color: #1b1b1b; - padding: 15px; - border-left: 0px solid #37474F; - border-right: 0px solid #37474F; - z-index: 1; -} -.toc-filter { - border-radius: 5px; - background: #fff; - color: #666666; - padding: 5px; - position: relative; - margin: 0 5px 0 5px; -} -.toc-filter > input { - border: 0; - color: #666666; - padding-left: 20px; - width: 100%; -} -.toc-filter > input:focus { - outline: 0; -} -.toc-filter > .filter-icon { - position: absolute; - top: 10px; - left: 5px; -} -.article { - margin-top: 120px; - margin-bottom: 115px; -} - -#_content>a{ - margin-top: 105px; -} - -.article.grid-right { - margin-left: 280px; -} - -.inheritance hr { - margin-top: 5px; - margin-bottom: 5px; -} -.article img { - max-width: 100%; -} -.sideaffix { - margin-top: 50px; - font-size: 12px; - max-height: 100%; - top: 100px; - bottom: 10px; - position: fixed; -} -.sideaffix.shiftup { - bottom: 70px; -} -.affix { - position: relative; - height: 100%; -} -.sideaffix > div.contribution { - margin-bottom: 20px; -} -.sideaffix > div.contribution > ul > li > a.contribution-link { - padding: 6px 10px; - font-weight: bold; - font-size: 14px; -} -.sideaffix > div.contribution > ul > li > a.contribution-link:hover { - background-color: #ffffff; -} -.sideaffix ul.nav > li > a:focus { - background: none; -} -.affix h5 { - font-weight: bold; - text-transform: uppercase; - padding-left: 10px; - font-size: 12px; -} -.affix > ul.level1 { - overflow: hidden; - padding-bottom: 10px; - height: calc(100% - 100px); - margin-right: -20px; -} -.affix ul > li > a:before { - color: #cccccc; - position: absolute; -} -.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 { - padding: 5px 12px; - color: #EEEEEE; -} -.affix > ul > li.active:last-child { - margin-bottom: 50px; -} -.affix > ul > li > a:before { - content: "|"; - font-size: 16px; - top: 1px; - left: 0; -} -.affix > ul > li.active > a, -.affix > ul > li.active > a:before { - color: #B3E5FC; - font-weight: bold; -} -.affix ul ul > li > a { - padding: 2px 15px; -} -.affix ul ul > li > a:before { - content: ">"; - font-size: 14px; - top: -1px; - left: 5px; -} -.affix ul > li > a:before, -.affix ul ul { - display: none; -} -.affix ul > li.active > ul, -.affix ul > li.active > a:before, -.affix ul > li > a:hover:before { - display: block; - white-space: nowrap; -} -.codewrapper { - position: relative; -} -.trydiv { - height: 0px; -} -.tryspan { - position: absolute; - top: 0px; - right: 0px; - border-style: solid; - border-radius: 0px 4px; - box-sizing: border-box; - border-width: 1px; - border-color: #37474F; - text-align: center; - padding: 2px 8px; - background-color: white; - font-size: 12px; - cursor: pointer; - z-index: 100; - display: none; - color: #767676; -} -.tryspan:hover { - background-color: #3b8bd0; - color: white; - border-color: #3b8bd0; -} -.codewrapper:hover .tryspan { - display: block; -} -.sample-response .response-content{ - max-height: 200px; -} -footer { - position: absolute; - left: 0; - right: 0; - bottom: 0; - z-index: 1000; -} -.footer { - border-top: 1px solid #5F5F5F; - background: #616161; - padding: 15px 0; -} -@media (min-width: 768px) { - #sidetoggle.collapse { - display: block; - } - .topnav .navbar-nav { - float: none; - white-space: nowrap; - } - .topnav .navbar-nav > li { - float: none; - display: inline-block; - } -} -@media only screen and (max-width: 768px) { - #mobile-indicator { - display: block; - } - /* TOC display for responsive */ - .article { - margin-top: 30px !important; - } - header { - position: static; - } - .topnav { - text-align: center; - } - .sidenav { - padding: 15px 0; - margin-left: -15px; - margin-right: -15px; - } - .sidefilter { - position: static; - width: auto; - float: none; - border: none; - } - .sidetoc { - position: static; - width: auto; - float: none; - padding-bottom: 0px; - border: none; - } - .toc .nav > li, .toc .nav > li >a { - display: inline-block; - } - .toc li:after { - margin-left: -3px; - margin-right: 5px; - content: ", "; - color: #666666; - } - .toc .level1 > li { - display: block; - } - - .toc .level1 > li:after { - display: none; - } - .article.grid-right { - margin-left: 0; - } - .grad-top, - .grad-bottom { - display: none; - } - .toc-toggle { - display: block; - } - .sidetoggle.ng-hide { - display: none !important; - } - /*.expand-all { - display: none; - }*/ - .sideaffix { - display: none; - } - .mobile-hide { - display: none; - } - .breadcrumb { - white-space: inherit; - } - - /* workaround for #hashtag url is no longer needed*/ - h1:before, - h2:before, - h3:before, - h4:before { - content: ''; - display: none; - } -} - -/* For toc iframe */ -@media (max-width: 260px) { - .toc .level2 > li { - display: block; - } - - .toc .level2 > li:after { - display: none; - } -} - -/* For code snippet line highlight */ -pre > code .line-highlight { - background-color: #ffffcc; -} - -/* Alerts */ -.alert h5 { - text-transform: uppercase; - font-weight: bold; - margin-top: 0; -} - -.alert h5:before { - position:relative; - top:1px; - display:inline-block; - font-family:'Glyphicons Halflings'; - line-height:1; - -webkit-font-smoothing:antialiased; - -moz-osx-font-smoothing:grayscale; - margin-right: 5px; - font-weight: normal; -} - -.alert-info { - color: #d9edf7; - background: #004458; - border-color: #005873 -} - -.alert-info h5:before { - content:"\e086" -} - -.alert-warning { - color: #fffaf2; - background: #80551a; - border-color: #99661f -} - -.alert-warning h5:before { - content:"\e127" -} - -.alert-danger { - color: #fff2f2; - background: #4d0000; - border-color: #660000 -} - -.alert-danger h5:before { - content:"\e107" -} - -/* For Embedded Video */ -div.embeddedvideo { - padding-top: 56.25%; - position: relative; - width: 100%; -} - -div.embeddedvideo iframe { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - width: 100%; - height: 100%; -} - -/* For printer */ -@media print{ - .article.grid-right { - margin-top: 0px; - margin-left: 0px; - } - .sideaffix { - display: none; - } - .mobile-hide { - display: none; - } - .footer { - display: none; - } -} - -/* 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 -} -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) -} diff --git a/docs/_template/dark/styles/dracula.css b/docs/_template/dark/styles/dracula.css index b87e33b10..155ad3c25 100644 --- a/docs/_template/dark/styles/dracula.css +++ b/docs/_template/dark/styles/dracula.css @@ -13,7 +13,6 @@ .hljs { display: block; overflow-x: auto; - padding: 0.5em; background: #282a36; } diff --git a/docs/_template/lightbox-featherlight/styles/plugin-featherlight.js b/docs/_template/dark/styles/plugin-featherlight.js similarity index 100% rename from docs/_template/lightbox-featherlight/styles/plugin-featherlight.js rename to docs/_template/dark/styles/plugin-featherlight.js diff --git a/docs/docfx.json b/docs/docfx.json index 78232becd..e84beb166 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -54,8 +54,7 @@ "dest":"_site", "template":[ "default", - "_template/dark", - "_template/lightbox-featherlight" + "_template/dark" ], "overwrite": "_overwrites/**/**.md", "globalMetadata":{ diff --git a/docs/guides/getting_started/first-bot.md b/docs/guides/getting_started/first-bot.md index 19af2de0e..6b30ca9aa 100644 --- a/docs/guides/getting_started/first-bot.md +++ b/docs/guides/getting_started/first-bot.md @@ -78,14 +78,14 @@ 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. +> [!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 From 746939cc5a947f41baf275c28d1fa0ecfccf9422 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 6 Apr 2018 16:01:15 +0800 Subject: [PATCH 091/183] Prep for theme switcher --- .../partials/head.tmpl.partial | 0 .../partials/scripts.tmpl.partial | 0 .../styles/dark.css | 0 .../styles/dracula.css | 0 .../light-dark-theme/styles/light.css | 197 ++++++++++++++++++ .../styles/plugin-featherlight.js | 0 docs/docfx.json | 2 +- 7 files changed, 198 insertions(+), 1 deletion(-) rename docs/_template/{dark => light-dark-theme}/partials/head.tmpl.partial (100%) rename docs/_template/{dark => light-dark-theme}/partials/scripts.tmpl.partial (100%) rename docs/_template/{dark => light-dark-theme}/styles/dark.css (100%) rename docs/_template/{dark => light-dark-theme}/styles/dracula.css (100%) create mode 100644 docs/_template/light-dark-theme/styles/light.css rename docs/_template/{dark => light-dark-theme}/styles/plugin-featherlight.js (100%) diff --git a/docs/_template/dark/partials/head.tmpl.partial b/docs/_template/light-dark-theme/partials/head.tmpl.partial similarity index 100% rename from docs/_template/dark/partials/head.tmpl.partial rename to docs/_template/light-dark-theme/partials/head.tmpl.partial diff --git a/docs/_template/dark/partials/scripts.tmpl.partial b/docs/_template/light-dark-theme/partials/scripts.tmpl.partial similarity index 100% rename from docs/_template/dark/partials/scripts.tmpl.partial rename to docs/_template/light-dark-theme/partials/scripts.tmpl.partial diff --git a/docs/_template/dark/styles/dark.css b/docs/_template/light-dark-theme/styles/dark.css similarity index 100% rename from docs/_template/dark/styles/dark.css rename to docs/_template/light-dark-theme/styles/dark.css diff --git a/docs/_template/dark/styles/dracula.css b/docs/_template/light-dark-theme/styles/dracula.css similarity index 100% rename from docs/_template/dark/styles/dracula.css rename to docs/_template/light-dark-theme/styles/dracula.css 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..3ccf50e2a --- /dev/null +++ b/docs/_template/light-dark-theme/styles/light.css @@ -0,0 +1,197 @@ +/* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. */ +@import url('https://fonts.googleapis.com/css?family=Titillium+Web'); +html, +body { + font-family: 'Titillium Web', 'Segoe UI', Tahoma, Helvetica, sans-serif; + height: 100%; + color: #212121; + background: #C0C0C0; + font-size: 15px; +} +button, +a { + color: #64B5F6; +} +button:hover, +button:focus, +a:hover, +a:focus { + color: #2196F3; +} +a.disable, +a.disable:hover { + color: #EEEEEE; +} +.divider { + color: #37474F; +} +hr { + border-color: #37474F; +} +header .navbar { + border-width: 0 0 0px; + border-radius: 0; +} +.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; +} + +body .toc{ + background-color: inherit; + overflow: visible; +} + +.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 { + 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 +} +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) +} diff --git a/docs/_template/dark/styles/plugin-featherlight.js b/docs/_template/light-dark-theme/styles/plugin-featherlight.js similarity index 100% rename from docs/_template/dark/styles/plugin-featherlight.js rename to docs/_template/light-dark-theme/styles/plugin-featherlight.js diff --git a/docs/docfx.json b/docs/docfx.json index e84beb166..42cbd56c1 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -54,7 +54,7 @@ "dest":"_site", "template":[ "default", - "_template/dark" + "_template/light-dark-theme" ], "overwrite": "_overwrites/**/**.md", "globalMetadata":{ From 6511b1ae4c2b5c0f9146e92f06372b93b70d96a0 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 6 Apr 2018 17:18:08 +0800 Subject: [PATCH 092/183] Implement rudimentary support for theme switching --- .../partials/footer.tmpl.partial | 19 ++ .../partials/head.tmpl.partial | 4 +- .../partials/scripts.tmpl.partial | 3 +- .../light-dark-theme/styles/dark.css | 4 +- .../light-dark-theme/styles/light.css | 175 +----------------- .../light-dark-theme/styles/styleswitcher.js | 69 +++++++ 6 files changed, 105 insertions(+), 169 deletions(-) create mode 100644 docs/_template/light-dark-theme/partials/footer.tmpl.partial create mode 100644 docs/_template/light-dark-theme/styles/styleswitcher.js diff --git a/docs/_template/light-dark-theme/partials/footer.tmpl.partial b/docs/_template/light-dark-theme/partials/footer.tmpl.partial new file mode 100644 index 000000000..1f6afa00e --- /dev/null +++ b/docs/_template/light-dark-theme/partials/footer.tmpl.partial @@ -0,0 +1,19 @@ +{{!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 index 6cf00b897..83c284fd7 100644 --- a/docs/_template/light-dark-theme/partials/head.tmpl.partial +++ b/docs/_template/light-dark-theme/partials/head.tmpl.partial @@ -11,8 +11,8 @@ - - + + diff --git a/docs/_template/light-dark-theme/partials/scripts.tmpl.partial b/docs/_template/light-dark-theme/partials/scripts.tmpl.partial index cd7d0074c..420568070 100644 --- a/docs/_template/light-dark-theme/partials/scripts.tmpl.partial +++ b/docs/_template/light-dark-theme/partials/scripts.tmpl.partial @@ -5,4 +5,5 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/docs/_template/light-dark-theme/styles/dark.css b/docs/_template/light-dark-theme/styles/dark.css index c92d777bc..a268ea4f8 100644 --- a/docs/_template/light-dark-theme/styles/dark.css +++ b/docs/_template/light-dark-theme/styles/dark.css @@ -1,5 +1,6 @@ /* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. */ @import url('https://fonts.googleapis.com/css?family=Titillium+Web'); +@import url('dracula.css'); html, body { font-family: 'Titillium Web', 'Segoe UI', Tahoma, Helvetica, sans-serif; @@ -177,7 +178,8 @@ body .toc{ code { color:white; background-color:#4a4c52; - border-radius:4px + border-radius:4px; + padding: 3px 7px; } pre { background-color: #282a36; diff --git a/docs/_template/light-dark-theme/styles/light.css b/docs/_template/light-dark-theme/styles/light.css index 3ccf50e2a..5b1d5abe9 100644 --- a/docs/_template/light-dark-theme/styles/light.css +++ b/docs/_template/light-dark-theme/styles/light.css @@ -4,193 +4,38 @@ html, body { font-family: 'Titillium Web', 'Segoe UI', Tahoma, Helvetica, sans-serif; height: 100%; - color: #212121; - background: #C0C0C0; + color: #000; + background: #fff; font-size: 15px; } -button, -a { - color: #64B5F6; -} -button:hover, -button:focus, -a:hover, -a:focus { - color: #2196F3; -} -a.disable, -a.disable:hover { - color: #EEEEEE; -} -.divider { - color: #37474F; -} -hr { - border-color: #37474F; -} header .navbar { border-width: 0 0 0px; border-radius: 0; } -.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; } - body .toc{ background-color: inherit; overflow: visible; } -.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 { - 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 -} -pre { - background-color: #282a36; + color:#222f3d; + background-color: #f9f9f9; + border-radius:4px; + padding: 3px 7px; } /* table */ .table-striped>tbody>tr:nth-of-type(odd) { - background-color:#333333; - color: #d3d3d3 + color:#333333; + background-color: #d3d3d3 } tbody>tr { - background-color:#424242; - color: #c0c0c0 + color:#424242; + background-color: #c0c0c0 } .table>tbody+tbody { border-top:2px solid rgb(173, 173, 173) 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..cd9807e3f --- /dev/null +++ b/docs/_template/light-dark-theme/styles/styleswitcher.js @@ -0,0 +1,69 @@ +function getSelectionChange(e){ + var selectValue = e.options[e.selectedIndex].value; + setActiveStyleSheet(selectValue); +} + +function setActiveStyleSheet(title) { + var i, a, main; + for(i=0; (a = document.getElementsByTagName("link")[i]); i++) { + if(a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title")) { + a.disabled = true; + if(a.getAttribute("title") == title) a.disabled = false; + } + } +} + +function getActiveStyleSheet() { + var i, a; + for(i=0; (a = document.getElementsByTagName("link")[i]); i++) { + if(a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title") && !a.disabled) return a.getAttribute("title"); + } + return null; +} + +function getPreferredStyleSheet() { + var i, a; + for(i=0; (a = document.getElementsByTagName("link")[i]); i++) { + if(a.getAttribute("rel").indexOf("style") != -1 + && a.getAttribute("rel").indexOf("alt") == -1 + && a.getAttribute("title") + ) return a.getAttribute("title"); + } + return null; +} + +function createCookie(name,value,days) { + if (days) { + var date = new Date(); + date.setTime(date.getTime()+(days*24*60*60*1000)); + var expires = "; expires="+date.toGMTString(); + } + else expires = ""; + document.cookie = name+"="+value+expires+"; path=/"; +} + +function readCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; +} + +window.onload = function(e) { + var cookie = readCookie("style"); + var title = cookie ? cookie : getPreferredStyleSheet(); + setActiveStyleSheet(title); +} + +window.onunload = function(e) { + var title = getActiveStyleSheet(); + createCookie("style", title, 365); +} + +var cookie = readCookie("style"); +var title = cookie ? cookie : getPreferredStyleSheet(); +setActiveStyleSheet(title); From 04b21a2f34745c9f0c521ad1f7071431e3fd2565 Mon Sep 17 00:00:00 2001 From: Hsu Still <341464@gmail.com> Date: Fri, 6 Apr 2018 18:14:28 +0800 Subject: [PATCH 093/183] Improve theme selection + Stylized option menu + The menu now defaults to whatever theme being the most preferred one --- .../partials/footer.tmpl.partial | 5 ++-- .../light-dark-theme/styles/dark.css | 25 +++++++++++++++++++ .../light-dark-theme/styles/light.css | 25 +++++++++++++++++++ .../light-dark-theme/styles/styleswitcher.js | 9 +++---- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/docs/_template/light-dark-theme/partials/footer.tmpl.partial b/docs/_template/light-dark-theme/partials/footer.tmpl.partial index 1f6afa00e..37efe0515 100644 --- a/docs/_template/light-dark-theme/partials/footer.tmpl.partial +++ b/docs/_template/light-dark-theme/partials/footer.tmpl.partial @@ -5,12 +5,11 @@ 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 0000000000000000000000000000000000000000..fd20b95d188751a2f3b4268e7ba7294b71afaf06 GIT binary patch literal 80742 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 B0lxqM literal 0 HcmV?d00001 diff --git a/docs/faq/basics/images/mention-escape.png b/docs/faq/basics/images/mention-escape.png new file mode 100644 index 0000000000000000000000000000000000000000..92797806155b8396211d6b0014d4f0e8d2aadfa9 GIT binary patch literal 19611 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 XrU2O)WYlV=ATMnV1NF+Mj{p7##ChBc literal 0 HcmV?d00001 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); From 6a4600830f7bd135a963541abeda02e9b6116748 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sun, 22 Apr 2018 11:46:56 +0800 Subject: [PATCH 140/183] Fix broken theme selector --- docs/_template/light-dark-theme/styles/styleswitcher.js | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/_template/light-dark-theme/styles/styleswitcher.js b/docs/_template/light-dark-theme/styles/styleswitcher.js index e80a17daf..a87b89525 100644 --- a/docs/_template/light-dark-theme/styles/styleswitcher.js +++ b/docs/_template/light-dark-theme/styles/styleswitcher.js @@ -1,5 +1,4 @@ const baseUrl = document.getElementById("docfx-style:rel").content; -var themeElement; function onThemeSelect(event) { const theme = event.target.value; From 0ffe70b7fc4bae5d2e8298bb928dd27d54f00904 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Thu, 26 Apr 2018 03:34:17 +0800 Subject: [PATCH 141/183] Add local image embed instruction --- .../Common/EmbedBuilder.Overwrites.md | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/_overwrites/Common/EmbedBuilder.Overwrites.md b/docs/_overwrites/Common/EmbedBuilder.Overwrites.md index de7da8f5d..2dcb1e004 100644 --- a/docs/_overwrites/Common/EmbedBuilder.Overwrites.md +++ b/docs/_overwrites/Common/EmbedBuilder.Overwrites.md @@ -16,6 +16,8 @@ uid: Discord.EmbedBuilder example: [*content] --- +#### Basic Usage + The example below builds an embed and sends it to the chat using the command system. @@ -27,9 +29,10 @@ public async Task SendRichEmbedAsync() { // Embed property can be set within object initializer Title = "Hello world!" - } + Description = "I am a description set by initializer." + }; // Or with methods - .AddField("Field title", + embed.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.") @@ -43,6 +46,23 @@ public async Task SendRichEmbedAsync() } ``` -#### Result +![Embed Example](images/embed-example.png) + +#### Usage with Local Images -![Embed Example](images/embed-example.png) \ No newline at end of file +The example below sends an image and has the image embedded in the rich +embed. See @Discord.IMessageChannel.SendFileAsync* for more information +about uploading a file or image. + +```cs +[Command("embedimage")] +public async Task SendEmbedWithImageAsync() +{ + var fileName = "image.png"; + var embed = new EmbedBuilder() + { + ImageUrl = $"attachment://{fileName}" + }.Build(); + await Context.Channel.SendFileAsync(fileName, embed: embed); +} +``` \ No newline at end of file From 956019fb51fa2bdf2514444784e477ffc4796e1b Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Thu, 26 Apr 2018 04:05:19 +0800 Subject: [PATCH 142/183] Add a bunch of XML docs --- docs/docfx.json | 1 + .../Attributes/OverrideTypeReaderAttribute.cs | 1 + .../Attributes/PreconditionAttribute.cs | 2 +- src/Discord.Net.Commands/CommandContext.cs | 7 +- src/Discord.Net.Commands/CommandService.cs | 33 ++- .../CommandServiceConfig.cs | 27 +- src/Discord.Net.Commands/ModuleBase.cs | 27 +- .../MultiMatchHandling.cs | 3 + src/Discord.Net.Commands/RunMode.cs | 5 + .../Channels/GuildChannelProperties.cs | 7 +- .../Entities/Channels/IChannel.cs | 2 +- .../Entities/Channels/IDMChannel.cs | 3 +- .../Entities/Channels/IGroupChannel.cs | 3 +- .../Entities/Channels/IGuildChannel.cs | 54 +++- .../Entities/Channels/IMessageChannel.cs | 65 +++++ .../Entities/Channels/IPrivateChannel.cs | 5 +- .../Entities/Channels/ITextChannel.cs | 35 ++- .../Entities/Channels/IVoiceChannel.cs | 2 + src/Discord.Net.Core/Entities/Emotes/Emoji.cs | 8 +- src/Discord.Net.Core/Entities/Emotes/Emote.cs | 24 +- .../Entities/Emotes/GuildEmote.cs | 2 +- .../Entities/Guilds/GuildProperties.cs | 7 +- .../Entities/Guilds/IGuild.cs | 164 ++++++++--- src/Discord.Net.Core/Entities/IDeletable.cs | 3 +- src/Discord.Net.Core/Entities/IMentionable.cs | 5 +- src/Discord.Net.Core/Entities/Image.cs | 27 +- .../Entities/Messages/EmbedBuilder.cs | 273 +++++++++++++++++- .../Entities/Messages/EmbedField.cs | 2 +- src/Discord.Net.Core/Entities/Roles/Color.cs | 3 + src/Discord.Net.Core/Entities/Roles/IRole.cs | 20 +- .../Entities/Users/IGuildUser.cs | 9 +- src/Discord.Net.Core/Entities/Users/IUser.cs | 5 +- .../Entities/Users/IVoiceState.cs | 2 +- .../Extensions/EmbedBuilderExtensions.cs | 1 + src/Discord.Net.Core/IDiscordClient.cs | 29 ++ src/Discord.Net.Core/Logging/LogManager.cs | 12 +- src/Discord.Net.Core/RequestOptions.cs | 2 +- src/Discord.Net.Core/Utils/MentionUtils.cs | 25 +- src/Discord.Net.Core/Utils/Preconditions.cs | 16 +- src/Discord.Net.Rest/BaseDiscordClient.cs | 36 ++- src/Discord.Net.Rest/DiscordRestClient.cs | 28 +- src/Discord.Net.Rest/DiscordRestConfig.cs | 5 +- .../Entities/Channels/ChannelHelper.cs | 2 +- .../Entities/Invites/RestInviteMetadata.cs | 3 +- .../Net/Converters/ImageConverter.cs | 3 +- .../Converters/PermissionTargetConverter.cs | 8 +- .../Net/Queue/RequestQueue.cs | 4 +- src/Discord.Net.Rest/Utils/TypingNotifier.cs | 16 +- .../Audio/AudioClient.cs | 14 +- .../Audio/Streams/BufferedWriteStream.cs | 8 +- src/Discord.Net.WebSocket/BaseSocketClient.cs | 25 +- .../Commands/SocketCommandContext.cs | 29 +- .../DiscordShardedClient.cs | 38 ++- .../DiscordSocketClient.cs | 8 +- .../DiscordSocketConfig.cs | 55 +++- .../Entities/Channels/ISocketAudioChannel.cs | 5 +- .../Channels/ISocketMessageChannel.cs | 48 ++- .../Channels/ISocketPrivateChannel.cs | 5 +- .../Channels/SocketCategoryChannel.cs | 6 +- .../Entities/Channels/SocketChannel.cs | 26 +- .../Entities/Channels/SocketDMChannel.cs | 41 ++- .../Entities/Channels/SocketGroupChannel.cs | 48 ++- .../Entities/Channels/SocketGuildChannel.cs | 26 +- .../Entities/Channels/SocketTextChannel.cs | 32 ++ .../Entities/Channels/SocketVoiceChannel.cs | 13 +- .../Entities/Guilds/SocketGuild.cs | 180 +++++++++++- .../Entities/Users/SocketGuildUser.cs | 23 +- 67 files changed, 1389 insertions(+), 267 deletions(-) diff --git a/docs/docfx.json b/docs/docfx.json index 8691c2732..ccd271999 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -37,6 +37,7 @@ "default", "_template/light-dark-theme" ], + "postProcessors": [ "ExtractSearchIndex" ], "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", diff --git a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs index a70a70f31..1ea2d2574 100644 --- a/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs @@ -19,6 +19,7 @@ namespace Discord.Commands /// /// The to be used with the parameter. + /// The given does not inherit from . public OverrideTypeReaderAttribute(Type overridenTypeReader) { if (!TypeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo())) diff --git a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs index 50a63c582..58d9c8ba4 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -13,7 +13,7 @@ namespace Discord.Commands /// /// 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). + /// require *all* preconditions to pass, just like normal (A && B). /// public string Group { get; set; } = null; diff --git a/src/Discord.Net.Commands/CommandContext.cs b/src/Discord.Net.Commands/CommandContext.cs index 9e0766a68..393cdf97a 100644 --- a/src/Discord.Net.Commands/CommandContext.cs +++ b/src/Discord.Net.Commands/CommandContext.cs @@ -16,7 +16,12 @@ namespace Discord.Commands /// Indicates whether the channel that the command is executed in is a private channel. public bool IsPrivate => Channel is IPrivateChannel; - + + /// + /// Initializes a new class with the provided client and message. + /// + /// The underlying client. + /// The underlying message. public CommandContext(IDiscordClient client, IUserMessage msg) { Client = client; diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index b4511a90c..57809d78b 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -13,11 +13,14 @@ namespace Discord.Commands { public class CommandService { + /// + /// Occurs when a command-related information is received. + /// 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. + /// Occurs 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>(); @@ -51,7 +54,16 @@ namespace Discord.Commands /// public ILookup TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value); + /// + /// Initializes a new class. + /// public CommandService() : this(new CommandServiceConfig()) { } + + /// + /// Initializes a new class with the provided configuration. + /// + /// The configuration class. + /// The is set to . public CommandService(CommandServiceConfig config) { _caseSensitive = config.CaseSensitiveCommands; @@ -121,6 +133,7 @@ namespace Discord.Commands /// A built module. /// public Task AddModuleAsync(IServiceProvider services) => AddModuleAsync(typeof(T), services); + /// /// Adds a command module from a . /// @@ -132,6 +145,8 @@ namespace Discord.Commands /// /// A built module. /// + /// This module has already been added. + /// The fails to be built; an invalid type may have been provided. public async Task AddModuleAsync(Type type, IServiceProvider services) { services = services ?? EmptyServiceProvider.Instance; @@ -209,7 +224,7 @@ namespace Discord.Commands ///
/// The to be removed from the service. /// - /// Returns whether the is successfully removed. + /// Returns whether the module is successfully removed. /// public async Task RemoveModuleAsync(ModuleInfo module) { @@ -223,7 +238,21 @@ namespace Discord.Commands _moduleLock.Release(); } } + /// + /// Removes the command module. + /// + /// The of the module. + /// + /// Returns whether the module is successfully removed. + /// public Task RemoveModuleAsync() => RemoveModuleAsync(typeof(T)); + /// + /// Removes the command module. + /// + /// The of the module. + /// + /// Returns whether the module is successfully removed. + /// public async Task RemoveModuleAsync(Type type) { await _moduleLock.WaitAsync().ConfigureAwait(false); diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs index 77c5b2262..5d33ddf11 100644 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -2,23 +2,40 @@ using System; namespace Discord.Commands { + /// + /// Represents a configuration class for . + /// public class CommandServiceConfig { - /// Gets or sets the default RunMode commands should have, if one is not specified on the Command attribute or builder. + /// + /// Gets or sets the default commands should have, if one is not specified on the + /// Command attribute or builder. + /// public RunMode DefaultRunMode { get; set; } = RunMode.Sync; + /// + /// Gets or sets the that separates an argument with another. + /// public char SeparatorChar { get; set; } = ' '; - /// Determines whether commands should be case-sensitive. + /// + /// Gets or sets whether commands should be case-sensitive. + /// public bool CaseSensitiveCommands { get; set; } = false; - /// 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 event. + /// public LogSeverity LogLevel { get; set; } = LogSeverity.Info; - /// Determines whether RunMode.Sync commands should push exceptions up to the caller. + /// + /// Gets or sets whether commands should push exceptions up to the caller. + /// public bool ThrowOnError { get; set; } = true; - /// Determines whether extra parameters should be ignored. + /// + /// Gets or sets whether extra parameters should be ignored. + /// public bool IgnoreExtraArgs { get; set; } = false; ///// Gets or sets the to use. diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index 77d945899..7239cac60 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -9,32 +9,45 @@ namespace Discord.Commands public abstract class ModuleBase : IModuleBase where T : class, ICommandContext { + /// + /// The underlying context of the command. + /// + /// + /// public T Context { get; private set; } /// - /// Sends a message to the source channel + /// 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 + /// + /// Contents of the message; optional only if is specified. + /// + /// Specifies if Discord should read this aloud using text-to-speech. + /// An embed to be displayed alongside the . 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. + /// The method to execute before executing the command. /// + /// The of the command to be executed. protected virtual void BeforeExecute(CommandInfo command) { } /// - /// The method to execute after executing the command. + /// The method to execute after executing the command. /// - /// + /// The of the command to be executed. protected virtual void AfterExecute(CommandInfo command) { } + /// + /// The method to execute when building the module. + /// + /// The used to create the module. + /// The builder used to build the module. protected virtual void OnModuleBuilding(CommandService commandService, ModuleBuilder builder) { } diff --git a/src/Discord.Net.Commands/MultiMatchHandling.cs b/src/Discord.Net.Commands/MultiMatchHandling.cs index 5dc84e266..319e58edd 100644 --- a/src/Discord.Net.Commands/MultiMatchHandling.cs +++ b/src/Discord.Net.Commands/MultiMatchHandling.cs @@ -1,5 +1,8 @@ namespace Discord.Commands { + /// + /// Specifies the behavior when multiple matches are found during the command parsing stage. + /// public enum MultiMatchHandling { /// Indicates that when multiple results are found, an exception should be thrown. diff --git a/src/Discord.Net.Commands/RunMode.cs b/src/Discord.Net.Commands/RunMode.cs index b5ab1116b..8e230b500 100644 --- a/src/Discord.Net.Commands/RunMode.cs +++ b/src/Discord.Net.Commands/RunMode.cs @@ -1,5 +1,10 @@ namespace Discord.Commands { + /// + /// Specifies the behavior of the command execution workflow. + /// + /// + /// public enum RunMode { /// diff --git a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs index be1bac363..fdbd0447c 100644 --- a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs @@ -20,16 +20,13 @@ namespace Discord /// 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. - /// public Optional Name { get; set; } /// - /// Moves the channel to the following position. This is 0-based! + /// Moves the channel to the following position. This property is zero-based. /// public Optional Position { get; set; } /// - /// Gets or sets the category for this channel. + /// Gets or sets the category ID for this channel. /// public Optional CategoryId { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Channels/IChannel.cs b/src/Discord.Net.Core/Entities/Channels/IChannel.cs index 85138ad60..4f14431c5 100644 --- a/src/Discord.Net.Core/Entities/Channels/IChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IChannel.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents a generic Discord channel. + /// Represents a generic channel. /// public interface IChannel : ISnowflakeEntity { diff --git a/src/Discord.Net.Core/Entities/Channels/IDMChannel.cs b/src/Discord.Net.Core/Entities/Channels/IDMChannel.cs index bde44b2ed..49e58038a 100644 --- a/src/Discord.Net.Core/Entities/Channels/IDMChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IDMChannel.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents a generic DM channel. + /// Represents a generic direct-message channel. /// public interface IDMChannel : IMessageChannel, IPrivateChannel { @@ -15,6 +15,7 @@ namespace Discord /// /// Closes this private channel, removing it from your channel list. /// + /// The options to be used when sending the request. Task CloseAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs index 75795f582..8ee2b622c 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs @@ -3,13 +3,14 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents a private generic group channel. + /// Represents a generic private group channel. /// public interface IGroupChannel : IMessageChannel, IPrivateChannel, IAudioChannel { /// /// Leaves this group. /// + /// The options to be used when sending the request. Task LeaveAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index 9a2651dfa..fdbe77653 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -5,34 +5,52 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents a guild channel (text, voice, category). + /// Represents a generic guild channel. /// + /// + /// + /// 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. /// + /// + /// The position of this channel in the guild's channel list, relative to others of the same type. + /// int Position { get; } /// /// Gets the parent ID (category) of this channel in the guild's channel list. /// + /// + /// The parent category ID associated with this channel, or if none is set. + /// ulong? CategoryId { get; } /// /// Gets the parent channel (category) of this channel. /// Task GetCategoryAsync(); /// - /// Gets the guild this channel is a member of. + /// Gets the guild associated with this channel. /// + /// + /// The guild that this channel belongs to. + /// IGuild Guild { get; } /// - /// Gets the id of the guild this channel is a member of. + /// Gets the guild ID associated with this channel. /// + /// + /// The guild ID that this channel belongs to. + /// ulong GuildId { get; } /// /// Gets a collection of permission overwrites for this channel. /// + /// + /// A collection of overwrites associated with this channel. + /// IReadOnlyCollection PermissionOverwrites { get; } /// @@ -50,49 +68,77 @@ namespace Discord /// /// If , don't try to reuse a similar invite (useful for creating many unique one time use invites). /// + /// + /// The options to be used when sending the request. + /// 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. /// + /// The options to be used when sending the request. Task> GetInvitesAsync(RequestOptions options = null); /// /// Modifies this guild channel. /// + /// The properties to modify the channel with. + /// The options to be used when sending the request. Task ModifyAsync(Action func, RequestOptions options = null); /// /// Gets the permission overwrite for a specific role, or if one does not exist. /// + /// The role to get the overwrite from. OverwritePermissions? GetPermissionOverwrite(IRole role); /// /// Gets the permission overwrite for a specific user, or if one does not exist. /// + /// The user to get the overwrite from. OverwritePermissions? GetPermissionOverwrite(IUser user); /// /// Removes the permission overwrite for the given role, if one exists. /// + /// The role to remove the overwrite from. + /// The options to be used when sending the request. Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null); /// /// Removes the permission overwrite for the given user, if one exists. /// + /// The user to remove the overwrite from. + /// The options to be used when sending the request. Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null); + /// /// Adds or updates the permission overwrite for the given role. /// + /// The role to add the overwrite to. + /// The overwrite to add to the role. + /// The options to be used when sending the request. Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null); /// /// Adds or updates the permission overwrite for the given user. /// + /// The user to add the overwrite to. + /// The overwrite to add to the user. + /// The options to be used when sending the request. Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null); /// /// Gets a collection of all users in this channel. /// + /// + /// The that determines whether the object should be fetched from cache. + /// + /// The options to be used when sending the request. new IAsyncEnumerable> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets a user in this channel with the provided ID. /// + /// The ID of the user. + /// + /// The that determines whether the object should be fetched from cache. + /// + /// The options to be used when sending the request. new Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index 1d855ba08..9837a3048 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -13,50 +13,115 @@ namespace Discord /// /// Sends a message to this message channel. /// + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); #if FILESYSTEM /// /// Sends a file to this message channel, with an optional caption. /// + /// The file path of the file. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// If you wish to upload an image and have it embedded in a embed, you may + /// upload the file and refer to the file with "attachment://filename.ext" in the + /// . + /// Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); #endif /// /// Sends a file to this message channel, with an optional caption. /// + /// The of the file to be sent. + /// The name of the attachment. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// If you wish to upload an image and have it embedded in a embed, you may + /// upload the file and refer to the file with "attachment://filename.ext" in the + /// . + /// 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 if not found. /// + /// The ID of the message. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// The message gotten from either the cache or the download, or if none is found. + /// Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// /// Gets the last N messages from this message channel. /// + /// The numbers of message to be gotten from. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. Flattening the paginated response into a collection of messages with + /// is required if you wish to access the messages. + /// IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets a collection of messages in this channel. /// + /// The ID of the starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. Flattening the paginated response into a collection of messages with + /// is required if you wish to access the messages. + /// 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. /// + /// The starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The that determines whether the object should be fetched from + /// cache. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. Flattening the paginated response into a collection of messages with + /// is required if you wish to access the messages. + /// 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. /// + /// The options to be used when sending the request. + /// + /// A collection of messages. + /// Task> GetPinnedMessagesAsync(RequestOptions options = null); /// /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. /// + /// The options to be used when sending the request. Task TriggerTypingAsync(RequestOptions options = null); /// /// Continuously broadcasts the "user is typing" message to all users in this channel until the returned /// object is disposed. /// + /// The options to be used when sending the request. 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 fecb4fc19..41b409141 100644 --- a/src/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IPrivateChannel.cs @@ -8,8 +8,11 @@ namespace Discord public interface IPrivateChannel : IChannel { /// - /// Users that can access this channel. + /// Gets the users that can access this channel. /// + /// + /// A collection of 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 1998083af..d1b2465ad 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -11,40 +11,67 @@ namespace Discord public interface ITextChannel : IMessageChannel, IMentionable, IGuildChannel { /// - /// Gets whether the channel is NSFW. + /// Determines whether the channel is NSFW. /// + /// + /// if the channel has the NSFW flag enabled; otherwise, . + /// bool IsNsfw { get; } /// /// Gets the current topic for this text channel. /// + /// + /// The topic set in the channel, or if none is set. + /// string Topic { get; } /// - /// Bulk deletes multiple messages. + /// Bulk-deletes multiple messages. /// + /// The messages to be bulk-deleted. + /// The options to be used when sending the request. Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); /// - /// Bulk deletes multiple messages. + /// Bulk-deletes multiple messages. /// + /// The IDs of the messages to be bulk-deleted. + /// The options to be used when sending the request. Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null); /// /// Modifies this text channel. /// + /// The properties to modify the channel with. + /// The options to be used when sending the request. Task ModifyAsync(Action func, RequestOptions options = null); /// /// Creates a webhook in this text channel. /// + /// The name of the webhook. + /// The avatar of the webhook. + /// The options to be used when sending the request. + /// + /// The created webhook. + /// Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null); /// - /// Gets the webhook in this text channel with the provided ID, or if not found. + /// Gets the webhook in this text channel with the provided ID. /// + /// The ID of the webhook. + /// The options to be used when sending the request. + /// + /// A webhook associated with the , or if not found. + /// Task GetWebhookAsync(ulong id, RequestOptions options = null); /// /// Gets the webhooks for this text channel. /// + /// The options to be used when sending the request. + /// + /// A collection of webhooks. + /// Task> GetWebhooksAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index e6e589235..f69e6e22e 100644 --- a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -21,6 +21,8 @@ namespace Discord /// /// Modifies this voice channel. /// + /// The properties to modify the channel with. + /// The options to be used when sending the request. Task ModifyAsync(Action func, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs index da3c512c5..c7a2dc5eb 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emoji.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emoji.cs @@ -6,10 +6,8 @@ namespace Discord public class Emoji : IEmote { // TODO: need to constrain this to Unicode-only emojis somehow - - /// - /// Gets the Unicode representation of this emote. - /// + + /// public string Name { get; } /// /// Gets the Unicode representation of this emote. @@ -28,7 +26,7 @@ namespace Discord /// /// Determines whether the specified emoji is equal to the current emoji. /// - /// The object to compare with the current object. + /// The object to compare with the current object. public override bool Equals(object other) { if (other == null) return false; diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs index 682ca33b7..32d49fede 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Globalization; namespace Discord @@ -6,15 +7,12 @@ namespace Discord /// /// A custom image-based emote. /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Emote : IEmote, ISnowflakeEntity { - /// - /// Gets the display name (tooltip) of this emote. - /// + /// public string Name { get; } - /// - /// Gets the ID of this emote. - /// + /// public ulong Id { get; } /// /// Gets whether this emote is animated. @@ -36,6 +34,10 @@ namespace Discord Animated = animated; } + /// + /// Determines whether the specified emote is equal to the current emote. + /// + /// The object to compare with the current object. public override bool Equals(object other) { if (other == null) return false; @@ -47,6 +49,7 @@ namespace Discord return string.Equals(Name, otherEmote.Name) && Id == otherEmote.Id; } + /// public override int GetHashCode() { unchecked @@ -57,7 +60,8 @@ namespace Discord /// Parses an from its raw format. /// The raw encoding of an emote; for example, <:dab:277855270321782784>. - /// An emote + /// An emote. + /// Invalid emote format. public static Emote Parse(string text) { if (TryParse(text, out Emote result)) @@ -65,6 +69,9 @@ namespace Discord throw new ArgumentException("Invalid emote format.", nameof(text)); } + /// Tries to parse an from its raw format. + /// The raw encoding of an emote; for example, <:dab:277855270321782784>. + /// An emote. public static bool TryParse(string text, out Emote result) { result = null; @@ -89,6 +96,9 @@ namespace Discord } private string DebuggerDisplay => $"{Name} ({Id})"; + /// + /// Returns the raw representation of the emote. + /// public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; } } diff --git a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs index 149a0f284..f734c3648 100644 --- a/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs @@ -31,7 +31,7 @@ namespace Discord private string DebuggerDisplay => $"{Name} ({Id})"; /// - /// Gets the raw representation of the emoji. + /// Gets the raw representation of the emote. /// public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; } diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs index fc33f3fe4..3c136b579 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs @@ -8,9 +8,8 @@ namespace Discord /// await Context.Guild.ModifyAsync(async x => /// { /// x.Name = "aaaaaah"; - /// x.RegionId = (await Context.Client.GetOptimalVoiceRegionAsync()).Id; /// }); - /// + /// /// /// public class GuildProperties @@ -61,11 +60,11 @@ namespace Discord /// public Optional AfkChannelId { get; set; } /// - /// Gets or sets the where System messages should be sent. + /// Gets or sets the where system messages should be sent. /// public Optional SystemChannel { get; set; } /// - /// Gets or sets the ID of the where System messages should be sent. + /// Gets or sets the ID of the where system messages should be sent. /// public Optional SystemChannelId { get; set; } /// diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 56c094621..664ac017d 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -20,8 +20,11 @@ namespace Discord /// int AFKTimeout { get; } /// - /// Returns if this guild is embeddable (e.g. widget). + /// Determines if this guild is embeddable (i.e. can use widget). /// + /// + /// Returns if this guild can be embedded via widgets. + /// bool IsEmbeddable { get; } /// /// Gets the default message notifications for users who haven't explicitly set their notification settings. @@ -37,29 +40,32 @@ namespace Discord /// VerificationLevel VerificationLevel { get; } /// - /// Returns the ID of this guild's icon, or if one is not set. + /// Returns the ID of this guild's icon, or if none is set. /// string IconId { get; } /// - /// Returns the URL of this guild's icon, or if one is not set. + /// Returns the URL of this guild's icon, or if none is set. /// string IconUrl { get; } /// - /// Returns the ID of this guild's splash image, or if one is not set. + /// Returns the ID of this guild's splash image, or if none is set. /// string SplashId { get; } /// - /// Returns the URL of this guild's splash image, or if one is not set. + /// Returns the URL of this guild's splash image, or if none is set. /// string SplashUrl { get; } /// + /// Determines if this guild is currently connected and ready to be used. + /// + /// /// 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 if not. + /// Gets the ID of the AFK voice channel for this guild, or if none is set. /// ulong? AFKChannelId { get; } /// @@ -67,11 +73,11 @@ namespace Discord /// ulong DefaultChannelId { get; } /// - /// Gets the ID of the embed channel for this guild if set, or if not. + /// Gets the ID of the embed channel set in the widget settings of this guild, or if none is set. /// ulong? EmbedChannelId { get; } /// - /// Gets the ID of the channel where randomized welcome messages are sent if set, or if not. + /// Gets the ID of the channel where randomized welcome messages are sent, or if none is set. /// ulong? SystemChannelId { get; } /// @@ -106,57 +112,62 @@ namespace Discord /// /// Modifies this guild. /// + /// The properties to modify the guild with. + /// The options to be used when sending the request. Task ModifyAsync(Action func, RequestOptions options = null); /// /// Modifies this guild's embed channel. /// + /// The properties to modify the guild widget with. + /// The options to be used when sending the request. Task ModifyEmbedAsync(Action func, RequestOptions options = null); /// /// Bulk modifies the order of channels in this guild. /// + /// The properties to modify the channel positions with. + /// The options to be used when sending the request. Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null); /// /// Bulk modifies the order of roles in this guild. /// + /// The properties to modify the role positions with. + /// The options to be used when sending the request. Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null); /// - /// Leaves this guild. If you are the owner, use - /// instead. + /// Leaves this guild. If you are the owner, use instead. /// + /// The options to be used when sending the request. Task LeaveAsync(RequestOptions options = null); /// /// Gets a collection of all users banned on this guild. /// + /// The options to be used when sending the request. Task> GetBansAsync(RequestOptions options = null); /// - /// Bans the provided from this guild and optionally prunes their recent messages. + /// Bans the provided user from this guild and optionally prunes their recent messages. /// - /// - /// The user to ban. - /// + /// 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. + /// 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. + /// The options to be used when sending the request. + /// is not between 0 to 7. 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 ID of the user to ban. - /// + /// 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. + /// 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. + /// The options to be used when sending the request. + /// is not between 0 to 7. Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null); /// - /// Unbans the provided if they are currently banned. + /// Unbans the provided user if they are currently banned. /// Task RemoveBanAsync(IUser user, RequestOptions options = null); /// @@ -167,66 +178,114 @@ namespace Discord /// /// Gets a collection of all channels in this guild. /// + /// + /// The that determines whether the object should be fetched from cache. + /// + /// The options to be used when sending the request. Task> GetChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// - /// Gets the channel in this guild with the provided ID, or if not found. + /// Gets the channel in this guild with the provided ID, or if not found. /// /// The channel ID. + /// + /// The that determines whether the object should be fetched from cache. + /// + /// The options to be used when sending the request. Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets a collection of all text channels in this guild. /// + /// + /// The that determines whether the object should be fetched from cache. + /// + /// The options to be used when sending the request. Task> GetTextChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// - /// Gets a text channel in this guild with the provided ID, or if not found. + /// Gets a text channel in this guild with the provided ID, or if not found. /// /// The text channel ID. + /// + /// The that determines whether the object should be fetched from cache. + /// + /// The options to be used when sending the request. Task GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets a collection of all voice channels in this guild. /// + /// + /// The that determines whether the object should be fetched from cache. + /// + /// The options to be used when sending the request. Task> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets a collection of all category channels in this guild. /// + /// + /// The that determines whether the object should be fetched from cache. + /// + /// The options to be used when sending the request. Task> GetCategoriesAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// - /// Gets the voice channel in this guild with the provided ID, or if not found. + /// Gets the voice channel in this guild with the provided ID, or if not found. /// /// The text channel ID. + /// + /// The that determines whether the object should be fetched from cache. + /// + /// The options to be used when sending the request. 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. + /// Gets the voice AFK channel in this guild with the provided ID, or if not found. /// + /// + /// The that determines whether the object should be fetched from cache. + /// + /// The options to be used when sending the request. Task GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// - /// Gets the default system text channel in this guild with the provided ID, or if + /// Gets the default system text channel in this guild with the provided ID, or if /// none is set. /// + /// + /// The that determines whether the object should be fetched from cache. + /// + /// The options to be used when sending the request. 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 + /// Gets the top viewable text channel in this guild with the provided ID, or if not /// found. /// + /// + /// The that determines whether the object should be fetched from cache. + /// + /// The options to be used when sending the request. Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// - /// Gets the embed channel in this guild. + /// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild, or + /// if none is set. /// + /// + /// The that determines whether the object should be fetched from cache. + /// + /// The options to be used when sending the request. Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Creates a new text channel. /// /// The new name for the text channel. + /// The options to be used when sending the request. Task CreateTextChannelAsync(string name, RequestOptions options = null); /// /// Creates a new voice channel. /// /// The new name for the voice channel. + /// The options to be used when sending the request. Task CreateVoiceChannelAsync(string name, RequestOptions options = null); /// /// Creates a new channel category. /// /// The new name for the category. + /// The options to be used when sending the request. Task CreateCategoryAsync(string name, RequestOptions options = null); Task> GetIntegrationsAsync(RequestOptions options = null); @@ -249,24 +308,33 @@ namespace Discord /// The guild permission that the role should possess. /// The color of the role. /// Whether the role is separated from others on the sidebar. + /// The options to be used when sending the request. 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. /// - Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); //TODO: shouldnt this be paged? + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. + Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets the user in this guild with the provided ID, or if not found. /// /// The user ID. + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets the current user for this guild. /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. Task GetCurrentUserAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets the owner of this guild. /// + /// The that determines whether the object should be fetched from cache. + /// The options to be used when sending the request. Task GetOwnerAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Downloads all users for this guild if the current list is incomplete. @@ -274,11 +342,12 @@ namespace Discord Task DownloadUsersAsync(); /// /// 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. + /// or, if is , 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 options to be used when sending the request. /// /// The number of users removed from this guild. /// @@ -288,32 +357,41 @@ namespace Discord /// Gets the webhook in this guild with the provided ID, or if not found. /// /// The webhook ID. + /// The options to be used when sending the request. Task GetWebhookAsync(ulong id, RequestOptions options = null); /// - /// Gets a collection of all webhooks from this guild. + /// Gets a collection of all webhook from this guild. /// + /// The options to be used when sending the request. Task> GetWebhooksAsync(RequestOptions options = null); - + /// /// Gets a specific emote from this guild. /// /// The guild emote ID. + /// The options to be used when sending the request. Task GetEmoteAsync(ulong id, RequestOptions options = null); /// - /// Creates a new emote in this guild. + /// Creates a new in this guild. /// /// The name of the guild emote. /// The image of the new emote. /// The roles to limit the emote usage to. + /// The options to be used when sending the request. Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null); + /// - /// Modifies an existing in this guild. + /// Modifies an existing in this guild. /// + /// The emote to be modified. + /// The properties to modify the emote with. + /// The options to be used when sending the request. Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null); /// - /// Deletes an existing from this guild. + /// Deletes an existing from this guild. /// - /// The guild emote to delete. + /// The emote to delete. + /// The options to be used when sending the request. Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/IDeletable.cs b/src/Discord.Net.Core/Entities/IDeletable.cs index ce019edcf..9696eb838 100644 --- a/src/Discord.Net.Core/Entities/IDeletable.cs +++ b/src/Discord.Net.Core/Entities/IDeletable.cs @@ -3,13 +3,14 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents whether the object is deletable or not. + /// Determines whether the object is deletable or not. /// public interface IDeletable { /// /// Deletes this object and all its children. /// + /// The options to be used when sending the request. Task DeleteAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/IMentionable.cs b/src/Discord.Net.Core/Entities/IMentionable.cs index 1fd9400b3..225806723 100644 --- a/src/Discord.Net.Core/Entities/IMentionable.cs +++ b/src/Discord.Net.Core/Entities/IMentionable.cs @@ -1,13 +1,16 @@ namespace Discord { /// - /// Represents whether the object is mentionable or not. + /// Determines whether the object is mentionable or not. /// public interface IMentionable { /// /// Returns a special string used to mention this object. /// + /// + /// A string that is recognized by Discord as a mention (e.g. <@168693960628371456>). + /// string Mention { get; } } } diff --git a/src/Discord.Net.Core/Entities/Image.cs b/src/Discord.Net.Core/Entities/Image.cs index 4bdd4be66..921902f5d 100644 --- a/src/Discord.Net.Core/Entities/Image.cs +++ b/src/Discord.Net.Core/Entities/Image.cs @@ -1,3 +1,4 @@ +using System; using System.IO; namespace Discord { @@ -11,7 +12,7 @@ namespace Discord /// public Stream Stream { get; } /// - /// Create the image with a . + /// Create the image with a . /// /// /// The to create the image with. Note that this must be some type of stream @@ -26,10 +27,30 @@ namespace Discord /// Create the image from a file path. /// /// - /// This file is NOT validated, and is passed directly into a - /// + /// This file path is NOT validated and is passed directly into a + /// . /// /// The path to the file. + /// + /// is a zero-length string, contains only white space, or contains one or more invalid + /// characters as defined by . + /// + /// is . + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// is in an invalid format. + /// + /// The specified is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// The file specified in was not found. + /// + /// An I/O error occurred while opening the file. public Image(string path) { Stream = File.OpenRead(path); diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index fa7d87410..d2c39d1bd 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -41,6 +41,9 @@ namespace Discord } /// Gets or sets the title of an . + /// Title length exceeds the maximum allowed by Discord. + /// + /// The title of the embed. public string Title { get => _title; @@ -50,7 +53,10 @@ namespace Discord _title = value; } } + /// Gets or sets the description of an . + /// Description length exceeds the maximum allowed by Discord. + /// The description of the embed. public string Description { get => _description; @@ -62,6 +68,8 @@ namespace Discord } /// Gets or sets the URL of an . + /// Url is not a well-formed . + /// The URL of the embed. public string Url { get => _url; @@ -72,6 +80,8 @@ namespace Discord } } /// Gets or sets the thumbnail URL of an . + /// Url is not a well-formed . + /// The thumbnail URL of the embed. public string ThumbnailUrl { get => _thumbnail?.Url; @@ -82,6 +92,8 @@ namespace Discord } } /// Gets or sets the image URL of an . + /// Url is not a well-formed . + /// The image URL of the embed. public string ImageUrl { get => _image?.Url; @@ -91,7 +103,13 @@ namespace Discord _image = new EmbedImage(value, null, null, null); } } + /// Gets or sets the list of of an . + /// An embed builder's fields collection is set to + /// . + /// Description length exceeds the maximum allowed by + /// Discord. + /// The list of existing . public List Fields { get => _fields; @@ -103,18 +121,42 @@ namespace Discord } } - /// Gets or sets the timestamp of an . + /// + /// Gets or sets the timestamp of an . + /// + /// + /// The timestamp of the embed, or if none is set. + /// public DateTimeOffset? Timestamp { get; set; } - /// Gets or sets the sidebar color of an . + /// + /// Gets or sets the sidebar color of an . + /// + /// + /// The color of the embed, or if none is set. + /// public Color? Color { get; set; } - /// Gets or sets the of an . + /// + /// Gets or sets the of an . + /// + /// + /// The author field builder of the embed, or if none is set. + /// public EmbedAuthorBuilder Author { get; set; } - /// Gets or sets the of an . + /// + /// Gets or sets the of an . + /// + /// + /// The footer field builder of the embed, or if none is set. + /// public EmbedFooterBuilder Footer { get; set; } /// /// Gets the total length of all embed properties. /// + /// + /// The combined length of , , , + /// , , and . + /// public int Length { get @@ -130,9 +172,12 @@ namespace Discord } /// - /// Sets the title of an . + /// Sets the title of an . /// /// The title to be set. + /// + /// The current builder. + /// public EmbedBuilder WithTitle(string title) { Title = title; @@ -142,6 +187,9 @@ namespace Discord /// Sets the description of an . /// /// The description to be set. + /// + /// The current builder. + /// public EmbedBuilder WithDescription(string description) { Description = description; @@ -151,6 +199,9 @@ namespace Discord /// Sets the URL of an . /// /// The URL to be set. + /// + /// The current builder. + /// public EmbedBuilder WithUrl(string url) { Url = url; @@ -160,15 +211,21 @@ namespace Discord /// Sets the thumbnail URL of an . ///
/// The thumbnail URL to be set. + /// + /// The current builder. + /// public EmbedBuilder WithThumbnailUrl(string thumbnailUrl) { ThumbnailUrl = thumbnailUrl; return this; } /// - /// Sets the image URL of an . + /// Sets the image URL of an . /// /// The image URL to be set. + /// + /// The current builder. + /// public EmbedBuilder WithImageUrl(string imageUrl) { ImageUrl = imageUrl; @@ -177,24 +234,33 @@ namespace Discord /// /// Sets the timestamp of an to the current time. /// + /// + /// The current builder. + /// public EmbedBuilder WithCurrentTimestamp() { Timestamp = DateTimeOffset.UtcNow; return this; } /// - /// Sets the timestamp of an . + /// Sets the timestamp of an . /// /// The timestamp to be set. + /// + /// The current builder. + /// public EmbedBuilder WithTimestamp(DateTimeOffset dateTimeOffset) { Timestamp = dateTimeOffset; return this; } /// - /// Sets the sidebar color of an . + /// Sets the sidebar color of an . /// /// The color to be set. + /// + /// The current builder. + /// public EmbedBuilder WithColor(Color color) { Color = color; @@ -202,9 +268,12 @@ namespace Discord } /// - /// Sets the of an . + /// Sets the of an . /// /// The author builder class containing the author field properties. + /// + /// The current builder. + /// public EmbedBuilder WithAuthor(EmbedAuthorBuilder author) { Author = author; @@ -214,6 +283,9 @@ namespace Discord /// Sets the author field of an with the provided properties. ///
/// The containing the author field properties. + /// + /// The current builder. + /// public EmbedBuilder WithAuthor(Action action) { var author = new EmbedAuthorBuilder(); @@ -227,6 +299,9 @@ namespace Discord /// The title of the author field. /// The icon URL of the author field. /// The URL of the author field. + /// + /// The current builder. + /// public EmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null) { var author = new EmbedAuthorBuilder @@ -239,9 +314,12 @@ namespace Discord return this; } /// - /// Sets the of an . + /// Sets the of an . /// /// The footer builder class containing the footer field properties. + /// + /// The current builder. + /// public EmbedBuilder WithFooter(EmbedFooterBuilder footer) { Footer = footer; @@ -251,6 +329,9 @@ namespace Discord /// Sets the footer field of an with the provided properties. ///
/// The containing the footer field properties. + /// + /// The current builder. + /// public EmbedBuilder WithFooter(Action action) { var footer = new EmbedFooterBuilder(); @@ -263,6 +344,9 @@ namespace Discord /// /// The title of the footer field. /// The icon URL of the footer field. + /// + /// The current builder. + /// public EmbedBuilder WithFooter(string text, string iconUrl = null) { var footer = new EmbedFooterBuilder @@ -280,6 +364,9 @@ namespace Discord /// The title of the field. /// The value of the field. /// Indicates whether the field is in-line or not. + /// + /// The current builder. + /// public EmbedBuilder AddField(string name, object value, bool inline = false) { var field = new EmbedFieldBuilder() @@ -289,11 +376,16 @@ namespace Discord AddField(field); return this; } + /// /// Adds a field with the provided to an /// . /// /// The field builder class containing the field properties. + /// Field count exceeds the maximum allowed by Discord. + /// + /// The current builder. + /// public EmbedBuilder AddField(EmbedFieldBuilder field) { if (Fields.Count >= MaxFieldCount) @@ -308,6 +400,9 @@ namespace Discord /// Adds an field with the provided properties. /// /// The containing the field properties. + /// + /// The current builder. + /// public EmbedBuilder AddField(Action action) { var field = new EmbedFieldBuilder(); @@ -317,11 +412,12 @@ namespace Discord } /// - /// Builds the into a Rich Embed format. + /// Builds the into a Rich Embed ready to be sent. /// /// /// The built embed object. /// + /// Total embed length exceeds the maximum allowed by Discord. public Embed Build() { if (Length > MaxEmbedLength) @@ -335,6 +431,9 @@ namespace Discord } } + /// + /// Represents a builder class for an embed field. + /// public class EmbedFieldBuilder { private string _name; @@ -352,6 +451,14 @@ namespace Discord /// /// Gets or sets the field name. /// + /// + /// Field name is , empty or entirely whitespace. + /// - or - + /// Field name length exceeds . + /// + /// + /// The name of the field. + /// public string Name { get => _name; @@ -366,19 +473,27 @@ namespace Discord /// /// Gets or sets the field value. /// + /// + /// Field value is , empty or entirely whitespace. + /// - or - + /// Field value length exceeds . + /// + /// + /// The value of the field. + /// 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 (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. + /// Determines whether the field should be in-line with each other. /// public bool IsInline { get; set; } @@ -386,6 +501,9 @@ namespace Discord /// Sets the field name. /// /// The name to set the field name to. + /// + /// The current builder. + /// public EmbedFieldBuilder WithName(string name) { Name = name; @@ -395,14 +513,20 @@ namespace Discord /// Sets the field value. /// /// The value to set the field value to. + /// + /// The current builder. + /// public EmbedFieldBuilder WithValue(object value) { Value = value; return this; } /// - /// Sets whether the field should be in-line with each other. + /// Determines whether the field should be in-line with each other. /// + /// + /// The current builder. + /// public EmbedFieldBuilder WithIsInline(bool isInline) { IsInline = isInline; @@ -410,19 +534,42 @@ namespace Discord } /// - /// Builds the field builder into a class. + /// Builds the field builder into a class. /// + /// + /// The current builder. + /// + /// + /// or is , empty or entirely whitespace. + /// - or - + /// or length exceeds the maximum allowed by Discord. + /// public EmbedField Build() => new EmbedField(Name, Value.ToString(), IsInline); } + /// + /// Represents a builder class for a author field. + /// public class EmbedAuthorBuilder { private string _name; private string _url; private string _iconUrl; + /// + /// Gets the maximum author name length allowed by Discord. + /// public const int MaxAuthorNameLength = 256; + /// + /// Gets or sets the author name. + /// + /// + /// Author name length is longer than . + /// + /// + /// The author name. + /// public string Name { get => _name; @@ -432,6 +579,13 @@ namespace Discord _name = value; } } + /// + /// Gets or sets the URL of the author field. + /// + /// Url is not a well-formed . + /// + /// The URL of the author field. + /// public string Url { get => _url; @@ -441,6 +595,13 @@ namespace Discord _url = value; } } + /// + /// Gets or sets the icon URL of the author field. + /// + /// Url is not a well-formed . + /// + /// The icon URL of the author field. + /// public string IconUrl { get => _iconUrl; @@ -451,33 +612,82 @@ namespace Discord } } + /// + /// Sets the name of the author field. + /// + /// The name of the author field. + /// + /// The current builder. + /// public EmbedAuthorBuilder WithName(string name) { Name = name; return this; } + /// + /// Sets the URL of the author field. + /// + /// The URL of the author field. + /// + /// The current builder. + /// public EmbedAuthorBuilder WithUrl(string url) { Url = url; return this; } + /// + /// Sets the icon URL of the author field. + /// + /// The icon URL of the author field. + /// + /// The current builder. + /// public EmbedAuthorBuilder WithIconUrl(string iconUrl) { IconUrl = iconUrl; return this; } + /// + /// Builds the author field to be used. + /// + /// + /// Author name length is longer than . + /// - or - + /// is not a well-formed . + /// - or - + /// is not a well-formed . + /// + /// + /// The built author field. + /// public EmbedAuthor Build() => new EmbedAuthor(Name, Url, IconUrl, null); } + /// + /// Represents a builder class for an embed footer. + /// public class EmbedFooterBuilder { private string _text; private string _iconUrl; + /// + /// Gets the maximum footer length allowed by Discord. + /// public const int MaxFooterTextLength = 2048; + /// + /// Gets or sets the footer text. + /// + /// + /// Author name length is longer than . + /// + /// + /// The footer text. + /// public string Text { get => _text; @@ -487,6 +697,13 @@ namespace Discord _text = value; } } + /// + /// Gets or sets the icon URL of the footer field. + /// + /// Url is not a well-formed . + /// + /// The icon URL of the footer field. + /// public string IconUrl { get => _iconUrl; @@ -497,17 +714,43 @@ namespace Discord } } + /// + /// Sets the name of the footer field. + /// + /// The text of the footer field. + /// + /// The current builder. + /// public EmbedFooterBuilder WithText(string text) { Text = text; return this; } + /// + /// Sets the icon URL of the footer field. + /// + /// The icon URL of the footer field. + /// + /// The current builder. + /// public EmbedFooterBuilder WithIconUrl(string iconUrl) { IconUrl = iconUrl; return this; } + /// + /// Builds the footer field to be used. + /// + /// + /// + /// length is longer than . + /// - or - + /// is not a well-formed . + /// + /// + /// A built footer field. + /// 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 40404167d..d7f37befa 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs @@ -17,7 +17,7 @@ namespace Discord /// public string Value { get; internal set; } /// - /// Gets whether the field should be in-line with each other. + /// Determines whether the field should be in-line with each other. /// public bool Inline { get; internal set; } diff --git a/src/Discord.Net.Core/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs index b84bbb313..cdee5284d 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -83,12 +83,14 @@ 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. + /// The argument value is not between 0 to 255. public Color(int r, int g, int b) { if (r < 0 || r > 255) @@ -108,6 +110,7 @@ namespace Discord /// 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. + /// The argument value is not between 0 to 1. public Color(float r, float g, float b) { if (r < 0.0f || r > 1.0f) diff --git a/src/Discord.Net.Core/Entities/Roles/IRole.cs b/src/Discord.Net.Core/Entities/Roles/IRole.cs index 9aa509cd4..f4cb4c64d 100644 --- a/src/Discord.Net.Core/Entities/Roles/IRole.cs +++ b/src/Discord.Net.Core/Entities/Roles/IRole.cs @@ -18,16 +18,28 @@ namespace Discord /// Color Color { get; } /// - /// Returns if users of this role are separated in the user list. + /// Determines whether the role can be separated in the user list. /// + /// + /// Returns if users of this role are separated in the user list; otherwise, returns + /// . + /// bool IsHoisted { get; } /// - /// Returns if this role is automatically managed by Discord. + /// Determines whether the role is managed by Discord. /// + /// + /// Returns if this role is automatically managed by Discord; otherwise, returns + /// . + /// bool IsManaged { get; } /// - /// Returns if this role may be mentioned in messages. + /// Determines whether the role is mentionable. /// + /// + /// Returns if this role may be mentioned in messages; otherwise, returns + /// . + /// bool IsMentionable { get; } /// /// Gets the name of this role. @@ -45,6 +57,8 @@ namespace Discord /// /// Modifies this role. /// + /// The properties to modify the role with. + /// The options to be used when sending the request. Task ModifyAsync(Action func, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs index 57093b3fd..4d54a49c4 100644 --- a/src/Discord.Net.Core/Entities/Users/IGuildUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IGuildUser.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents a Discord user that is in a guild. + /// Represents a generic guild user. /// public interface IGuildUser : IUser, IVoiceState { @@ -46,31 +46,38 @@ namespace Discord /// Kicks this user from this guild. /// /// The reason for the kick which will be recorded in the audit log. + /// The options to be used when sending the request. Task KickAsync(string reason = null, RequestOptions options = null); /// /// Modifies this user's properties in this guild. /// + /// The properties to modify the user with. + /// The options to be used when sending the request. Task ModifyAsync(Action func, RequestOptions options = null); /// /// Adds a to this user in this guild. /// /// The role to be added to the user. + /// The options to be used when sending the request. Task AddRoleAsync(IRole role, RequestOptions options = null); /// /// Adds to this user in this guild. /// /// The roles to be added to the user. + /// The options to be used when sending the request. Task AddRolesAsync(IEnumerable roles, RequestOptions options = null); /// /// Removes a from this user in this guild. /// /// The role to be removed from the user. + /// The options to be used when sending the request. Task RemoveRoleAsync(IRole role, RequestOptions options = null); /// /// Removes from this user in this guild. /// /// The roles to be removed from the user. + /// The options to be used when sending the request. Task RemoveRolesAsync(IEnumerable roles, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index 9b62be362..f651a23f3 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents a Discord user. + /// Represents a generic user. /// public interface IUser : ISnowflakeEntity, IMentionable, IPresence { @@ -41,8 +41,7 @@ namespace Discord string Username { get; } /// - /// Returns a private message channel to this user, creating one if it does not already - /// exist. + /// Returns a direct message channel to this user, or create 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 ee1f74bae..725ef2870 100644 --- a/src/Discord.Net.Core/Entities/Users/IVoiceState.cs +++ b/src/Discord.Net.Core/Entities/Users/IVoiceState.cs @@ -26,7 +26,7 @@ namespace Discord /// bool IsSuppressed { get; } /// - /// Gets the voice channel this user is currently in, if any. + /// Gets the voice channel this user is currently in, or if none. /// IVoiceChannel VoiceChannel { get; } /// diff --git a/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs index b650aa401..efa168be3 100644 --- a/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs +++ b/src/Discord.Net.Core/Extensions/EmbedBuilderExtensions.cs @@ -30,6 +30,7 @@ namespace Discord builder.WithAuthor($"{user.Nickname ?? user.Username}#{user.Discriminator}", user.GetAvatarUrl()); /// Converts a object to a . + /// The embed type is not . public static EmbedBuilder ToEmbedBuilder(this IEmbed embed) { if (embed.Type != EmbedType.Rich) diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index a383c37da..344dff6d5 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -5,6 +5,9 @@ using System.Threading.Tasks; namespace Discord { + /// + /// Represents a generic Discord client. + /// public interface IDiscordClient : IDisposable { ConnectionState ConnectionState { get; } @@ -14,11 +17,37 @@ namespace Discord Task StartAsync(); Task StopAsync(); + /// + /// Gets the application information associated with this account. + /// Task GetApplicationInfoAsync(RequestOptions options = null); + /// + /// Gets a generic channel with the provided ID. + /// + /// The ID of the channel. + /// The that determines whether the object should be fetched from cache. Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a list of private channels. + /// + /// + /// The that determines whether the object should be fetched from cache. + /// Task> GetPrivateChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a list of direct message channels. + /// + /// + /// The that determines whether the object should be fetched from cache. + /// Task> GetDMChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// + /// Gets a list of group channels. + /// + /// + /// The that determines whether the object should be fetched from cache. + /// Task> GetGroupChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task> GetConnectionsAsync(RequestOptions options = null); diff --git a/src/Discord.Net.Core/Logging/LogManager.cs b/src/Discord.Net.Core/Logging/LogManager.cs index 995a5d96a..35727c33d 100644 --- a/src/Discord.Net.Core/Logging/LogManager.cs +++ b/src/Discord.Net.Core/Logging/LogManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Logging @@ -24,7 +24,10 @@ namespace Discord.Logging if (severity <= Level) await _messageEvent.InvokeAsync(new LogMessage(severity, source, null, ex)).ConfigureAwait(false); } - catch { } + catch + { + // ignored + } } public async Task LogAsync(LogSeverity severity, string source, string message, Exception ex = null) { @@ -33,7 +36,10 @@ namespace Discord.Logging if (severity <= Level) await _messageEvent.InvokeAsync(new LogMessage(severity, source, message, ex)).ConfigureAwait(false); } - catch { } + catch + { + // ignored + } } #if FORMATSTR public async Task LogAsync(LogSeverity severity, string source, FormattableString message, Exception ex = null) diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 2a03819cf..2318f3f98 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -44,7 +44,7 @@ namespace Discord /// /// Initializes a new class with the default request timeout set in - /// . + /// . /// public RequestOptions() { diff --git a/src/Discord.Net.Core/Utils/MentionUtils.cs b/src/Discord.Net.Core/Utils/MentionUtils.cs index 36779de6e..edfd3b12c 100644 --- a/src/Discord.Net.Core/Utils/MentionUtils.cs +++ b/src/Discord.Net.Core/Utils/MentionUtils.cs @@ -31,11 +31,12 @@ namespace Discord /// /// Parses a provided user mention string. /// + /// Invalid mention format. public static ulong ParseUser(string text) { if (TryParseUser(text, out ulong id)) return id; - throw new ArgumentException("Invalid mention format", nameof(text)); + throw new ArgumentException("Invalid mention format.", nameof(text)); } /// /// Tries to parse a provided user mention string. @@ -59,11 +60,12 @@ namespace Discord /// /// Parses a provided channel mention string. /// + /// Invalid mention format. public static ulong ParseChannel(string text) { if (TryParseChannel(text, out ulong id)) return id; - throw new ArgumentException("Invalid mention format", nameof(text)); + throw new ArgumentException("Invalid mention format.", nameof(text)); } /// /// Tries to parse a provided channel mention string. @@ -84,11 +86,12 @@ namespace Discord /// /// Parses a provided role mention string. /// + /// Invalid mention format. public static ulong ParseRole(string text) { if (TryParseRole(text, out ulong id)) return id; - throw new ArgumentException("Invalid mention format", nameof(text)); + throw new ArgumentException("Invalid mention format.", nameof(text)); } /// /// Tries to parse a provided role mention string. @@ -163,22 +166,22 @@ namespace Discord if (user != null) return $"@{guildUser?.Nickname ?? user?.Username}"; else - return $""; + return ""; case TagHandling.NameNoPrefix: if (user != null) return $"{guildUser?.Nickname ?? user?.Username}"; else - return $""; + return ""; case TagHandling.FullName: if (user != null) return $"@{user.Username}#{user.Discriminator}"; else - return $""; + return ""; case TagHandling.FullNameNoPrefix: if (user != null) return $"{user.Username}#{user.Discriminator}"; else - return $""; + return ""; case TagHandling.Sanitize: if (guildUser != null && guildUser.Nickname == null) return MentionUser($"{SanitizeChar}{tag.Key}", false); @@ -200,13 +203,13 @@ namespace Discord if (channel != null) return $"#{channel.Name}"; else - return $""; + return ""; case TagHandling.NameNoPrefix: case TagHandling.FullNameNoPrefix: if (channel != null) return $"{channel.Name}"; else - return $""; + return ""; case TagHandling.Sanitize: return MentionChannel($"{SanitizeChar}{tag.Key}"); } @@ -225,13 +228,13 @@ namespace Discord if (role != null) return $"@{role.Name}"; else - return $""; + return ""; case TagHandling.NameNoPrefix: case TagHandling.FullNameNoPrefix: if (role != null) return $"{role.Name}"; else - return $""; + return ""; case TagHandling.Sanitize: return MentionRole($"{SanitizeChar}{tag.Key}"); } diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 300f584e4..1af6dc35d 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Discord { @@ -86,7 +86,7 @@ namespace Discord private static ArgumentException CreateNotEqualException(string name, string msg, T value) { - if (msg == null) return new ArgumentException($"Value may not be equal to {value}", name); + if (msg == null) return new ArgumentException($"Value may not be equal to {value}.", name); else return new ArgumentException(msg, name); } @@ -109,7 +109,7 @@ namespace Discord private static ArgumentException CreateAtLeastException(string name, string msg, T value) { - if (msg == null) return new ArgumentException($"Value must be at least {value}", name); + if (msg == null) return new ArgumentException($"Value must be at least {value}.", name); else return new ArgumentException(msg, name); } @@ -132,7 +132,7 @@ namespace Discord private static ArgumentException CreateGreaterThanException(string name, string msg, T value) { - if (msg == null) return new ArgumentException($"Value must be greater than {value}", name); + if (msg == null) return new ArgumentException($"Value must be greater than {value}.", name); else return new ArgumentException(msg, name); } @@ -155,7 +155,7 @@ namespace Discord private static ArgumentException CreateAtMostException(string name, string msg, T value) { - if (msg == null) return new ArgumentException($"Value must be at most {value}", name); + if (msg == null) return new ArgumentException($"Value must be at most {value}.", name); else return new ArgumentException(msg, name); } @@ -178,11 +178,12 @@ namespace Discord private static ArgumentException CreateLessThanException(string name, string msg, T value) { - if (msg == null) return new ArgumentException($"Value must be less than {value}", name); + if (msg == null) return new ArgumentException($"Value must be less than {value}.", name); else return new ArgumentException(msg, name); } // Bulk Delete + /// Messages are younger than 2 weeks.. public static void YoungerThanTwoWeeks(ulong[] collection, string name) { var minimum = SnowflakeUtils.ToSnowflake(DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(14))); @@ -193,12 +194,13 @@ namespace Discord throw new ArgumentOutOfRangeException(name, "Messages must be younger than two weeks old."); } } + /// The everyone role cannot be assigned to a user public static void NotEveryoneRole(ulong[] roles, ulong guildId, string name) { for (var i = 0; i < roles.Length; i++) { if (roles[i] == guildId) - throw new ArgumentException($"The everyone role cannot be assigned to a user", name); + throw new ArgumentException("The everyone role cannot be assigned to a user.", name); } } } diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index f8642b96c..8ba7ea718 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -24,11 +24,20 @@ namespace Discord.Rest internal API.DiscordRestApiClient ApiClient { get; } internal LogManager LogManager { get; } + /// + /// Gets the login state of the client. + /// public LoginState LoginState { get; private set; } + /// + /// Gets the logged-in user. + /// public ISelfUser CurrentUser { get; protected set; } + /// + /// Gets the type of the authentication token. + /// public TokenType TokenType => ApiClient.AuthTokenType; - /// Creates a new REST-only discord client. + /// Creates a new REST-only Discord client. internal BaseDiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client) { ApiClient = client; @@ -48,8 +57,7 @@ namespace Discord.Rest }; ApiClient.SentRequest += async (method, endpoint, millis) => await _restLogger.VerboseAsync($"{method} {endpoint}: {millis} ms").ConfigureAwait(false); } - - /// + public async Task LoginAsync(TokenType tokenType, string token, bool validateToken = true) { await _stateLock.WaitAsync().ConfigureAwait(false); @@ -87,8 +95,7 @@ namespace Discord.Rest } internal virtual Task OnLoginAsync(TokenType tokenType, string token) => Task.Delay(0); - - /// + public async Task LogoutAsync() { await _stateLock.WaitAsync().ConfigureAwait(false); @@ -130,49 +137,68 @@ namespace Discord.Rest => ClientHelper.GetRecommendShardCountAsync(this, options); //IDiscordClient + /// ConnectionState IDiscordClient.ConnectionState => ConnectionState.Disconnected; + /// ISelfUser IDiscordClient.CurrentUser => CurrentUser; + /// Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => throw new NotSupportedException(); + /// Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); + /// Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); + /// Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); + /// Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); + /// Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); + /// Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => Task.FromResult(null); + /// Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); + /// Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); + /// Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => throw new NotSupportedException(); + /// Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(null); + /// Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); + /// Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(null); + /// Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions options) => Task.FromResult(null); + /// Task IDiscordClient.StartAsync() => Task.Delay(0); + /// Task IDiscordClient.StopAsync() => Task.Delay(0); } diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index 2dc3ebe05..471f8ee6c 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -33,65 +33,49 @@ namespace Discord.Rest _applicationInfo = null; return Task.Delay(0); } - - /// + public async Task GetApplicationInfoAsync(RequestOptions options = null) { return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false)); } - /// public Task GetChannelAsync(ulong id, RequestOptions options = null) => ClientHelper.GetChannelAsync(this, id, options); - /// public Task> GetPrivateChannelsAsync(RequestOptions options = null) => ClientHelper.GetPrivateChannelsAsync(this, options); public Task> GetDMChannelsAsync(RequestOptions options = null) => ClientHelper.GetDMChannelsAsync(this, options); public Task> GetGroupChannelsAsync(RequestOptions options = null) => ClientHelper.GetGroupChannelsAsync(this, options); - - /// + public Task> GetConnectionsAsync(RequestOptions options = null) => ClientHelper.GetConnectionsAsync(this, options); - - /// + public Task GetInviteAsync(string inviteId, RequestOptions options = null) => ClientHelper.GetInviteAsync(this, inviteId, options); - - /// + public Task GetGuildAsync(ulong id, RequestOptions options = null) => ClientHelper.GetGuildAsync(this, id, options); - /// public Task GetGuildEmbedAsync(ulong id, RequestOptions options = null) => ClientHelper.GetGuildEmbedAsync(this, id, options); - /// public IAsyncEnumerable> GetGuildSummariesAsync(RequestOptions options = null) => ClientHelper.GetGuildSummariesAsync(this, null, null, options); - /// public IAsyncEnumerable> GetGuildSummariesAsync(ulong fromGuildId, int limit, RequestOptions options = null) => ClientHelper.GetGuildSummariesAsync(this, fromGuildId, limit, options); - /// public Task> GetGuildsAsync(RequestOptions options = null) => ClientHelper.GetGuildsAsync(this, options); - /// public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options); - - /// + public Task GetUserAsync(ulong id, RequestOptions options = null) => ClientHelper.GetUserAsync(this, id, options); - /// public Task GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) => ClientHelper.GetGuildUserAsync(this, guildId, id, options); - - /// + public Task> GetVoiceRegionsAsync(RequestOptions options = null) => ClientHelper.GetVoiceRegionsAsync(this, options); - /// public Task GetVoiceRegionAsync(string id, RequestOptions options = null) => ClientHelper.GetVoiceRegionAsync(this, id, options); - /// public Task GetWebhookAsync(ulong id, RequestOptions options = null) => ClientHelper.GetWebhookAsync(this, id, options); diff --git a/src/Discord.Net.Rest/DiscordRestConfig.cs b/src/Discord.Net.Rest/DiscordRestConfig.cs index 4a7aae287..68fa68e03 100644 --- a/src/Discord.Net.Rest/DiscordRestConfig.cs +++ b/src/Discord.Net.Rest/DiscordRestConfig.cs @@ -1,7 +1,10 @@ -using Discord.Net.Rest; +using Discord.Net.Rest; namespace Discord.Rest { + /// + /// Represents a configuration class for . + /// public class DiscordRestConfig : DiscordConfig { /// Gets or sets the provider used to generate new REST connections. diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 6784f7f6a..7723861fa 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -288,7 +288,7 @@ namespace Discord.Rest } public static IDisposable EnterTypingState(IMessageChannel channel, BaseDiscordClient client, RequestOptions options) - => new TypingNotifier(client, channel, options); + => new TypingNotifier(channel, options); //Webhooks public static async Task CreateWebhookAsync(ITextChannel channel, BaseDiscordClient client, string name, Stream avatar, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs index 2bb5ed209..d253d562e 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInviteMetadata.cs @@ -3,7 +3,7 @@ using Model = Discord.API.InviteMetadata; namespace Discord.Rest { - /// Represents additional information regarding the REST invite object. + /// Represents additional information regarding the REST-based invite object. public class RestInviteMetadata : RestInvite, IInviteMetadata { private long _createdAtTicks; @@ -48,6 +48,7 @@ namespace Discord.Rest _createdAtTicks = model.CreatedAt.UtcTicks; } + /// IUser IInviteMetadata.Inviter => Inviter; } } diff --git a/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs b/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs index a5a440d8b..9bbcfb8a3 100644 --- a/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using Newtonsoft.Json; using Model = Discord.API.Image; @@ -13,6 +13,7 @@ namespace Discord.Net.Converters public override bool CanRead => true; public override bool CanWrite => true; + /// Cannot read from image. public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new InvalidOperationException(); diff --git a/src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs b/src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs index 0ed566a84..de2e379d7 100644 --- a/src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/PermissionTargetConverter.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using System; namespace Discord.Net.Converters @@ -11,6 +11,7 @@ namespace Discord.Net.Converters public override bool CanRead => true; public override bool CanWrite => true; + /// Unknown permission target. public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { switch ((string)reader.Value) @@ -20,10 +21,11 @@ namespace Discord.Net.Converters case "role": return PermissionTarget.Role; default: - throw new JsonSerializationException("Unknown permission target"); + throw new JsonSerializationException("Unknown permission target."); } } + /// Invalid permission target. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { switch ((PermissionTarget)value) @@ -35,7 +37,7 @@ namespace Discord.Net.Converters writer.WriteValue("role"); break; default: - throw new JsonSerializationException("Invalid permission target"); + throw new JsonSerializationException("Invalid permission target."); } } } diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs index 943b76359..e3b34eb8f 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; #if DEBUG_LIMITS using System.Diagnostics; @@ -117,7 +117,7 @@ namespace Discord.Net.Queue if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) _buckets.TryRemove(bucket.Id, out RequestBucket ignored); } - await Task.Delay(60000, _cancelToken.Token); //Runs each minute + await Task.Delay(60000, _cancelToken.Token).ConfigureAwait(false); //Runs each minute } } catch (OperationCanceledException) { } diff --git a/src/Discord.Net.Rest/Utils/TypingNotifier.cs b/src/Discord.Net.Rest/Utils/TypingNotifier.cs index b4bd2f44b..745dbd36d 100644 --- a/src/Discord.Net.Rest/Utils/TypingNotifier.cs +++ b/src/Discord.Net.Rest/Utils/TypingNotifier.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; @@ -6,21 +6,19 @@ namespace Discord.Rest { internal class TypingNotifier : IDisposable { - private readonly BaseDiscordClient _client; private readonly CancellationTokenSource _cancelToken; private readonly IMessageChannel _channel; private readonly RequestOptions _options; - public TypingNotifier(BaseDiscordClient discord, IMessageChannel channel, RequestOptions options) + public TypingNotifier(IMessageChannel channel, RequestOptions options) { - _client = discord; _cancelToken = new CancellationTokenSource(); _channel = channel; _options = options; - var _ = Run(); + _ = RunAsync(); } - private async Task Run() + private async Task RunAsync() { try { @@ -31,7 +29,11 @@ namespace Discord.Rest { await _channel.TriggerTypingAsync(_options).ConfigureAwait(false); } - catch { } + catch + { + // ignored + } + await Task.Delay(9500, token).ConfigureAwait(false); } } diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 1f33b3cc5..c3cbc9ca7 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -1,4 +1,4 @@ -using Discord.API.Voice; +using Discord.API.Voice; using Discord.Audio.Streams; using Discord.Logging; using Discord.Net.Converters; @@ -65,7 +65,7 @@ namespace Discord.Audio ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider, Discord.UdpSocketProvider); ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false); - ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync($"Sent Discovery").ConfigureAwait(false); + ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync("Sent Discovery").ConfigureAwait(false); //ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false); ApiClient.ReceivedEvent += ProcessMessageAsync; ApiClient.ReceivedPacket += ProcessPacketAsync; @@ -291,7 +291,7 @@ namespace Discord.Audio { if (packet.Length != 70) { - await _audioLogger.DebugAsync($"Malformed Packet").ConfigureAwait(false); + await _audioLogger.DebugAsync("Malformed Packet").ConfigureAwait(false); return; } string ip; @@ -303,7 +303,7 @@ namespace Discord.Audio } catch (Exception ex) { - await _audioLogger.DebugAsync($"Malformed Packet", ex).ConfigureAwait(false); + await _audioLogger.DebugAsync("Malformed Packet", ex).ConfigureAwait(false); return; } @@ -343,7 +343,7 @@ namespace Discord.Audio { if (!RTPReadStream.TryReadSsrc(packet, 0, out var ssrc)) { - await _audioLogger.DebugAsync($"Malformed Frame").ConfigureAwait(false); + await _audioLogger.DebugAsync("Malformed Frame").ConfigureAwait(false); return; } if (!_ssrcMap.TryGetValue(ssrc, out var userId)) @@ -362,7 +362,7 @@ namespace Discord.Audio } catch (Exception ex) { - await _audioLogger.DebugAsync($"Malformed Frame", ex).ConfigureAwait(false); + await _audioLogger.DebugAsync("Malformed Frame", ex).ConfigureAwait(false); return; } //await _audioLogger.DebugAsync($"Received {packet.Length} bytes from user {userId}").ConfigureAwait(false); @@ -371,7 +371,7 @@ namespace Discord.Audio } catch (Exception ex) { - await _audioLogger.WarningAsync($"Failed to process UDP packet", ex).ConfigureAwait(false); + await _audioLogger.WarningAsync("Failed to process UDP packet", ex).ConfigureAwait(false); return; } } diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index fb302f132..1365ddac2 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -116,7 +116,7 @@ namespace Discord.Audio.Streams timestamp += OpusEncoder.FrameSamplesPerChannel; } #if DEBUG - var _ = _logger?.DebugAsync($"Buffer underrun"); + var _ = _logger?.DebugAsync("Buffer underrun"); #endif } } @@ -140,7 +140,7 @@ namespace Discord.Audio.Streams if (!_bufferPool.TryDequeue(out byte[] buffer)) { #if DEBUG - var _ = _logger?.DebugAsync($"Buffer overflow"); //Should never happen because of the queueLock + var _ = _logger?.DebugAsync("Buffer overflow"); //Should never happen because of the queueLock #endif return; } @@ -149,7 +149,7 @@ namespace Discord.Audio.Streams if (!_isPreloaded && _queuedFrames.Count == _queueLength) { #if DEBUG - var _ = _logger?.DebugAsync($"Preloaded"); + var _ = _logger?.DebugAsync("Preloaded"); #endif _isPreloaded = true; } @@ -173,4 +173,4 @@ namespace Discord.Audio.Streams return Task.Delay(0); } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 923b2c23b..f628720ec 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -26,18 +26,12 @@ namespace Discord.WebSocket : base(config, client) => _baseconfig = config; private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) => new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent); - - /// + public abstract Task GetApplicationInfoAsync(RequestOptions options = null); - /// public abstract SocketUser GetUser(ulong id); - /// public abstract SocketUser GetUser(string username, string discriminator); - /// public abstract SocketChannel GetChannel(ulong id); - /// public abstract SocketGuild GetGuild(ulong id); - /// public abstract RestVoiceRegion GetVoiceRegion(string id); /// public abstract Task StartAsync(); @@ -47,47 +41,56 @@ namespace Discord.WebSocket public abstract Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing); public abstract Task SetActivityAsync(IActivity activity); public abstract Task DownloadUsersAsync(IEnumerable guilds); - - /// + public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options ?? RequestOptions.Default); - /// public Task> GetConnectionsAsync(RequestOptions options = null) => ClientHelper.GetConnectionsAsync(this, options ?? RequestOptions.Default); - /// public Task GetInviteAsync(string inviteId, RequestOptions options = null) => ClientHelper.GetInviteAsync(this, inviteId, options ?? RequestOptions.Default); // IDiscordClient + /// async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync(options).ConfigureAwait(false); + /// Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetChannel(id)); + /// Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(PrivateChannels); + /// async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync(options).ConfigureAwait(false); + /// async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => await GetInviteAsync(inviteId, options).ConfigureAwait(false); + /// Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetGuild(id)); + /// Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Guilds); + /// async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => await CreateGuildAsync(name, region, jpegIcon, options).ConfigureAwait(false); + /// Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); + /// Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(GetVoiceRegion(id)); + /// Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) => Task.FromResult>(VoiceRegions); } diff --git a/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs index 29d78ab45..0fb47ceff 100644 --- a/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs +++ b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs @@ -5,20 +5,37 @@ 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. + /// + /// Gets the that the command is executed with. + /// public DiscordSocketClient Client { get; } - /// Gets the that the command is executed in. + /// + /// Gets the that the command is executed in. + /// public SocketGuild Guild { get; } - /// Gets the that the command is executed in. + /// + /// Gets the that the command is executed in. + /// public ISocketMessageChannel Channel { get; } - /// Gets the who executed the command. + /// + /// Gets the who executed the command. + /// public SocketUser User { get; } - /// Gets the that the command is interpreted from. + /// + /// 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. + /// + /// Indicates whether the channel that the command is executed in is a private channel. + /// public bool IsPrivate => Channel is IPrivateChannel; + /// + /// Initializes a new class with the provided client and message. + /// + /// The underlying client. + /// The underlying message. public SocketCommandContext(DiscordSocketClient client, SocketUserMessage msg) { Client = client; diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index ad89067df..c65722e6f 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -19,24 +19,29 @@ namespace Discord.WebSocket private int _totalShards; private bool _automaticShards; - /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. + /// public override int Latency { get => GetLatency(); protected set { } } + /// public override UserStatus Status { get => _shards[0].Status; protected set { } } + /// public override IActivity Activity { get => _shards[0].Activity; protected set { } } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; - public override IReadOnlyCollection Guilds => GetGuilds().ToReadOnlyCollection(() => GetGuildCount()); - public override IReadOnlyCollection PrivateChannels => GetPrivateChannels().ToReadOnlyCollection(() => GetPrivateChannelCount()); + /// + public override IReadOnlyCollection Guilds => GetGuilds().ToReadOnlyCollection(GetGuildCount); + /// + public override IReadOnlyCollection PrivateChannels => GetPrivateChannels().ToReadOnlyCollection(GetPrivateChannelCount); public IReadOnlyCollection Shards => _shards; + /// public override IReadOnlyCollection VoiceRegions => _shards[0].VoiceRegions; - /// Creates a new REST/WebSocket discord client. + /// Creates a new REST/WebSocket Discord client. public DiscordShardedClient() : this(null, new DiscordSocketConfig()) { } - /// Creates a new REST/WebSocket discord client. + /// Creates a new REST/WebSocket Discord client. public DiscordShardedClient(DiscordSocketConfig config) : this(null, config, CreateApiClient(config)) { } - /// Creates a new REST/WebSocket discord client. + /// Creates a new REST/WebSocket Discord client. public DiscordShardedClient(int[] ids) : this(ids, new DiscordSocketConfig()) { } - /// Creates a new REST/WebSocket discord client. + /// Creates a new REST/WebSocket Discord client. public DiscordShardedClient(int[] ids, DiscordSocketConfig config) : this(ids, config, CreateApiClient(config)) { } private DiscordShardedClient(int[] ids, DiscordSocketConfig config, API.DiscordSocketApiClient client) : base(config, client) @@ -213,7 +218,9 @@ namespace Discord.WebSocket public override RestVoiceRegion GetVoiceRegion(string id) => _shards[0].GetVoiceRegion(id); - /// Downloads the users list for the provided guilds, if they don't have a complete list. + /// + /// Downloads the users list for the provided guilds if they don't have a complete list. + /// public override async Task DownloadUsersAsync(IEnumerable guilds) { for (int i = 0; i < _shards.Length; i++) @@ -233,11 +240,13 @@ namespace Discord.WebSocket return (int)Math.Round(total / (double)_shards.Length); } + /// public override async Task SetStatusAsync(UserStatus status) { for (int i = 0; i < _shards.Length; i++) await _shards[i].SetStatusAsync(status).ConfigureAwait(false); } + /// public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing) { IActivity activity = null; @@ -247,6 +256,7 @@ namespace Discord.WebSocket activity = new Game(name, type); await SetActivityAsync(activity).ConfigureAwait(false); } + /// public override async Task SetActivityAsync(IActivity activity) { for (int i = 0; i < _shards.Length; i++) @@ -316,34 +326,46 @@ namespace Discord.WebSocket } //IDiscordClient + /// async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync().ConfigureAwait(false); + /// Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetChannel(id)); + /// Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(PrivateChannels); + /// async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync().ConfigureAwait(false); + /// async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => await GetInviteAsync(inviteId).ConfigureAwait(false); + /// Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetGuild(id)); + /// Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Guilds); + /// async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); + /// Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); + /// Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) => Task.FromResult>(VoiceRegions); + /// Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(GetVoiceRegion(id)); } diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 80c16ea79..c9f8fa5f5 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -508,7 +508,7 @@ namespace Discord.WebSocket { type = "GUILD_AVAILABLE"; _lastGuildAvailableTime = Environment.TickCount; - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_AVAILABLE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_AVAILABLE)").ConfigureAwait(false); var guild = State.GetGuild(data.Id); if (guild != null) @@ -533,7 +533,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_CREATE)").ConfigureAwait(false); var guild = AddGuild(data, State); if (guild != null) @@ -614,7 +614,7 @@ namespace Discord.WebSocket if (data.Unavailable == true) { type = "GUILD_UNAVAILABLE"; - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_UNAVAILABLE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UNAVAILABLE)").ConfigureAwait(false); var guild = State.GetGuild(data.Id); if (guild != null) @@ -630,7 +630,7 @@ namespace Discord.WebSocket } else { - await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_DELETE)").ConfigureAwait(false); + await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_DELETE)").ConfigureAwait(false); var guild = RemoveGuild(data.Id); if (guild != null) diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index 3f9c18863..17f200c08 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -1,41 +1,72 @@ -using Discord.Net.Udp; +using Discord.Net.Udp; using Discord.Net.WebSockets; using Discord.Rest; namespace Discord.WebSocket { + /// + /// Represents a configuration class for . + /// public class DiscordSocketConfig : DiscordRestConfig { + /// + /// Gets or sets the encoding gateway should use. + /// public const string GatewayEncoding = "json"; - /// Gets or sets the websocket host to connect to. If null, the client will use the /gateway endpoint. + /// + /// Gets or sets the WebSocket host to connect to. If , the client will use the + /// /gateway endpoint. + /// public string GatewayHost { get; set; } = null; - /// Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. + /// + /// Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. + /// public int ConnectionTimeout { get; set; } = 30000; - /// Gets or sets the id for this shard. Must be less than TotalShards. + /// + /// Gets or sets the ID for this shard. Must be less than . + /// public int? ShardId { get; set; } = null; - /// Gets or sets the total number of shards for this application. + /// + /// Gets or sets the total number of shards for this application. + /// public int? TotalShards { get; set; } = null; - /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. + /// + /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero + /// disables the message cache entirely. + /// public int MessageCacheSize { get; set; } = 0; - /// - /// Gets or sets the max number of users a guild may have for offline users to be included in the READY packet. Max is 250. + /// + /// Gets or sets the max number of users a guild may have for offline users to be included in the READY + /// packet. Max is 250. /// public int LargeThreshold { get; set; } = 250; - /// Gets or sets the provider used to generate new websocket connections. + /// + /// Gets or sets the provider used to generate new WebSocket connections. + /// public WebSocketProvider WebSocketProvider { get; set; } - /// Gets or sets the provider used to generate new udp sockets. + /// + /// Gets or sets the provider used to generate new UDP sockets. + /// public UdpSocketProvider UdpSocketProvider { get; set; } - /// Gets or sets whether or not all users should be downloaded as guilds come available. + /// + /// Gets or sets whether or not all users should be downloaded as guilds come available. + /// public bool AlwaysDownloadUsers { get; set; } = false; - /// Gets or sets the timeout for event handlers, in milliseconds, after which a warning will be logged. Null disables this check. + /// + /// Gets or sets the timeout for event handlers, in milliseconds, after which a warning will be logged. Null + /// disables this check. + /// public int? HandlerTimeout { get; set; } = 3000; + /// + /// Initializes a default configuration. + /// public DiscordSocketConfig() { WebSocketProvider = DefaultWebSocketProvider.Instance; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs index 7056a4df5..3cb978abf 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketAudioChannel.cs @@ -1,5 +1,8 @@ -namespace Discord.WebSocket +namespace Discord.WebSocket { + /// + /// Represents a generic WebSocket-based audio channel. + /// public interface ISocketAudioChannel : IAudioChannel { } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index 026bd8378..6d769b9c4 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -5,18 +5,52 @@ using System.Threading.Tasks; namespace Discord.WebSocket { + /// + /// Represents a generic WebSocket-based channel that can send and receive messages. + /// public interface ISocketMessageChannel : IMessageChannel { /// Gets all messages in this channel's cache. IReadOnlyCollection CachedMessages { get; } - /// Sends a message to this message channel. + /// + /// Sends a message to this message channel. + /// + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. 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. + /// + /// The file path of the file. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// If you wish to upload an image and have it embedded in a embed, you may + /// upload the file and refer to the file with "attachment://filename.ext" in the + /// . + /// 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. + /// + /// The of the file to be sent. + /// The name of the attachment. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// If you wish to upload an image and have it embedded in a embed, you may + /// upload the file and refer to the file with "attachment://filename.ext" in the + /// . + /// new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); SocketMessage GetCachedMessage(ulong id); @@ -26,7 +60,13 @@ namespace Discord.WebSocket IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); /// Gets a collection of messages in this channel. IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); - /// Gets a collection of pinned messages in this channel. + /// + /// Gets a collection of pinned messages in this channel. + /// + /// The options to be used when sending the request. + /// + /// A collection of messages. + /// new Task> GetPinnedMessagesAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs index 4e91673dd..08da2237c 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketPrivateChannel.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Discord.WebSocket { + /// + /// Represents a generic WebSocket-based channel that is private to select recipients. + /// public interface ISocketPrivateChannel : IPrivateChannel { new IReadOnlyCollection Recipients { get; } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index e7a165c2f..1d05b4974 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -3,14 +3,14 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; -using System.Text; using System.Threading.Tasks; -using Discord.Audio; -using Discord.Rest; using Model = Discord.API.Channel; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based category channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel { diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs index 502e61d15..ec842c8a3 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -1,4 +1,3 @@ -using Discord.Rest; using System; using System.Collections.Generic; using System.Diagnostics; @@ -8,16 +7,27 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public abstract class SocketChannel : SocketEntity, IChannel { + /// + /// Gets when the channel is created. + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// + /// Gets a collection of users from the WebSocket cache. + /// public IReadOnlyCollection Users => GetUsersInternal(); internal SocketChannel(DiscordSocketClient discord, ulong id) : base(discord, id) { } + + /// Unexpected channel type is created. internal static ISocketPrivateChannel CreatePrivate(DiscordSocketClient discord, ClientState state, Model model) { switch (model.Type) @@ -33,6 +43,17 @@ namespace Discord.WebSocket internal abstract void Update(ClientState state, Model model); //User + /// + /// Gets the user from the WebSocket cache. + /// + /// + /// This method does NOT attempt to fetch the user if they don't exist in the cache. To guarantee a return + /// from an existing user that doesn't exist in cache, use . + /// + /// The ID of the user. + /// + /// The user. + /// public SocketUser GetUser(ulong id) => GetUserInternal(id); internal abstract SocketUser GetUserInternal(ulong id); internal abstract IReadOnlyCollection GetUsersInternal(); @@ -40,10 +61,13 @@ namespace Discord.WebSocket internal SocketChannel Clone() => MemberwiseClone() as SocketChannel; //IChannel + /// string IChannel.Name => null; + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overridden + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => AsyncEnumerable.Empty>(); //Overridden } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 451240e66..8008d434a 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -10,13 +10,17 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based direct-message channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketDMChannel : SocketChannel, IDMChannel, ISocketPrivateChannel, ISocketMessageChannel { private readonly MessageCache _messages; - public SocketUser Recipient { get; private set; } + public SocketUser Recipient { get; } + /// public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); public new IReadOnlyCollection Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); @@ -39,10 +43,12 @@ namespace Discord.WebSocket Recipient.Update(state, model.Recipients.Value[0]); } + /// public Task CloseAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); //Messages + /// public SocketMessage GetCachedMessage(ulong id) => _messages?.Get(id); public async Task GetMessageAsync(ulong id, RequestOptions options = null) @@ -58,26 +64,35 @@ namespace Discord.WebSocket => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options); + /// public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); + /// public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit); + /// public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); + /// 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) => ChannelHelper.EnterTypingState(this, Discord, options); @@ -97,24 +112,33 @@ namespace Discord.WebSocket return null; } + /// + /// Returns the recipient user. + /// public override string ToString() => $"@{Recipient}"; private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; internal new SocketDMChannel Clone() => MemberwiseClone() as SocketDMChannel; //SocketChannel + /// internal override IReadOnlyCollection GetUsersInternal() => Users; + /// internal override SocketUser GetUserInternal(ulong id) => GetUser(id); - //IDMChannel + //IDMChannel + /// IUser IDMChannel.Recipient => Recipient; //ISocketPrivateChannel + /// IReadOnlyCollection ISocketPrivateChannel.Recipients => ImmutableArray.Create(Recipient); //IPrivateChannel + /// IReadOnlyCollection IPrivateChannel.Recipients => ImmutableArray.Create(Recipient); //IMessageChannel + /// async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -122,30 +146,41 @@ 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); - //IChannel + //IChannel + /// string IChannel.Name => $"@{Recipient}"; + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 27713609c..1d131876a 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -15,7 +15,7 @@ using VoiceStateModel = Discord.API.VoiceState; namespace Discord.WebSocket { /// - /// Represents a private WebSocket group channel. + /// Represents a WebSocket-based private group channel. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketGroupChannel : SocketChannel, IGroupChannel, ISocketPrivateChannel, ISocketMessageChannel, ISocketAudioChannel @@ -24,10 +24,12 @@ namespace Discord.WebSocket private string _iconId; private ConcurrentDictionary _users; - private ConcurrentDictionary _voiceStates; + private readonly ConcurrentDictionary _voiceStates; + /// public string Name { get; private set; } + /// public IReadOnlyCollection CachedMessages => _messages?.Messages ?? ImmutableArray.Create(); public new IReadOnlyCollection Users => _users.ToReadOnlyCollection(); public IReadOnlyCollection Recipients @@ -65,15 +67,18 @@ namespace Discord.WebSocket _users = users; } + /// public Task LeaveAsync(RequestOptions options = null) => ChannelHelper.DeleteAsync(this, Discord, options); + /// Voice is not yet supported for group channels. public Task ConnectAsync() { throw new NotSupportedException("Voice is not yet supported for group channels."); } //Messages + /// public SocketMessage GetCachedMessage(ulong id) => _messages?.Get(id); public async Task GetMessageAsync(ulong id, RequestOptions options = null) @@ -89,26 +94,35 @@ namespace Discord.WebSocket => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options); + /// public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); + /// public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit); + /// public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); + /// 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) => ChannelHelper.EnterTypingState(this, Discord, options); @@ -118,6 +132,17 @@ namespace Discord.WebSocket => _messages?.Remove(id); //Users + /// + /// Gets the group user from the WebSocket cache. + /// + /// + /// This method does NOT attempt to fetch the user if they don't exist in the cache. To guarantee a return + /// from an existing user that doesn't exist in cache, use . + /// + /// The ID of the user. + /// + /// The user in the group. + /// public new SocketGroupUser GetUser(ulong id) { if (_users.TryGetValue(id, out SocketGroupUser user)) @@ -167,21 +192,29 @@ namespace Discord.WebSocket return null; } + /// + /// Returns the name of the group. + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id}, Group)"; internal new SocketGroupChannel Clone() => MemberwiseClone() as SocketGroupChannel; //SocketChannel + /// internal override IReadOnlyCollection GetUsersInternal() => Users; + /// internal override SocketUser GetUserInternal(ulong id) => GetUser(id); //ISocketPrivateChannel + /// IReadOnlyCollection ISocketPrivateChannel.Recipients => Recipients; //IPrivateChannel + /// IReadOnlyCollection IPrivateChannel.Recipients => Recipients; //IMessageChannel + /// async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -189,31 +222,42 @@ 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); //IAudioChannel + /// Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } //IChannel + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 84072a2ab..8d6f22133 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -9,12 +9,20 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { - /// The WebSocket variant of . Represents a guild channel (text, voice, category). + /// + /// Represents a WebSocket-based guild channel. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketGuildChannel : SocketChannel, IGuildChannel { private ImmutableArray _overwrites; + /// + /// Gets the guild associated with this channel. + /// + /// + /// The guild that this channel belongs to. + /// public SocketGuild Guild { get; } /// public string Name { get; private set; } @@ -22,11 +30,23 @@ namespace Discord.WebSocket public int Position { get; private set; } /// public ulong? CategoryId { get; private set; } + /// + /// Gets the parent category of this channel. + /// + /// + /// The parent category ID associated with this channel, or if none is set. + /// public ICategoryChannel Category => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; /// public IReadOnlyCollection PermissionOverwrites => _overwrites; + /// + /// Returns a collection of users that are able to view the channel. + /// + /// + /// A collection of users that can access the channel (i.e. the users seen in the user list). + /// public new virtual IReadOnlyCollection Users => ImmutableArray.Create(); internal SocketGuildChannel(DiscordSocketClient discord, ulong id, SocketGuild guild) @@ -131,7 +151,11 @@ namespace Discord.WebSocket public new virtual SocketGuildUser GetUser(ulong id) => null; + /// + /// Gets the name of the channel. + /// public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Id}, Guild)"; internal new SocketGuildChannel Clone() => MemberwiseClone() as SocketGuildChannel; //SocketChannel diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 368a2a730..5cc35cf14 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -10,6 +10,9 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based channel in a guild that can send and receive messages. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketTextChannel : SocketGuildChannel, ITextChannel, ISocketMessageChannel { @@ -73,12 +76,16 @@ namespace Discord.WebSocket => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options); + /// public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); + /// public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessageId, dir, limit); + /// public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); + /// public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); @@ -104,6 +111,7 @@ namespace Discord.WebSocket /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); + /// public IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); @@ -128,10 +136,34 @@ namespace Discord.WebSocket } //Webhooks + /// + /// Creates a webhook in this text channel. + /// + /// The name of the webhook. + /// The avatar of the webhook. + /// The options to be used when sending the request. + /// + /// The created webhook. + /// public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); + /// + /// Gets the webhook in this text channel with the provided ID. + /// + /// The ID of the webhook. + /// The options to be used when sending the request. + /// + /// A webhook associated with the , or if not found. + /// public Task GetWebhookAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetWebhookAsync(this, Discord, id, options); + /// + /// Gets the webhooks for this text channel. + /// + /// The options to be used when sending the request. + /// + /// A collection of webhooks. + /// public Task> GetWebhooksAsync(RequestOptions options = null) => ChannelHelper.GetWebhooksAsync(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index e8a669845..568c5ad7b 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -1,4 +1,4 @@ -using Discord.Audio; +using Discord.Audio; using Discord.Rest; using System; using System.Collections.Generic; @@ -10,12 +10,18 @@ using Model = Discord.API.Channel; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based voice channel in a guild. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketVoiceChannel : SocketGuildChannel, IVoiceChannel, ISocketAudioChannel { + /// public int Bitrate { get; private set; } + /// public int? UserLimit { get; private set; } + /// public override IReadOnlyCollection Users => Guild.Users.Where(x => x.VoiceChannel?.Id == Id).ToImmutableArray(); @@ -37,14 +43,17 @@ namespace Discord.WebSocket UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; } + /// public Task ModifyAsync(Action func, RequestOptions options = null) => ChannelHelper.ModifyAsync(this, Discord, func, options); + /// public async Task ConnectAsync(Action configAction = null) { return await Guild.ConnectAudioAsync(Id, false, false, configAction).ConfigureAwait(false); } + /// public override SocketGuildUser GetUser(ulong id) { var user = Guild.GetUser(id); @@ -57,8 +66,10 @@ namespace Discord.WebSocket internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; //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(); } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 5acb359c3..c4c223e64 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -21,6 +21,10 @@ using VoiceStateModel = Discord.API.VoiceState; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based guild object. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketGuild : SocketEntity, IGuild { private readonly SemaphoreSlim _audioLock; @@ -46,11 +50,13 @@ namespace Discord.WebSocket 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. - /// + /// + /// Gets the number of members. + /// + /// + /// The number of members is returned by Discord and is the most accurate. You may see discrepancy between + /// the 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; } @@ -84,11 +90,23 @@ namespace Discord.WebSocket public bool IsSynced => _syncPromise.Task.IsCompleted; public Task SyncPromise => _syncPromise.Task; public Task DownloaderPromise => _downloaderPromise.Task; + /// + /// Returns the associated with this guild. + /// public IAudioClient AudioClient => _audioClient; + /// + /// Returns the first viewable text channel. + /// + /// + /// This property does not guarantee the user can send message to it. + /// public SocketTextChannel DefaultChannel => TextChannels .Where(c => CurrentUser.GetPermissions(c).ViewChannel) .OrderBy(c => c.Position) .FirstOrDefault(); + /// + /// Returns the AFK voice channel, or if none is set. + /// public SocketVoiceChannel AFKChannel { get @@ -97,6 +115,9 @@ namespace Discord.WebSocket return id.HasValue ? GetVoiceChannel(id.Value) : null; } } + /// + /// Gets the embed channel set in the widget settings of this guild, or if none is set. + /// public SocketGuildChannel EmbedChannel { get @@ -105,6 +126,9 @@ namespace Discord.WebSocket return id.HasValue ? GetChannel(id.Value) : null; } } + /// + /// Gets the channel where randomized welcome messages are sent, or if none is set. + /// public SocketTextChannel SystemChannel { get @@ -113,14 +137,32 @@ namespace Discord.WebSocket return id.HasValue ? GetTextChannel(id.Value) : null; } } + /// + /// Returns a collection of text channels present in this guild. + /// public IReadOnlyCollection TextChannels => Channels.Select(x => x as SocketTextChannel).Where(x => x != null).ToImmutableArray(); + /// + /// Returns a collection of voice channels present in this guild. + /// public IReadOnlyCollection VoiceChannels => Channels.Select(x => x as SocketVoiceChannel).Where(x => x != null).ToImmutableArray(); + /// + /// Returns a collection of category channels present in this guild. + /// public IReadOnlyCollection CategoryChannels => Channels.Select(x => x as SocketCategoryChannel).Where(x => x != null).ToImmutableArray(); + /// + /// Returns the current logged-in user. + /// public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; + /// + /// Returns the @everyone role in this guild. + /// public SocketRole EveryoneRole => GetRole(Id); + /// + /// Returns a collection of channels present in this guild. + /// public IReadOnlyCollection Channels { get @@ -130,9 +172,26 @@ namespace Discord.WebSocket return channels.Select(x => state.GetChannel(x) as SocketGuildChannel).Where(x => x != null).ToReadOnlyCollection(channels); } } + /// + /// Gets a collection of emotes created in this guild. + /// public IReadOnlyCollection Emotes => _emotes; + /// + /// Gets a collection of features enabled in this guild. + /// public IReadOnlyCollection Features => _features; + /// + /// Gets a collection of users in this guild. + /// + /// + /// This property may not always return all the members for large guilds (i.e. guilds containing 100+ users). + /// You may need to enable to fetch the full user list + /// upon startup, or use to manually download the users. + /// public IReadOnlyCollection Users => _members.ToReadOnlyCollection(); + /// + /// Gets a collection of roles in this guild. + /// public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); internal SocketGuild(DiscordSocketClient client, ulong id) @@ -315,7 +374,13 @@ namespace Discord.WebSocket => GuildHelper.LeaveAsync(this, Discord, options); //Bans - /// Gets a collection of the banned users in this guild. + /// + /// Gets a collection of the banned users in this guild. + /// + /// The options to be used when sending the request. + /// + /// A collection of bans. + /// public Task> GetBansAsync(RequestOptions options = null) => GuildHelper.GetBansAsync(this, Discord, options); @@ -334,6 +399,13 @@ namespace Discord.WebSocket => GuildHelper.RemoveBanAsync(this, Discord, userId, options); //Channels + /// + /// Returns a guild channel with the provided ID. + /// + /// The channel ID. + /// + /// The guild channel associated with the ID. + /// public SocketGuildChannel GetChannel(ulong id) { var channel = Discord.State.GetChannel(id) as SocketGuildChannel; @@ -341,14 +413,52 @@ namespace Discord.WebSocket return channel; return null; } + /// + /// Returns a text channel with the provided ID. + /// + /// The channel ID. + /// + /// The text channel associated with the ID. + /// public SocketTextChannel GetTextChannel(ulong id) => GetChannel(id) as SocketTextChannel; + /// + /// Returns a voice channel with the provided ID. + /// + /// The channel ID. + /// + /// The voice channel associated with the ID. + /// public SocketVoiceChannel GetVoiceChannel(ulong id) => GetChannel(id) as SocketVoiceChannel; + /// + /// Creates a text channel with the provided name. + /// + /// The name of the new channel. + /// The options to be used when sending the request. + /// + /// The created text channel. + /// public Task CreateTextChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); + /// + /// Creates a voice channel with the provided name. + /// + /// The name of the new channel. + /// The options to be used when sending the request. + /// + /// The created voice channel. + /// public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options); + /// + /// Creates a category channel with the provided name. + /// + /// The name of the new channel. + /// The options to be used when sending the request. + /// + /// The created category channel. + /// public Task CreateCategoryChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); @@ -373,16 +483,43 @@ namespace Discord.WebSocket => GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options); //Invites + /// + /// Returns a collection of invites associated with this channel. + /// + /// The options to be used when sending the request. + /// + /// A collection of invites. + /// public Task> GetInvitesAsync(RequestOptions options = null) => GuildHelper.GetInvitesAsync(this, Discord, options); //Roles + /// + /// Returns a role with the provided role ID. + /// + /// The ID of the role. + /// + /// The role associated with the ID. + /// public SocketRole GetRole(ulong id) { if (_roles.TryGetValue(id, out SocketRole value)) return value; return null; } + /// + /// Creates a role. + /// + /// The name of the new role. + /// + /// The permissions that the new role possesses. Set to to use the default permissions. + /// + /// The color of the role. Set to to use the default color. + /// Used to determine if users of this role are separated in the user list. + /// The options to be used when sending the request. + /// + /// The created role. + /// public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false, RequestOptions options = null) => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, options); @@ -400,12 +537,20 @@ namespace Discord.WebSocket } //Users + /// + /// Gets the user with the provided ID. + /// + /// The ID of the user. + /// + /// The user associated with the ID. + /// public SocketGuildUser GetUser(ulong id) { if (_members.TryGetValue(id, out SocketGuildUser member)) return member; return null; } + /// public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); @@ -459,6 +604,9 @@ namespace Discord.WebSocket return null; } + /// + /// Downloads the users of this guild to the WebSocket cache. + /// public async Task DownloadUsersAsync() { await Discord.DownloadUsersAsync(new[] { this }).ConfigureAwait(false); @@ -469,8 +617,23 @@ namespace Discord.WebSocket } //Webhooks + /// + /// Returns the webhook with the provided ID. + /// + /// The ID of the webhook. + /// The options to be used when sending the request. + /// + /// A webhook associated with the ID. + /// public Task GetWebhookAsync(ulong id, RequestOptions options = null) => GuildHelper.GetWebhookAsync(this, Discord, id, options); + /// + /// Gets a collection of webhooks that exist in the guild. + /// + /// The options to be used when sending the request. + /// + /// A collection of webhooks. + /// public Task> GetWebhooksAsync(RequestOptions options = null) => GuildHelper.GetWebhooksAsync(this, Discord, options); @@ -592,7 +755,7 @@ namespace Discord.WebSocket try { var timeoutTask = Task.Delay(15000); - if (await Task.WhenAny(promise.Task, timeoutTask) == timeoutTask) + if (await Task.WhenAny(promise.Task, timeoutTask).ConfigureAwait(false) == timeoutTask) throw new TimeoutException(); return await promise.Task.ConfigureAwait(false); } @@ -663,6 +826,9 @@ namespace Discord.WebSocket } } + /// + /// Gets the name of the guild. + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; internal SocketGuild Clone() => MemberwiseClone() as SocketGuild; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index cad354dae..356ba63ac 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -12,7 +12,9 @@ using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { - /// The WebSocket variant of . Represents a Discord user that is in a guild. + /// + /// Represents a WebSocket guild user. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketGuildUser : SocketUser, IGuildUser { @@ -20,6 +22,9 @@ namespace Discord.WebSocket private ImmutableArray _roleIds; internal override SocketGlobalUser GlobalUser { get; } + /// + /// Gets the guild the user is in. + /// public SocketGuild Guild { get; } /// public string Nickname { get; private set; } @@ -50,17 +55,27 @@ namespace Discord.WebSocket public bool IsMuted => VoiceState?.IsMuted ?? false; /// public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); + /// + /// Returns a collection of roles that the user possesses. + /// public IReadOnlyCollection Roles => _roleIds.Select(id => Guild.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length); + /// + /// Returns the voice channel the user is in, or if none. + /// 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 if user is the server owner. + /// + /// Returns the position of the user within the role hierarchy. + /// + /// + /// The returned value equal to the position of the highest role the user has, or + /// if user is the server owner. + /// public int Hierarchy { get From 9ee8c085ea344dcd0553d879859d95e1533fc785 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Thu, 26 Apr 2018 20:20:28 +0800 Subject: [PATCH 143/183] Add a bunch of XML docs --- .../Entities/Channels/RpcDMChannel.cs | 1 + .../Entities/Channels/RpcGroupChannel.cs | 1 + src/Discord.Net.Core/Audio/AudioOutStream.cs | 17 ++++++++++++---- src/Discord.Net.Core/Audio/AudioStream.cs | 19 +++++++++++++++--- .../Entities/Messages/EmbedBuilder.cs | 20 +++++++++---------- src/Discord.Net.Core/Logging/LogMessage.cs | 4 ++-- src/Discord.Net.Core/Net/HttpException.cs | 12 ++++++++++- src/Discord.Net.Core/Net/IRequest.cs | 3 +++ .../Net/WebSocketClosedException.cs | 11 +++++++--- src/Discord.Net.Core/Utils/Cacheable.cs | 4 ++-- src/Discord.Net.Rest/BaseDiscordClient.cs | 1 + .../Entities/Channels/RestCategoryChannel.cs | 12 +++++++++++ .../Entities/Channels/RestGroupChannel.cs | 5 ++++- .../Entities/Channels/RestVoiceChannel.cs | 6 ++++-- .../Entities/Guilds/RestGuild.cs | 7 +++++-- src/Discord.Net.WebSocket/BaseSocketClient.cs | 4 ++-- .../Channels/SocketCategoryChannel.cs | 4 ++++ .../Entities/Channels/SocketGroupChannel.cs | 4 +++- .../Entities/Users/SocketWebhookUser.cs | 2 +- 19 files changed, 103 insertions(+), 34 deletions(-) diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs index 42e590aca..195411678 100644 --- a/experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs @@ -9,6 +9,7 @@ using Model = Discord.API.Rpc.Channel; namespace Discord.Rpc { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RpcDMChannel : RpcChannel, IRpcMessageChannel, IRpcPrivateChannel, IDMChannel { public IReadOnlyCollection CachedMessages { get; private set; } diff --git a/experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs index 1c9355047..9d484c25d 100644 --- a/experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs +++ b/experiment/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs @@ -10,6 +10,7 @@ using Model = Discord.API.Rpc.Channel; namespace Discord.Rpc { + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RpcGroupChannel : RpcChannel, IRpcMessageChannel, IRpcAudioChannel, IRpcPrivateChannel, IGroupChannel { public IReadOnlyCollection CachedMessages { get; private set; } diff --git a/src/Discord.Net.Core/Audio/AudioOutStream.cs b/src/Discord.Net.Core/Audio/AudioOutStream.cs index 7019ba8cd..cbc3167a2 100644 --- a/src/Discord.Net.Core/Audio/AudioOutStream.cs +++ b/src/Discord.Net.Core/Audio/AudioOutStream.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; namespace Discord.Audio @@ -7,8 +7,17 @@ namespace Discord.Audio { public override bool CanWrite => true; - public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } - public override void SetLength(long value) { throw new NotSupportedException(); } - public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + /// + /// Reading this stream is not supported. + public override int Read(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); + /// + /// Setting the length to this stream is not supported. + public override void SetLength(long value) => + throw new NotSupportedException(); + /// + /// Seeking this stream is not supported.. + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); } } diff --git a/src/Discord.Net.Core/Audio/AudioStream.cs b/src/Discord.Net.Core/Audio/AudioStream.cs index 532a6bb7f..d5b36aef2 100644 --- a/src/Discord.Net.Core/Audio/AudioStream.cs +++ b/src/Discord.Net.Core/Audio/AudioStream.cs @@ -28,17 +28,30 @@ namespace Discord.Audio public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); } + /// + /// Reading stream length is not supported. public override long Length => throw new NotSupportedException(); + /// + /// Getting or setting this stream position is not supported. public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } - public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } - public override void SetLength(long value) { throw new NotSupportedException(); } - public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + /// + /// Reading this stream is not supported. + public override int Read(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); + /// + /// Setting the length to this stream is not supported. + public override void SetLength(long value) => + throw new NotSupportedException(); + /// + /// Seeking this stream is not supported.. + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index d2c39d1bd..4568bd56c 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -41,7 +41,7 @@ namespace Discord } /// Gets or sets the title of an . - /// Title length exceeds the maximum allowed by Discord. + /// Title length exceeds . /// /// The title of the embed. public string Title @@ -55,7 +55,7 @@ namespace Discord } /// Gets or sets the description of an . - /// Description length exceeds the maximum allowed by Discord. + /// Description length exceeds . /// The description of the embed. public string Description { @@ -107,8 +107,8 @@ namespace Discord /// Gets or sets the list of of an . /// An embed builder's fields collection is set to /// . - /// Description length exceeds the maximum allowed by - /// Discord. + /// Description length exceeds . + /// /// The list of existing . public List Fields { @@ -282,7 +282,7 @@ namespace Discord /// /// Sets the author field of an with the provided properties. /// - /// The containing the author field properties. + /// The delegate containing the author field properties. /// /// The current builder. /// @@ -328,7 +328,7 @@ namespace Discord /// /// Sets the footer field of an with the provided properties. /// - /// The containing the footer field properties. + /// The delegate containing the footer field properties. /// /// The current builder. /// @@ -382,7 +382,7 @@ namespace Discord /// . /// /// The field builder class containing the field properties. - /// Field count exceeds the maximum allowed by Discord. + /// Field count exceeds . /// /// The current builder. /// @@ -399,7 +399,7 @@ namespace Discord /// /// Adds an field with the provided properties. /// - /// The containing the field properties. + /// The delegate containing the field properties. /// /// The current builder. /// @@ -417,7 +417,7 @@ namespace Discord /// /// The built embed object. /// - /// Total embed length exceeds the maximum allowed by Discord. + /// Total embed length exceeds . public Embed Build() { if (Length > MaxEmbedLength) @@ -542,7 +542,7 @@ namespace Discord /// /// or is , empty or entirely whitespace. /// - or - - /// or length exceeds the maximum allowed by Discord. + /// or exceeds the maximum length allowed by Discord. /// public EmbedField Build() => new EmbedField(Name, Value.ToString(), IsInline); diff --git a/src/Discord.Net.Core/Logging/LogMessage.cs b/src/Discord.Net.Core/Logging/LogMessage.cs index 7d6f8c13c..d70bb01f5 100644 --- a/src/Discord.Net.Core/Logging/LogMessage.cs +++ b/src/Discord.Net.Core/Logging/LogMessage.cs @@ -26,8 +26,8 @@ namespace Discord public Exception Exception { get; } /// - /// Initializes a new with the severity, source, - /// of the event, and optionally, an exception. + /// Initializes a new struct with the severity, source, message of the event, and + /// optionally, an exception. /// /// The severity of the event. /// The source of the event. diff --git a/src/Discord.Net.Core/Net/HttpException.cs b/src/Discord.Net.Core/Net/HttpException.cs index 3fabe0fa8..c49273451 100644 --- a/src/Discord.Net.Core/Net/HttpException.cs +++ b/src/Discord.Net.Core/Net/HttpException.cs @@ -11,10 +11,20 @@ namespace Discord.Net /// /// Gets the HTTP status code returned by Discord. /// + /// + /// An + /// HTTP status code + /// from Discord. + /// public HttpStatusCode HttpCode { get; } /// - /// Gets the JSON error code returned by Discord, or if none. + /// Gets the JSON error code returned by Discord. /// + /// + /// A + /// JSON error code + /// from Discord, or if none. + /// public int? DiscordCode { get; } /// /// Gets the reason of the exception. diff --git a/src/Discord.Net.Core/Net/IRequest.cs b/src/Discord.Net.Core/Net/IRequest.cs index d3c708dd5..1f23e65cd 100644 --- a/src/Discord.Net.Core/Net/IRequest.cs +++ b/src/Discord.Net.Core/Net/IRequest.cs @@ -2,6 +2,9 @@ using System; namespace Discord.Net { + /// + /// Represents a generic request to be sent to Discord. + /// public interface IRequest { DateTimeOffset? TimeoutAt { get; } diff --git a/src/Discord.Net.Core/Net/WebSocketClosedException.cs b/src/Discord.Net.Core/Net/WebSocketClosedException.cs index 2936c01a9..492bd82e0 100644 --- a/src/Discord.Net.Core/Net/WebSocketClosedException.cs +++ b/src/Discord.Net.Core/Net/WebSocketClosedException.cs @@ -2,13 +2,18 @@ using System; namespace Discord.Net { /// - /// Describes an exception that causes the WebSocket to close during a session. + /// Describes an exception that causes the WebSocket to close during a session. /// public class WebSocketClosedException : Exception { /// /// Gets the close code sent by Discord. /// + /// + /// A + /// close code + /// from Discord. + /// public int CloseCode { get; } /// /// Gets the reason of the interruption. @@ -16,8 +21,8 @@ namespace Discord.Net public string Reason { get; } /// - /// Initializes a new instance of the using the Discord close code - /// and the optional reason. + /// Initializes a new instance of the using a Discord close code + /// and an 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/Utils/Cacheable.cs b/src/Discord.Net.Core/Utils/Cacheable.cs index 2eb5a8542..15358dda0 100644 --- a/src/Discord.Net.Core/Utils/Cacheable.cs +++ b/src/Discord.Net.Core/Utils/Cacheable.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents a that contains an entity that may be cached. + /// Represents a cached entity. /// /// The type of entity that is cached. /// The type of this entity's ID. @@ -25,7 +25,7 @@ namespace Discord /// /// /// 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; } diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index 8ba7ea718..db04b036e 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -174,6 +174,7 @@ namespace Discord.Rest Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ImmutableArray.Create()); /// + /// Creating a guild is not supported with the base client. Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => throw new NotSupportedException(); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs index 1700999d9..c6cbeeecc 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs @@ -26,18 +26,30 @@ namespace Discord.Rest private string DebuggerDisplay => $"{Name} ({Id}, Category)"; // IGuildChannel + /// + /// This method is not supported with category channels. IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => throw new NotSupportedException(); + /// + /// This method is not supported with category channels. Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => throw new NotSupportedException(); + /// + /// This method is not supported with category channels. Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) => throw new NotSupportedException(); + /// + /// This method is not supported with category channels. Task> IGuildChannel.GetInvitesAsync(RequestOptions options) => throw new NotSupportedException(); //IChannel + /// + /// This method is not supported with category channels. IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => throw new NotSupportedException(); + /// + /// This method is not supported with category channels. Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => throw new NotSupportedException(); } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 81c1b85eb..d9a6c1a31 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -154,7 +154,10 @@ namespace Discord.Rest => EnterTypingState(options); //IAudioChannel - Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } + /// + /// Connecting to a group channel is not supported. + Task IAudioChannel.ConnectAsync(Action configAction) => + throw new NotSupportedException(); //IChannel Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index 300ebd08d..5b44136e0 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -1,4 +1,4 @@ -using Discord.Audio; +using Discord.Audio; using System; using System.Collections.Generic; using System.Diagnostics; @@ -41,7 +41,9 @@ namespace Discord.Rest private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; //IAudioChannel - Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } + /// + /// Connecting to a REST-based channel is not supported. + Task IAudioChannel.ConnectAsync(Action configAction) => throw new NotSupportedException(); //IGuildChannel Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 15d106804..fc5ef307f 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -1,4 +1,4 @@ -using Discord.Audio; +using Discord.Audio; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -417,7 +417,10 @@ namespace Discord.Rest else return ImmutableArray.Create(); } - Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } + /// + /// Downloading users is not supported with a REST-based guild. + Task IGuild.DownloadUsersAsync() => + throw new NotSupportedException(); async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) => await GetWebhookAsync(id, options).ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index f628720ec..6393005ac 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -8,7 +8,7 @@ namespace Discord.WebSocket { public abstract partial class BaseSocketClient : BaseDiscordClient, IDiscordClient { - protected readonly DiscordSocketConfig _baseconfig; + protected readonly DiscordSocketConfig BaseConfig; /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public abstract int Latency { get; protected set; } @@ -23,7 +23,7 @@ namespace Discord.WebSocket public abstract IReadOnlyCollection VoiceRegions { get; } internal BaseSocketClient(DiscordSocketConfig config, DiscordRestApiClient client) - : base(config, client) => _baseconfig = config; + : base(config, client) => BaseConfig = config; private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) => new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index 1d05b4974..1305233e4 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -55,8 +55,12 @@ namespace Discord.WebSocket => ImmutableArray.Create>(Users).ToAsyncEnumerable(); Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// + /// This method is not supported with category channels. Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) => throw new NotSupportedException(); + /// + /// This method is not supported with category channels. Task> IGuildChannel.GetInvitesAsync(RequestOptions options) => throw new NotSupportedException(); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 1d131876a..94bf70493 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -251,7 +251,9 @@ namespace Discord.WebSocket //IAudioChannel /// - Task IAudioChannel.ConnectAsync(Action configAction) { throw new NotSupportedException(); } + /// Connecting to a group channel is not supported. + Task IAudioChannel.ConnectAsync(Action configAction) => + throw new NotSupportedException(); //IChannel /// diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index d4ddb4fa8..9221ea03a 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; From c2e4be9ca82967861df39b7c77a45e5db391cfc1 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 28 Apr 2018 17:18:48 +0800 Subject: [PATCH 144/183] Fix broken search + DocFX by default ships with an older version of jQuery, switching to a newer version confuses parts of the DocFX Javascript. --- docs/_template/light-dark-theme/partials/scripts.tmpl.partial | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/_template/light-dark-theme/partials/scripts.tmpl.partial b/docs/_template/light-dark-theme/partials/scripts.tmpl.partial index f353cf0c5..324d16794 100644 --- a/docs/_template/light-dark-theme/partials/scripts.tmpl.partial +++ b/docs/_template/light-dark-theme/partials/scripts.tmpl.partial @@ -3,10 +3,6 @@ - From 1cf68abd4b5ef981f2d41dc05b32e5a3a8a17f5a Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 28 Apr 2018 17:19:22 +0800 Subject: [PATCH 145/183] Minor fixes for CONTRIBUTING.md and README.md --- docs/CONTRIBUTING.md | 24 +++++++----------------- docs/README.md | 18 +++++++++--------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 296b6d1cb..3c4ebac71 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,18 +1,18 @@ # Contributing to Docs -I don't really have any strict conditions for writing documentation, +We don't really have any strict conditions for writing documentation, but just keep these few guidelines in mind: * Keep code samples in the `guides/samples` folder -* When referencing an object in the API, link to it's page in the -API documentation. +* When referencing an object in the API, link to it's page in the + API documentation. * Documentation should be written in clear and proper English* -\* If anyone is interested in translating documentation into other -languages, please open an issue or contact me on +\* If anyone is interested in translating documentation into other +languages, please open an issue or contact me on Discord (`foxbot#0282`). -### Layout +## Layout Documentation should be written in a FAQ/Wiki style format. @@ -33,14 +33,4 @@ Example of long link syntax: Please consult the [API Documentation] for more information. [API Documentation]: xref:System.String -``` - -### Compiling - -Documentation is compiled into a static site using [DocFx]. -We currently use the most recent build off the dev branch. - -After making changes, compile your changes into the static site with -`docfx`. You can also view your changes live with `docfx --serve`. - -[DocFx]: https://dotnet.github.io/docfx/ \ No newline at end of file +``` \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index b483339c4..eaf4725c3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,16 +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] +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. +2. (Optional) If you intend to target a specific version, ensure that + you have the correct version checked out. +3. 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 From 540e0f2047c04eb8c473be01efcde527f7ab48b4 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 28 Apr 2018 22:04:17 +0800 Subject: [PATCH 146/183] Clean up filterConfig.yml + New config exposes Discord.Net namespace since it has several common public exceptions that may be helpful to users --- docs/filterConfig.yml | 12 ++++-------- src/Discord.Net.Core/Net/RateLimitedException.cs | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/filterConfig.yml b/docs/filterConfig.yml index 715b39606..410a80d25 100644 --- a/docs/filterConfig.yml +++ b/docs/filterConfig.yml @@ -1,11 +1,7 @@ apiRules: - exclude: - uidRegex: ^Discord\.API$ + uidRegex: ^Discord\.Net\..*$ + type: Namespace - exclude: - uidRegex: ^Discord\.API.*$ -- exclude: - uidRegex: ^Discord\.Net\.Converters$ -- exclude: - uidRegex: ^Discord\.Net.*$ -- exclude: - uidRegex: ^RegexAnalyzer$ \ No newline at end of file + uidRegex: ^Discord\.Analyzers$ + type: Namespace \ No newline at end of file diff --git a/src/Discord.Net.Core/Net/RateLimitedException.cs b/src/Discord.Net.Core/Net/RateLimitedException.cs index 969c0eafc..6e084886e 100644 --- a/src/Discord.Net.Core/Net/RateLimitedException.cs +++ b/src/Discord.Net.Core/Net/RateLimitedException.cs @@ -3,7 +3,7 @@ using System; namespace Discord.Net { /// - /// An exception that indicates the user is being rate limited by Discord. + /// Describes an exception that indicates the user is being rate limited by Discord. /// public class RateLimitedException : TimeoutException { From 509235840184bd44a7ed85f8d164e4575acec258 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Mon, 30 Apr 2018 01:03:24 +0800 Subject: [PATCH 147/183] Add XML docs --- .../samples/first-bot/structure.cs | 2 +- .../Discord.Net.Rpc/DiscordRpcApiClient.cs | 2 +- .../Discord.Net.Rpc/DiscordRpcConfig.cs | 4 +- .../Builders/CommandBuilder.cs | 1 + src/Discord.Net.Commands/CommandService.cs | 18 +- src/Discord.Net.Commands/Info/CommandInfo.cs | 2 +- .../Map/CommandMapNode.cs | 1 + .../Readers/PrimitiveTypeReader.cs | 2 + src/Discord.Net.Core/Audio/IAudioClient.cs | 2 +- src/Discord.Net.Core/CDN.cs | 4 +- .../Commands/ICommandContext.cs | 2 +- .../Entities/Activities/Game.cs | 2 +- .../Channels/ReorderChannelProperties.cs | 2 +- .../Entities/Emotes/EmoteProperties.cs | 4 +- .../Entities/Guilds/IGuild.cs | 172 ++++++++++++++++-- src/Discord.Net.Core/Entities/IUpdateable.cs | 2 +- src/Discord.Net.Core/Entities/Image.cs | 4 +- .../Entities/Messages/EmbedBuilder.cs | 2 +- .../Entities/Messages/EmbedField.cs | 2 +- .../Entities/Messages/EmbedVideo.cs | 2 +- .../Entities/Messages/IMessage.cs | 2 +- .../Entities/Messages/MessageProperties.cs | 25 +-- .../Permissions/ChannelPermissions.cs | 3 +- .../Entities/Roles/ReorderRoleProperties.cs | 2 +- .../Entities/Roles/RoleProperties.cs | 2 +- .../Entities/Users/GuildUserProperties.cs | 10 +- .../Entities/Webhooks/WebhookProperties.cs | 4 +- .../Utils/ConcurrentHashSet.cs | 20 +- src/Discord.Net.Core/Utils/Optional.cs | 1 + src/Discord.Net.Core/Utils/Preconditions.cs | 4 +- src/Discord.Net.Rest/ClientHelper.cs | 2 + src/Discord.Net.Rest/DiscordRestApiClient.cs | 19 +- src/Discord.Net.Rest/DiscordRestClient.cs | 16 ++ .../Entities/Channels/ChannelHelper.cs | 38 +++- .../Entities/Channels/IRestMessageChannel.cs | 2 +- .../Entities/Channels/IRestPrivateChannel.cs | 2 +- .../Entities/Channels/RestCategoryChannel.cs | 2 +- .../Entities/Channels/RestChannel.cs | 9 +- .../Entities/Channels/RestDMChannel.cs | 20 +- .../Entities/Channels/RestGuildChannel.cs | 13 ++ .../Entities/Guilds/GuildHelper.cs | 17 +- .../Entities/Guilds/RestBan.cs | 4 +- .../Entities/Guilds/RestGuild.cs | 86 ++++++++- .../Entities/Guilds/RestGuildEmbed.cs | 4 +- .../Entities/Guilds/RestGuildIntegration.cs | 14 +- .../Entities/Guilds/RestUserGuild.cs | 8 +- .../Entities/Invites/RestInvite.cs | 4 +- .../Entities/Messages/MessageHelper.cs | 4 +- .../Entities/Messages/RestMessage.cs | 5 + .../Entities/Messages/RestUserMessage.cs | 25 ++- .../Entities/RestApplication.cs | 8 + .../Entities/Roles/RestRole.cs | 15 +- .../Entities/Users/RestConnection.cs | 13 +- .../Entities/Users/RestGuildUser.cs | 9 + .../Entities/Users/RestSelfUser.cs | 10 +- .../Entities/Users/RestUser.cs | 14 ++ .../Entities/Webhooks/RestWebhook.cs | 15 +- src/Discord.Net.Rest/Net/DefaultRestClient.cs | 4 +- .../Net/DefaultRestClientProvider.cs | 1 + .../Net/Queue/RequestQueueBucket.cs | 2 +- src/Discord.Net.Rest/Net/RateLimitInfo.cs | 4 +- .../Audio/Streams/OpusDecodeStream.cs | 9 +- .../Audio/Streams/RTPReadStream.cs | 5 +- .../Audio/Streams/SodiumEncryptStream.cs | 10 +- .../BaseSocketClient.Events.cs | 2 +- src/Discord.Net.WebSocket/BaseSocketClient.cs | 142 ++++++++++++++- src/Discord.Net.WebSocket/ClientState.cs | 6 +- .../Commands/SocketCommandContext.cs | 4 +- .../DiscordSocketApiClient.cs | 9 +- .../DiscordSocketClient.cs | 44 ++++- .../Entities/Channels/SocketChannel.cs | 1 + .../Entities/Channels/SocketChannelHelper.cs | 9 +- .../Entities/Channels/SocketTextChannel.cs | 1 - .../Entities/Guilds/SocketGuild.cs | 16 +- .../Entities/Messages/SocketMessage.cs | 22 ++- .../Entities/Messages/SocketReaction.cs | 5 +- .../Entities/Messages/SocketSystemMessage.cs | 3 +- .../Entities/Messages/SocketUserMessage.cs | 28 ++- .../Entities/Roles/SocketRole.cs | 15 +- .../Entities/Users/SocketGuildUser.cs | 2 +- .../Entities/Users/SocketPresence.cs | 5 +- .../Entities/Users/SocketSelfUser.cs | 14 +- .../Entities/Users/SocketUnknownUser.cs | 5 +- .../Entities/Users/SocketUser.cs | 14 +- .../Entities/Users/SocketVoiceState.cs | 20 +- .../Entities/Users/SocketWebhookUser.cs | 6 + .../Net/DefaultWebSocketClient.cs | 4 +- .../Net/DefaultWebSocketClientProvider.cs | 3 +- .../WebhookClientHelper.cs | 5 +- 89 files changed, 920 insertions(+), 183 deletions(-) diff --git a/docs/guides/getting_started/samples/first-bot/structure.cs b/docs/guides/getting_started/samples/first-bot/structure.cs index a9a018c3a..9ec1043c9 100644 --- a/docs/guides/getting_started/samples/first-bot/structure.cs +++ b/docs/guides/getting_started/samples/first-bot/structure.cs @@ -36,7 +36,7 @@ class Program // you must set the MessageCacheSize. You may adjust the number as needed. //MessageCacheSize = 50, - // If your platform doesn't have native websockets, + // If your platform doesn't have native WebSockets, // add Discord.Net.Providers.WS4Net from NuGet, // add the `using` at the top, and uncomment this line: //WebSocketProvider = WS4NetProvider.Instance diff --git a/experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs b/experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs index 50d467054..300784f4e 100644 --- a/experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs +++ b/experiment/Discord.Net.Rpc/DiscordRpcApiClient.cs @@ -192,7 +192,7 @@ namespace Discord.API internal override async Task DisconnectInternalAsync() { if (_webSocketClient == null) - throw new NotSupportedException("This client is not configured with websocket support."); + throw new NotSupportedException("This client is not configured with WebSocket support."); if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; diff --git a/experiment/Discord.Net.Rpc/DiscordRpcConfig.cs b/experiment/Discord.Net.Rpc/DiscordRpcConfig.cs index fd6b74ff1..90df8d1a7 100644 --- a/experiment/Discord.Net.Rpc/DiscordRpcConfig.cs +++ b/experiment/Discord.Net.Rpc/DiscordRpcConfig.cs @@ -14,7 +14,7 @@ namespace Discord.Rpc /// Gets or sets the time, in milliseconds, to wait for a connection to complete before aborting. public int ConnectionTimeout { get; set; } = 30000; - /// Gets or sets the provider used to generate new websocket connections. + /// Gets or sets the provider used to generate new WebSocket connections. public WebSocketProvider WebSocketProvider { get; set; } public DiscordRpcConfig() @@ -24,7 +24,7 @@ namespace Discord.Rpc #else WebSocketProvider = () => { - throw new InvalidOperationException("The default websocket provider is not supported on this platform.\n" + + throw new InvalidOperationException("The default WebSocket provider is not supported on this platform.\n" + "You must specify a WebSocketProvider or target a runtime supporting .NET Standard 1.3, such as .NET Framework 4.6+."); }; #endif diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs index 70045453f..7dcba8362 100644 --- a/src/Discord.Net.Commands/Builders/CommandBuilder.cs +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -117,6 +117,7 @@ namespace Discord.Commands.Builders return this; } + /// Only the last parameter in a command may have the Remainder or Multiple flag. internal CommandInfo Build(ModuleInfo info, CommandService service) { //Default name to primary alias diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 57809d78b..a8ef5b62a 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -40,17 +40,17 @@ namespace Discord.Commands internal readonly LogManager _logManager; /// - /// Represents all modules loaded within . + /// Represents all modules loaded within . /// public IEnumerable Modules => _moduleDefs.Select(x => x); /// - /// Represents all commands loaded within . + /// Represents all commands loaded within . /// public IEnumerable Commands => _moduleDefs.SelectMany(x => x.Commands); /// - /// Represents all loaded within . + /// 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); @@ -122,12 +122,12 @@ namespace Discord.Commands } /// - /// Add a command module from a . + /// Add a command module from a . /// /// The type of module. /// /// The for your dependency injection solution, if using one - otherwise, pass - /// . + /// . /// /// /// A built module. @@ -135,12 +135,12 @@ namespace Discord.Commands public Task AddModuleAsync(IServiceProvider services) => AddModuleAsync(typeof(T), services); /// - /// Adds a command module from a . + /// Adds a command module from a . /// /// The type of module. /// /// The for your dependency injection solution, if using one - otherwise, pass - /// . + /// . /// /// /// A built module. @@ -174,12 +174,12 @@ namespace Discord.Commands } } /// - /// Add command modules from an . + /// 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. diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs index de1462a3f..65581dcc3 100644 --- a/src/Discord.Net.Commands/Info/CommandInfo.cs +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -16,7 +16,7 @@ namespace Discord.Commands /// /// /// This object contains the information of a command. This can include the module of the command, various - /// descriptions regarding the command, and its . + /// descriptions regarding the command, and its . /// [DebuggerDisplay("{Name,nq}")] public class CommandInfo diff --git a/src/Discord.Net.Commands/Map/CommandMapNode.cs b/src/Discord.Net.Commands/Map/CommandMapNode.cs index db69af415..4f798e718 100644 --- a/src/Discord.Net.Commands/Map/CommandMapNode.cs +++ b/src/Discord.Net.Commands/Map/CommandMapNode.cs @@ -23,6 +23,7 @@ namespace Discord.Commands _commands = ImmutableArray.Create(); } + /// Cannot add commands to the root node. public void AddCommand(CommandService service, string text, int index, CommandInfo command) { int nextSegment = NextSegment(text, index, service._separatorChar); diff --git a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs index 011854a23..cb74139df 100644 --- a/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs @@ -17,10 +17,12 @@ namespace Discord.Commands private readonly TryParseDelegate _tryParse; private readonly float _score; + /// must be within the range [0, 1]. public PrimitiveTypeReader() : this(PrimitiveParsers.Get(), 1) { } + /// must be within the range [0, 1]. public PrimitiveTypeReader(TryParseDelegate tryParse, float score) { if (score < 0 || score > 1) diff --git a/src/Discord.Net.Core/Audio/IAudioClient.cs b/src/Discord.Net.Core/Audio/IAudioClient.cs index 9be8ceef5..018c8bc05 100644 --- a/src/Discord.Net.Core/Audio/IAudioClient.cs +++ b/src/Discord.Net.Core/Audio/IAudioClient.cs @@ -15,7 +15,7 @@ namespace Discord.Audio /// Gets the current connection state of this client. ConnectionState ConnectionState { get; } - /// Gets the estimated round-trip latency, in milliseconds, to the voice websocket server. + /// Gets the estimated round-trip latency, in milliseconds, to the voice WebSocket server. int Latency { get; } /// Gets the estimated round-trip latency, in milliseconds, to the voice UDP server. int UdpLatency { get; } diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index 0c713f135..bd83b64e9 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -13,7 +13,7 @@ namespace Discord 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 . + /// Returns the user avatar URL based on the and . /// public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, ImageFormat format) { @@ -52,7 +52,7 @@ namespace Discord => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.{(animated ? "gif" : "png")}"; /// - /// Returns the rich presence asset URL based on the asset ID and . + /// Returns the rich presence asset URL based on the asset ID and . /// public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format) { diff --git a/src/Discord.Net.Core/Commands/ICommandContext.cs b/src/Discord.Net.Core/Commands/ICommandContext.cs index 8b682ba1c..d56eb38a0 100644 --- a/src/Discord.Net.Core/Commands/ICommandContext.cs +++ b/src/Discord.Net.Core/Commands/ICommandContext.cs @@ -1,7 +1,7 @@ namespace Discord.Commands { /// - /// Represents the context of a command. This may include the client, guild, channel, user, and message. + /// Represents a context of a command. This may include the client, guild, channel, user, and message. /// public interface ICommandContext { diff --git a/src/Discord.Net.Core/Entities/Activities/Game.cs b/src/Discord.Net.Core/Entities/Activities/Game.cs index 471cc9f64..62b2853d5 100644 --- a/src/Discord.Net.Core/Entities/Activities/Game.cs +++ b/src/Discord.Net.Core/Entities/Activities/Game.cs @@ -18,7 +18,7 @@ namespace Discord /// Creates a with the provided and . /// /// The name of the game. - /// The type of activity. Default is . + /// The type of activity. Default is . public Game(string name, ActivityType type = ActivityType.Playing) { Name = name; diff --git a/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs index 02cb2547c..255279fa8 100644 --- a/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs @@ -1,7 +1,7 @@ namespace Discord { /// - /// Properties that are used to reorder an . + /// Properties that are used to reorder an . /// public class ReorderChannelProperties { diff --git a/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs b/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs index 255bf0721..721345afe 100644 --- a/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs +++ b/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs @@ -8,11 +8,11 @@ namespace Discord public class EmoteProperties { /// - /// Gets or sets the name of the . + /// Gets or sets the name of the . /// public Optional Name { get; set; } /// - /// Gets or sets the roles that can access this . + /// Gets or sets the roles that can access this . /// public Optional> Roles { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 664ac017d..057b94788 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents a generic guild object. + /// Represents a generic guild/server. /// public interface IGuild : IDeletable, ISnowflakeEntity { @@ -23,7 +23,7 @@ namespace Discord /// Determines if this guild is embeddable (i.e. can use widget). /// /// - /// Returns if this guild can be embedded via widgets. + /// if this guild can be embedded via widgets; otherwise . /// bool IsEmbeddable { get; } /// @@ -91,58 +91,91 @@ namespace Discord /// /// Gets the currently associated with this guild. /// + /// + /// currently associated with this guild. + /// IAudioClient AudioClient { get; } /// /// Gets the built-in role containing all users in this guild. /// + /// + /// Built-in role that represents an @everyone role in this guild. + /// IRole EveryoneRole { get; } /// /// Gets a collection of all custom emotes for this guild. /// + /// + /// A collection of all custom emotes for this guild. + /// IReadOnlyCollection Emotes { get; } /// /// Gets a collection of all extra features added to this guild. /// + /// + /// A collection of enabled features in this guild. + /// IReadOnlyCollection Features { get; } /// /// Gets a collection of all roles in this guild. /// + /// + /// A collection of roles found within this guild. + /// IReadOnlyCollection Roles { get; } /// /// Modifies this guild. /// - /// The properties to modify the guild with. + /// The delegate containing the properties to modify the guild with. /// The options to be used when sending the request. + /// + /// An awaitable . + /// Task ModifyAsync(Action func, RequestOptions options = null); /// /// Modifies this guild's embed channel. /// - /// The properties to modify the guild widget with. + /// The delegate containing the properties to modify the guild widget with. /// The options to be used when sending the request. + /// + /// An awaitable . + /// Task ModifyEmbedAsync(Action func, RequestOptions options = null); /// /// Bulk modifies the order of channels in this guild. /// - /// The properties to modify the channel positions with. + /// The properties used to modify the channel positions with. /// The options to be used when sending the request. + /// + /// An awaitable . + /// Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null); /// /// Bulk modifies the order of roles in this guild. /// - /// The properties to modify the role positions with. + /// The properties used to modify the role positions with. /// The options to be used when sending the request. Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null); + /// + /// An awaitable . + /// /// /// Leaves this guild. If you are the owner, use instead. /// /// The options to be used when sending the request. + /// + /// An awaitable . + /// Task LeaveAsync(RequestOptions options = null); /// /// Gets a collection of all users banned on this guild. /// /// The options to be used when sending the request. + /// + /// An awaitable containing a collection of banned users with reasons. + /// Task> GetBansAsync(RequestOptions options = null); /// /// Bans the provided user from this guild and optionally prunes their recent messages. @@ -154,6 +187,9 @@ namespace Discord /// The reason of the ban to be written in the audit log. /// The options to be used when sending the request. /// is not between 0 to 7. + /// + /// An awaitable . + /// 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. @@ -165,14 +201,27 @@ namespace Discord /// The reason of the ban to be written in the audit log. /// The options to be used when sending the request. /// is not between 0 to 7. + /// + /// An awaitable . + /// Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null); /// /// Unbans the provided user if they are currently banned. /// + /// The user to be unbanned. + /// The options to be used when sending the request. + /// + /// An awaitable . + /// Task RemoveBanAsync(IUser user, RequestOptions options = null); /// /// Unbans the provided user ID if it is currently banned. /// + /// The snowflake ID of the user to be unbanned. + /// The options to be used when sending the request. + /// + /// An awaitable . + /// Task RemoveBanAsync(ulong userId, RequestOptions options = null); /// @@ -182,15 +231,22 @@ namespace Discord /// The that determines whether the object should be fetched from cache. /// /// The options to be used when sending the request. + /// + /// An awaitable containing a collection of generic channels found within this guild. + /// Task> GetChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// - /// Gets the channel in this guild with the provided ID, or if not found. + /// Gets the channel in this guild with the provided ID. /// /// The channel ID. /// /// The that determines whether the object should be fetched from cache. /// /// The options to be used when sending the request. + /// + /// An awaitable containing the generic channel with the specified ID, or + /// if none is found. + /// Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets a collection of all text channels in this guild. @@ -199,6 +255,9 @@ namespace Discord /// The that determines whether the object should be fetched from cache. /// /// The options to be used when sending the request. + /// + /// An awaitable containing a collection of text channels found within 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. @@ -208,6 +267,10 @@ namespace Discord /// The that determines whether the object should be fetched from cache. /// /// The options to be used when sending the request. + /// + /// An awaitable containing the text channel with the specified ID, or + /// if none is found. + /// Task GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets a collection of all voice channels in this guild. @@ -216,6 +279,9 @@ namespace Discord /// The that determines whether the object should be fetched from cache. /// /// The options to be used when sending the request. + /// + /// An awaitable containing a collection of voice channels found within this guild. + /// Task> GetVoiceChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets a collection of all category channels in this guild. @@ -224,68 +290,97 @@ namespace Discord /// The that determines whether the object should be fetched from cache. /// /// The options to be used when sending the request. + /// + /// An awaitable containing a collection of category channels found within 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. + /// Gets the voice channel in this guild with the provided ID. /// /// The text channel ID. /// /// The that determines whether the object should be fetched from cache. /// /// The options to be used when sending the request. + /// + /// An awaitable containing the voice channel with the specified ID, or + /// if none is found. + /// 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. + /// Gets the AFK voice channel in this guild. /// /// /// The that determines whether the object should be fetched from cache. /// /// The options to be used when sending the request. + /// + /// An awaitable containing the AFK voice channel set within this guild, or + /// if none is set. + /// 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. + /// Gets the default system text channel in this guild with the provided ID. /// /// /// The that determines whether the object should be fetched from cache. /// /// The options to be used when sending the request. + /// + /// An awaitable containing the system channel within this guild, 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. + /// Gets the top viewable text channel in this guild with the provided ID. /// /// /// The that determines whether the object should be fetched from cache. /// /// The options to be used when sending the request. + /// + /// An awaitable containing the first viewable text channel in this guild, or + /// if none is found. + /// Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// - /// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild, or - /// if none is set. + /// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild. /// /// /// The that determines whether the object should be fetched from cache. /// /// The options to be used when sending the request. + /// + /// An awaitable containing the embed channel set within the server's widget settings, or + /// if none is set. + /// Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Creates a new text channel. /// /// The new name for the text channel. /// The options to be used when sending the request. + /// + /// An awaitable containing the newly created text channel. + /// Task CreateTextChannelAsync(string name, RequestOptions options = null); /// /// Creates a new voice channel. /// /// The new name for the voice channel. /// The options to be used when sending the request. + /// + /// An awaitable containing the newly created voice channel. + /// Task CreateVoiceChannelAsync(string name, RequestOptions options = null); /// /// Creates a new channel category. /// /// The new name for the category. /// The options to be used when sending the request. + /// + /// An awaitable containing the newly created category channel. + /// Task CreateCategoryAsync(string name, RequestOptions options = null); Task> GetIntegrationsAsync(RequestOptions options = null); @@ -309,13 +404,24 @@ namespace Discord /// The color of the role. /// Whether the role is separated from others on the sidebar. /// The options to be used when sending the request. + /// + /// An awaitable containing the newly crated role. + /// 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. /// - /// The that determines whether the object should be fetched from cache. + /// The that determines whether the object should be fetched from + /// cache. /// The options to be used when sending the request. + /// + /// An awaitable containing a collection of users found within this guild. + /// + /// + /// This may return an incomplete list on the WebSocket implementation because Discord only sends offline + /// users on large guilds. + /// Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets the user in this guild with the provided ID, or if not found. @@ -323,22 +429,34 @@ namespace Discord /// The user ID. /// The that determines whether the object should be fetched from cache. /// The options to be used when sending the request. + /// + /// An awaitable containing the guild user with the specified ID, otherwise . + /// Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets the current user for this guild. /// /// The that determines whether the object should be fetched from cache. /// The options to be used when sending the request. + /// + /// An awaitable containing the currently logged-in user within this guild. + /// Task GetCurrentUserAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets the owner of this guild. /// /// The that determines whether the object should be fetched from cache. /// The options to be used when sending the request. + /// + /// An awaitable containing 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. /// + /// + /// An awaitable . + /// Task DownloadUsersAsync(); /// /// Removes all users from this guild if they have not logged on in a provided number of @@ -349,7 +467,7 @@ namespace Discord /// Whether this prune action is a simulation. /// The options to be used when sending the request. /// - /// The number of users removed from this guild. + /// An awaitable containing the number of users to be or has been removed from this guild. /// Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); @@ -358,11 +476,17 @@ namespace Discord /// /// The webhook ID. /// The options to be used when sending the request. + /// + /// An awaitable containing the webhook with the specified ID, otherwise . + /// Task GetWebhookAsync(ulong id, RequestOptions options = null); /// /// Gets a collection of all webhook from this guild. /// /// The options to be used when sending the request. + /// + /// An awaitable containing a collection of webhooks found within the guild. + /// Task> GetWebhooksAsync(RequestOptions options = null); /// @@ -370,6 +494,9 @@ namespace Discord /// /// The guild emote ID. /// The options to be used when sending the request. + /// + /// An awaitable containing the emote found with the specified ID, or if not found. + /// Task GetEmoteAsync(ulong id, RequestOptions options = null); /// /// Creates a new in this guild. @@ -378,20 +505,29 @@ namespace Discord /// The image of the new emote. /// The roles to limit the emote usage to. /// The options to be used when sending the request. + /// + /// An awaitable containing the created emote. + /// Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null); /// /// Modifies an existing in this guild. /// /// The emote to be modified. - /// The properties to modify the emote with. + /// The delegate containing the properties to modify the emote with. /// The options to be used when sending the request. + /// + /// An awaitable containing the newly modified emote. + /// Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null); /// /// Deletes an existing from this guild. /// /// The emote to delete. /// The options to be used when sending the request. + /// + /// An awaitable . + /// Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Entities/IUpdateable.cs b/src/Discord.Net.Core/Entities/IUpdateable.cs index b4bbe169a..3ae4613f5 100644 --- a/src/Discord.Net.Core/Entities/IUpdateable.cs +++ b/src/Discord.Net.Core/Entities/IUpdateable.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents whether the object is updatable or not. + /// Defines whether the object is updateable or not. /// public interface IUpdateable { diff --git a/src/Discord.Net.Core/Entities/Image.cs b/src/Discord.Net.Core/Entities/Image.cs index 921902f5d..dd77ec6ae 100644 --- a/src/Discord.Net.Core/Entities/Image.cs +++ b/src/Discord.Net.Core/Entities/Image.cs @@ -33,9 +33,9 @@ namespace Discord /// The path to the file. /// /// is a zero-length string, contains only white space, or contains one or more invalid - /// characters as defined by . + /// characters as defined by . /// - /// is . + /// is . /// /// The specified path, file name, or both exceed the system-defined maximum length. For example, on /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index 4568bd56c..087b30993 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -172,7 +172,7 @@ namespace Discord } /// - /// Sets the title of an . + /// Sets the title of an . /// /// The title to be set. /// diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs index d7f37befa..3ae000022 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs @@ -3,7 +3,7 @@ using System.Diagnostics; namespace Discord { /// - /// Represents a field for an . + /// Represents a field for an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedField diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs index 525c25562..4368f74a4 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs @@ -3,7 +3,7 @@ using System.Diagnostics; namespace Discord { /// - /// A video featured in an . + /// A video featured in an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedVideo diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index e390fa682..d66a6b883 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Discord { /// - /// Represents a Discord message object. + /// Represents a message object. /// public interface IMessage : ISnowflakeEntity, IDeletable { diff --git a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs index 9daec32d3..d52b309b5 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs @@ -4,20 +4,21 @@ namespace Discord /// 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 if and only if an 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.") + /// .Build(); + /// }); /// /// public class MessageProperties @@ -26,7 +27,7 @@ namespace Discord /// Gets or sets the content of the message. /// /// - /// This must be less than 2000 characters. + /// This must be less than the constant defined by . /// public Optional Content { get; set; } /// diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index 58d25daac..99134bb90 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -19,7 +19,8 @@ namespace Discord public static readonly ChannelPermissions DM = new ChannelPermissions(0b00000_1000110_1011100110000_000000); /// Gets a that grants all permissions for group channels. public static readonly ChannelPermissions Group = new ChannelPermissions(0b00000_1000110_0001101100000_000000); - /// Gets a that grants all permissions for a given channelType. + /// Gets a that grants all permissions for a given channel type. + /// Unknown channel type. public static ChannelPermissions All(IChannel channel) { switch (channel) diff --git a/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs index fcfa08da5..e13083f1d 100644 --- a/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs +++ b/src/Discord.Net.Core/Entities/Roles/ReorderRoleProperties.cs @@ -1,7 +1,7 @@ namespace Discord { /// - /// Properties that are used to reorder an . + /// Properties that are used to reorder an . /// public class ReorderRoleProperties { diff --git a/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs index 7769a2a04..79372b86d 100644 --- a/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs +++ b/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs @@ -23,7 +23,7 @@ namespace Discord /// public Optional Name { get; set; } /// - /// Gets or sets the role's . + /// Gets or sets the role's . /// public Optional Permissions { get; set; } /// diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs index c731f1459..beb2c392f 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -7,10 +7,10 @@ namespace Discord /// /// /// - /// await (Context.User as IGuildUser)?.ModifyAsync(x => - /// { - /// x.Nickname = $"festive {Context.User.Username}"; - /// }); + /// await guildUser.ModifyAsync(x => + /// { + /// x.Nickname = $"festive {guildUser.Username}"; + /// }); /// /// /// @@ -35,7 +35,7 @@ namespace Discord /// /// /// To clear the user's nickname, this value can be set to or - /// . + /// . /// public Optional Nickname { get; set; } /// diff --git a/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs index 52adbe99a..387ee6106 100644 --- a/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs +++ b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs @@ -27,14 +27,14 @@ namespace Discord /// 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; } /// /// 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/Utils/ConcurrentHashSet.cs b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs index 1fc11587e..233d1b0b0 100644 --- a/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs +++ b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -157,12 +157,16 @@ namespace Discord : this(collection, EqualityComparer.Default) { } public ConcurrentHashSet(IEqualityComparer comparer) : this(DefaultConcurrencyLevel, DefaultCapacity, true, comparer) { } + /// is public ConcurrentHashSet(IEnumerable collection, IEqualityComparer comparer) : this(comparer) { if (collection == null) throw new ArgumentNullException(nameof(collection)); InitializeFromCollection(collection); } + /// + /// or is + /// public ConcurrentHashSet(int concurrencyLevel, IEnumerable collection, IEqualityComparer comparer) : this(concurrencyLevel, DefaultCapacity, false, comparer) { @@ -197,7 +201,7 @@ namespace Discord { foreach (var value in collection) { - if (value == null) throw new ArgumentNullException("key"); + if (value == null) throw new ArgumentNullException(nameof(value)); if (!TryAddInternal(value, _comparer.GetHashCode(value), false)) throw new ArgumentException(); @@ -206,10 +210,10 @@ namespace Discord if (_budget == 0) _budget = _tables._buckets.Length / _tables._locks.Length; } - + /// is public bool ContainsKey(T value) { - if (value == null) throw new ArgumentNullException("key"); + if (value == null) throw new ArgumentNullException(nameof(value)); return ContainsKeyInternal(value, _comparer.GetHashCode(value)); } private bool ContainsKeyInternal(T value, int hashcode) @@ -230,9 +234,10 @@ namespace Discord return false; } + /// is public bool TryAdd(T value) { - if (value == null) throw new ArgumentNullException("key"); + if (value == null) throw new ArgumentNullException(nameof(value)); return TryAddInternal(value, _comparer.GetHashCode(value), true); } private bool TryAddInternal(T value, int hashcode, bool acquireLock) @@ -279,9 +284,10 @@ namespace Discord } } + /// is public bool TryRemove(T value) { - if (value == null) throw new ArgumentNullException("key"); + if (value == null) throw new ArgumentNullException(nameof(value)); return TryRemoveInternal(value); } private bool TryRemoveInternal(T value) @@ -467,4 +473,4 @@ namespace Discord Monitor.Exit(_tables._locks[i]); } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Utils/Optional.cs b/src/Discord.Net.Core/Utils/Optional.cs index 9284645f5..348179699 100644 --- a/src/Discord.Net.Core/Utils/Optional.cs +++ b/src/Discord.Net.Core/Utils/Optional.cs @@ -11,6 +11,7 @@ namespace Discord private readonly T _value; /// Gets the value for this parameter. + /// This property has no value set. public T Value { get diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 1af6dc35d..569917312 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -183,7 +183,7 @@ namespace Discord } // Bulk Delete - /// Messages are younger than 2 weeks.. + /// Messages are younger than 2 weeks. public static void YoungerThanTwoWeeks(ulong[] collection, string name) { var minimum = SnowflakeUtils.ToSnowflake(DateTimeOffset.UtcNow.Subtract(TimeSpan.FromDays(14))); @@ -194,7 +194,7 @@ namespace Discord throw new ArgumentOutOfRangeException(name, "Messages must be younger than two weeks old."); } } - /// The everyone role cannot be assigned to a user + /// The everyone role cannot be assigned to a user. public static void NotEveryoneRole(ulong[] roles, ulong guildId, string name) { for (var i = 0; i < roles.Length; i++) diff --git a/src/Discord.Net.Rest/ClientHelper.cs b/src/Discord.Net.Rest/ClientHelper.cs index 73e1a08f0..155430a7f 100644 --- a/src/Discord.Net.Rest/ClientHelper.cs +++ b/src/Discord.Net.Rest/ClientHelper.cs @@ -1,3 +1,4 @@ +using System; using Discord.API.Rest; using System.Collections.Generic; using System.Collections.Immutable; @@ -24,6 +25,7 @@ namespace Discord.Rest return RestChannel.Create(client, model); return null; } + /// Unexpected channel type. public static async Task> GetPrivateChannelsAsync(BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetMyPrivateChannelsAsync(options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index c8ff616e2..8ffe7dbf9 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -58,6 +58,9 @@ namespace Discord.API SetBaseUrl(DiscordConfig.APIUrl); } + + /// Unknown OAuth token type. + /// A delegate callback throws an exception. internal void SetBaseUrl(string baseUrl) { RestClient = _restClientProvider(baseUrl); @@ -65,6 +68,7 @@ namespace Discord.API RestClient.SetHeader("user-agent", UserAgent); RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); } + /// Unknown OAuth token type. internal static string GetPrefixedToken(TokenType tokenType, string token) { switch (tokenType) @@ -76,7 +80,7 @@ namespace Discord.API case TokenType.Bearer: return $"Bearer {token}"; default: - throw new ArgumentException("Unknown OAuth token type", nameof(tokenType)); + throw new ArgumentException("Unknown OAuth token type.", nameof(tokenType)); } } internal virtual void Dispose(bool disposing) @@ -463,6 +467,7 @@ namespace Discord.API endpoint = () => $"channels/{channelId}/messages?limit={limit}"; return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); } + /// Message content is too long, length must be less or equal to . public async Task CreateMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); @@ -471,12 +476,14 @@ namespace Discord.API Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); 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)); + throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); return await SendJsonAsync("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + /// Message content is too long, length must be less or equal to . + /// This operation may only be called with a token. public async Task CreateWebhookMessageAsync(ulong webhookId, CreateWebhookMessageParams args, RequestOptions options = null) { if (AuthTokenType != TokenType.Webhook) @@ -488,11 +495,12 @@ namespace Discord.API Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); 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)); + throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); options = RequestOptions.CreateOrClone(options); return await SendJsonAsync("POST", () => $"webhooks/{webhookId}/{AuthToken}?wait=true", args, new BucketIds(), clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + /// Message content is too long, length must be less or equal to . public async Task UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) { Preconditions.NotNull(args, nameof(args)); @@ -507,6 +515,9 @@ namespace Discord.API var ids = new BucketIds(channelId: channelId); return await SendMultipartAsync("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } + + /// Message content is too long, length must be less or equal to . + /// This operation may only be called with a token. public async Task UploadWebhookFileAsync(ulong webhookId, UploadWebhookFileParams args, RequestOptions options = null) { if (AuthTokenType != TokenType.Webhook) @@ -559,6 +570,7 @@ namespace Discord.API break; } } + /// Message content is too long, length must be less or equal to . public async Task ModifyMessageAsync(ulong channelId, ulong messageId, Rest.ModifyMessageParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); @@ -1267,6 +1279,7 @@ namespace Discord.API } //Helpers + /// Client is not logged in. protected void CheckState() { if (LoginState != LoginState.LoggedIn) diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index 471f8ee6c..6092e008c 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -22,12 +22,14 @@ namespace Discord.Rest ApiClient.Dispose(); } + /// internal override async Task OnLoginAsync(TokenType tokenType, string token) { var user = await ApiClient.GetMyUserAsync(new RequestOptions { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); ApiClient.CurrentUserId = user.Id; base.CurrentUser = RestSelfUser.Create(this, user); } + /// internal override Task OnLogoutAsync() { _applicationInfo = null; @@ -80,9 +82,11 @@ namespace Discord.Rest => ClientHelper.GetWebhookAsync(this, id, options); //IDiscordClient + /// async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync(options).ConfigureAwait(false); + /// async Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -90,6 +94,7 @@ namespace Discord.Rest else return null; } + /// async Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -97,6 +102,7 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + /// async Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -104,6 +110,7 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + /// async Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -112,12 +119,15 @@ namespace Discord.Rest return ImmutableArray.Create(); } + /// async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync(options).ConfigureAwait(false); + /// async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => await GetInviteAsync(inviteId, options).ConfigureAwait(false); + /// async Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -125,6 +135,7 @@ namespace Discord.Rest else return null; } + /// async Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -132,9 +143,11 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + /// async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => await CreateGuildAsync(name, region, jpegIcon, options).ConfigureAwait(false); + /// async Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -143,11 +156,14 @@ namespace Discord.Rest return null; } + /// async Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) => await GetVoiceRegionsAsync(options).ConfigureAwait(false); + /// async Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => await GetVoiceRegionAsync(id, options).ConfigureAwait(false); + /// async Task IDiscordClient.GetWebhookAsync(ulong id, RequestOptions 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 7723861fa..65b0da6a8 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; using UserModel = Discord.API.User; -using WebhookModel = Discord.API.Webhook; namespace Discord.Rest { @@ -77,14 +76,8 @@ namespace Discord.Rest int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) { var args = new CreateChannelInviteParams { IsTemporary = isTemporary, IsUnique = isUnique }; - if (maxAge.HasValue) - args.MaxAge = maxAge.Value; - else - args.MaxAge = 0; - if (maxUses.HasValue) - args.MaxUses = maxUses.Value; - else - args.MaxUses = 0; + args.MaxAge = maxAge.GetValueOrDefault(0); + args.MaxUses = maxUses.GetValueOrDefault(0); var model = await client.ApiClient.CreateChannelInviteAsync(channel.Id, args, options).ConfigureAwait(false); return RestInviteMetadata.Create(client, null, channel, model); } @@ -160,6 +153,7 @@ namespace Discord.Rest return builder.ToImmutable(); } + /// Message content is too long, length must be less or equal to . public static async Task SendMessageAsync(IMessageChannel channel, BaseDiscordClient client, string text, bool isTTS, Embed embed, RequestOptions options) { @@ -169,6 +163,30 @@ namespace Discord.Rest } #if FILESYSTEM + /// + /// is a zero-length string, contains only white space, or contains one or more + /// invalid characters as defined by . + /// + /// + /// is . + /// + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// + /// The specified path is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// + /// The file specified in was not found. + /// + /// is in an invalid format. + /// An I/O error occurred while opening the file. + /// Message content is too long, length must be less or equal to . public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, string filePath, string text, bool isTTS, Embed embed, RequestOptions options) { @@ -177,6 +195,7 @@ namespace Discord.Rest return await SendFileAsync(channel, client, file, filename, text, isTTS, embed, options).ConfigureAwait(false); } #endif + /// Message content is too long, length must be less or equal to . public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, Stream stream, string filename, string text, bool isTTS, Embed embed, RequestOptions options) { @@ -249,6 +268,7 @@ namespace Discord.Rest return user; } + /// Resolving permissions requires the parent guild to be downloaded. public static IAsyncEnumerable> GetUsersAsync(IGuildChannel channel, IGuild guild, BaseDiscordClient client, ulong? fromUserId, int? limit, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index 8668d890a..2895dc17d 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace Discord.Rest { /// - /// Represents a REST channel that can send and receive messages. + /// Represents a REST-based channel that can send and receive messages. /// public interface IRestMessageChannel : IMessageChannel { diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs index 628dcb776..f387ac2d4 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestPrivateChannel.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Discord.Rest { /// - /// Represents a REST channel that is private to select recipients. + /// Represents a REST-based channel that is private to select recipients. /// public interface IRestPrivateChannel : IPrivateChannel { diff --git a/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs index c6cbeeecc..eb3fdd94b 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs @@ -7,7 +7,7 @@ using Model = Discord.API.Channel; namespace Discord.Rest { /// - /// Represents a REST category channel. + /// Represents a REST-based category channel. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestCategoryChannel : RestGuildChannel, ICategoryChannel diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs index 04cc5a937..c6d765787 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -8,12 +8,14 @@ namespace Discord.Rest { public class RestChannel : RestEntity, IChannel, IUpdateable { + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); internal RestChannel(BaseDiscordClient discord, ulong id) : base(discord, id) { } + /// Unexpected channel type. internal static RestChannel Create(BaseDiscordClient discord, Model model) { switch (model.Type) @@ -28,6 +30,7 @@ namespace Discord.Rest return new RestChannel(discord, model.Id); } } + /// Unexpected channel type. internal static IRestPrivateChannel CreatePrivate(BaseDiscordClient discord, Model model) { switch (model.Type) @@ -42,13 +45,17 @@ namespace Discord.Rest } internal virtual void Update(Model model) { } + /// public virtual Task UpdateAsync(RequestOptions options = null) => Task.Delay(0); //IChannel + /// string IChannel.Name => null; + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overridden + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => AsyncEnumerable.Empty>(); //Overridden } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index ca71e719b..a3161c72c 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -10,7 +10,7 @@ using Model = Discord.API.Channel; namespace Discord.Rest { /// - /// Represents a REST DM channel. + /// Represents a REST-based DM channel. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestDMChannel : RestChannel, IDMChannel, IRestPrivateChannel, IRestMessageChannel @@ -37,6 +37,7 @@ namespace Discord.Rest Recipient.Update(model.Recipients.Value[0]); } + /// public override async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetChannelAsync(Id, options).ConfigureAwait(false); @@ -93,16 +94,20 @@ namespace Discord.Rest public override string ToString() => $"@{Recipient}"; private string DebuggerDisplay => $"@{Recipient} ({Id}, DM)"; - //IDMChannel + //IDMChannel + /// IUser IDMChannel.Recipient => Recipient; //IRestPrivateChannel + /// IReadOnlyCollection IRestPrivateChannel.Recipients => ImmutableArray.Create(Recipient); //IPrivateChannel + /// IReadOnlyCollection IPrivateChannel.Recipients => ImmutableArray.Create(Recipient); //IMessageChannel + /// async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -110,6 +115,7 @@ namespace Discord.Rest else return null; } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -117,6 +123,7 @@ namespace Discord.Rest else return AsyncEnumerable.Empty>(); } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -124,6 +131,7 @@ namespace Discord.Rest else return AsyncEnumerable.Empty>(); } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -131,25 +139,33 @@ namespace Discord.Rest else return AsyncEnumerable.Empty>(); } + /// 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); //IChannel + /// string IChannel.Name => $"@{Recipient}"; + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => ImmutableArray.Create>(Users).ToAsyncEnumerable(); } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index c8eeabf00..7c3cc53c9 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -146,6 +146,7 @@ namespace Discord.Rest public override string ToString() => Name; //IGuildChannel + /// IGuild IGuildChannel.Guild { get @@ -156,32 +157,44 @@ namespace Discord.Rest } } + /// 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) => AsyncEnumerable.Empty>(); //Overridden //Overridden in Text/Voice + /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overridden in Text/Voice //IChannel + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => AsyncEnumerable.Empty>(); //Overridden in Text/Voice + /// Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); //Overridden in Text/Voice } diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 38c5fe9e2..b279f06a3 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -1,4 +1,4 @@ -using Discord.API.Rest; +using Discord.API.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -14,10 +14,11 @@ namespace Discord.Rest internal static class GuildHelper { //General + /// is . public static async Task ModifyAsync(IGuild guild, BaseDiscordClient client, Action func, RequestOptions options) { - if (func == null) throw new NullReferenceException(nameof(func)); + if (func == null) throw new ArgumentNullException(nameof(func)); var args = new GuildProperties(); func(args); @@ -62,10 +63,11 @@ namespace Discord.Rest return await client.ApiClient.ModifyGuildAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } + /// is . public static async Task ModifyEmbedAsync(IGuild guild, BaseDiscordClient client, Action func, RequestOptions options) { - if (func == null) throw new NullReferenceException(nameof(func)); + if (func == null) throw new ArgumentNullException(nameof(func)); var args = new GuildEmbedProperties(); func(args); @@ -139,6 +141,7 @@ namespace Discord.Rest var models = await client.ApiClient.GetGuildChannelsAsync(guild.Id, options).ConfigureAwait(false); return models.Select(x => RestGuildChannel.Create(client, guild, x)).ToImmutableArray(); } + /// is . public static async Task CreateTextChannelAsync(IGuild guild, BaseDiscordClient client, string name, RequestOptions options) { @@ -148,6 +151,7 @@ namespace Discord.Rest var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestTextChannel.Create(client, guild, model); } + /// is . public static async Task CreateVoiceChannelAsync(IGuild guild, BaseDiscordClient client, string name, RequestOptions options) { @@ -157,6 +161,7 @@ namespace Discord.Rest var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestVoiceChannel.Create(client, guild, model); } + /// is . public static async Task CreateCategoryChannelAsync(IGuild guild, BaseDiscordClient client, string name, RequestOptions options) { @@ -191,6 +196,7 @@ namespace Discord.Rest } //Roles + /// is . public static async Task CreateRoleAsync(IGuild guild, BaseDiscordClient client, string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) { @@ -292,11 +298,12 @@ namespace Discord.Rest Image = image.ToModel() }; if (roles.IsSpecified) - apiargs.RoleIds = roles.Value?.Select(xr => xr.Id)?.ToArray(); + apiargs.RoleIds = roles.Value?.Select(xr => xr.Id).ToArray(); var emote = await client.ApiClient.CreateGuildEmoteAsync(guild.Id, apiargs, options).ConfigureAwait(false); return emote.ToEntity(); } + /// is . public static async Task ModifyEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, Action func, RequestOptions options) { @@ -310,7 +317,7 @@ namespace Discord.Rest Name = props.Name }; if (props.Roles.IsSpecified) - apiargs.RoleIds = props.Roles.Value?.Select(xr => xr.Id)?.ToArray(); + apiargs.RoleIds = props.Roles.Value?.Select(xr => xr.Id).ToArray(); var emote = await client.ApiClient.ModifyGuildEmoteAsync(guild.Id, id, apiargs, options).ConfigureAwait(false); return emote.ToEntity(); diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs b/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs index 104bec903..2c65c8b59 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using Model = Discord.API.Ban; namespace Discord.Rest @@ -7,6 +7,7 @@ namespace Discord.Rest public class RestBan : IBan { public RestUser User { get; } + /// public string Reason { get; } internal RestBan(RestUser user, string reason) @@ -23,6 +24,7 @@ namespace Discord.Rest private string DebuggerDisplay => $"{User}: {Reason}"; //IBan + /// IUser IBan.User => User; } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index fc5ef307f..5a79565ba 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -10,6 +10,9 @@ using Model = Discord.API.Guild; namespace Discord.Rest { + /// + /// Represents a REST-based guild/server. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestGuild : RestEntity, IGuild, IUpdateable { @@ -17,32 +20,50 @@ namespace Discord.Rest private ImmutableArray _emotes; private ImmutableArray _features; + /// 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; } + /// public ulong? AFKChannelId { get; private set; } + /// public ulong? EmbedChannelId { get; private set; } + /// public ulong? SystemChannelId { get; private set; } + /// public ulong OwnerId { get; private set; } + /// public string VoiceRegionId { get; private set; } + /// public string IconId { get; private set; } + /// public string SplashId { get; private set; } internal bool Available { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); [Obsolete("DefaultChannelId is deprecated, use GetDefaultChannelAsync")] public ulong DefaultChannelId => Id; + /// public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); + /// public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); public RestRole EveryoneRole => GetRole(Id); public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); + /// public IReadOnlyCollection Emotes => _emotes; + /// public IReadOnlyCollection Features => _features; internal RestGuild(BaseDiscordClient client, ulong id) @@ -103,37 +124,48 @@ namespace Discord.Rest } //General + /// public async Task UpdateAsync(RequestOptions options = null) => Update(await Discord.ApiClient.GetGuildAsync(Id, options).ConfigureAwait(false)); + /// public Task DeleteAsync(RequestOptions options = null) => GuildHelper.DeleteAsync(this, Discord, options); + /// + /// is . public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await GuildHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } + + /// + /// is . public async Task ModifyEmbedAsync(Action func, RequestOptions options = null) { var model = await GuildHelper.ModifyEmbedAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } + + /// + /// is . public async Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null) { var arr = args.ToArray(); await GuildHelper.ReorderChannelsAsync(this, Discord, arr, options).ConfigureAwait(false); } + /// public async Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null) { var models = await GuildHelper.ReorderRolesAsync(this, Discord, args, options).ConfigureAwait(false); foreach (var model in models) { var role = GetRole(model.Id); - if (role != null) - role.Update(model); + role?.Update(model); } } - + + /// public Task LeaveAsync(RequestOptions options = null) => GuildHelper.LeaveAsync(this, Discord, options); @@ -141,13 +173,17 @@ namespace Discord.Rest 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); @@ -261,6 +297,7 @@ namespace Discord.Rest public Task GetOwnerAsync(RequestOptions options = null) => GuildHelper.GetUserAsync(this, Discord, OwnerId, options); + /// public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); @@ -270,28 +307,45 @@ namespace Discord.Rest public Task> GetWebhooksAsync(RequestOptions options = null) => GuildHelper.GetWebhooksAsync(this, Discord, options); + /// + /// Returns the name of the guild. + /// + /// + /// The name of the guild. + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; //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); + /// + /// is . 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); //IGuild + /// bool IGuild.Available => Available; + /// IAudioClient IGuild.AudioClient => null; + /// IRole IGuild.EveryoneRole => EveryoneRole; + /// IReadOnlyCollection IGuild.Roles => Roles; + /// async Task> IGuild.GetBansAsync(RequestOptions options) => await GetBansAsync(options).ConfigureAwait(false); + /// async Task> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -299,6 +353,7 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + /// async Task IGuild.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -306,6 +361,7 @@ namespace Discord.Rest else return null; } + /// async Task> IGuild.GetTextChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -313,6 +369,7 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + /// async Task IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -320,6 +377,7 @@ namespace Discord.Rest else return null; } + /// async Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -327,6 +385,7 @@ namespace Discord.Rest else return ImmutableArray.Create(); } + /// async Task> IGuild.GetCategoriesAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -334,6 +393,7 @@ namespace Discord.Rest else return null; } + /// async Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -341,6 +401,7 @@ namespace Discord.Rest else return null; } + /// async Task IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -348,6 +409,7 @@ namespace Discord.Rest else return null; } + /// async Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -355,6 +417,7 @@ namespace Discord.Rest else return null; } + /// async Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -362,6 +425,7 @@ namespace Discord.Rest else return null; } + /// async Task IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -369,26 +433,35 @@ namespace Discord.Rest else return null; } + /// 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); + /// async Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -396,6 +469,7 @@ namespace Discord.Rest else return null; } + /// async Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -403,6 +477,7 @@ namespace Discord.Rest else return null; } + /// async Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -410,6 +485,7 @@ namespace Discord.Rest else return null; } + /// async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -418,12 +494,14 @@ namespace Discord.Rest return ImmutableArray.Create(); } /// - /// Downloading users is not supported with a REST-based guild. + /// Downloading users is not supported for a REST-based guild. Task IGuild.DownloadUsersAsync() => throw new NotSupportedException(); + /// async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) => await GetWebhookAsync(id, options).ConfigureAwait(false); + /// async Task> IGuild.GetWebhooksAsync(RequestOptions options) => await GetWebhooksAsync(options).ConfigureAwait(false); } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs index f26a62d8d..00f3fae69 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildEmbed.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using Model = Discord.API.GuildEmbed; namespace Discord @@ -19,7 +19,7 @@ namespace Discord return new RestGuildEmbed(model.Enabled, model.ChannelId); } - public override string ToString() => ChannelId?.ToString(); + public override string ToString() => ChannelId?.ToString() ?? "Unknown"; private string DebuggerDisplay => $"{ChannelId} ({(IsEnabled ? "Enabled" : "Disabled")})"; } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs index eadda53f2..9759e64d2 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuildIntegration.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Integration; @@ -10,18 +10,28 @@ namespace Discord.Rest { private long _syncedAtTicks; + /// public string Name { get; private set; } + /// public string Type { get; private set; } + /// public bool IsEnabled { get; private set; } + /// public bool IsSyncing { get; private set; } + /// public ulong ExpireBehavior { get; private set; } + /// public ulong ExpireGracePeriod { get; private set; } + /// public ulong GuildId { get; private set; } + /// public ulong RoleId { get; private set; } public RestUser User { get; private set; } + /// public IntegrationAccount Account { get; private set; } internal IGuild Guild { get; private set; } + /// public DateTimeOffset SyncedAt => DateTimeUtils.FromTicks(_syncedAtTicks); internal RestGuildIntegration(BaseDiscordClient discord, IGuild guild, ulong id) @@ -78,6 +88,7 @@ namespace Discord.Rest public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id}{(IsEnabled ? ", Enabled" : "")})"; + /// IGuild IGuildIntegration.Guild { get @@ -87,6 +98,7 @@ namespace Discord.Rest throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); } } + /// IUser IGuildIntegration.User => User; } } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs index de5a5f7d9..b75d6288e 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestUserGuild.cs @@ -9,12 +9,17 @@ namespace Discord.Rest public class RestUserGuild : RestEntity, IUserGuild { private string _iconId; - + + /// public string Name { get; private set; } + /// public bool IsOwner { get; private set; } + /// public GuildPermissions Permissions { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// public string IconUrl => CDN.GetGuildIconUrl(Id, _iconId); internal RestUserGuild(BaseDiscordClient discord, ulong id) @@ -40,6 +45,7 @@ namespace Discord.Rest { await Discord.ApiClient.LeaveGuildAsync(Id, options).ConfigureAwait(false); } + /// public async Task DeleteAsync(RequestOptions options = null) { await Discord.ApiClient.DeleteGuildAsync(Id, options).ConfigureAwait(false); diff --git a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs index 6df97f6f7..62fdaeaba 100644 --- a/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs +++ b/src/Discord.Net.Rest/Entities/Invites/RestInvite.cs @@ -59,7 +59,8 @@ namespace Discord.Rest public override string ToString() => Url; private string DebuggerDisplay => $"{Url} ({GuildName} / {ChannelName})"; - + + /// IGuild IInvite.Guild { get @@ -71,6 +72,7 @@ namespace Discord.Rest throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); } } + /// IChannel IInvite.Channel { get diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 071628da0..34e64cd5a 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -10,11 +10,13 @@ namespace Discord.Rest { internal static class MessageHelper { + /// Only the author of a message may modify the message. + /// Message content is too long, length must be less or equal to . 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."); + throw new InvalidOperationException("Only the author of a message may modify the message."); var args = new MessageProperties(); func(args); diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index 84fef4c18..6d18beaad 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -35,6 +35,7 @@ namespace Discord.Rest /// public virtual IReadOnlyCollection MentionedRoleIds => ImmutableArray.Create(); public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); + /// public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); /// @@ -75,10 +76,14 @@ namespace Discord.Rest public override string ToString() => Content; + /// MessageType IMessage.Type => MessageType.Default; IUser IMessage.Author => Author; + /// IReadOnlyCollection IMessage.Attachments => Attachments; + /// IReadOnlyCollection IMessage.Embeds => Embeds; + /// IReadOnlyCollection IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); } } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index 0d1f3be2b..de295af94 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -17,23 +17,33 @@ namespace Discord.Rest private ImmutableArray _embeds; private ImmutableArray _tags; private ImmutableArray _reactions; - + + /// public override bool IsTTS => _isTTS; + /// public override bool IsPinned => _isPinned; + /// public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); + /// public override IReadOnlyCollection Attachments => _attachments; + /// public override IReadOnlyCollection Embeds => _embeds; + /// public override IReadOnlyCollection MentionedChannelIds => MessageHelper.FilterTagsByKey(TagType.ChannelMention, _tags); + /// public override IReadOnlyCollection MentionedRoleIds => MessageHelper.FilterTagsByKey(TagType.RoleMention, _tags); + /// public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); + /// public override IReadOnlyCollection Tags => _tags; + /// public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emote, x => new ReactionMetadata { ReactionCount = x.Count, IsMe = x.Me }); internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author, MessageSource source) : base(discord, id, channel, author, source) { } - internal static new RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) + internal new static RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) { var entity = new RestUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); entity.Update(model); @@ -124,30 +134,37 @@ namespace Discord.Rest } } + /// public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await MessageHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } + /// public Task AddReactionAsync(IEmote emote, RequestOptions options = null) => MessageHelper.AddReactionAsync(this, emote, Discord, options); + /// public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); + /// public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); + /// public Task> GetReactionUsersAsync(IEmote emote, int limit = 100, ulong? afterUserId = null, RequestOptions options = null) => MessageHelper.GetReactionUsersAsync(this, emote, x => { x.Limit = limit; x.AfterUserId = afterUserId ?? Optional.Create(); }, Discord, options); - - + + /// public Task PinAsync(RequestOptions options = null) => MessageHelper.PinAsync(this, Discord, options); + /// public Task UnpinAsync(RequestOptions options = null) => MessageHelper.UnpinAsync(this, Discord, options); public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) => MentionUtils.Resolve(this, startIndex, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + /// public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); diff --git a/src/Discord.Net.Rest/Entities/RestApplication.cs b/src/Discord.Net.Rest/Entities/RestApplication.cs index d8986a470..198ce1a61 100644 --- a/src/Discord.Net.Rest/Entities/RestApplication.cs +++ b/src/Discord.Net.Rest/Entities/RestApplication.cs @@ -19,6 +19,7 @@ namespace Discord.Rest public string Description { get; private set; } /// public string[] RPCOrigins { get; private set; } + /// public ulong Flags { get; private set; } /// @@ -52,6 +53,7 @@ namespace Discord.Rest Owner = RestUser.Create(Discord, model.Owner.Value); } + /// Unable to update this object from a different application token. public async Task UpdateAsync() { var response = await Discord.ApiClient.GetMyApplicationAsync().ConfigureAwait(false); @@ -60,6 +62,12 @@ namespace Discord.Rest Update(response); } + /// + /// Gets the name of the application. + /// + /// + /// Name of the application. + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; } diff --git a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs index 486f41b9e..5ea7f4462 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Role; @@ -9,16 +9,25 @@ namespace Discord.Rest public class RestRole : RestEntity, IRole { internal IGuild Guild { get; } + /// public Color Color { get; private set; } + /// public bool IsHoisted { get; private set; } + /// public bool IsManaged { get; private set; } + /// public bool IsMentionable { get; private set; } + /// public string Name { get; private set; } + /// public GuildPermissions Permissions { get; private set; } + /// public int Position { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public bool IsEveryone => Id == Guild.Id; + /// public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id); internal RestRole(BaseDiscordClient discord, IGuild guild, ulong id) @@ -43,20 +52,24 @@ namespace Discord.Rest Permissions = new GuildPermissions(model.Permissions); } + /// public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await RoleHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); Update(model); } + /// public Task DeleteAsync(RequestOptions options = null) => RoleHelper.DeleteAsync(this, Discord, options); + /// public int CompareTo(IRole role) => RoleUtils.Compare(this, role); public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; //IRole + /// IGuild IRole.Guild { get diff --git a/src/Discord.Net.Rest/Entities/Users/RestConnection.cs b/src/Discord.Net.Rest/Entities/Users/RestConnection.cs index b8b83be3e..0c91493d2 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestConnection.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestConnection.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using Model = Discord.API.Connection; @@ -8,10 +8,15 @@ namespace Discord [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestConnection : IConnection { + /// public string Id { get; } + /// public string Type { get; } + /// public string Name { get; } + /// public bool IsRevoked { get; } + /// public IReadOnlyCollection IntegrationIds { get; } internal RestConnection(string id, string type, string name, bool isRevoked, IReadOnlyCollection integrationIds) @@ -28,6 +33,12 @@ namespace Discord return new RestConnection(model.Id, model.Type, model.Name, model.Revoked, model.Integrations.ToImmutableArray()); } + /// + /// Gets the name of the connection. + /// + /// + /// Name of the connection. + /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id}, {Type}{(IsRevoked ? ", Revoked" : "")})"; } diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index 051ed73d5..fdfe9b5a1 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -24,7 +24,9 @@ namespace Discord.Rest /// public ulong GuildId => Guild.Id; + /// + /// Resolving permissions requires the parent guild to be downloaded. public GuildPermissions GuildPermissions { get @@ -112,6 +114,7 @@ namespace Discord.Rest => UserHelper.RemoveRolesAsync(this, Discord, roles, options); /// + /// Resolving permissions requires the parent guild to be downloaded. public ChannelPermissions GetPermissions(IGuildChannel channel) { var guildPerms = GuildPermissions; @@ -119,6 +122,7 @@ namespace Discord.Rest } //IGuildUser + /// IGuild IGuildUser.Guild { get @@ -130,10 +134,15 @@ namespace Discord.Rest } //IVoiceState + /// 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/RestSelfUser.cs b/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs index ab5ec4a3b..9f84306ea 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestSelfUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.User; @@ -8,8 +8,11 @@ namespace Discord.Rest [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestSelfUser : RestUser, ISelfUser { + /// public string Email { get; private set; } + /// public bool IsVerified { get; private set; } + /// public bool IsMfaEnabled { get; private set; } internal RestSelfUser(BaseDiscordClient discord, ulong id) @@ -22,6 +25,7 @@ namespace Discord.Rest entity.Update(model); return entity; } + /// internal override void Update(Model model) { base.Update(model); @@ -34,6 +38,8 @@ namespace Discord.Rest IsMfaEnabled = model.MfaEnabled.Value; } + /// + /// Unable to update this object using a different token. public override async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetMyUserAsync(options).ConfigureAwait(false); @@ -42,6 +48,8 @@ namespace Discord.Rest Update(model); } + /// + /// Unable to modify this object using a different token. public async Task ModifyAsync(Action func, RequestOptions options = null) { if (Id != Discord.CurrentUser.Id) diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index f0c771ae0..47c3f8b98 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -8,16 +8,26 @@ namespace Discord.Rest [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestUser : RestEntity, IUser, IUpdateable { + /// public bool IsBot { get; private set; } + /// public string Username { get; private set; } + /// public ushort DiscriminatorValue { get; private set; } + /// public string AvatarId { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); + /// public string Discriminator => DiscriminatorValue.ToString("D4"); + /// public string Mention => MentionUtils.MentionUser(Id); + /// public virtual IActivity Activity => null; + /// public virtual UserStatus Status => UserStatus.Offline; + /// public virtual bool IsWebhook => false; internal RestUser(BaseDiscordClient discord, ulong id) @@ -48,6 +58,7 @@ namespace Discord.Rest Username = model.Username.Value; } + /// public virtual async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetUserAsync(Id, options).ConfigureAwait(false); @@ -57,9 +68,11 @@ namespace Discord.Rest public Task GetOrCreateDMChannelAsync(RequestOptions options = null) => UserHelper.CreateDMChannelAsync(this, Discord, options); + /// public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); + /// public string GetDefaultAvatarUrl() => CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); @@ -67,6 +80,7 @@ namespace Discord.Rest private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; //IUser + /// async Task IUser.GetOrCreateDMChannelAsync(RequestOptions options) => await GetOrCreateDMChannelAsync(options).ConfigureAwait(false); } diff --git a/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs b/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs index 47cc50a9c..1fdc95a63 100644 --- a/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs +++ b/src/Discord.Net.Rest/Entities/Webhooks/RestWebhook.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.Webhook; @@ -11,14 +11,21 @@ namespace Discord.Rest internal IGuild Guild { get; private set; } internal ITextChannel Channel { get; private set; } + /// public ulong ChannelId { get; } + /// public string Token { get; } + /// public string Name { get; private set; } + /// public string AvatarId { get; private set; } + /// public ulong? GuildId { get; private set; } + /// public IUser Creator { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); internal RestWebhook(BaseDiscordClient discord, IGuild guild, ulong id, string token, ulong channelId) @@ -59,12 +66,14 @@ namespace Discord.Rest Name = model.Name.Value; } + /// public async Task UpdateAsync(RequestOptions options = null) { var model = await Discord.ApiClient.GetWebhookAsync(Id, options).ConfigureAwait(false); Update(model); } + /// public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => CDN.GetUserAvatarUrl(Id, AvatarId, size, format); @@ -74,6 +83,7 @@ namespace Discord.Rest Update(model); } + /// public Task DeleteAsync(RequestOptions options = null) => WebhookHelper.DeleteAsync(this, Discord, options); @@ -81,10 +91,13 @@ namespace Discord.Rest private string DebuggerDisplay => $"Webhook: {Name} ({Id})"; //IWebhook + /// IGuild IWebhook.Guild => Guild ?? throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + /// ITextChannel IWebhook.Channel => Channel ?? throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + /// Task IWebhook.ModifyAsync(Action func, RequestOptions options) => ModifyAsync(func, options); } diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index ec789be59..4b4c1e045 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -82,6 +82,8 @@ namespace Discord.Net.Rest return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); } } + + /// Unsupported param type. public async Task SendAsync(string method, string endpoint, IReadOnlyDictionary multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null) { string uri = Path.Combine(_baseUrl, endpoint); @@ -111,7 +113,7 @@ namespace Discord.Net.Rest content.Add(new StreamContent(stream), p.Key, fileValue.Filename); continue; } - default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\""); + default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\"."); } } } diff --git a/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs b/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs index e0e776549..67b47096e 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClientProvider.cs @@ -6,6 +6,7 @@ namespace Discord.Net.Rest { public static readonly RestClientProvider Instance = Create(); + /// The default RestClientProvider is not supported on this platform. public static RestClientProvider Create(bool useProxy = false) { return url => diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs index 3346681b5..5894aa768 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueueBucket.cs @@ -126,7 +126,7 @@ namespace Discord.Net.Queue if ((request.Options.RetryMode & RetryMode.RetryTimeouts) == 0) throw; - await Task.Delay(500); + await Task.Delay(500).ConfigureAwait(false); continue; //Retry } /*catch (Exception) diff --git a/src/Discord.Net.Rest/Net/RateLimitInfo.cs b/src/Discord.Net.Rest/Net/RateLimitInfo.cs index 9421221ed..e9a5fde9e 100644 --- a/src/Discord.Net.Rest/Net/RateLimitInfo.cs +++ b/src/Discord.Net.Rest/Net/RateLimitInfo.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace Discord.Net @@ -15,7 +15,7 @@ namespace Discord.Net internal RateLimitInfo(Dictionary headers) { IsGlobal = headers.TryGetValue("X-RateLimit-Global", out string temp) && - bool.TryParse(temp, out var isGlobal) ? isGlobal : false; + bool.TryParse(temp, out var isGlobal) && isGlobal; Limit = headers.TryGetValue("X-RateLimit-Limit", out temp) && int.TryParse(temp, out var limit) ? limit : (int?)null; Remaining = headers.TryGetValue("X-RateLimit-Remaining", out temp) && diff --git a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs index 58c4f4c70..1861e3554 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; @@ -22,19 +22,22 @@ namespace Discord.Audio.Streams _decoder = new OpusDecoder(); } + /// Header received with no payload. public override void WriteHeader(ushort seq, uint timestamp, bool missed) { if (_hasHeader) - throw new InvalidOperationException("Header received with no payload"); + throw new InvalidOperationException("Header received with no payload."); _hasHeader = true; _nextMissed = missed; _next.WriteHeader(seq, timestamp, missed); } + + /// Received payload without an RTP header. public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { if (!_hasHeader) - throw new InvalidOperationException("Received payload without an RTP header"); + throw new InvalidOperationException("Received payload without an RTP header."); _hasHeader = false; if (!_nextMissed) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs index 2cedea114..120f67e0d 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/RTPReadStream.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; namespace Discord.Audio.Streams @@ -20,6 +21,8 @@ namespace Discord.Audio.Streams _nonce = new byte[24]; } + /// The token has had cancellation requested. + /// The associated has been disposed. public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); diff --git a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs index bacc9be47..2b5cc0a18 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/SodiumEncryptStream.cs @@ -20,21 +20,25 @@ namespace Discord.Audio.Streams _client = (AudioClient)client; _nonce = new byte[24]; } - + + /// Header received with no payload. public override void WriteHeader(ushort seq, uint timestamp, bool missed) { if (_hasHeader) - throw new InvalidOperationException("Header received with no payload"); + throw new InvalidOperationException("Header received with no payload."); _nextSeq = seq; _nextTimestamp = timestamp; _hasHeader = true; } + /// Received payload without an RTP header. + /// The token has had cancellation requested. + /// The associated has been disposed. public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken) { cancelToken.ThrowIfCancellationRequested(); if (!_hasHeader) - throw new InvalidOperationException("Received payload without an RTP header"); + throw new InvalidOperationException("Received payload without an RTP header."); _hasHeader = false; if (_client.SecretKey == null) diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index e881a7855..304592442 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.WebSocket diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 6393005ac..9a47d0881 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -10,42 +10,174 @@ namespace Discord.WebSocket { protected readonly DiscordSocketConfig BaseConfig; - /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. + /// + /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. + /// public abstract int Latency { get; protected set; } - public abstract UserStatus Status { get; protected set; } + /// + /// Gets the status for the logged-in user. + /// + public abstract UserStatus Status { get; protected set; } + /// + /// Gets the activity for the logged-in user. + /// public abstract IActivity Activity { get; protected set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; + /// + /// Gets the current logged-in user. + /// public new SocketSelfUser CurrentUser { get => base.CurrentUser as SocketSelfUser; protected set => base.CurrentUser = value; } + /// + /// Gets a collection of guilds that the logged-in user is currently in. + /// public abstract IReadOnlyCollection Guilds { get; } + /// + /// Gets a collection of private channels that are currently open for the logged-in user. + /// public abstract IReadOnlyCollection PrivateChannels { get; } + /// + /// Gets a collection of available voice regions for the logged-in user. + /// public abstract IReadOnlyCollection VoiceRegions { get; } internal BaseSocketClient(DiscordSocketConfig config, DiscordRestApiClient client) : base(config, client) => BaseConfig = config; private static DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) => new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent); - + + /// + /// Gets a Discord application information for the logged-in user. + /// + /// The options to be used when sending the request. + /// + /// Application information. This reflects your application information you submitted when creating a + /// Discord application via the Developer Portal. + /// public abstract Task GetApplicationInfoAsync(RequestOptions options = null); + /// + /// Gets a user who shares a mutual guild with logged-in user with the provided snowflake ID. + /// + /// The user snowflake ID. + /// + /// A user who shares a mutual guild with the logged-in user and who is also present in the WebSocket cache; + /// or when the user cannot be found. + /// public abstract SocketUser GetUser(ulong id); + + /// + /// Gets a user who shares a mutual guild with the logged-in user with the provided username and discriminator value combo. + /// + /// The name of the user. + /// The discriminator value of the user. + /// + /// A user who shares a mutual guild with the logged-in user and who is also present in the WebSocket cache; + /// or when the user cannot be found. + /// public abstract SocketUser GetUser(string username, string discriminator); + /// + /// Gets a channel that the logged-in user is accessible to with the provided ID. + /// + /// The channel snowflake ID. + /// + /// A generic channel object (voice, text, category, etc.); or when the channel + /// cannot be found. + /// public abstract SocketChannel GetChannel(ulong id); + /// + /// Gets a guild that the logged-in user is accessible to with the provided ID. + /// + /// The guild snowflake ID. + /// + /// A guild; or when the guild cannot be found. + /// public abstract SocketGuild GetGuild(ulong id); + /// + /// Gets a voice region with the provided ID. + /// + /// The unique identifier of the voice region. + /// + /// A voice region; or if none can be found. + /// public abstract RestVoiceRegion GetVoiceRegion(string id); /// public abstract Task StartAsync(); /// public abstract Task StopAsync(); + /// + /// Sets the current status of the logged-in user (e.g. Online, Do not Disturb). + /// + /// The new status to be set. + /// + /// An awaitable . + /// public abstract Task SetStatusAsync(UserStatus status); + /// + /// Sets the game of the logged-in user. + /// + /// The name of the game. + /// If streaming, the URL of the stream. Must be a valid Twitch URL. + /// The type of the game. + /// + /// An awaitable . + /// public abstract Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing); + /// + /// Sets the of the logged-in user. + /// + /// + /// This method sets the of the user. Please note that Rich Presence cannot be + /// set via this method or client. Rich Presence is strictly limited to RPC clients only. Furthermore, + /// Discord will only accept setting of name and the type of activity. + /// + /// The activty to be set. + /// + /// An awaitable . + /// public abstract Task SetActivityAsync(IActivity activity); - public abstract Task DownloadUsersAsync(IEnumerable guilds); - + /// + /// Attempts to download users into the user cache for the selected guilds. + /// + /// The guilds to download the members from. + /// + /// An awaitable . + /// + public abstract Task DownloadUsersAsync(IEnumerable guilds); + + /// + /// Creates a guild for the logged-in user who is in less than 10 active guilds. + /// + /// + /// This method creates a new guild on behalf of the logged-in user. Note that due to Discord's limitation, + /// this method will only work for users that are in less than 10 guilds. + /// + /// The name of the new guild. + /// The voice region to create the guild with. + /// The icon of the guild. + /// The options to be used when sending the request. + /// + /// An awaitable containing the newly created guild. + /// public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options ?? RequestOptions.Default); + /// + /// Gets the connections that the logged-in user has set up. + /// + /// The options to be used when sending the request. + /// + /// An awaitable containing a collection of connections. + /// public Task> GetConnectionsAsync(RequestOptions options = null) => ClientHelper.GetConnectionsAsync(this, options ?? RequestOptions.Default); + /// + /// Gets an invite with the provided invite identifier. + /// + /// The invitation identifier. + /// The options to be used when sending the request. + /// + /// An awaitable containing the invite information. + /// public Task GetInviteAsync(string inviteId, RequestOptions options = null) => ClientHelper.GetInviteAsync(this, inviteId, options ?? RequestOptions.Default); diff --git a/src/Discord.Net.WebSocket/ClientState.cs b/src/Discord.Net.WebSocket/ClientState.cs index f07976a0a..44b44e689 100644 --- a/src/Discord.Net.WebSocket/ClientState.cs +++ b/src/Discord.Net.WebSocket/ClientState.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -72,9 +72,9 @@ namespace Discord.WebSocket switch (channel) { case SocketDMChannel dmChannel: - _dmChannels.TryRemove(dmChannel.Recipient.Id, out var ignored); + _dmChannels.TryRemove(dmChannel.Recipient.Id, out var _); break; - case SocketGroupChannel groupChannel: + case SocketGroupChannel _: _groupChannels.TryRemove(id); break; } diff --git a/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs index 0fb47ceff..f4d517909 100644 --- a/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs +++ b/src/Discord.Net.WebSocket/Commands/SocketCommandContext.cs @@ -2,7 +2,9 @@ using Discord.WebSocket; namespace Discord.Commands { - /// The WebSocket variant of , which may contain the client, user, guild, channel, and message. + /// + /// Represents a WebSocket-based context of a command. This may include the client, guild, channel, user, and message. + /// public class SocketCommandContext : ICommandContext { /// diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index 8ae41cc59..20ff85be6 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -120,12 +120,14 @@ namespace Discord.API } finally { _stateLock.Release(); } } + /// The client must be logged in before connecting. + /// This client is not configured with WebSocket support. internal override async Task ConnectInternalAsync() { if (LoginState != LoginState.LoggedIn) - throw new InvalidOperationException("You must log in before connecting."); + throw new InvalidOperationException("The client must be logged in before connecting."); if (WebSocketClient == null) - throw new NotSupportedException("This client is not configured with websocket support."); + throw new NotSupportedException("This client is not configured with WebSocket support."); //Re-create streams to reset the zlib state _compressed?.Dispose(); @@ -176,10 +178,11 @@ namespace Discord.API } finally { _stateLock.Release(); } } + /// This client is not configured with WebSocket support. internal override async Task DisconnectInternalAsync() { if (WebSocketClient == null) - throw new NotSupportedException("This client is not configured with websocket support."); + throw new NotSupportedException("This client is not configured with WebSocket support."); if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index c9f8fa5f5..64f254782 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -46,7 +46,9 @@ namespace Discord.WebSocket public ConnectionState ConnectionState => _connection.State; /// public override int Latency { get; protected set; } + /// public override UserStatus Status { get; protected set; } = UserStatus.Online; + /// public override IActivity Activity { get; protected set; } //From DiscordSocketConfig @@ -58,14 +60,17 @@ namespace Discord.WebSocket internal WebSocketProvider WebSocketProvider { get; private set; } internal bool AlwaysDownloadUsers { get; private set; } internal int? HandlerTimeout { get; private set; } - + internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; + /// public override IReadOnlyCollection Guilds => State.Guilds; + /// public override IReadOnlyCollection PrivateChannels => State.PrivateChannels; public IReadOnlyCollection DMChannels => State.PrivateChannels.Select(x => x as SocketDMChannel).Where(x => x != null).ToImmutableArray(); public IReadOnlyCollection GroupChannels => 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. @@ -128,6 +133,7 @@ namespace Discord.WebSocket } private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config) => new API.DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost); + /// internal override void Dispose(bool disposing) { if (disposing) @@ -136,7 +142,8 @@ namespace Discord.WebSocket ApiClient.Dispose(); } } - + + /// internal override async Task OnLoginAsync(TokenType tokenType, string token) { if (_parentClient == null) @@ -147,6 +154,7 @@ namespace Discord.WebSocket else _voiceRegions = _parentClient._voiceRegions; } + /// internal override async Task OnLogoutAsync() { await StopAsync().ConfigureAwait(false); @@ -154,8 +162,10 @@ namespace Discord.WebSocket _voiceRegions = ImmutableDictionary.Create(); } + /// public override async Task StartAsync() => await _connection.StartAsync().ConfigureAwait(false); + /// public override async Task StopAsync() => await _connection.StopAsync().ConfigureAwait(false); @@ -277,7 +287,7 @@ namespace Discord.WebSocket return null; } - /// Downloads the users list for the provided guilds, if they don't have a complete list. + /// public override async Task DownloadUsersAsync(IEnumerable guilds) { if (ConnectionState == ConnectionState.Connected) @@ -316,6 +326,7 @@ namespace Discord.WebSocket } } + /// public override async Task SetStatusAsync(UserStatus status) { Status = status; @@ -325,6 +336,7 @@ namespace Discord.WebSocket _statusSince = null; await SendStatusAsync().ConfigureAwait(false); } + /// public override async Task SetGameAsync(string name, string streamUrl = null, ActivityType type = ActivityType.Playing) { if (!string.IsNullOrEmpty(streamUrl)) @@ -335,6 +347,7 @@ namespace Discord.WebSocket Activity = null; await SendStatusAsync().ConfigureAwait(false); } + /// public override async Task SetActivityAsync(IActivity activity) { Activity = activity; @@ -351,8 +364,8 @@ namespace Discord.WebSocket var gameModel = new GameModel(); // Discord only accepts rich presence over RPC, don't even bother building a payload - if (Activity is RichGame game) - throw new NotSupportedException("Outgoing Rich Presences are not supported"); + if (Activity is RichGame) + throw new NotSupportedException("Outgoing Rich Presences are not supported via WebSocket."); if (Activity != null) { @@ -479,7 +492,7 @@ namespace Discord.WebSocket await TimedInvokeAsync(_readyEvent, nameof(Ready)).ConfigureAwait(false); await _gatewayLogger.InfoAsync("Ready").ConfigureAwait(false); }); - var _ = _connection.CompleteAsync(); + _ = _connection.CompleteAsync(); } break; case "RESUMED": @@ -1173,7 +1186,7 @@ namespace Discord.WebSocket var msg = SocketChannelHelper.RemoveMessage(channel, this, data.Id); bool isCached = msg != null; - var cacheable = new Cacheable(msg, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id)); + var cacheable = new Cacheable(msg, data.Id, isCached, async () => await channel.GetMessageAsync(data.Id).ConfigureAwait(false)); await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheable, channel).ConfigureAwait(false); } @@ -1609,6 +1622,7 @@ namespace Discord.WebSocket return guild; } + /// Unexpected channel type is created. internal ISocketPrivateChannel AddPrivateChannel(API.Channel model, ClientState state) { var channel = SocketChannel.CreatePrivate(this, state, model); @@ -1781,43 +1795,59 @@ namespace Discord.WebSocket internal int GetAudioId() => _nextAudioId++; //IDiscordClient + /// async Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => await GetApplicationInfoAsync().ConfigureAwait(false); + /// Task IDiscordClient.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetChannel(id)); + /// Task> IDiscordClient.GetPrivateChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(PrivateChannels); + /// Task> IDiscordClient.GetDMChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(DMChannels); + /// Task> IDiscordClient.GetGroupChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(GroupChannels); + /// async Task> IDiscordClient.GetConnectionsAsync(RequestOptions options) => await GetConnectionsAsync().ConfigureAwait(false); + /// async Task IDiscordClient.GetInviteAsync(string inviteId, RequestOptions options) => await GetInviteAsync(inviteId).ConfigureAwait(false); + /// Task IDiscordClient.GetGuildAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetGuild(id)); + /// Task> IDiscordClient.GetGuildsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Guilds); + /// async Task IDiscordClient.CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon, RequestOptions options) => await CreateGuildAsync(name, region, jpegIcon).ConfigureAwait(false); + /// Task IDiscordClient.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); + /// Task IDiscordClient.GetUserAsync(string username, string discriminator, RequestOptions options) => Task.FromResult(GetUser(username, discriminator)); + /// Task> IDiscordClient.GetVoiceRegionsAsync(RequestOptions options) => Task.FromResult>(VoiceRegions); + /// Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(GetVoiceRegion(id)); + /// async Task IDiscordClient.StartAsync() => await StartAsync().ConfigureAwait(false); + /// async Task IDiscordClient.StopAsync() => await StopAsync().ConfigureAwait(false); } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs index ec842c8a3..e0fb93d6a 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannel.cs @@ -58,6 +58,7 @@ namespace Discord.WebSocket internal abstract SocketUser GetUserInternal(ulong id); internal abstract IReadOnlyCollection GetUsersInternal(); + private string DebuggerDisplay => $"Unknown ({Id}, Channel)"; internal SocketChannel Clone() => MemberwiseClone() as SocketChannel; //IChannel diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs index ca53315aa..e6339b6d9 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -57,7 +57,7 @@ namespace Discord.WebSocket else return ImmutableArray.Create(); } - + /// Unexpected type. public static void AddMessage(ISocketMessageChannel channel, DiscordSocketClient discord, SocketMessage msg) { @@ -66,9 +66,10 @@ namespace Discord.WebSocket case SocketDMChannel dmChannel: dmChannel.AddMessage(msg); break; case SocketGroupChannel groupChannel: groupChannel.AddMessage(msg); break; case SocketTextChannel textChannel: textChannel.AddMessage(msg); break; - default: throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + default: throw new NotSupportedException($"Unexpected {nameof(ISocketMessageChannel)} type."); } } + /// Unexpected type. public static SocketMessage RemoveMessage(ISocketMessageChannel channel, DiscordSocketClient discord, ulong id) { @@ -77,7 +78,7 @@ namespace Discord.WebSocket case SocketDMChannel dmChannel: return dmChannel.RemoveMessage(id); case SocketGroupChannel groupChannel: return groupChannel.RemoveMessage(id); case SocketTextChannel textChannel: return textChannel.RemoveMessage(id); - default: throw new NotSupportedException("Unexpected ISocketMessageChannel type"); + default: throw new NotSupportedException($"Unexpected {nameof(ISocketMessageChannel)} type."); } } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 5cc35cf14..ae8ab54da 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -111,7 +111,6 @@ namespace Discord.WebSocket /// public Task TriggerTypingAsync(RequestOptions options = null) => ChannelHelper.TriggerTypingAsync(this, Discord, options); - /// public IDisposable EnterTypingState(RequestOptions options = null) => ChannelHelper.EnterTypingState(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index c4c223e64..1f027e321 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -357,9 +357,12 @@ namespace Discord.WebSocket => GuildHelper.DeleteAsync(this, Discord, options); /// + /// is . public Task ModifyAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyAsync(this, Discord, func, options); + /// + /// is . public Task ModifyEmbedAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyEmbedAsync(this, Discord, func, options); /// @@ -431,31 +434,37 @@ namespace Discord.WebSocket /// public SocketVoiceChannel GetVoiceChannel(ulong id) => GetChannel(id) as SocketVoiceChannel; + /// /// Creates a text channel with the provided name. /// /// The name of the new channel. /// The options to be used when sending the request. + /// is . /// /// The created text channel. /// public Task CreateTextChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateTextChannelAsync(this, Discord, name, options); + /// /// Creates a voice channel with the provided name. /// /// The name of the new channel. /// The options to be used when sending the request. + /// is . /// /// The created voice channel. /// public Task CreateVoiceChannelAsync(string name, RequestOptions options = null) => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options); + /// /// Creates a category channel with the provided name. /// /// The name of the new channel. /// The options to be used when sending the request. + /// is . /// /// The created category channel. /// @@ -507,6 +516,7 @@ namespace Discord.WebSocket return value; return null; } + /// /// Creates a role. /// @@ -517,6 +527,7 @@ namespace Discord.WebSocket /// The color of the role. Set to to use the default color. /// Used to determine if users of this role are separated in the user list. /// The options to be used when sending the request. + /// is . /// /// The created role. /// @@ -645,6 +656,7 @@ namespace Discord.WebSocket public Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null) => GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options); /// + /// is . public Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null) => GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options); /// @@ -931,9 +943,9 @@ namespace Discord.WebSocket /// 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.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index 5442c888a..d339a20ed 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -8,27 +8,38 @@ using Model = Discord.API.Message; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based message. + /// public abstract class SocketMessage : SocketEntity, IMessage { private long _timestampTicks; public SocketUser Author { get; } public ISocketMessageChannel Channel { 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 MentionedChannels => ImmutableArray.Create(); public virtual IReadOnlyCollection MentionedRoles => ImmutableArray.Create(); public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); + /// public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); + /// public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); internal SocketMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) @@ -54,6 +65,7 @@ namespace Discord.WebSocket Content = model.Content.Value; } + /// public Task DeleteAsync(RequestOptions options = null) => MessageHelper.DeleteAsync(this, Discord, options); @@ -61,13 +73,21 @@ namespace Discord.WebSocket internal SocketMessage Clone() => MemberwiseClone() as SocketMessage; //IMessage + /// IUser IMessage.Author => Author; + /// IMessageChannel IMessage.Channel => Channel; + /// MessageType IMessage.Type => MessageType.Default; + /// IReadOnlyCollection IMessage.Attachments => Attachments; + /// IReadOnlyCollection IMessage.Embeds => Embeds; + /// IReadOnlyCollection IMessage.MentionedChannelIds => MentionedChannels.Select(x => x.Id).ToImmutableArray(); + /// IReadOnlyCollection IMessage.MentionedRoleIds => MentionedRoles.Select(x => x.Id).ToImmutableArray(); + /// IReadOnlyCollection IMessage.MentionedUserIds => MentionedUsers.Select(x => x.Id).ToImmutableArray(); } } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs index e8fa17a35..bfd6aa042 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -1,4 +1,4 @@ -using Model = Discord.API.Gateway.Reaction; +using Model = Discord.API.Gateway.Reaction; namespace Discord.WebSocket { @@ -9,6 +9,7 @@ namespace Discord.WebSocket public ulong MessageId { get; } public Optional Message { get; } public ISocketMessageChannel Channel { get; } + /// public IEmote Emote { get; } internal SocketReaction(ISocketMessageChannel channel, ulong messageId, Optional message, ulong userId, Optional user, IEmote emoji) @@ -30,6 +31,7 @@ namespace Discord.WebSocket return new SocketReaction(channel, model.MessageId, message, model.UserId, user, emote); } + /// public override bool Equals(object other) { if (other == null) return false; @@ -41,6 +43,7 @@ namespace Discord.WebSocket return UserId == otherReaction.UserId && MessageId == otherReaction.MessageId && Emote.Equals(otherReaction.Emote); } + /// public override int GetHashCode() { unchecked diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs index e6c67159f..c37f04124 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using Model = Discord.API.Message; namespace Discord.WebSocket @@ -6,6 +6,7 @@ namespace Discord.WebSocket [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketSystemMessage : SocketMessage, ISystemMessage { + /// public MessageType Type { get; private set; } internal SocketSystemMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author) diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 5489ad2bb..58e87017c 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -17,24 +17,34 @@ namespace Discord.WebSocket private ImmutableArray _attachments; private ImmutableArray _embeds; private ImmutableArray _tags; - private List _reactions = new List(); - + private readonly List _reactions = new List(); + + /// public override bool IsTTS => _isTTS; + /// public override bool IsPinned => _isPinned; + /// public override DateTimeOffset? EditedTimestamp => DateTimeUtils.FromTicks(_editedTimestampTicks); + /// public override IReadOnlyCollection Attachments => _attachments; + /// public override IReadOnlyCollection Embeds => _embeds; + /// public override IReadOnlyCollection Tags => _tags; + /// public override IReadOnlyCollection MentionedChannels => MessageHelper.FilterTagsByValue(TagType.ChannelMention, _tags); + /// public override IReadOnlyCollection MentionedRoles => MessageHelper.FilterTagsByValue(TagType.RoleMention, _tags); + /// public override IReadOnlyCollection MentionedUsers => MessageHelper.FilterTagsByValue(TagType.UserMention, _tags); + /// public IReadOnlyDictionary Reactions => _reactions.GroupBy(r => r.Emote).ToDictionary(x => x.Key, x => new ReactionMetadata { ReactionCount = x.Count(), IsMe = x.Any(y => y.UserId == Discord.CurrentUser.Id) }); internal SocketUserMessage(DiscordSocketClient discord, ulong id, ISocketMessageChannel channel, SocketUser author, MessageSource source) : base(discord, id, channel, author, source) { } - internal static new SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) + internal new static SocketUserMessage Create(DiscordSocketClient discord, ClientState state, SocketUser author, ISocketMessageChannel channel, Model model) { var entity = new SocketUserMessage(discord, model.Id, channel, author, MessageHelper.GetSource(model)); entity.Update(state, model); @@ -121,30 +131,40 @@ namespace Discord.WebSocket _reactions.Clear(); } + /// + /// Only the author of a message may modify the message. + /// Message content is too long, length must be less or equal to . public Task ModifyAsync(Action func, RequestOptions options = null) => MessageHelper.ModifyAsync(this, Discord, func, options); + /// public Task AddReactionAsync(IEmote emote, RequestOptions options = null) => MessageHelper.AddReactionAsync(this, emote, Discord, options); + /// public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions options = null) => MessageHelper.RemoveReactionAsync(this, user, emote, Discord, options); + /// public Task RemoveAllReactionsAsync(RequestOptions options = null) => MessageHelper.RemoveAllReactionsAsync(this, Discord, options); + /// public Task> GetReactionUsersAsync(IEmote emote, int limit = 100, ulong? afterUserId = null, RequestOptions options = null) => MessageHelper.GetReactionUsersAsync(this, emote, x => { x.Limit = limit; x.AfterUserId = afterUserId ?? Optional.Create(); }, Discord, options); + /// public Task PinAsync(RequestOptions options = null) => MessageHelper.PinAsync(this, Discord, options); + /// public Task UnpinAsync(RequestOptions options = null) => MessageHelper.UnpinAsync(this, Discord, options); public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) => MentionUtils.Resolve(this, startIndex, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + /// public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); - + private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; internal new SocketUserMessage Clone() => MemberwiseClone() as SocketUserMessage; } diff --git a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs index c366258cc..14af11e07 100644 --- a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Collections.Generic; using System.Diagnostics; @@ -13,16 +13,25 @@ namespace Discord.WebSocket { public SocketGuild Guild { get; } + /// public Color Color { get; private set; } + /// public bool IsHoisted { get; private set; } + /// public bool IsManaged { get; private set; } + /// public bool IsMentionable { get; private set; } + /// public string Name { get; private set; } + /// public GuildPermissions Permissions { get; private set; } + /// public int Position { get; private set; } + /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public bool IsEveryone => Id == Guild.Id; + /// public string Mention => IsEveryone ? "@everyone" : MentionUtils.MentionRole(Id); public IEnumerable Members => Guild.Users.Where(x => x.Roles.Any(r => r.Id == Id)); @@ -49,8 +58,10 @@ namespace Discord.WebSocket Permissions = new GuildPermissions(model.Permissions); } + /// public Task ModifyAsync(Action func, RequestOptions options = null) => RoleHelper.ModifyAsync(this, Discord, func, options); + /// public Task DeleteAsync(RequestOptions options = null) => RoleHelper.DeleteAsync(this, Discord, options); @@ -58,9 +69,11 @@ namespace Discord.WebSocket private string DebuggerDisplay => $"{Name} ({Id})"; internal SocketRole Clone() => MemberwiseClone() as SocketRole; + /// public int CompareTo(IRole role) => RoleUtils.Compare(this, role); //IRole + /// IGuild IRole.Guild => Guild; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 356ba63ac..73f5f0b8a 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -13,7 +13,7 @@ using PresenceModel = Discord.API.Presence; namespace Discord.WebSocket { /// - /// Represents a WebSocket guild user. + /// Represents a WebSocket-based guild user. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketGuildUser : SocketUser, IGuildUser diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 7d7ba16ce..8942bdf32 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -1,13 +1,14 @@ -using System.Diagnostics; +using System.Diagnostics; using Model = Discord.API.Presence; namespace Discord.WebSocket { - //TODO: C#7 Candidate for record type [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct SocketPresence : IPresence { + /// public UserStatus Status { get; } + /// public IActivity Activity { get; } internal SocketPresence(UserStatus status, IActivity activity) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index b7c02c2db..972ba6ea0 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -1,4 +1,4 @@ -using Discord.Rest; +using Discord.Rest; using System; using System.Diagnostics; using System.Threading.Tasks; @@ -9,17 +9,26 @@ namespace Discord.WebSocket [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketSelfUser : SocketUser, ISelfUser { + /// public string Email { get; private set; } + /// public bool IsVerified { get; private set; } + /// public bool IsMfaEnabled { get; private set; } 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 SocketSelfUser(DiscordSocketClient discord, SocketGlobalUser globalUser) @@ -53,7 +62,8 @@ namespace Discord.WebSocket } return hasGlobalChanges; } - + + /// public Task ModifyAsync(Action func, RequestOptions options = null) => UserHelper.ModifyAsync(this, Discord, func, options); diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index c7f6cb846..4cfaa686d 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using Model = Discord.API.User; @@ -15,7 +15,8 @@ namespace Discord.WebSocket public override bool IsWebhook => false; 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 SocketUnknownUser(DiscordSocketClient discord, ulong id) : base(discord, id) diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index 45cd5deec..9a101cddb 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -1,11 +1,15 @@ using Discord.Rest; using System; +using System.Diagnostics; using System.Threading.Tasks; using Model = Discord.API.User; namespace Discord.WebSocket { - /// The WebSocket variant of . Represents a Discord user. + /// + /// Represents a WebSocket-based user. + /// + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public abstract class SocketUser : SocketEntity, IUser { /// @@ -68,7 +72,7 @@ namespace Discord.WebSocket /// public async Task GetOrCreateDMChannelAsync(RequestOptions options = null) - => GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options) as IDMChannel; + => GlobalUser.DMChannel ?? await UserHelper.CreateDMChannelAsync(this, Discord, options).ConfigureAwait(false) as IDMChannel; /// public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) @@ -78,6 +82,12 @@ namespace Discord.WebSocket public string GetDefaultAvatarUrl() => CDN.GetDefaultUserAvatarUrl(DiscriminatorValue); + /// + /// Gets the full name of the user (e.g. Example#0001). + /// + /// + /// The full name of the user. + /// 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/SocketVoiceState.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs index 480103326..428405431 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs @@ -1,10 +1,9 @@ -using System; +using System; using System.Diagnostics; using Model = Discord.API.VoiceState; namespace Discord.WebSocket { - //TODO: C#7 Candidate for record type [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct SocketVoiceState : IVoiceState { @@ -22,14 +21,23 @@ namespace Discord.WebSocket } private readonly Flags _voiceStates; - + + /// + /// Gets the voice channel that the user is currently in; or if none. + /// public SocketVoiceChannel VoiceChannel { get; } + /// public string VoiceSessionId { get; } + /// public bool IsMuted => (_voiceStates & Flags.Muted) != 0; + /// public bool IsDeafened => (_voiceStates & Flags.Deafened) != 0; + /// public bool IsSuppressed => (_voiceStates & Flags.Suppressed) != 0; + /// public bool IsSelfMuted => (_voiceStates & Flags.SelfMuted) != 0; + /// public bool IsSelfDeafened => (_voiceStates & Flags.SelfDeafened) != 0; internal SocketVoiceState(SocketVoiceChannel voiceChannel, string sessionId, bool isSelfMuted, bool isSelfDeafened, bool isMuted, bool isDeafened, bool isSuppressed) @@ -55,6 +63,12 @@ namespace Discord.WebSocket return new SocketVoiceState(voiceChannel, model.SessionId, model.SelfMute, model.SelfDeaf, model.Mute, model.Deaf, model.Suppress); } + /// + /// Gets the name of the voice channel. + /// + /// + /// The name of the voice channel. + /// public override string ToString() => VoiceChannel?.Name ?? "Unknown"; private string DebuggerDisplay => $"{VoiceChannel?.Name ?? "Unknown"} ({_voiceStates})"; internal SocketVoiceState Clone() => this; diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index 9221ea03a..d75437c7b 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -63,26 +63,32 @@ namespace Discord.WebSocket /// ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); /// + /// Webhook users cannot be kicked. Task IGuildUser.KickAsync(string reason, RequestOptions options) => throw new NotSupportedException("Webhook users cannot be kicked."); /// + /// Webhook users cannot be modified. Task IGuildUser.ModifyAsync(Action func, RequestOptions options) => throw new NotSupportedException("Webhook users cannot be modified."); /// + /// Roles are not supported on webhook users. Task IGuildUser.AddRoleAsync(IRole role, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); /// + /// Roles are not supported on webhook users. Task IGuildUser.AddRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); /// + /// Roles are not supported on webhook users. Task IGuildUser.RemoveRoleAsync(IRole role, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); /// + /// Roles are not supported on webhook users. Task IGuildUser.RemoveRolesAsync(IEnumerable roles, RequestOptions options) => throw new NotSupportedException("Roles are not supported on webhook users."); diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index a250acec9..2301d3e45 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -1,4 +1,4 @@ -#if DEFAULTWEBSOCKET +#if DEFAULTWEBSOCKET using System; using System.Collections.Generic; using System.ComponentModel; @@ -248,4 +248,4 @@ namespace Discord.Net.WebSockets } } } -#endif \ No newline at end of file +#endif diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs index 68bd67c5b..ca05d1c56 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClientProvider.cs @@ -8,6 +8,7 @@ namespace Discord.Net.WebSockets #if DEFAULTWEBSOCKET public static readonly WebSocketProvider Instance = Create(); + /// The default WebSocketProvider is not supported on this platform. public static WebSocketProvider Create(IWebProxy proxy = null) { return () => @@ -30,4 +31,4 @@ namespace Discord.Net.WebSockets }; #endif } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Webhook/WebhookClientHelper.cs b/src/Discord.Net.Webhook/WebhookClientHelper.cs index 992ae03ab..07148f34a 100644 --- a/src/Discord.Net.Webhook/WebhookClientHelper.cs +++ b/src/Discord.Net.Webhook/WebhookClientHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -12,11 +12,12 @@ namespace Discord.Webhook { internal static class WebhookClientHelper { + /// Could not find a webhook with the supplied credentials. public static async Task GetWebhookAsync(DiscordWebhookClient client, ulong 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."); + throw new InvalidOperationException("Could not find a webhook with the supplied credentials."); return RestInternalWebhook.Create(client, model); } public static async Task SendMessageAsync(DiscordWebhookClient client, From 929077d71741d81337259944ea32674977501223 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Tue, 1 May 2018 05:36:06 +0800 Subject: [PATCH 148/183] Read token from Environment Variable instead of hardcode --- docs/guides/commands/samples/require_owner.cs | 3 ++- docs/guides/commands/samples/typereader.cs | 3 ++- docs/guides/concepts/samples/connections.cs | 2 +- docs/guides/concepts/samples/events.cs | 2 +- docs/guides/concepts/samples/logging.cs | 2 +- docs/guides/getting_started/samples/first-bot/client.cs | 5 ++--- docs/guides/getting_started/samples/first-bot/complete.cs | 3 +-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/guides/commands/samples/require_owner.cs b/docs/guides/commands/samples/require_owner.cs index aa218539e..a4e95cfcb 100644 --- a/docs/guides/commands/samples/require_owner.cs +++ b/docs/guides/commands/samples/require_owner.cs @@ -1,4 +1,5 @@ -// (Note: This precondition is obsolete, it is recommended to use the RequireOwnerAttribute that is bundled with Discord.Commands) +// (Note: This precondition is obsolete, it is recommended to use the +// RequireOwnerAttribute that is bundled with Discord.Commands) using Discord.Commands; using Discord.WebSocket; diff --git a/docs/guides/commands/samples/typereader.cs b/docs/guides/commands/samples/typereader.cs index b792a9269..a2a4627d2 100644 --- a/docs/guides/commands/samples/typereader.cs +++ b/docs/guides/commands/samples/typereader.cs @@ -1,4 +1,5 @@ -// Note: This example is obsolete, a boolean type reader is bundled with Discord.Commands +// Note: This example is obsolete, a boolean type reader is bundled +// with Discord.Commands using Discord; using Discord.Commands; diff --git a/docs/guides/concepts/samples/connections.cs b/docs/guides/concepts/samples/connections.cs index f96251a39..b610da524 100644 --- a/docs/guides/concepts/samples/connections.cs +++ b/docs/guides/concepts/samples/connections.cs @@ -10,7 +10,7 @@ public class Program { _client = new DiscordSocketClient(); - await _client.LoginAsync(TokenType.Bot, "bot token"); + await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("DiscordToken")); await _client.StartAsync(); Console.WriteLine("Press any key to exit..."); diff --git a/docs/guides/concepts/samples/events.cs b/docs/guides/concepts/samples/events.cs index cf0492cb5..dce625b33 100644 --- a/docs/guides/concepts/samples/events.cs +++ b/docs/guides/concepts/samples/events.cs @@ -14,7 +14,7 @@ public class Program var _config = new DiscordSocketConfig { MessageCacheSize = 100 }; _client = new DiscordSocketClient(_config); - await _client.LoginAsync(TokenType.Bot, "bot token"); + await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("DiscordToken")); await _client.StartAsync(); _client.MessageUpdated += MessageUpdated; diff --git a/docs/guides/concepts/samples/logging.cs b/docs/guides/concepts/samples/logging.cs index a2ddf7b90..2419452f0 100644 --- a/docs/guides/concepts/samples/logging.cs +++ b/docs/guides/concepts/samples/logging.cs @@ -15,7 +15,7 @@ public class Program _client.Log += Log; - await _client.LoginAsync(TokenType.Bot, "bot token"); + await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("DiscordToken")); await _client.StartAsync(); await Task.Delay(-1); diff --git a/docs/guides/getting_started/samples/first-bot/client.cs b/docs/guides/getting_started/samples/first-bot/client.cs index b3bf39865..6ff75b8a3 100644 --- a/docs/guides/getting_started/samples/first-bot/client.cs +++ b/docs/guides/getting_started/samples/first-bot/client.cs @@ -6,10 +6,9 @@ public async Task MainAsync() _client.Log += Log; - // Remember to keep this private or to read this + // Remember to keep token private or to read it // from an external source! - string token = "abcdefg..."; - await _client.LoginAsync(TokenType.Bot, token); + await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("DiscordToken")); await _client.StartAsync(); // Block this task until the program is closed. diff --git a/docs/guides/getting_started/samples/first-bot/complete.cs b/docs/guides/getting_started/samples/first-bot/complete.cs index 965b57a5e..8c3903d41 100644 --- a/docs/guides/getting_started/samples/first-bot/complete.cs +++ b/docs/guides/getting_started/samples/first-bot/complete.cs @@ -14,8 +14,7 @@ public class Program // Remember to keep this private or to read this // from an external source! - string token = "abcdefg..."; - await _client.LoginAsync(TokenType.Bot, token); + await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("DiscordToken")); await _client.StartAsync(); // Block this task until the program is closed. From 02570a4dd7db6acbb53ff2f9105357ed1c2e0a00 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Thu, 3 May 2018 22:06:18 +0800 Subject: [PATCH 149/183] Add XMLDocs --- src/Discord.Net.Commands/CommandService.cs | 8 ++++++-- src/Discord.Net.Commands/Extensions/MessageExtensions.cs | 4 ++-- src/Discord.Net.Commands/Results/TypeReaderResult.cs | 2 ++ .../Entities/Messages/MessageProperties.cs | 2 +- src/Discord.Net.Rest/API/Common/InviteMetadata.cs | 2 +- src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs | 1 + 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index a8ef5b62a..39cae845e 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -60,10 +60,12 @@ namespace Discord.Commands public CommandService() : this(new CommandServiceConfig()) { } /// - /// Initializes a new class with the provided configuration. + /// Initializes a new class with the provided configuration. /// /// The configuration class. - /// The is set to . + /// + /// The cannot be set to . + /// public CommandService(CommandServiceConfig config) { _caseSensitive = config.CaseSensitiveCommands; @@ -132,6 +134,8 @@ namespace Discord.Commands /// /// A built module. /// + /// This module has already been added. + /// The fails to be built; an invalid type may have been provided. public Task AddModuleAsync(IServiceProvider services) => AddModuleAsync(typeof(T), services); /// diff --git a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs index 436f1bb98..e4a9f7de4 100644 --- a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs @@ -8,7 +8,7 @@ namespace Discord.Commands public static class MessageExtensions { /// - /// Gets whether the message starts with the provided . + /// Gets whether the message starts with the provided character. /// public static bool HasCharPrefix(this IUserMessage msg, char c, ref int argPos) { @@ -21,7 +21,7 @@ namespace Discord.Commands return false; } /// - /// Gets whether the message starts with the provided . + /// Gets whether the message starts with the provided string. /// public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos, StringComparison comparisonType = StringComparison.Ordinal) { diff --git a/src/Discord.Net.Commands/Results/TypeReaderResult.cs b/src/Discord.Net.Commands/Results/TypeReaderResult.cs index ea475bb46..ae0f89d6c 100644 --- a/src/Discord.Net.Commands/Results/TypeReaderResult.cs +++ b/src/Discord.Net.Commands/Results/TypeReaderResult.cs @@ -34,6 +34,8 @@ namespace Discord.Commands /// public bool IsSuccess => !Error.HasValue; + + /// TypeReaderResult was not successful. public object BestMatch => IsSuccess ? (Values.Count == 1 ? Values.Single().Value : Values.OrderByDescending(v => v.Score).First().Value) : throw new InvalidOperationException("TypeReaderResult was not successful."); diff --git a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs index d52b309b5..c2892117a 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs @@ -4,7 +4,7 @@ namespace Discord /// Properties that are used to modify an with the specified changes. /// /// - /// The content of a message can be cleared with if and only if an is present. + /// The content of a message can be cleared with if and only if an is present. /// /// /// diff --git a/src/Discord.Net.Rest/API/Common/InviteMetadata.cs b/src/Discord.Net.Rest/API/Common/InviteMetadata.cs index 586307523..a78017ffb 100644 --- a/src/Discord.Net.Rest/API/Common/InviteMetadata.cs +++ b/src/Discord.Net.Rest/API/Common/InviteMetadata.cs @@ -1,4 +1,4 @@ -#pragma warning disable CS1591 +#pragma warning disable CS1591 using Newtonsoft.Json; using System; diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 65b0da6a8..8976a8557 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -256,6 +256,7 @@ namespace Discord.Rest } //Users + /// Resolving permissions requires the parent guild to be downloaded. public static async Task GetUserAsync(IGuildChannel channel, IGuild guild, BaseDiscordClient client, ulong id, RequestOptions options) { From 6769c37ad7e664845948c50ce403217a9951ef07 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Thu, 3 May 2018 22:39:26 +0800 Subject: [PATCH 150/183] Compress some assets & add OAuth2 URL generator --- docs/guides/getting_started/first-bot.md | 13 ++++++++----- .../images/install-vs-nuget.png | Bin 119670 -> 49542 bytes .../images/intro-client-id.png | Bin 5109 -> 0 bytes .../images/intro-create-app.png | Bin 52035 -> 23986 bytes .../images/intro-create-bot.png | Bin 45743 -> 20044 bytes docs/guides/getting_started/installing.md | 2 +- 6 files changed, 9 insertions(+), 6 deletions(-) delete mode 100644 docs/guides/getting_started/images/intro-client-id.png diff --git a/docs/guides/getting_started/first-bot.md b/docs/guides/getting_started/first-bot.md index 622709eca..84d5da915 100644 --- a/docs/guides/getting_started/first-bot.md +++ b/docs/guides/getting_started/first-bot.md @@ -38,15 +38,18 @@ 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**. +2. Navigate to `OAuth2 URL Generator` and click on `Generate OAuth2 URL`. - ![Step 2](images/intro-client-id.png) + ![Step 2](images/intro-generate-oauth.png) -3. Create an OAuth2 authorization URL +3. Select the permissions that you wish to assign your bot with. - - `https://discordapp.com/oauth2/authorize?client_id=&scope=bot` + > [!NOTE] + > This will assign the bot with a special "managed" role that no + > one else can use. The permissions can be changed later in the + > roles settings if you ever change your mind! -4. Open the authorization URL in your browser. +4. Open the generated authorization URL in your browser. 5. Select a server. 6. Click on authorize. diff --git a/docs/guides/getting_started/images/install-vs-nuget.png b/docs/guides/getting_started/images/install-vs-nuget.png index 64da79a9ff2c16ded006fb55e2cd89151b9a2340..ecf627d11ee9a3eeffecb25d4bdb4988a687dbc4 100644 GIT binary patch literal 49542 zcmcG#WmH>T@HYxAEmo{hD8(Vc-JRlAio2CUaf(YISfRKE_u^38-HIh>fda(~1oxmp zZ=V0V-n;Jm<*s!<-18xsJ$ugCzcX8A&&)oN8Xpz$aNgmdp`qa^E6Hi0p}l~kp*=6f zdXD0F-8L7Xp*_3R_@E=t!NHB@J0>Y9^LP)nwY8O!QaC$10|FWO_=Gq(d^2>pTh3-V zbhBnN^jmzmIec1uI8dCkS(H$VuWrUH2Zz2c2X|v*W5(>6y1FI=l7y=CX*v5Ao^Ups zB_oS^T5DD1j}d6W>{+X*sKnFL6I$9`)6;EKRJ4=RS2W*$ajD~?qEeQYUvBRnIXD8z z-*XEJ3j6r@5~ZzXWMs@CW@l%!&{FqW&RWs@<~ZKxXUxtvRga-%Y@!Fxq9v{4rQPv^ zMsSOEEPcn92Ieu$`<9*_XEU;hw79uDPtdAQX*syDKO3OOuVAJ1#vMMARUc&dWIZix zJ_}oDX_pc82w1C{Y|7-Q)74o5FcGd%WFgG{1^mLkSBxL#bNlGe06fzgH z52N|gK=3)6wf+ETxk{0{^4zeMk%O!4Y>q5qIp0!2wD)xX9Es*N{q%JDbhwwcFxYkW zH>7Kg0X*u%;d?gQnvv0p|80aNcl`}y@!j`n>9EOX)*3P9V-+8H1P3v*@c%P#A#W?jNV163m+PR@$_V;W0s2jCZ%WPIlhJM%lUh_Rv zJN>6t)Gz%{UvxS2HAfJs(9PM|RvcV1!OYdPzNPKGU2xkhvuBo~irSwRU*&*KpQwZY zp^xf`e{g&zT0wq^jdT3zS8dXdNNuV=%{J--xc&ECq;~d_9H?Z zEeQG?4fPkQ_P-hbCh4E{0hcGTIbt1+S2N!ObUq(}3*?d1Z56^sdtRyKetGGZ~A02Iu9LXg_ zrkWwyLp^%x(@os->nRHwt+G{owgv4VQw5Fur5b-sC+ zOvSarpx?GWfA8(EsRDnfFagO7+Zf+U00v>fxhsCd)KXul}BEOk%byyUbqUY+KxFS6a; zrVVXN9!uvOv%)J7y8X6%q|2)RJCCL6)1yMFHJ=RU6E?K>n#lfr6*DHgFF>x%;_NHS%7)>{lD2kW1}@;FQSu{cQqYu%zQtcK1>FVn2(qP zD;`Roq4_XaD*Plxo*&Q@f3g{zn9%z$BOqrMP_3}a$_6T>Mmm<9Zzn>dBaYsoL1Pqt zQiBZ33AQ2~K8O2?=81=jRVzv+I%Cy2j<){ja9@xxoQp>K!u6l9z7T;HsuM$PmIQcb zs68l3RTIrFT1E4R>pXvBBsF6LJ>sjURZ2wttXl;`SFcaCIB0qG`B5-Z?k>1p#P zKNZb#Td%0EY^%YOTjU2j1S!xvyj;B{X9OCW*@kncU>u7u8U_Ut)r&9CEEKxkd-YzS z${@7&Sc#Zum1K#SK|w5a`Dke9i?j$dLQZ-DG;{_O!TtZ4Fn)t{UIbOlmG?Yd?3Dzj z$h%Fd&&=6`WX;I`V?J`XstIk0#{lGj@(81Pg^U^~^tS-6maCdEme2P!YGyL9wi2a3$!P4nz z0kUReH8a6uPiQoVN2xbrj6r>Apvn&W z1ajz@FQ?5csN6N~ENQAk(=0Q4)A7+wP;FO!xNDR;E6Qo2Y!|hHZk7R82#A%z^_xKb8sCe5?L@NfI2YxalmWYcz(0 z@YqKC1aC{$Onne5fpbtNs3TfJC&kB@;MKg4+1ljh*F9W zrBsiYSyr223WWG)Sw+g19O<}@6E#FS`;@T zI%HR=kEh(ASt;iNn9uz1vBPJ(UIyKlmDX!8liZd9-b;BU2FRi&5;KWCRON7(Q|rHX z`|hQeLY+-rs?LtLMT@wd^gq6co+J?ap!MNm<@UN8fY8Wzg4sh!SgyX@blblzdBl{XrsYk< z#QFe7e?|-n>O~c~|6hx>&l78e!*;M;8ym`3BEhb&3FZBG>h$2FNkPnVxz7_%?m&y4 zQjnm}Z^)4rEQlF$#mBuSa`{JaI8?SzCFY)e4gL|%l-#`9b@FhI9=-gONFz`Eahl6-=e$6U1oL#b zR&r&0O2Bj2(`m)PkOaYNfboEy6M|3ut=I1r(Jo8-i`gpGF^+vgo!=G)iL_j}?z?d5 z_IZnIMyv;P{~qU6_lu^BkmcE-6HNaTWKL=A=v@G14`19UBdEVZk7|jV(qX*kA|9e> zQ`i-V{qfh;)+c&oN}YcxgKAXQOwToO|u$qykRm0G{0`agO!P5!iM9nq#hID&Zc+GZV+c0kKQxPqjY>+ zTVJYqe?QD)vxh%!_r_KnCm)oxa{UE{SXF*$~RBQjHQG4AQvz+;+*J;z4xHChGXvC-Ady1&5a60p3*Qo^o&W+(2~~QX?=D)<;!1#L;i-d!BHQM{+$i znE!J*;>w5iYLN-}ZvCn<9lCDb&I^$e7Z%PA5SJMahj_2J<}@i(K6V~8TLaB6Gj)%L z1z`P<51TT4A-WZM=8S`GVjG%9jfEq`@u zq$C|^gdaK9OUnpR;Y%uK+Nd;yg{=3T?{qMkJMh~_<^c%?GdZtgSyj3Z$4eQxZuP;Z z6tFxJNr?aqWBpRb_)DM4Q3qQ{3wcGnX^U<~V&}NTu`WMsbT)N)Ejv-Xl51+6qO4w! zt+*EV^Ez{O>NXg2h|!YwJt5Icg3b-ec7toy5VOn=3Kx{GGtg(O=*u1ZBT{*^5F*6K z3+|lre}12}!&|6^1;MA7=4{5f0`UzFFSQnG!YtYpGR;#RtWsW%oN-UQ(pN-;V}{pH zeM0A$KO%G0IP6%=6*tPA-wdP!y-9^kl4-3MWRt&mZhI~y_;&P6$1TrZK<-Z;rN)8T z7mXp<&xl-EUcQehosKquK%xHNu5n-)jL)mhHrby*XYDdZB%3Mzjdvxif9;y+`9?3B z!uMf1@cEe>f*HI#(onaVB7C~|byP3>#f?PE<{P#YdXgQ#?tp)@VaFaYbxFQ&FhgG( zgeKo}3k*z{KaW_%c+59xBOMPH4q2Y?{|!WP><5H}!51Gkrer}6`)GShK%es=gbV18_|NxxQ0T3jR{h%#&v#cvtQ=?&R6Y z95Eg@Kx_U_{luxrb?5mrooYJzxa3ZQYm1j#xE&|*a+7o^k0V0qLsv?OuLu`IK$idODBmPbz z(++D*ZskFmYm*;BS8S;1ZNOZ-Qe~@@3;c^qz@?G;12>uDm0+J&Zv5~h)t!FYJ4P7s z0C^JECx6Cw^VLKqd#UEXb(zUtKGVz25ayQ|f*xzU) zp2#8M(x_=g2O~x?-~OM4djhY6x5dG^5gMj4CHNb&c-PzJ+-o*)5bG?dbz z&jBbq@|qFxe^%?lCxdme)jnF3MuE}7%hz1|XFFOCT`OTKy2UerSl~t4R`a_P5t+uv~oRq}9oxjMb|`VMz!6kVAu^2xvnRdp+)&x3Uz?}>MeqpNm%)!#c=U`_AUS_5N!WEL3w|*&#Jn@}m*le%xSTmNa z9NqJtOHP6&?%F{+LN=4<&0;Fb1Q)_HimXbKO@x7Xsm;c{eIO)#7^U=22(AHP zV_;2;BfVJty`*r0b53KkwMKJ)?_Ovk7$@hA@|Z`93NLTC-qr7f4iW2be7qiSMo<4_ zBB6AC#7u+A*^ZnX`M<~!87~9V?ko9>X+)EGVjcp@*g-6CI?zmY-|qQ$06~BWZJIkq zPR6#jVnT}Q_N`joN8BP0v)XPFx9(hUvx*t{dePrM(t%i2$XB=P`SBAP{0D~ZU(Rtl_m_bs-+Pn-kX2oMpEe?8$vgfYhetu4 z>Wc`zcPdeTX^TJJrMxmX_dl`-&-*9}te+%^EOhj~?#WDKSIX6vaF#=W-#0fqBx}C+ zeXnwSf!4A%gZ&iU7b@lXLkU4L8P%)`;gMBa!=cMk_N8@56PM`+#sig8(?a1fnYq<% zo=nZ%T@M?E;k?%6S9|Z~)xXaENZ_i5W4m4LDAOZTK@YEqU^CBby?QiZY?eXC@x`l* z=UzA3Yp)-*U^b8JpjXmoMIEiCAw=O}*Kk*U*gYiSQm+ucwn`A*PsVg=b=LXJ{-I7* z7lP?;GE$h(qAYpcr#?<*H&1y6sETNGFselxWm4-1>%lCmOA-S@=DlKhaj+6UYA<{< z;XB;@%I^?X|1q%>R7n(B3`1tXN!5NID}UJ~QeyyxzD>&NOVoFSC@QVmL+%lVRHkaz zlP=Byr(4_4xSUrF_cewvW|hq{5vr9k-CXpL9jbZHll-r%KES0wgXDPla{bx<@nn7j z!;S`qXz5?ka(H4DMa_s3(>D&y)QUzU{Mz4Vybwh_g)tKd`-`?l2dmc`mDoD}cimaER>K>0X+vEDl_ zkb&7v{fr^i?ih2_G`D?nr;C@Lb0TG;rQv236VAsA5ZlnI0Dth%GsdH4!1d;v|euTQ^ zlMRiRd7uu`f%(j~Qymv1ZwH`9p2_G!0=wIeOb(}~qU&ZWybUozb_&i4ZB&5-+y4w| z;5N=~ARPqWke5_)S|?$C-rR+E63Ym*#0!=e$@F=?nMxAO+w9j=8_h0t@rM)ic8M)% z4-neMSBWn4dHXb|VQqXM)460+;4ZNoo;N;FZ$Py-p50HT75c;kdKlG+(h8QSiY;!L zdm^{_!+`9wJ)CZy_%bS5BaIkBoDF-y`p1gwKKT4IMoY9zb)>y3i0k6|MC@lBv}aH1DkZ6+ee_{VwlQ!r)+k=lazn)V6=U3q4b0qutN#AjKpu8y?H2w$ z?zHXi%Cw*d=eUn#1e|51UQ`j;&I+Q4O#~+uFc(5XJe9i--ZeQiuQ;1K|D{sZNf4KB zP5eqSkwcb5OOypC3Z0!Aw9r9#p_Z|NdrKIN81>h7kH$^8PnKeJi9h;pH|Ix{1Mo9) zm^#<@tJp!4E-=d+(u8XD`D9sO4f9A(+WinW5mv(L^K}<}ec@UjLSd`4&N36m&QVs= zcdg5f4hcaK*`ERNl9Li&=Y4b$TN*I7C7%46(>0laB$`v~kCvvj% zp6x!1!cbP|GiL`0KL{97?XeRvU$6zgm3h19W2HGbR&Koc<`o`M0Hrhd%dGG7iO#_A z??OcJ$+ef6HJ5egIC(taT5{}oY+~#&c)@vRm2sj77Uk9(R~5{zbUYk6S8knnw1^e>&l{gXLKct~pc=xQItk zptdJpwoyyi2)IrNrhgnfWVCl|*PoKB62OTPMsYD=*Buo#6tP2#i%8=GeL2K@ZW(;% z+cG3@{zQruHDr!rxR89KD=96hzYq!c{hk4r z@1*QnR0wcJ4wLi|C)}lX4KF{R=QX&u_Y@x$mi>{a)hXum>tiGHe$iA!_gbw{H#tPP zI4+AeW>*2r@iPKKCh7GFSy8^_T-^g|2xu_%Z*nd8F(%mtAZVWyrE&v5#fz*+rDa1f zA8#PxtMUk~vLXHLJe3uud@fzku36~(!U=@1#@X!jS+^yr18y$a8)Eh=X3)BS>HN>; zCH=^(4^6s@H#!>IL0|}G>`RdV(4Xw%cm-AJhv~wAh(jO9^*tSIDTb;|Dfg#){IC}$ zg2@?R7wume^Q=QYBpc9bkewKx>S30guNHZJ7~Fxel>GdKT<*=Tiu4o*viRp-nne&W zc$ON-CBAapx!)AQYI|wMAtj7zG58*W`7q_)?o9;=&yk?HwceS+q0mV9&H9<{v49hI z{IM1ie`fWm#$6&W)pjcZoN$CgH4>~J&WAgtnJ11ht*YcqP_%ri=r-sV5s;vP z5V!(aK|suiuBS>ZeVnL5z+{N`F=^zax6VLDg4Po&$o8=xI2w{Ri8})3PK9^GiL9)( z0|=rq6U6-^>ZdMgKdBEk+S5gUwoCURQqBGO{OJV3B)?X+1-Z92p-L5h`u^(C6rrrc z1Pk{lP!nHxrcw_l+x?}3P~ps!=Ho_3_6)$M9NK2w!?bMcvfwEpBL)xDXDXs%IXWdddFt9Gj$iNKcM{ZAH|41T~8?T>Chq>w`2q2Qj|{tNDlk4BBsyp znEp^_9PFX9FaQ~UMJ>9ijRdbdf{PSXgm_CQlvR^z3zL38k-bWi133i{eMWlFPWG{X zI6R+K48j#%2?7cV8LHp|)Qw4driMx|E8$M>wLYhj!z?KvcJtRWF1|qk0U=0Q;OlXC zN7S`rmfRqrTiEnMmmr4yRlW>4n1-+OI(o2bRI5=sE; zC6*9IA64?OR0u{$-{8M0^-vzd6<$?E9mEI#(6gUCv%&(3nWO`a6s|qWBg%kybrEzR zqN0>eY^9PqRW=YbjUK?uJq&S3p>x%NNUMiN31+h3IT9bZWl^$1bq0A9RU`)rvB{w7 zY^=$|Jh3ckjzd-Ll(5tyB0X)2JN;zJGI)xz(yB=5UA-&Z!MFRZq7eU5&0gQv>PX324j-r%F-m1A-B7n)^#(76egj)A6 zEFMtz4-43g`<_ijfJpTt9g_c-sPIuqm;*^lXT3SZdty&g3xVeL(Gn5#NAP~*4Nf7j z-!cG6j5cT|kxpDykFGqxxiIxwl4l6dv6WGvM@~5=4{7DdPaH}e*rAXalORr$D6X#C zMys6?Z&$Mr&@mJTcMyh8xZrXAi2O9J_bHwpXcs!GkdY~xs*lp6q%!L5I#dzd4JqP* zPhchyW zIbJiiBs0IV$Y&*|>2Du&DT$4mAwu7o>(-J=zh_4zCqd8ZAZf3OE##fr80Aq*_8mQF zKKnS~J|O9XI*Db}M|n*Y45|N7@}ugbqqxA+WFPDWl>?i0sLG>|pLfhRZo*KDd$*%cUr};z5Ic zL5ujqkmuAME#64?ALWRmF8-fXGD8nz)-gSSe}woJbQmhQ;{3nvc|wtCXugD?{Y3NO zvy!{km%l@x+VS9J0-=>W)%s+T@_n3X@crXn=Avrp61%w_m)E&jRl8%)jKjW=B$e;y zBKs%832+|r4N_{tCU2ckTX11`F8O3w^5bhphZZ)}bMrX2qF@ zo%WBL)>EYL3~GytBW-3l%Z4=WT=bT?h$PAM*sp%NNl!P|=zoGDm(~(1gwZMyM2M+4 zbkN^Xee;=zOXDu1FUBXIOPJdE;r(p(U4OMh?hA;Z;RkyPP|Uwmw1G(}fib1G+oz9X z*I8V+;=Ab-BEzZW zSl^a$*<5fBIS|wF#^w6~xcpSgC;QdJB*fOVnHHLK{^o9JQtll#h{?kKL^EQRwXvOL z1nihc>zR3HUqQ*?*s4+!@4(t#&RC4qh>&sBAM>@ZlJ>281`?cj%UN7Ra;&AM*JO$L zp{I6HhyE~%S!l=U^)^_Yw0i%q%BEIM7q65=V^q~@K>1=&ZZ&>_)92?u>!XvU|8zLL zK=gM+N_)EXT4&ssq9skSOOALiJ)@=Z&DQFU+Gjy;RHZYre`CZ0{0#1Hr!JkvT8HGy zERZ5pgx+k){dhwLJl;>inTZB!7P}6d3YxR;qF|WZkP$0b=tpX4lyVJ^Bz-r0{_*FT zzbQ-nB3@c|*7QsYnz@3$8n>2s!KxEmLY{g2tDoU%drnA=$Gxk0&&;@yQg?SIDqzU@ ziCyM8dcarf8zrtop{c2MM-FM~3xWGLP+1v*g5r<)mw~kLE;(ia)24eR1=(1Q8^5%g z66SsDf)mnKNmm!|y|@XXWXqIrn?DxNb3t4Nm<5ziMf~^dBMJw_@0qG_@pu2^#(Sd6 zyqARGussS5|7<)?-snR&;a2A00CTevSnI&kCI+AIkI00Rw5Aszr44M6F{)ghN;j~8 zLfnG;gZ0%PJ)VLA)?c2!!JX4;@*+5mBk~c2D&O}9k3+;i^M5q_?ST2*@P=`R|9poU zJ~)3Ia%E+~D4k#rHrLmUUtV${KXMbyFFkm1!`tpFLvJeiCnm2~5FfeTAHS6Wa#Sz9 zLYI6FALofH?jcXH9ezcyb@q>jZjHbJ7eV1-rHcj6H&vOMR-^kH%<6DCUOD@}(CDJj z%>56Vc3d|eg*2Ss(N+HgM;1 zZhlK(S_6@Usnq+?U0>n|_@~WOa=JBkxb#sxf&Ri25V~@fI^Y&k&Lha@KU8JJ(zeaW}#Cz}5SDd^us~TCu0^Y=!NaXbRE+2B5A)uq= zoD>83$kKR4fXF)PaT;ggx!T3cPJJPrYaT^o*1nnP8m0c}tna`Njk?Cr+NQIg+}%A{ z?x69-`|fz0s=&V;fhl3Z!aU;75% zpN6Kl12@#ipe?#Nts_86HW2SfgBL6^eXq=hAW*+zj!4UXNAZ9O2l69QpFoJpCG7?Q z1RCU`C(*mcXUlvJP6(pqTJCr$M-6?Os8|CPY7ui~u2w@23xgJ#!=-SWXc4{th(H&y z_YV!efgtcK+pX8guUB0o=DN4V3#@223<#G26Ig|AG3g4+(Qlf0gp*!+OH`H;x^#rN zNnIRlseXPN+vO

u>bzWfL-GfgbT5i=IHSGX>nBmG<8~(Kkk!H7+sK5pmE>^~9F= z9lKJ;ArE_+Grr9QiT56I)Y(h~R_yEunk;NYBLML}^%s3lMtnjsG8FpU5GT5mrSI*Y zm|JXwctYtZH2P@h+1;POKw{884MJon5eTg#^1dnrdPj#q3lK_%MmtAC4&{cmE+6c#>Z?X2h-Jr@j4niHfV7zH(iIN zMlkgZUU`%o+cci19B}>GTZK*mA$8iSBFW&=NzvZI%1^V*8VfJwW(CQY+daNVsNsIU z`pR^u09@Z~` zXUen70H)4^a$}2AmAR0)JEa#MCsPXG))J6I{I7FK#c-BzzUb9+d^2f%grQ1q+qP!o z5alPy-Phgurj++{()oWm+JBP`KSw#Tf4KL6YiklGxhK*TJY;6dtVQIJfVb#pKd140 z%z=4LaEg~Xl<%lNH3VJ5-!6qr`rRJAlP}lSQqAQ!xmxG92CB;exP0|tA?&~>@NTH| zQMY8{yor<_Of0H^|07C_-5DYWb8ihB9d6+Bt@)sLa4uL545$S!Mv13c_oE}mG(N7lLIaxbnH9ysc z*pYa5ivHKgV=z40!y)-~{C{x~tXMS!9*vm8E6M+H)7KJlVF!T%*_i(mkRb85RoeWo zhoV%v!%%yxC`cFFk1aOu6=2PE{tsqc0dw1q5U}PCMj$6LI$AMy5jmhq0z4^$A0$ep z-KT7AT2HR_ENPDC)}SWl0bmLfz}eep3pnscT&XOq8i zFNP&Ord(H{(5FoN{mPA`iE&UA&lhDXtDBD>WEYrSjpbnx+X{%mppsPm6%E+D7`cg$ z1dr!!QozLVaFTkJk-%L2{-3H+WqXqQwN!ETc<`Y-fJWP_pxG?jSu>R#LdZdC z(NkkhJ$Ix#;S!Og8{T)(5Q4w(W;MA)Equo|DtGKF`XB6?h+WnS?{7W$0Y%1#{~BwP z>sVzMgp?`eg~}Me+&g`641)X>2R@xHGv7_1@KM95GG-(nTAx%Q&cY34@+Jw&KpJ2k zBoJ(WST9ldwl}lKonAV&KH$pGmy*MkrSDs24$1F7wrcU=htqMMH}4>&Ila<&Dp&;L zb&wW`Ot-Otug>Li?f!Ga@>cL$K+kj}z9`gC&$7{&(Td830^OH_sRWQ%)EBAy>yanT4m z>Ospi{ZgxpTExr{8(CPv+n#joC~i)nb8#hS*5@@1e_IazGkYPGFD2?8H`S zX_Z-*B{JLM6RF|j#7v%Yx9vA7JcM7MG`2dvAPa6Sw4n&YJ?ce;Kn#8Ve+Xj+q>S7> zN4Bfp4}F6>Wl+6Zm>nr$3yp;r7Y8ueLZ7>|?9O&CyWBEN+v}+g_GYuJv+xZuX8Y^z zFSdx4cm{@+bYW*30RWY2%Yo(3iyxG_f{|zlg z;N+a3dz|%MFX;uXU0fa6^Qoly-Wgf@hK22Im2uRQiWJ5~g69zr1C7fEJ0Z6#2|i zY{t(uQqeT)9O4-Cx;jd;H51}DHY}iIrbpt%fD7A{UOi(274a%>O@5%5v_7lOSWh;@ zX{FR<(S!{{WMM)%o$knQv;L8Vn+iJQf)11QdoUi*T!BHd{w~bEk<4sxkO&q&P6t|I z7{O-c#~^|QYFlHnwTlPDz+ovEWW|9@wh_49D0Q1;zpyAu`Nz~4ZiABoQ#1oW6-*1D z|0Urb>x@_zHQLyF``Diz#jjAa zsDQEo*TPHP$bC@B(33lAdQ-s1tL*fu&vH3D?=k0Zn2wT}VvY4H^ci#dDlI9nOh>Xj zkZYmLLDWVv3!nr6FXhMM=b7AjX>^~!ItAp&kre3~?;(7Za(C;KdZe;5NHba%ABmDv+tEz9D3iS@HB}Hv# zxjGUPRv2pjJ4}8zH(@v zkr#6RSZrLQ&zf?yq*x;?ObjviF+mw?5gDL)ycGj7;2^cS#*t|-p1NzJH3jVYFkOza z*do&Z)zX5tXQwaEmCo3aT{E=E!t^U)F@3~BMzAByRE{{6C;TFlZAuA1^E~}0jR$zRbV$=R)PBnfrE-J$eT4JXIZ8dxy zE%@0Sr@%6Dw>ddL>)TxiXF4XzMz@eV!rKM}Ubjy<3>{+S!>j5e;3@siD^10Pgd@#< zt1Dw9EL;$A(v3G4wiBwW5@9(Ircg-B*lP%8cui^W%H7fhiEp=3oW15B8?`NF#>Njc z{*X&SX+i^io!Lz4_klWr@iw>qp8V=EN;iW?gQ-+JctGbw2VkJYF0w#{j#pup%p!yT zjN?_8G~rvegw6}aEv?k>3?K7*XzIOsu0c4Y1$qlnoFwxX;fRXc=+6IwQ(X9lT<>D= zeKIcf27=Vk9I=bUhiU2xZgh7w*#NmzcjG6N&4eC)_w@0BP`XH} zWYOOqrS&U=9F56TpdZI@$lt zeEeJf{C_?}kbayCF(K>=gU161C?HZ0aYv;`B}6S95|u;x|A9&Dw?k?#OEPqpf4FRY zWf5=8Na9>1P6#6cf;8m3HacnHxAc3=R4T{wGG`mF-QwXBAto}MTV~YBHu0E}xgJzeqYt}jzMqf!3FAh`~L6BWrOvC)8aM4>Qr3Gg#5BFzX+fL?b5vtw^)P!4P;N`a=2fvXTYUIR2~XwCLC6`}9a+f@-@Z z4_aeH!KXZc`t+NXq4w$*hjx5#Y}6VUK~k)`atQux>`M4(Yi5#mWzk4!?#8{x=&R4+ zsULu=xCgKl@!P0W@O7?ajtn8$wEUaAR5j;LI}wzQu^)cNKS!)Z!rVLRMO6X4e^M-CEFHV?O5Rk*{!NGqh@WFP>^OHL}ZVQQvD9bjh9Fd(BUVvprRdd?=*9%=07Z$?~kp_c&5>?W5o zgUsmO!mpga9uvgLzDP2P`Cp_?scpv!w11(0HTd|3;yzzD@~k*$Mr{n6DQr{If>hUR z&o7@w-Et?T@UtiX2RpKhed9hdD7*l2hoOUEKQXWp4&F7&rwOSV{ZGF!aIuNS`F4*7 zmks!zGl8LTsaET+Uq9sPuui1BkIdr&xpI3Y_lkR#B;zaJN^yax$0h7wmJ;c#ajrw_ z8q3n4i-g}hWvjJf5{|Swq{Qi()F@=E+~H1rXZPQ>vYs)$u3gxYk6k)DJDlQDr}6Y+ zk8^HGf6uODf`a zORRx5VEj^Btu;8UYI{i_k_7>zNoC#R5q>?F6yvHQOyczOi>g_+85amNB)66XW}>zvxz@Rqz%m+|a7+!7YhFo+@#m7%)5qLLrAj5h+Q5P8cLys?I!uS#8-wJWd5q6r8V||# zyE+RX-suH^9!ne5@WQOrWBc5?pmb>s9?zXYaaeSXJi^6(;LHA9Jmcb2pzmZde2c-5 zPxZTZ2>h%GIzWPil-51u0s000Q$UofujXKwv&`dP{Ww`!rmP>kmaG6m)|xjpx3}U^ z2f=$oa*2`4(8pRnkv2)j$=rD7-OUr&j5b*OaD2);;L|&?8tj3g!toOZRJdf-Fe>Rj zhP0q9<6?Dry58SU^*m;K+Y^AidQW0?N&oJ7z)4n0qJnnv3F>IuJ4M7e564nk?sCU9 zdybSTWaq|9a4KjukQvEYc&T8F#AgK+PXVM#`mZM4w>gZP9UqGA?@+m7?=A0ji2uFJz|I;i76?)(3^v@jaOrUShuqm>4m=|#(PTI83~|GV-n zEdFn!y}Ks?OTctdQ)v}^G)sT{3T43v|3eW3&k(8*h^_3vLT+~xCYT=jVudw~i)!_WB6Qe=-3K$flS2`617&|dq~ zvU3QD|G(1IpFK*&_h>1d)7+42YL1mI+g}@lsu8olLgm+s=gnRs@7uApGc3o+w1~M?;TT4$sXft zXjB7k%U1E&kzY-95i0$K5bs180KvFameWGuLsVKU%oaURnHLXEFEFVTEb6Y2q^x5f zrVzl9woeM{S9IBYS*o*zu>v*ZQdS=~At%?J5z%le(0kQOcZ6}lt!&iQTFb^xjB`!@)6xvx@l{q_DF)LSvazE?b;<#>!+fuK-%L-qI zL@+i_0>Bv2V)>f2J3|ZSqM$UOEY0;>`l{ae{s&p&Cf3|ilrn{2S#o}@U*2rdk=a$l z6M>grrkuJnWy-5x`(q3tiuiwMl&Ohbh?R|sZ&+ZKyEQ7Sl@t8@niVpreEHJ{MD}sq zrA-KB9gA!a>39&*iTk^;E3e*~isV;X6U!Ehp~{$_<&BP)WXOAjU@dj%hQUI z<2pOip!fW?07;wm?@0eWPw?qYN+-fxNfIHS#~92K&R8bLsdbFyh@(>^@EtDi1L03; zD5f_%A1u<@qFxx1|{%cHxbS`_x#U>~Ca z$W>8c98)O=tSN){)O!!Ns>!-i4(IX8X}qJ*rF*9N9sT2U4po?AjtJT{+BRoOD_X>J z?phag(ztI&WsE=A?X+gkPR_{@@hM~G5+aizN_lvE?^B0pN5s+ubcz@$N8j@R*Gses zm6J?_I!m?b!pZ3p)d2`XSvYZ|_y8eFfPhC%a_TWrN@B^4pyTQg|5Y56N%TFuMpc3s z^j0!a-6H{-7f==01qQEH?%o-uc=1=weZa}Qv7+u{X!$#U8lrIO`fh$d+AM{xGCo%TVgm>q_}Lvy{BQ#pNerq&qBk2 z$%-qsx&>rxXZ#V#!$+!Dn#fZxb5llY!NMpuk*dFk>CPlbI>*10*jq^_1X-Mtn5e#%z;nX8m_Y#?Jh8&XMVanE%e*xY%TNhN9*)<5Qy} z0>58ui2@(iAE#obtcZ|EJG4E&AspPM3CQIHcL(_WaGk1B^h13Ro(`Tjqf(P-B^4@) zC0r5>de+MqH9g=#5&486-IxA52`{A+(hhgx9Ls*NuNAp>yDh#vVA3-P+*?0*2eG+_ zLzuc6;Kd1Etp>mvewG#A8`=v=`OJ2TD_@O-*uMwb?akMxc?JGGRi=wATkyq-|f^+#l(St9K3`Nh@%Hqn7$W1>sTJo$K{4BeABW~R>&-r z;K+c@7snEJD+4opt%AV=D_-o|YjVw_I?;F*4(up#1RlG%wl>1_mSo*+paO8}g5A46 z3Cz>ZMAsgtQF+`dj;x4}X^QVI(}0}k9>18 z)@v58)U8>m@qaP)*HLXXU;HQxrIg~7;!a6`qAl9uQrz98(Bc#;5}e{5g1fuBLt7{$ zXiJL~hu|JG5a6cI_pbN%zH7bfuKU+HD`)oXJ$vSynJu54oru}8F8sP(2;dm!XDNPV zQcsG42`F0HcIPDRabiEF#3Qt3FyeaidA|REQ%Az>5fk5#&`9TNoPu18`*vMKt-3NK zNHSnONsvuA-;m$oPY>V55WP`v;Mx0jxY9QUWrjM%E)t_z(uPPYLo>KpQV(TFuM*-J z`87EV#}L1AugI{@NCn}}-eS-tgf zaeP}lWuYu}LiRWhajp#_zk@Vi!&pHg|H7s-Te+XiDBkgmDJ6|1Xj92nSLBxVoKR&x zs{So;Zvtm$+nKE1|Mb$~I}S5a!67vE%>?kKSv*W}gW5zU;Ytx)j>ggBAWEL482GPE zEWn>ba8EIQojndWuumy%DAi^4Ro!>IeG4RNfs#=I!O~2s?WKs=%N{2;Af1Ovd4%0eIsL3;vMF>bLz&w0@+{VKj(*y>; ziYYJaclompa)VU5Hw%En!XK4CtfpIK4#YGrtr@(?UhJN1jiW0|=-^0&<}rUDVwBN& zG#skpo5m-F^iOTJ$mcs)hhC#nNfT;@TD$k|WR+iNgHa@@{-Kt4Fzqz|l}#M)S^&%< z&&4S4TUHi?_*+VmONPhEAe_4fFteVHb?DU*0l-iAP@2EbMKrA4*IkC5LVIlPw|%hN zhQl;5(OzRjOG?6?N*={2Mu04&UzWJU7_lVZ_a?v%UGL$C1z<91+TGb&hH#es!2ltv zi-`XML^AKH1P*_EIy>eo7Ni}H?MNg7Of1NG%bIxY{|61=Der9=^|&8#eoMx%&GX0! z=)L=<3!J6@lXN{B^+NwUcgcyn)Z={YM{z|yFF?4nu9Q<+dVVGdB>$@yctWSWcji*e zEL{T;LynP-cACsj9J~DZ*mu)es&%vfhKr;d+?T^6E8w&dG4PNSV86#~ju4wL@4UG0 zp1&mrMzvCR-3&wwX$;V~dRYJ4_wZ|bRk8l188^%tci6fkFWL%r#ynTCC*z>}lhouC zGmm=Xw&M@U5MM}f&g6T!BsJeG(Col~amV4=T1Rmdi|`+bHvTvBgJ=gSKm$UI+^zD@ z7vsA2pQ0%~zUD-yjwRHjIiBGVpo5f3flEq(piJi(~ zg?cQQEd(m8{)Wan88tDYSo~61{#T}d^O=h+V)e7lL;}$E>QL0LNiEy2o1X&|=h7X0 zL-wGJppV#pX98oVNp|s~dyPB?N*0YL95-+An00*~Xw^msvuhRV6OJ}oWteX&0i`d0 zb~|R}27$cCCp>i&NN81Ng}CU$#>}+;_VUG|J--xk$LNj@`VDU&jA}6}hJPP1##q^; zwxOL*KvB5+P4w&xi#RNFHDqepb)bOHh}-u^ef!r@!4Bi`RV+tL(Ow95^mV(!!SVe)%pECu%y z{b$yc5wpv)Y4HBbf*i&3Ls4b=1bM&N!?d*FfaSE%o+;u%4kL7A8<5&F9!{)sbY%22 zPLATJvm2=?n#>G4^9q2Crl(A&qEg<=p^XMb^uZGjY41F7unCuKcR}exa)8sWBf78~ z=6UV%i~R$RHJiYw*W|m>q&3PxEdwtNI~%__PaZ94+E26l*yRw0(TFGrGE;!yN_=y* zbF_@%j7Mp-?=*u=dK1$5M>&)qq4Rd#r_%iqxI*P7o2q53Cs24tic@8tTe>Fy37f#C zShd#|z1_*>mr$j{Wu)Q-yAuotd%NO|XXK74rxvv>hk=&MXOMP_VLjqa6UY zkg2;FyIA|qw^jL|hQ{X|Ud??Cjhnje-?XY1pzc(~JVcF*>}&<#-M}Q1WbTxH&|>iO zmUW7cY17d?5Q5Ymt6yLUTdW9>7uX6+O#bw;;ilO^Q~i%Gr2LU6DRK(5XsStnC&5GOVlo81rY5w7ZE7zYfUVowpz4pPsgSt&X)nb&qqg}S_*tRgeK z`ymScA~+F^*X^Z;p;9d`UB36Dcq3wZdrP(Y1&AbY@lDKzIG=#SS6t&xcio?~E-7K? zC?g*s(z8cl>elEn&^9h9If;S6YY zl8ra8m(fKS$^S3;A^KnPLlMNq>R(z!1w1?BK-s*{YKZmcts)uFqXOQn8UYL%dYp&$ z_%><0fO+prHw$~aKOqi`3(hC1=LC5}o*-S729~u?8G-!|bC~9@TLD zv^hk7K_Hw~@dx`mv*U^6f~S=rnV|MNrsL<=iHDcE7s;Cfft^Cu07)+AIBmoRP*O}Z z7kg@p3c?Wh~lE`E(+7x?`OPo4wt;eMbus zrusDnGI_z)x5DFJI5h>GHM@~U^cCZAQxQBixcE)q;^uOBM6{u3f9+aKj)p7+c@rOE z0fV#1s9Z7TSnC?NUP1u%T+bds{(G4YbHv<$#%~I%3iMc%8ZWfQ z0;s?}+O;~OtotlL4X8FXCx~(OrOgno>JKG^u;aXJkZk{A#9@{$LMd4G1xW9M(q9<@ z&sDE+xgTw6+z)$bsFe(N_RWb5?0J*!kaFMmx|pYPoBU$CHQlE%Zkf}e5MpTpr5Twq zhp%?3COb0p?&GxeE1|SCY5_c;cBNwDOwqpzXqcT5m;buw#Mo7|fee%}1d`96BE}pB zc^LL@)QJ?r2$sSayj-CPmrY{oa6#{|n6OAC2C#p||G5d_g=|l#-;2X!|GWj2LBs|X z8DO#*@FcM?rT=b>4*oRaewQM{_~{sC@{J?j@U5ILPJ_lF#!=x65A&NZ@%*@7JXe|0 zC%1c<^We;{VJsj;RfHk~X|fn3au6N-VQ%Ds8PKGHAUOK!eMC4}1e>-iA#mn$9@sI5 z+C9vLf(@joj7Y*(5A+l5#V5&wv&3ej&cqUCH86{k{Xe)R2bm@OKJTFHJIGwACU{RZ z9GNjBay)i4QKK)~_#p7M{> z(-pF7E#Bgbt!!t4ydsSDxw4zbghh|}k8dR=MYnxcCMC{tOgnvRf3^|QnvamEWtl>m zSB7c2!4IxM0}+I_^li542Ah8-Q};C-SKq%wD8qM38uV8* zQQc0T?J`jb`eGuLTlK#puh&{YZM#pA@qXTKmnw_-&28pRv?F{k>jUW#-TliB{H~Ua zcel;pNS->ghtGMQd$6q0@hwZ0@lo%*)^IU+0s{gAM2hIb`DMcUXtymknw1drT13{I zWotyROt-S@ox$Hs#U<{wuI=h)9szpuLUZ4}?nn;3SMc0+g_4P#Sklo&NB;WWih>z^ zqTI%ebyaDUCMAG@?O>e0eosNul^5yRp|YIMX=t35{Za@)q4AH{Ws5_^m~ta}mVHtN zti2Z^d?twOp?(MZo)Ka%Y_?>g!kO&j-G{z@k?A(BuGyatL=DW~k%0qM-d{eiY*P4{ z-d~L3e*~4K6#Zf7%xsY*6fQ({zak#WiK|8lp8d==Ha2k2052S|iu3E+cq|`JhY*iM zC69*6mZt-;$5y&C2thWLhmRTvY#e@uVE&x2HC-;_#-Hi^Bpt}sM=kX}b9;*1T=^{6 z=BEVjZ?4SXqisDZ0uZE*q&CWyiaptY!%7J5=0hT)v-fM^a(Iw-vVxhdrTptIW_$*FXtjtGgArH-m8bjNJ1Q~s1xWm2 zE$V&B#Ir0fFeR3dL>^|${JVjA`fi~#Re_9#RV-PXYD$p;66Ysy!HS68*s#Fw{!@=_ zTfn@?=aKvE5dq`yU+CemWwX&3%p9%+^)!A|s!@A~4mgQOFJoDD#@Vq4lnw!DU7tk0 zy(1z8EA$aT&k}UjWgC{$_u8vgTN>AK;j135gO8*#mT*|pkrSvAcm58tXtmR0 zzS^xj;WEHW$%^N79Fg1euG()Yz=-Q+gFn(mXW?6Z&@-jjFAh|oFRzI}wlgSXbIvar z2CnkSC4}N*XKRl$UY`n*Q+jwsGA)c)H%N2hJA*itH10qgXnp0TWqjQ>6w5zgQ zBKtQEd-#XRx_-{vu#O~-;bhzcE|n3^vN{aeywJjom<$d#2f>t=@k4U<^lF;1%!xw- zI-$FIs(P38xONoVTT*sOTJV>qivoK#0I zTq?BVyzIfBLHzBNM5*XhiYE~LR?T+Gdf ztV|lXx{j|tCKcWvgcDJS6vSoqJeht)`o2bXITL%x8w+%T%DJOET%vC8<2nMuhwI<| zn(GSU+5H|hsaC+{uZc8$11u8FdPh&j_AS*Y&1(QFeo%4Is0y4x5OsS)X>E!W zC8Yo5TRUA}YwTn1))VOXZm;3=psCS)bB_p-)1h+d){6l>>w63rpJ-ovif|qk%%jlI zjD3$SAMq3Gar+Y;3hKQ{^Bv8$Gcq^*0j0g^R_DwyxT_s zGv@WxN5^%ywY`%HIE1rd?xS(f>)tyf^b^G;%RkNBYC-hklsM-qNW2a4-yuXE<-2xl zQIpe_E3gf|{G|!f*baon{tiM1gr*bzs@#VjQ2yEM#&uR9lERi$4m@A9E$2P*5q2CS zhNQo7(q71Tx<&)mK_Jf9+U29*)_!U8K6jmP1y&O$iy zf8_Ldv1Z__+9*bUEsBWhW?O0zY*W^qv&V}H81gfF{sLv=ycO|Az3;757N%H%gS8G zE`Cc5T|^SfUF9gboL9u0Pb7M=PZ!}Ux89)S45GutQ0zmcSRi84pT|Gh?QQf&9xaX1 z#?r>u0+4tlkwc;K)?qo``a@A=!4{aXD_VDzKEZPfrM{#F|Es-b9O69jx0YXf15&{~ z@h-Q~po?4+1l`|nGPJ0eDrE+g985Hg*Bs&T>GopqsZF)*B~ohK19vEppOe#yJ>mT;{mi?76=U8C+HTa5coXehf3eKn3d&4 zIbu27(X4JJ5XQ)WWD@F6;i?5XwcVlzu5|M5Osd7%S7tI$ks+A$lot^6nF)$63Xx#1 z#!N)GQy|9x%b#mFHgBF6bG5wR(h;wO&*ntJG1;Hh>1e}3c_INCEfE<+JBA2;uTOK+ zVp1{rl0j!^+8@=h-%M#^Kw zJ_rC}A)CgliD)lg?$bNC9v_}6^`ETmyAvIz#gI}8hI8U7Gvxf86FxplRT_%)r%+w^ zP#Ka;I(-}W%%JwR`Hhwd&v-P^VSbqH*c;Um2hZ{^tSuM97h|F;Ec!=+Vv*QP$a_pB zb8d?Q_;~X4t>5;=MePeAzBAuq&iXhUr0($LBNo5*+ChuN$hztCRUOknk2W=I!;LW2 zknDu`RgEV916ujXJC~NH5HvdYIu-6`w~@-Oi`e?UKka>WMS8~7dwYB&#<#OQ&#@qy zvUzO+u@G=neF>1v*?QL7SsDH2Hy%Pd8BKcZ1C(U={Fd9@-|--2=8rn97Xb7Nbym9a z#{D+pG`Xau!4*$?9F)cCsIde933Vj2Z++2)yL}}Tkn2vFl2oeWAhhbToMdGn);V@4 z&vMt}Ljm<7m5jE=N{ZckOS}h?(_G@*L1Sg-K`c$3X34~<>245d| zw5axTRMsY#?kqFfOfy!7>6M10oMvu0d5TV(R{k{|Js+cpU@hFv zSq@SjRzeqAqoSm-9diGM)X#bOEH4@gc!Hz7!gKE>99F=EixVPOzl&9+ma{uP{ z)@AFhe!S`ENhIIdX$B%^9C_ov8XxxcgTqIDY=`vum(-clylm@Rz!VAaHhGH)Y!bta zNN(MBtN@DY(vKv>PZHx52TtZqHhtb3g8vpMbp}ZKE(WMI? z?}K|z2~_H|q}_{oXJju-lV~laqM)9h7hMVnUuht3^PUs}MTAzT2}}bc^=nNwU&#O~ zwvzexwIYf5^b(2mju?4a83|)fKZml{xOFd-P+yLL`AT&ol^UH*i%T1{dFIB`oFHM- z;&AD$;_bKG)!7RdU3vlFI9c1Sq}4G)Uc2-J_(%Qa=#nhp+$c`a1|WZ0;#!jA zzvXWR^Oo#9t5TkOjC(7U{svis2Cw~$ zy-nl3SYgN9c%f6&hg8~s@U2E+X#DSZd}mVSiD9cz`fxbe+joml<}m!Q4$V5VuB)EO zvx#QWxPb=4jfZL%qGS85>eJihyUDomS75PVI41P}%|((8d1_Xlz{eDd*6@e8{3KM- z6j-}dpk#46xCRr31%5rH(&t#p$D!avXG-HJHT17zs!CwL9dtFPSnFlHFq6)Ilr4~B zkq`cLN}X0xW&gFY2U-}VIF<^`1#|t^kG6847Ld|fxAJ{PSpC`9#;y2+I9joM^2)IE zi0!Azzdy>AAqpwMfAG1#5Hlfzo}qW6xFmJNMkG9HZNKZ6`*(3iq})g3QQ-cStDVNi z)6=EA2Uf5c1~p4AygThp)K$-?PTV}(4P-SruHz%~8_=_3Jhf@&G9`AL{u7S_rlU7u zlt(}57O!mn`N+d-1E9g#%~^L9%bM!VtCjX>(M$S6EzyCH7{*jocXzAtp`mvt zy`*W#1*V;F@3Cm|RmLJ)-?Q|dgBEE-{HOC7&gMoF`-TvB^N&R3z>_L*vm=Hzr~Au& z?2-M^+2IVp4KdogmV;#vO}m$BGsN;nj4-tVRd~^o3dCB_Hh8OX(+`1)T6N zy5!5Yjq@9kV)AOFs4WCIxp(nsMp#XhJ>pk+lS}~nK80D6{sC_`#6n@{gS|?3aN43u zB)4{G#Y8-p8Y$rAEocuM)~0B|Sw6sUe5xv;2*nkFZ6EStz(UDfF9WSG%;fr%z+xK9 zz#L+%zn49aj^Vo&7yx24V@iA*`dY@7z#0kXzwX0v)C$5*%gbm?p~+%9q?{AN`_8TL z$)&DZb#qfSrA1ugc4p0uHF_yG8U!=>&g`X>X`y!aNe~wj`SkCZQ-1;q`eP za|yl2+GAM1V9+q78=dl{aoT>}ZFQ4;shwlMgEAi6=dUOMykd6o)wmnb(6e?IaI(^M zpMKuwT2K6P36^uNrm_^pBeLSVRCm69B>oJ7%Y}E*sJg8nX*b&qA6aYA(>?)u;(H|M5xYb~XqpwCd&{@`QMQEBmk~ z&U&1)s35_v(nvriIDL;z7&N$+!~K@68ZKCpz9H9p_6J0N7uZKz_O_00x^-eh(&bKL zZ8E~&+2dc_ddbqxyKgH6TXeE8h^%`?3Q<50Iqq6Tmk&&H8?2oFooYXt_{s3*LN)KP z282@db_Fcf?oR(U!xk3%azogBH}bMYUH(&Q&xbTpm%TXXPhy44sJ;I-4BgTcN zM(kb_1JpX@ui{Amp3SJ5+aijWkR2O>iGiS_l4hOa4Pi%sVZ8CxfNT*HJy^^x6dqYG z-s2*!a*v9k7_F}f(nH8;m+ob6(eb?o$#cd#I#tMc#r_aR|u9+dS zXR{cJ3@Ad_@mmeIaY#3(jj)lCWDYwhB91My2e>+Q9X~6DAd}fb-8>7f63l5_3aH(b ze5#d8Yn0VulSbw;Vrv+0Vz@>crRlwAr9)-`tapniD8%}4JB&uI05sO>&r8}6Kkzt^ z5|^K?-3aq_e? zm7AsdTb_3gZ?U9=l(5KdU&bosxxhx)v3JWL-jgr8x@M9B#<@yMe#W`Pz%@l;#9V}Q zWF{h8)-97CM#R{9bvV1}72O0h*VIfLVzp2*P@@oi^%7WFgAvpa2 zWL0sS?R2GVq&ofa)aKUxL`u(idope$Av)W{qa`$2Xh)X`^z#W!W4CF00P?Zgpnloi z{=Kbo-faK)?Mmna%!p6%+l$W-gOf*M}RT6fukkPV^y#9A;p2 zYC?HbkX{r9#P>$;+MkL3`x%`<8jcfdGW0f$9=&t#FfoBnqI;XYlW3l9xK@!_HLc5^ zT5Elq85+7LX{31kOf=g}eRN>!9-|fE5 z%(e5;V-gDnXigrhTJ+d}Z?reSkL#CwJ=VU5FSJkAxR`$e23@>{;OvknGEk(VVEc~3 zJc|@R^4~V!U$?_?glBjtFn&G*i@}wbkzHQaFJdTXj?Eo_53ZhzD0Ykj7P#U)TzZE) zR`FhsKd+{0WA?aB7CFkxMmtOVkI)_PfFe0hlLG{;FAc>QngfHpb$ENteZ{DSz40)1 z@Kk5NuAc_w*FPSu&)Tea?(=I0IX5A0c7MGH?-jB6)aF1AtYBqSnkpOkypv3~--X@{-FVpp^|Ty%Hg)8Z)M)JN(v2!ejs88Xr^s-R2Z@Yw2KSu7_fuzSt=|J* zE&BxACA6uP>rHr9_`0h=B)uJiYyrHndJCiMNn)RI0mSo=M8SvMb(f2Z^| z{{5UjJ?=#AT>tlz@1@nH%4qb0QOO&zMUm$Ej|yX#>~`gDlFnStORAjWF*B3Wdz?(5 z>vqVb0)b>axSftDQL$DvSr2`7c{M-9Ct$jrH$pRGUTsw48$7+6rqDCQc44O{xw!Ao zxu>@wB~uz?f7be=<4NT#59ruwD|<*BFFLR$vB`l`Tzv4MrKIl>Br-v%ukwQyKd8vO z;plVNK9(Cj!lcT+s#N1w#bJaNhvSFeyO>!iRpb<>S$pPb74|0ECT{gwG&KrC5k+c< zI{cNhMJdS_SxcwtzwZX$H}v}fFUY}SpVbf)yWUP3BTY@zld$&6G7mui@mCX#kMV%| z{MPa|3z|SRrk5a>EN0|9?~<-D)%YuJ&_V*hCD%sL{7NwZ8$g_!+@l^D3o1sEo^`5! z;B3=zq^`-at4yX~Rl+Cz6KV~c{Ltv2T_x1k?Wesox~`TCNi28lX;L=z320N9%+IQ8 zWLe*uv2V)PC38lVJ9F@SMUS>lF;G_xr}Mjj&@_uSxAe3GSmfn`?(dFLoTe^7DCyy< z*5|d09y49%WbN8hMPoqMtQ^s7nG&qMvF1xl^GKUTV%!~%Z7Y72`ED*Ok8w@hLG)PQ zNWsfLy2$R1?&b<@K*3}U04tbH)`lBYhKM^$C22$hKj8RTeWwor*H`T?@b_$r%ji_u zQ!2wJh=v*_&>SuVXReV64Ftmwe*(ioznrg)Ank0%RqHMUBOJ?{l|3J=9U)i3?Slk9 zVmJjJAC%3HUfYeV(ZLpM;^FCFZk2oK*A-wfsVi6-isD#@>h$M@HiQ|u#QS6=;g}j0 zwLIGOG&~NGc#xiY;AVg)C9%MbENa}w-o!D=KmL8lQIn9%Q!S0;MnxqmBj2qQ8>DsJ zcecaTH-Joz0AAuv;;Q+k85xOBdkTD{PyJA~L`>P}W2RUy{h|aNEW=#6GNsjt_#I;1 z-M)YYv_!9bEAj^+Q~Hs~THSnnIm}|+e8zaE7U zHv5vFuPB?xG*s7ZZwKH)sbC9=y?NY@+#g6*{uJVRoMS9ZzVJW{U$%Uwm}i{las;f< z@&R4Yz~veJlqQFffk8^l&%yq%MBNQ{>BD)RS`g#;5yXYf=DXDdakgTh(W(R}I4>zE zzh|r$r#vz~>1aOOAibx+-maO_JPFl={`~sN8zVp`7`j3KW&mxd{F?zq2DCyG*RVhS zdrkns0TBPR28%I)f*JnJFxEL0fLM{CVEF%M*8WE$jX;wGHhO=m7aG)}V`PB4+X&e# zHp!mKk6C&mM+Gw6QArX-P9hBi8N<4 zBRg|?-|lCN^Y^c~+|3*dTOUtm_S#%#Js3U#B}*@VxJmp77N&-)l|2~tfq91+Cb~cG z@4e?f>ONm38#0skChpOjN`!>-r)!jd`Y@ZHVb__g6XmXzzp~=?X1eVM&@7&!HNQ*Y z$FtY~nt#2R=Yi`*`+N2+Ld6z~P527hdEp`FJGbccdelI4m0HT()p*eG9W(E3v_s(C z&u+8?4)usIe^S=9e-!v#DCWdEY(dY^?gv}&@ys-n3t#5_8@ONmp|Nn4@Y;LYNGXz) zC?sb71>NK7!^h&V%sCP?MTf zM@{cfn{%p9x2ee4i|60*5%Iof;m@vN=4F8{YCZ?%Het4(yYj;{aIibPn~$HB3!Ws} zTVc?J=Wu%&8rfhCL`yL|9}Ywu4eh;(k`v)H{8~;Icr5_URy~vn1hYMU9`zQbWmSY> zT$(ZSNh{&!yC}Thw%@f2RT9BMER?kB*;!E2gaaBTWi*9CflZ zT($VL&et_Ye*fc9GwJN!NpblD#nt(x~2eJYo?%{ciL!R-gFI?f+EKM~q-ytI^7k z#Q+L;$B<|~kVA{Ry)>+#&+{1>C7Vz0DwZoLPqHS*8^3F&GHd9~zX=2@FvjapYii(x zw=@oqypd`y$fo6xF*mWYe1l|$m!=z9>i@vW`EPM~G4n#xciMXC-fD&_%8{`;#p+G5_r-fXBeEOb}16R?47&I zt(J6Ss!p$>VM{U0ydJXgstlB7)`Ik8t9jqVR|yEndZ~fBS#P8ahX{FlbL0Q+n zA6!27^bxP*jO1fv;uQ@oZQ|#xLuM>F`GDlg+Q&U&MGZnl3%kA4#Ed^(AnN{s3&ld`v6tHz z=SpiaF;4Z~2m4b$V{CBIoxU(-6~9#paOc-{v-yRtPr&Ey{z9^11T!Xu1}=zB z#07=zDZdrpbUgZy^N{k|ZTks02%ja6trytquvhk@e#AURDMJ-oW2Iw-9<1S!`jhl6 zc@RCq+}`hWAkcV=^bDel16$^t_nnT!VdUjm-S~KX^+N9%!h1%5KDXKF`sCv_6+#BN z#TE|6*@CO)89p^UQ?^r=lj_Ds@G$VFa6CJ?XFv>Ux7k{^NZWI$zvJyZ@MSp*1>|7B zf!Qx@HE4Q2wg7PDJb&cmU2_lOaCDo+VPS(hi9$}BmLcYXp1;=(KJs$U3x3pbJpJ~@ zE<|i1-i{^7rD+*_rMkem>!xRAKFyrpOPSAnwwfI1ajxgoc) z#c0akev#3GCFZJnJctLjtzImhP9*@XYFqe}Ql`c*C+g-Ho%4yW!Q4Aaao2QdG?eOS zzHvzX6G|N_PR3H76Cw5x;Qo<8!?*gp>fFH|$?8|R#OUBqY|-Oec8HyFVOw5+Wp31F zKFuWlX@?2MzVH7=uXKuw*zi0@N`vS0I=K85> z4ruj3>F1vZ`_+}sOvjmRl=(3xi1|takRd0OvV^nk%CE2r4|%xFZpy#C{H2oHc&??M z1i|&4!3v@N%walw_Ng>E=>&3<6GKqNb;y&rH1&b0Aen0IGn3knmx-)1Z{MEw>bM7Q zHDzvn7p$mDK}UDaj1DL|swK&w-At^Zj2j>SEut-5RMkQ0rc0Bw_hLPIfYbVBk4tHMd_AWJ{#P{VV6(?jMbJvg(L`SKWsDtWW;ck zJ4IKDVSHDd9FBx=^$hN?g1sk@O_Q|VgbMx;0680*B5>^>jEX}^lTC$4v*Z-sPtJQA zF)`w-@K3tr3@-4b41xynC!T`pf73+TZzQ2(xKWPHszWQa%zxx-@L`QUOdTY}e)tB1k3bl&$U2Bh@ea)FurLUdbY8T#aQhY3yG zZMZwj0ga|{n*=o$ZZYxVYU7`U{-ynMo*5qC{Un7yl@^)n}89R01W^!kAAm`F^Re7w-!-SL2r3p|ZOlZO~n!yUJme83H(vQ+%7-3*FE`{=aQZa4Ub} z)31BS^iZqP#BWUG5(4g;SCXebN?$36lix-133k-pM>cQwaFlmOsywN*H~6#J_QgrJ zO{EtHsg!JeLGqa5$@dh?T*mkmqKaXtB>D5?ciGns!+lw3f$9%(U)TM4MG~Q-#RU0k zBdtiwtS&u(S&`I%vyH*?4bv`(gjA4J_gvHMnavilECDN656m!s{5*TxR@96AdPbFW zvOgW=J9d&M-YQ;_2M;o%*Q2H_H`$O&wE1o-4P!L9C73?9969Td^MwgiscU}0+I@Z2Gt-nz zZl)*9ncA$LkCeN$RGzDKld}=3E^QJJSev5zm?aZ0{ntPW;YtoXCi;x8&sDw7x(ocM z*&4uTY(WFkDBwUIbjcvjV%w6Yzirnl_N9$zKd%G(qvZdi0sToy(Kg@ zxJIcwg2or0HN>$$detfiX0?*Jy8ZfrFx5bpx5zT)P*F3?EzXM-8fGG7Wde1$Ll|YF zhaHF%H$jUp(DW&)Q$bo?x}Fo;=cCg6oh4l}fO?~Vr^g0>a#WJ=fT1j8vsoS37nfPhp zVyOC>q@Mf2#W$#?iqm0VVapz&_}`QGJv;4a<1x5aA2IA$OZ4y#A-t@zd9UbU1ct+I z&Cs^ySAEFri$JkE(Sf~OXyA$MAle|Mlty4Qgu}_}JA;0&bY0qZe*RO(+y=RVD%15d z)WA6Pi%b1j>a$mxSI4{ve&sMMu3a3J{i8{RZ>V)IdmGZIwuSb`6nXYeU(80>2@X{M zMr!EH0_*P--5-c3&YvqXUEYjiJ;C0Ax?ec~I1a;#YpOTk-GyfVN(sRl3>z=-I`YFT(9Y=IxM@q;57zExb zA&S#&gv}?@dva}CeoHWzC5KDhCfV&Vx6?=?2q8Ea%RXq2kZa+8_O|GMUXr>0Z1k@Q z|JmDK|C#6w|1;6)|1;4K{xi`n|7W8AKal7vNqF5H2G&t$<)5O0C+uz~cy;o~yUB%P z7X}@JKxzf(@;>Z#gf* zf6uQ49L90sX%C{Cj;`jM2O;+jll;%Mr@KmfN~%$TL20NB9B(|B<8qxFSPZ>3_@9H? z)1cj%?sxIZ+jke1%%mS9Vg}W&BpLrk^%CgM8?BH-ytNiIh`T?>CrEtQ&MZ7yX<294 zCo>zKNnG}QFdDnqz<;3udY?{4xo#_Y7WXBQcy?Xv*8er?O z0f1`Z+_;vk-iM#))OpMuoASr!A;*zJvCd)sT~Etb9C~*Q3hI#MyC$L$3(lU$@Rzc` zSNbd2!PQF+FJ9No?kpmAgxJttklsdlL}SaPXm%+aC;eXc^JX`kyeAQY{*{;|^r~?B zyT3?+S#}H?|D2zJG`i?0mpd#0au>`j1W^F>s>-W z1*k3u-JFa<+Ff$ISwSw`)`(HamAWTvAt%fcEcSKTkQvl&NsALZ-mzMn0m<)Sgv`bp zB*xFh&kC;5gln*Fv^BfNk4c6*vPv$Dl}9y~zz;>%ceCbShQoW?g|E}488^lhgcE8u zlf=GG7_xvY2D_Dxdz%`@e>>)`n9;Msw&M+wdn9G-g+H2obGYSn5|Cs|aSmPT-zD!l z^@WrLBq&hFB#J&>rdfF-yX@AA57j|!#P8m+30cLRNfS2LWt~l--vUwVePQ%p5*9XW zJv@O#qH`b3oOI>LhIu8Z507dOCFMpw*rhe%y;8}ym#w;~ zzm$&L4yBmA#b!jjc@51T!9p(F4sGRTgXd70`Lor5_b2cGeo3zQfP#6oWD)F$nW5Q+@mqpn6iL;r$4cOBE2tk2NdqlfCdA-`P=Jj0m3>u3EHmxCr@W^CStybF)xPpNwV9n?Ig|*aRmKID z4EnsedcXPmM~39>C$xQIyHrV*D6E4m46U1%nV8&R8MW*ez((34voTFI!fK*Vw3zdN z_%}I|Y@AfC7wzr#AvjaarkLPzBk(K+4^qq~=>4~%YyCTu4<~^J;<6H3N>p&Q2Xd=p z7w)eSJ($q5b^32eK&_wPh_OSV^s+7m^r0be^S*GPI5HxQCt$=LiMR_LD8Bsy#!1mh zs6$9Ut`oGJ9JM~k3bC0@Dmo1I{}M&)`;s>~+!6QWbt>Ws9EU-(AP83^jKYToH_-4g z^JINzh<}_5?fhvdQ-5;M7c_t7k2-@MkKy=(@V9lvXx5jeD(FK>lT@IU&+*U3x}>Dp z2qE6_{g*|k`B;N5{sdh!N<`_$0FPIU71T&uJCFn+qQ`tvghxTypCUc5JN$WH$#Zly z+5#eKw+Za%-5nBRriE71Jc`*rB<%W#IqYaPaP3f%#jJ)Rh-4#{CEnqUOc_mv!%bGD z)TH&?WvDyPD|nF1bNOEqN;KBFuDRts%<#P$TCQm|wd$ig-aYEwRpCqRKaW(=ivd-} zxY&h5_)pyXvDKezb@ydu_tuXt!%aL;wfOI( z%0_v8bU|2O8lPfFVu&fee3C?lGRi3?&9Uw*ML2G=hI?AgIoA99$8grbq2Yra=7>vo zg`eP5`E6=BT0%5tJ>tSI@chbgVZJd`PqI()ZT4z~$I5b9wTFw#4-A$KfPi-jtulG#y!8o^f6ltUw>7~h@28O9XIR0Kj1F@hYc58#tG6usc37FWz$8!ZhGh0H!9=Pq$AxCl~!=fm( z0j&DvN@!Zaf-bv!*1kR@$=cT)VY`m5s>ESLG_#mBRaqe%W}4et+Vy%I6ONVT0d21t zC_MP$5oZu~A%56XGAZubD|b>&)el$v!+x9Ob8+4 z9r=U^afJ9A3BEa!r07^|3u2a@4mM-!P{ zHfi%DHJ7$~TPpQdHcx8=r^@7+R<5H}^@LXfSVx#>&apexpH?S}a;ZTV>(kl`h3Ox> zOvDSp29-%x%QqOE^HWaX6*<+&|EsgN3~M`R`h_VJD^}bgxO;K8;-xsHNO5-!?k*v? z6pFhR_X34Lk>XMc1%g`%8uX<1{k-qFuJi4jeBf`hJF~O1!tCsCyGHMld#+bghc0%5 zy1M=8rc+NF2{?z;qIx2IoW`7U%h-`%`7?P~wb(SX0-A(=0jcW=wLXTUj!aK3hnC&axHsL;)$cW3SGdYxs?Fq3X^h zq!e0&p%TfmkgsO6F!P2~q7{nX_2uSC!1LjCa*QI+PIAhnaFfvWjt`zL8)8kyXnrBm z(W)Jk;^G0y+MT}8tE!0!{3wo+H7M9|d^pHw%%b5p=YqZIvoy7V^Eq}E@UkRxO6Cq= zJQUpPrZG@6uiT_KBuvyO4C(Lc)qDM{?Sd{zgzCNHgXrSR$F)*Hdz6e1SPfVgAkB_i zy)tPjx{lvS#)kd)t14`NHIWxMRZe0AWwZkn|dIYyC>dE zhUR~?<2}#YHV)7Pu?(bn1T-^fyrUe&LHHI>t2_$*^t^;zSE3`9bV*uk!!}mp>l{BS z%0B%ZIMwv*Tq}@F=@aQ}8moGyj_4WFm90jG)Qsc7Q3UZ$IwKz*OHjl;0x-@~r0%xF zbwDY;*iNzP%sNr~cTs^uM+EVF8OXr>(J<=XNWbWZJQIy~N^RFxAu0bzDn002*}o#P zN?R#duN%B2-I(Cn5e}Xem+}v=;?H4MK+KTFK>cTzxBSIDGEb8#otLXu?_4!~=>JWR z=n-T6a+FSQ_@QBNEXwjX?yzlE zPH5-Px=*vYPnU7shfLztmg#DpFY-V;mOu~-+hj)<;mX}wW)I|RJT9i7oBgJ2sq0mY z?}|ZFShYYeruUqxR3OKT2`+OLavopL zS&%By&-AHFG=dCg4G_zex)jhKP5mVwkLX9E%cW7Px6|?9B7`jp<=EdlZH#wH7U`$i zePuqUzX_^2(j%LX8B%<{QO440lYZ> z%!yAb)=CZt`SF#U7IruuAz>pB0NI`Z|Hs?ohysipA`c4gc`2b}92J??(j$<8k{PQJ z8U^_?`Z)-q`QvMRV5RZkZ^@#~gR*j2h^1~}6PbZ;krG9}oV}@c593ah<9r4>aDrJR z?9_;CZYc%eHMU*2@N|MtLkcm#2;7M{Sm8IIMv?JPOD?b$_d=hA9%3}vdz!{ z;SrBues~$ZN9r46j{zggFlqc;Gq0k?ZRq#kYK5*t$650%NxZ7-z1WZSSWY!%-UW`@ z>CoN|A-EW{F8t;6#ZMl?b!DaVY~p!n)FpnWnuzCed!ExMYsidZya? zeOw;C|J9wxcXgArL!5*u4EUkJ7@I!6RMT2eIlJDuihQ$Sf9Uu@vn zp__=1pEw{X=}?|`cEsT9Z`7AhH#BnQ_n-GEOrBU1!^PeK*KAcs$=Ucj%oR{iQZ)jC zwD1pKEgBqkHR#j1@|(1?W5U=tXMjwa-sJ6XQ!szemxD%g9%%VgJ~$vdlU*rIwzQrC`psVt6YrF%2)+uzYtz z)=$oTlYGYFJ2e3W+C)yD`Rx{vuldjBN?@~Ix{x!P!8!bNtq_DUYhUJ5B7o1d3@-K4}9xAv`h&e zMRAvlPmgxa*)B9 zuOchtIAzzci@99_I~f3^reLH+^Ac1~uLqVUPc^<`#P~&=9UJ3?v+DFAK}E_aQkz1o zhX}1Wy5NNu4O+2Yv^@n{uiQnNU9G+@_9LDL?BcYG9-##Cki!s7Db4{AE}u-x$%Yc# zxV&TxeaZ_rA`jRqQ>kM)+GVtB&p1*o^2}l~T403$@Z))y5T}IJXE|7$BCGlSUv08j z3d+acC> zuV=H*Kb|}E(TMg^75ZDU*Hrg^ObYBXC$?u6VKQ@N=8h zR#`k-VJyL)&$(Wd*~W;ji+U+^efS;p2nzjve4Fyl#`+Yi-@?q`>J$0i_^ImjiKckX=;TYrYJl6hj#bg8mph9XilPSQi;`s zDDI2SAA9|;a1CU=@x(*KV5e4&ff@es!OOG^tUx)Vw;E$dd7a`Jh?wmCCg^!i0aUAW zvVDKc->Dd1kSRrE)w$8v;OhIlb1S6L_sXrmx5?5W#K}o>jBu9ULCw_cQA?4B4#>g- zdkPwJJ+PcJgprJ*J(-U&G^YpnYN)}4QxN;oFzGO#4%DU6YLoiVFkBVEDKrV+S|R>^ zHNKoc__xqk8NAjBP((Vj#tl;bJR!aJ!)%vEC`q`WScd(&5Hi{>9_p zQEA@}+kJnL2dUpo7yWCPSDr9xHt4}|)bStc`mdyRdP*V4jJ7Z-CTZ$1SMgwTq~1d% zJf^Qn3O(1AC)2p zg6$Lsv@vvCL`@R0 z6gKPleFxQF0%xrISIzxhYInN-SC229?x{V$eY}mL?o0QhwX}in+4(SpaC{|F?9uOU zUuWCW_W${a|5I-NHRh2HtA*Wxb?R~PU>v?=JZDg=YRY2$ z?^_O2^Al+ei;TH*tYswi^CL^TM2Z*gT6(E0w5kQ5%=2#W**H-!qizL{i|`wVi_q;$ z>uWLuvOPcjn4=4`Cq6@#L?6=7ZXBIkxVJMelJe*TUVNcA2DomLJW8{-f#HM{79lNREU71BzX9BD(+ zxQITmC&gqxy}BHf>7UDJlE%q1P>l1PT`F%1@Sy+gBR5aAU7A=Q2I`+KuJg3|=#_tD z(}oZSUxj8(+Us@*&G+#!3_XkcG(Gg}HA z*vmQoFhRqfwj{HiHWSvr-iZ;kVj=DAd+2@p+vm^KBbKer9Vr1`%0Wkg6Dx357sj`Q zC*9F>uc(NCBy|+7l^CYc`py!S}|-Ck>Q}pdNb)}ACFki$edz(z)Vz`&_j+Qc zmc$RbeUHM@&{OK(j(F4c+PD(C15*l;jY0686+%WSU_;0xFqUXT7??=^7NkI}LC@IN z{PQJ_R4my%>kL3j>pV*HXK47xB^Sx(N6o@|)us8;uDzKYDd~egz4j^5o9mzOyX&e$ zF)oPfHx`s^)oz7?O>1-6_U%h6fR_PEw6r1-R>i5 zQK&4zJu%;O+6OOxBA%qghGOFgpEs`i)S^(*bWlJD>Wx!Mi3G&*dv~ zg6=FAAM)(+3x`Mjc;g8+98c_9UEg~hbx*tUlcRGSn zj*4E9<8!O-XM7mTV?m*N5n)N@SQVw?yBa*sMkaFf!v!<6!9i4w*)bT@ZM8R5Ls|FNjoLFc1-M2|?cVk<@v+M|bMjhK>0-{G91q+xBDc>`?ixv4U(LK(ttgp7hTcdu z+iv~*+vYs2?qobO|5*+`-B*YUBM#%;U>w{pELCa5d5G3VMKXK^Gqgu({F|8A26~ZS zK!TAu858B$HJ7Q=IRC?4mlvhmlG*-N?@PQkMwDk`Y2}V)CwR|kn0)e3VER4m*A|v% zig|PvsF5;^m%za4lC*;5+}`3D)u1pVJa7V6=E;}BUE-pih3YCc+Wx!su6~^;ropuR z`A0L(7qero-`XX9y}jVVk2tbB?u~IW8wJ^#pB6KM`wad{N^s)=h2ZMVtM5OL7w?X6 z?|d@lrRz;V$DL;V@ZH&me^}4R;-#R)SIU<0LcGzp&MRA54Y|W=dDM^)kLTg1&GWFZ zbNN>YKPu6%(mOQ5U`s0raM}eo8c5g?nwZ&PG}xmzMx4>G&?KfUYrdgJv|$;IQE)Ri1dn8 z)s8X>w6~0^)3T)-`-TG~ebOdLX!#3G0o}E0q-AT0^t9jd735cVu2(V^0FT2{LEAWu zSCqxHh!Bucmr}+D@r*caSzxQYY5pjWn%{7aYmn#g=*7;RjZ&(jwX5koM!Xqjd;s%I zKeglZtmdFMQe4Er6VyZCJ69n48+ZriXolm~LwOe9`+jvZ8=cnhJOklTS8DU!Hv1qS zs!oz>Re95Qw+gwfwfRq@E31$4#7L_0HqGhr9ib?0D{DTU7)qC`xV8AL!OEflS@S4H!4oH@dvgWYjIkb zl|LmEJ|Tk&S~}KUh!=gRqh`%YQE8J6K3Lf3f$ds`a7!bc@e^y+t-~;{$#6Y*eqeap zIQ_ce@-T6ruQf$WE0}Z;W!s?`)G#FX*%Q$3F(-diYvHh*p)ULhJsm&N4_x3a?-&_* zLYWJ9QEZB@4j&Tqb0IOdw$_tOY!sv6Y?0#>G9>EZ(#|I8#fIgxQC&oTdj0fug*Q_9 zPaz)gv)*`AnAb?yjYN6$L8>`dB;$xcuV(%W!S z)t`Bi=i}|9*ItvZW-qeE%`rMgC33@hPzOmUmJR=_ibl}i{TM>eM2TUbn)nsftcuoH zAxzDK<9E1=Ml428CvFN3EVJGm11tSXQ@)@?FEw$AOUO9*d`M?h=d3c?YO=FQV| zvu4rHyD!tMk{umbOFDQ*gctKD& zP={Wy!MN|RVV6L!BuB{JR-BUY^qMR{LVfwbZ$^yBJ_@HR)alxa-d%h5`-6xWzC;K{ z+2QJh)9Y|rY*El2^5?Joq*Eg{iD8%76sr)CiT8NCUoC&{^r1DC0v55X;Y4m*sCmX?K|`RKh27d{34HrlmL#5<10# z3U34jBjwp&9-@ElJb%v$!EzN zWpnLI3nK9!Dx=qQA_BFAb6;gK0G|)jQBkf4C1<1GiB{J3Q}?yC0#{`8rad7omF_SGo1UG5eg^t zDJ((I0Z>EYMnS_;7HN28Q9sn+V0 z|D}u<(@2)W@{Waemmu3#N4SZFkWKxSX`?)^0@i0QC(1zp-k)}xAyV%qJDfev`Zl`e z53R-`lp=?@#$Qq_ZR{9^jM5%`{v$S>EgH;#JNLoYZ|L^+wrwa$DoSGg0{*B|Cih^V zQh)H~>?RGp!dBC&_{3Z#z5;A708x`Z+V=-sCAR!yf3wW9tNjB%Dp>R2NgLE7BmZ0Lo z%ckiB_#VwEUB%`K5JaX(JPyb8&Xn(+mv=zEo&L(f5&Q5bqR4@kfM1pXjsRBo9R;RC-6Ru7&t$NKG_j$}d?COF>RPVW0FJzqV><|Ms zZ(!N|RzE4(h2%G7i$kf;Z5Eru+M3Bkf3dvP$BrW)*E`>f>lCwHk!7Dvz??i!zpgLX zQZ|t!26LJRf0@bV=faWg2SUol5R+f7|I`y9aW&FlspOj!t1rvYo$Ruq_G`@{gIc~o zIJ)Z5=j9c*08Nkhwy}J{{R$28jTImyp(j(junqGZY{Z2Jr^~M|@Im9?XD+Vx zXXs~&@MdIU{igJ?w^&EHi)ExD#r;GB9dZu4Tq>jr3*t?4DZ=q8!!HsX3?=J@p{*$T zHpKG#9s1>ZxgUX+sNzH4)Y85u978{23b#UJ%XIF#muSN3do z-w9%sKXJvDXNX-OMVyD?7L5v$#m2qYZ(6ASlAE;dub3yz5#C?rzt*9ScPUx~Cwq4tzGCFL0orpn$fQ)=jEcUMEG zplR$Q&>3w;Xkc~6YHi$-G!c#T%<|cqKf6qU;BAM;luX~3dbVqbMli7mpl0E@Ucbl4 zT2HGkH);*BHav1g1vzSyT@Np#(=1@Oove8c+jm%0L-fa^cdRT{bD~&q7c=SSONhiu z<5OWtgDx7(2)i^OW7C5V`gpyj<)tb^vxkneYelV=M6w`QXUME{wlo!whS1y@vPsk3 zSSC}ieMVOOW*8^bnIt<)-|0~lR(*dm?)3h-X;V_=Zn2TQ)qD2&z1)8}1NkbK`>Bytn?(wZF2 z(T*V!Pk@SsP)QEHCO^WTEhlQ(Efw)-dbLis(fu{MSf-W-VIes1F3xQnm=jI${<6h3=!^}~v(*bt3P9a6&2k%=K%-~M>xC;`8vmi ztmE2at2N6D4B9)UK+l1Rr$UL{y-Pzb zLctW48~DXyIwWw{BO_COJ!3{<2a2-dSm|%;OpxnFKqXz9CHWlH^h3`G7ige*(GK2I zl~ie0v9rQAQ~R_f>YwHKgy_P(UjWwyAghr{oPVq}`qjBVU8(7t)IYDZT2dl?B8xVE z4*A_HljWiXp~TjB0>F$Ppf6IDpysrQKXmF)d>{rPP=RJppj;j z{}->MkMn<=VQCSKj@rn1pXyeWS%gPYry9PFs~mpI$rz~|Ss$CUG$PjpGK%$MAz<{u zpf^qR8y*}Hh*b&)aK?rV!`D6iS7&Es%MJs#mpX5zI&DYJd_xLFFHSCP5mD6Y44jAs zUc|1zq(($UIpX%s;P^|pJE5HRa-$5`2)9+Pmln z=(Zy*B4FHpKZ6>!GoEfNjfiniI3$G$w{v~)I0hmj;-gJ+=%!yzlMcBZ4vRdzFHO4c z`=WHOQZHCZhtCeAK>&8WEHBdO2lbnn^|^0X5r&OfQMAmKtI+tqi5%bdj0g>U<02>5 z^3G!Ls$G(luy^jKH|}miHo2pdsxL}8oo+wq`SOc?&1Z_EUjh|XiFeF%h5Apapun|o zIa?HPw9gYZ^o0lO=D5U+X*d?r3sZS7&{Go&&rsRVV|WoFbzu+uPd55!uW-oCBgMk~ zQM;}0=a?E0H}{e=Q^eBFVMq{t8*bOWhZ{<1WTWo6NV_)5+E zjqs#BXp>aayP4SDt76GrAjo3g8~2d1fnS|Z-v+0oS@vmV$68#I+n$s+pfMNi~uHY*WB4#9zh)B`vc=xv8N z!7n|iq~hwA6{JstWM|1&ZnoA*eTR19S$bsG=Rf{kpB|Kq3wHr2olors9yVqV4f|S{ zF0RnIPWL{OB1}}+G4pe6;d(%IpWO`F448?MBgSM^-RC6(JMQ-9ZQvfLqvNCuKVU7;5wD~f8Ggyzrs>Fe5F#$GLn9BrPOrQ%ALfm@Y-B3bTP1)gc z0NjHI*!OBhN(8>;1{SPx`h}U$R0*vNjh2|!y2%x>_cU6 zS<1qKFJs}3Vq!z)dye$-Z*sw_ZI&^H@_%Z!T?7=2v0 zi>n)T$RhO*`|BZtI3k$ClG`hddC}*Oko)PNLPx1no)sn`)^D{rLsNvO{w|I~}-D zZ81E@GQLeU3G~U~+hOx2gFWdr(1X9=z}_Wkh|<*^#}z-ZsdegdTC-fn_2A{+s?8j! zMzXei##Sp%|4doH(~}%^aJSZU=IN6Zb)U==)pKgzpjQlMK#U)Ve$WuoP@P(>HyF^W zFYecp2qRbV=MNEv!|aG~>HsL+FkpV2_~r-zm6FUyNYox1;E`|Y@6~k z0n_J@!kY=vi~K#R?TdivfOXHQw7HF_2cv~HOL3k;F>m-oIvZZf4LS@aTNK8-ZL;k! z^NrJ|G*T}B1VK`)FlpM1CeRZ#I*tB{{=`_7V zAD%R72&t*oMH(GNTY~of-WIIOAPxhRD>y`kw_wj)I)7&GnwJL zj=;Jjko07#_e4^ZGu`z?Y#&}LxBgJFW)9dp5ITE%EM25M>VNT|7Z~{5VpoV%owQI z7|7GOBB@DrksiN#KJ>A2ojTd}d3ft3W5~<`s%*WW4p; zD|6!!;{*F5eEM#-B@0K=NP42>Va*vjq|yFdCMFa7^l9uqH?=2u@0&-`eJ+gzStt1K zevP6p{+=MxsQJlk!y&q&yG_!p;mBE^zVgsW{Lfu);XAw?X9dAO?}G>F@Faf$Eu4Pe z-r`pD#w{j&7hc!>unJH9^Lqa=pnmhp39zKU928=&Si9ZEz58-OIM2MhM(|FHy7^~; z__#Zmg6>(m8VeeBZUn*Og-yxR#07HQF~(>0n7!%8fJfO&MV~NmJU@-?-*+L0 z9VoBE*JFP|VD&Nx?HxY4J`Xh92Q8k{Jl%(`S9RZ*-A@K9)0xeNB0~2Fj=%y z2WJ)+6qr$w2w2Ipdv2g$ED8F>g}X7w4nnzW<}~JYw|+t)fa6=l3GSevsC)x#wX4j_ zn#g1=w-9S&t$_W;mH>Ft&gk`biIIGz!Pok3py;+>5vr>pBPrO(JQ;S+aBd{2(M)sb zhO7e!?%Ockr8VI<(6$Ro&DC0!d0k>yZK(8pncs}@_SpN}?uBWY`$3FYl+uOGbT0B>OPTLM?gRSVFtdoWXVQ7VVDG=F3 zzk2b%cI(HTq~(qvJ|6JoOzEyKN(%D2<@VY5N-y=^)*G8@OYKpp&`rT<;5 zx-2vG&(|uY()Ug0uUxO!n2tMaLBc%O#&Ym&J$8TggbJOTLyB8Y>z|2+FJ1g4ue~+{=jP>GE;g?pwhSTZg)e|B+GiV3~#RtK;r8}&EqJ|Y6?S*D|eRvR0) z_n|%d|1BT8j)YO@lJ>Tn5e#8jNI>{e9I1WLPgl9%VZ_K0@)ABny30;cn|=)KF|9)x zzW$P+E3F98iR?jj|I7;+0-}HTyW;=&$gM4@M+x!W$d0F;{($!eKJKDDIc9#mOsMON`4+~t?(X&%BOPr_cxF!-<*R}6@QT*b98QHgC z?n62}!unUtP&SA31!UN7;c}Fbi$w`D!CyG7)-0{ZBZ~`ehsr(Ur~MwzpV8T|o+6LT z(@kVnWu{VG_Dsj(l;sMF-x@jtHi9h0gxrktLkL^TjD3?Mj${v&d$-nbTiNSS=B%CH z$qIcpMFHA>>5(k3ZD@-OyrN?<|2KpPw1x%VBpuYVCRz~|W|X+drN~gxrK$vN2-IN1 z2uz0s4gxe#p@cN{D7#ASo}Su$Tu>fhktRe(_jv*BpSa=?SS~G3L-_jR-#I}U0*a97 z6>6Z3HK@VR70^>(n5KkC=#%1!0EmKpJ&mNzoTBOQeXoXsr$X?&)Z0Oo<@8?H@(LAJ zCtS|qN;)`BCo}H7cgwYQzJTAeTH3cjlFKpU-1T;aTWA)u`!B$G;9>vNeQL~?e5~7+ z><<1$nc(5wOM}1WV|zK#L<`12p@9`i2dsn1UMp3I2`U;)v{6e5a&WMM3(@}dZn5S) z_S!w>z~&Eb0(+DXJXYeu21_l4X^34W+c_U87*$|r(IIc3hbT@|ZnAh$4 z@7^i}wT#x=R~bwt$WV(5aD45mHd|hq8ph~V!MP80ioIuI3`A0Zg#(Tsi~6LCGv;{< z$42R%3@|Ziky3jA7;_z<#z65ZbBRQFHZEU(WF%gwvgzwcd*fKn4PA%jL$c3jB{g>#gvKB82w-3kmsVljnvw($jBN1nDnLWs$e)G@e~t+AL5!!Mh}!_ z80C8K5CpHq(kO&nHZbXHV?HqQ;BN_KGs8cI z1E|nXg+UZPZKfeWS7Y%6D3e?dX;&KQOLOjbAC++^5$A1O!Z!uyGWqTW?NGN-bF%+x z+fg#gNxb+cmmuSYg2S@Hn5pafie0-r$Fiw@7%UZ&C#xS_QXuZNrVK(VIh--!R0`a3 znZYBvM!pj=M+Q(Frdc!v0bRjA;UiyxUFij>kWHhyFAGxue2I;i;QpuMXoXl7Iz&;6 zqM>;RG4wvpzZ>v~HTvepprhVFjo^>^Km3&){DOS9ku6+e^m&zOax4b(+Rt1Vp9s#ReYcLfY6@InX8wiHua;g2jFQ?5_ZTF;B z#gR7~u#R^A8}kR0>xN#*415Y*NP7VDj%-ffrC&kc3zaigxh^piaPt^~C7|-v1iD?B zu23&doC|I$F$LI*=bZV!ihEw|bu#zbgr`XK@ZL|BsxNd2Tu{IjDvUf}1WqHFYMRSe zBAZl1{vwIcUSC)=ZW8gw+6e@ZElNUr!1@%h+63Db=O@4-&20I(g@purlsck4^^3_7 z!6TX1;8>!m_>}d4{may^z7m-(?C&FS8<49V%ZirWD_X^NqBBn1sV~3+vOw)(MRmuP_q= z51;O|y>JV(fc$B!>w8nua7I&ayrr9aeqz9ft(U}n>uWW(Lu3B6!kH9X8PjGypGfb- z*0j={8RzJ6Y_~_kG@SCuE#e8S^K$ESRt0UNJj63&7e0V(!HPJI45Vv0I6s74V+`5f zWuqB0>oYn&{`e8?7&WLzn(<_C%2^iuh}vcn@5U&&iRWnQSBPs*gg<;B;D~(vOk#*Z;^e;{5BSjaFRpv5(5~S_ecT>#;54*aIYl)P2)uxuSpX~+FuRrOze{;40V5Ku%u*ApO{Y#$EuqdKP-F+;z zSLTa7?)mh=oUDt7&bo2ib zjd==ezuWQF>bNU5!n<=_IaM}w7)+RX3WA~dvxxtxWBph=k1+O&5w7*qA{4bdx%U{+ z9nw_%NDRCZ054ZNhRzF-wlA7vR)4Mtgm8(;Y5fL+(_$>#tz<37hdIPU+ETyA@!~Yk zouTxI*+gewDhzStL%wZdpz&oz@oK!8=PhU2hL2W$8qlZvui-ex7{=dkr+Lf2gRQBb z>S`|yYIW=u5581@J*M`!k%>JFA8iEp{5*K}bZR)gx?UXMF)!NL8w#>}k6XgA6d}K7 zrt3c%rEIz%1#N#gw#j1&mJC*K!5=f|iE-MLa)W1Tv;|8V`aLNw{37v1OMQU9MI2iT zq&#_$ez_4MesWR|biI`*3Z`AZE7 z6;35l6amvl#V?^&%RSYFM+ecqQE3FY!Y3E4q0L?28l=sBute&xC;}j2mqajsM~s>S zNFV2k0Ct$*au5lU?TC|$Sv4i(*f!5hB#MX$6s3>5LF|y{?XENr5ERrh)cQRfS>~0^ z#L%f^lB~Ri&4`EeHGv`-rmwvSW zrM;H;@lhDw3jG>cylmM+l{$Qn0Fci%nRi=#Ue*X^gG-RBNZxrU5S;y!I_&vemC+p; zksXXt^S?L9OnAG`s!u+$s7M!RV-0yFOd&!DRE`O1#(ozB=D?sV><9ri!AXU9WKQk* zT3N9yFx1+Q*P7hQ2|`AIoV*&NohtTud@?D zH@!u5WY`X!0JSTL@XKiK`*}&b>OOa#dhg2C)cGI0j#tyTQwZqicP#LLz0dq&?|M51 zzy@w!T7+BdrfdBd4+U4f0f}sLxUsl<*6#D_CrALG2t5Dc#h7bE$we;5&AMdJ(_gjs zer&)Bb#cHdc;g>_2Lcv%=rGqOOG@(9FVvFcd*+5Je~m?B$7*dDoAoN@{5qRD0<>db z80;GxP!sUUy`)TY$+773eLkx3AFW>vsx4PBcUa$sM1OBm+RJxjW?2i+inEM~C#-7C zmNc4t`C=P1c()u+p|N>{|MPdxVA`D96w_)L?C%vRslG3Vz4^eMsi+?oaU@r9aS9#4+of!loXO&U{D`&dU3pALuytW5*w7N63{Fy0+wO z;HpdZqh-fYFjM!}9doVMqE`yLV7>2AlEZHmkqyG_;@I3epCY?Z>7m1W@ z1mpMN2I9x8`M)t?17>7^vubEVe!lM|ccxAZ+4g1wra%S5LY`qGgp%7-xN02Ch(m>` zm1#Z!A5x(K30(*!96v0!HMg7IGs6E8+GuUmOgLMr{2IHU=s?dwx9pcVdzvx@@<>eh?eV#+7 z4zaPZ@fh5^X3ECKA;iYEuiyX&>q>Lq^*gLTd%R8cudu2^7Hh(} z=6UQmH@)Qi`3p)l?1$Oew{-R&H#z(A;PdA?vC6VnIS(|yQEbKdgo4F&j$VBj;&bBa zfwSUY&&nP=9W_J$Sy*I7EjG)a`&CSEgKU#1Tk%%D##!%+>gbsi!u$0?V2~w?sDF;4 zGyQ+RvZ;sL?fZK&b1anS@An+D11f*NhZu|h9|j~2J_b6{jPFX$SfKhjNx^v|C9s%p%S4L$1Ib{p^bx*wCF7zP9 z!B#w!r=SCZ`g4QL(mdR*$&F0g9sn_=`kv548aaGI|6>k$z>R})xc_`!DwJnkgqPmj zQOKa<`EX(%swj>E>B!`vbN;fygXP=4YQ!@QsL$6wIKGs|0e%%Hi@<2=#VvmKe(?MH zG>8qaR7sSIp0{f6_j3_@566ii>%=Bru?NT!0;QwL{Tj19ro=wMDYT@&lB1D zE@8Qk3Kmjcbe;jm`xC=WI(q7)BT=Au37_oOMO>T3!r&R$kde;kYV1e;LGuVl7`^U& z3xG1bpyaHMN^USWD$NzOJNfIE_`HT>OQ{jy&s;k01sm+$s;vY1a6^EA8%cswG$>~H z3h9uVdP4OBzg#uj(^&gTS%U_l+=VZ+t7=mG1?S&VPpk^fH&mtNgy2(9FLV zAn=?BxY_Nmzyr*_*@X!`SYC8tk!}q-t^M#*b3Eq>r}f7I-9P;{N+LK&4{rv2jc;*@ zG3TQkF;+zgPWzrNP#`0CMD_)B|IuK*e7{h5f@uVSdH()XOy z+&pq`l{S{{iBx>XArXi=#xC@RxVR`%7R)#%==LM>c0BLeC&R_2MbFuam@0$tDQagY^w->T-Zu-q!NSi4{Nnp=WEGro4$&F#%~N^)UJ$mgGP(1DBvJ0q^neXfc;A za0sp9BnJ@p<_7KbYKa|H7XIeCp(3LGTDGp8YU>dxzr*PZ!SK??$q$_S-;ljqja~Kk zgx`%lomdTunll>Y=MeD8x}Xzh(6cCw<6xInz3rajh<)4GOwZchR|!6yOndph#GP_v|$?S$Qx9I(TmswCZ-?4;yEoDK>u4GQiVp zI{C@*P@ZI3NyMYcnYVl?`($8h6>&CC`~;d8As1&DZrN`xKL#!R#*nTck`Ji#dmq(w zU$F}d(hmz(4Rc4z1fnKo$LK%mF7(G2~o0iO@79~f}bX^htpo%!#kUqzBA_gJnHCMcP z>DKkdR(j1lG$Xxd`fiB$NH#cq&WydCL!jbK&egXedh50=7N4nkX|B{qi3?}o>T>i; zw~B-*B^*Mj2k^};<~7AgBJ6ei!@(vE;XkWiy`Hsk@;y{hAs5?}i(a@|4u7WEP#|_d zfINZwol=IISsjbBYy*Z0tVl4)4wRS>2MyK!!*?9>v8UCul3mS@oh(J1)rqYK;l#f6>Pyftr|YuKgFL-)$DC2C9UP!^ zS99Ujn?ZNYm>Ob#^*ZU^R_GIk2+#CHVn>`%{Uq0$J)qLujLREOB>p_2qjo%u$IY&> z8g*&(s?u1!d0iMyK63JH{Ga=5N%y!_W)Qc_#6Dar-DA@PLHY2kb>D=6kNkPSC(Ri4 zxGBhMVp7I|JSj9q=*3+~BL0_azIQ<7!F2tpn7FqHfHm*O<*BX+_gulhQ&eDj1F#BR zb~8pJ-}DI?ur9gykx|Pa}a-r6`AN#?|CM)QJA;fmg$8j8~%rrPwWCZjdI< z@p#`~8)37ItH+Ff5PREew)Xe&kdlcK66MmZzvns9OPj&0oj_OdDfye%`Ud}G;JCgd z!#a2*I6O08b=>Q;*xUVo9@fDUOGxIdbsqQfX$p$|uZcjH`j<5X*R)w1#CR3$%S<;} z=Fea5|o_{Ehj!VA;A{P+I8XZ0A?TE;j|!04y`o1%xMx^Qqj zjZDG&EO@YJ%A;#SGLn1^vkxi}1|#O03O zBtGmr8@EWJ;+151Hl9JB0E)b2W}_16;mQnwx(Z{LgO!l_)c7^HBoFS^HAV zX#|8hTJgkFxk#zll4b&Z9}`WFE3VfYjDV6M&j7V{R@2!xdr^=3hQ;=^>u{@FUQrsl zz9V9mf1$mZfhwZD2dj;;6|B(9o~EUq8itbV<=BI z-v>DfI;Y${$Q|Z#@L_N%QT!(acqhkxl&b?i_drj?+`mJsPupPEvU0S0ynmxBBjpdN z^h4`W!b=O>4~-E=iPxqU7Z{f=@_e?_6JP+GBUrY0odkZe zsPLj=ztETK+p`8bjT}N%T0wPEY_aVrheR`jqrt8{t`{2*-hAE|x%@Wc?%{EvzO$W8 z0g6*I0ap4OI2qbL=P>TI>ysBp$Im~eaBer0;m$QgF$odsZly1z`fKq-A zKON6LvmtnLG7(*iH(tvf>(g~rnU^?*R~V@Mu|RuGrEcv+rMCKgVlh&H1bx+6O8{XoGh2BSXa`hwd+T^Jv=`B5Iz6Z zBs;yi$kUb0HrQ?@?eo(DFNqPITOLK4d2S3nx|I`Ym)8T5kKoXsTWI#dnHZ=jN?X1)05` zo*Jg}T-jAq7n+6E^ovcd5KmiPt8_6)wlTX&68ohht)`GhIaV$18)Mj)0yl0`HbT`W zYQh2ITK04Db$*`d&efPmaKHed&{rr3t?KTcoxhQp3JH8iLY7e>G?k?70mW0JVN+y> z0p!8z&hB8UrvIiub3;tpM7=;Ghs;{Z^P)zMdsWg3yt4fWV@2m#h~fS(0kXsBd#d7RTY1wQ4ZpJ=9RMn zbKbMVdbOHCwQw3`&e>#~hX5|sow^?l@EPRL#Yx*VjFoHK@;+{M0t3RY;cJ=CMbQFwbR z^Qzo|>|LYi68jdFwdR}PFK*I_ZL0MRa6M=Fq77tigbLr79;^~QXQ6ujz@6ud_b4#n zkQbCP=y%yt!(-@}7}0{idyXh=>+{p)MELOL?}>&r8YNjB4k*-Q21m>*S1F_go=4={ z)GkWTp{7ZJDaiaEL95aQxP<$gk3aNkJy-&)y&N3Jn*VS%sVC1hujZ#A2js7oiXVf& zGwH^iXK&Lu_JgE;M>o^~3C#~{$=}Oi&-M_ietw1Jt+#xF2m!`-Kqp78{@@zw=DW9} z26G#nXEq#7fZpT??9E{@jy1g#+a8I$Dqn|YK__*y7H21vqKJ7hhp@*2o|6Qg)49|R zuUQfCOhwmz7F)Ev+xLbOHJg!vjNRmUhV;7@@>E{9vpPYo{_zqhK(|*8FO!bQCvH2* zBc%FG}3OYSnt*lV(VYYUuI$BCa@!Mp40AQ4}f173%gkfIko!g%KD z4`0n53nSSaSo)NPxPIt)W9VeP;OhePU6FB(lVtqMy}Y#9;xpFP@9el#D<|KlfDX&q_PrpIRe;u@LbbYl*VBpLFzEVf|qw#HcftfaFtLtOI_KLzZWU=C+`x`{e8|09W5EskotmgN2+V`Y&e%4nw527^yN^AWmwD{81yhZCbrxZQ_&{eflr!bU-W~&E$^Xm&#g5EiL zqIn8Lx6UE$hB-3-#-eN;>7&!&|%Y4wW`8~0hv=*!&& z=g^OULs-E)=jzvoskZ+6LY{r4YJ7^rGS^VKD-%F!-fkou^8J8{&itHRli$$d>H4L* z$c35-`LeZNdJWKk(0V2-Rc70i9wmLfVU#l-rq3{Tl84=AlD3&+?Pk9`aU$j~QL?+U zZ5|Ef>8exgFYN??60b*{XlYj+l|=wH3o`wGA8^NHkCkX20kee!w^B8_iCmzKc%y7s zcv}Yb$w_cQ(1ao_MXw}&K2Q6|(vV}yNCQs!;a<*mz#pu2b06!s)rG+Q?%3_5a4vyX z5*(hD2_v7RHHk4#2QF5oygsFTOVod?(F18fngtel^dUJsO6^038-g1?nx9}!&jf59 z>1+P77B#7=Kl7+A!#M%fQBw>7E-#Q~Rrb!vuvlv&abwFg+lm^$MIN*$TU+Uie8$nf z0IKsTgiQ!Fp>YDz{9yn2;_AtKZAPK2E&9rE2cOF8#Pyw&OSPEW8^(e`DesRL23P~1 zOjYd&>htFtrF$$Nhw>m93&hE^$Jue76W?3Zh?)E)xvz!Q6uDJSQiz>=qf;J}ar&(b z1Lckj{bmhQKAaGSa|ox<>eV1ZXfW`?Ar;y9aIQZ5$V3@EDvZaG%r@gJ#)J!mw=SW+ zP&G`Ho^en-4W=I@44)2j+gjW)UdVPkK67^YoM}upplFO)sz{sKfLg~7@rlC{b zrg;^+QKjU4X!`8+vc`x6ho^JHs1)`T5mTVHf0^t9td6|K1F@bFqYRkuch{_d_{Z?a zgZ*Q|KDQuABLe_^b$;*$ff^IbDF(ZPS+7K$+Y_OQrM0_Hek|`;Pwo*JUSk2x0Y?BZ z)uskT`6QIoaZaINCHW_#UtA_%M9`E&d30npJCRJ2rvlq@#~)nmYzHLdQSMLCv!0|u zE~G*(ry>l?o`sV#A;1k|j)n%O_Vh8t;5b3?l=Crgs+xD+!Y7<)+0Xk{Ew0rgCzd4o zySO>lchoRFDO;c0Wj=OIB*EcA z6oO5X<*ZI%_`jInfUwl;fXcfQy6AS z1-WGTt#)O#XU_txzK;}ZFSX3-71^3$3{cf?A)}RhKEL0`+s<)HzIO7*W~X18BQOI~lxDroV1p<{9<=x|A459fqJ}`)Kp$El|%b? zS;V{|c<4>V`nugkcf?w9pEc%&s}RAB%08%n1KL15kLBLsEF)YA(F|67tAtyJM~Cq| zTOQ$tv=*EN0`XQi5mdyX!D_oQ;o*ze`-%A)ow{jN_qvs*?(?X?W(`4c>+90vxbp*c zVFSUl-`2=mb;PD^5N!OM+E~aw2P|WqOZ$fN%5SO3H&MZha;Oir^T7Q=t^0+(kIY*X zAjo+bqWCen<@|koRoUm|L}UwyTEA5#4~$o}O!qim4iG&sDExC`fwRjL{5iK(<5WrQ zG^FL>C+JchJy^4VUrSKafqOWM}>^3Fdkj(;uo3+zfY-i_n83o4LiiAFgu9O zLf%h-(I#`+q=-Q8JWA?GMv&v38h@nx5D&ls?;;5*ca5T?dMED})tq%2vf9~+LNM)u zRE;5~fQ+D{ZSOs8$!g)Xz4_Ci;d|59&7Q*TL2rqL0#Iej8(F3Nr^_+1rYuL#8;6gF z@Xf3~!MYkUM%dNpP3(anW87|oM;Rhv0$lL0)E=n)m7RF*5ZJO~yaQLRGIyhS*b*Hy@mR3_rTnTy<%4{6#I))N$_<_HEy`B!kjMP3n~N z!%%J(Bs#uh@rXNRq^B1f+36+*lKpmjxuGE%n2!XGRKj;4fxtNMD zQryEuxd`8A!<2UdG={4Xd4z#6p^Fh=I-@z4H456a0Lpegc)#FI70Xh-R9!#6)1u9C zsH>PaRcX2&W!hQV$BoYa+=WYmtO77`+m~*sgWd}GUY}jN9t;wtqq9-70+1=plU5$? z$;4S_!-kj}L9eS_>+3Z7?N0P4ajlsU7O01>ac#|h3$OwvCi9-?rHI1P4qn|j@x#{& zugSOHHD=CpotW%0mANm$3GdSpc`)Q%D4)1DIKbHIfzvosk$Ap+KRY<5Q4`YhfkP;G zYhq%uO{vpp??DigoHF%Iz&6pMT$Hz3%IJy&x5}^=zSuyN;&}_7qCidV`}xlnn9#{8~~Nsfv%D1NGQVhR1FKj-dAN+paIFw z<}u_(j(XsG9O@&A^WXwtZH^wwSo`LmZ=6uwi#wx6Px!_J3z-qWwrwT6U^u#@&EMtxAp^ZfMF)LzuL3 z*}bFWY2Pp)u2r0q?b|v%qX}K~jA8GZU(h(US}zErt2ePim9o)xJEL^8Qpk%4g9J%-xOKH*DDU*6q&(xSQ~AHdvUpVz1C(D;iZVP7f8@+ut5}e6g*mzy zXZMEOt8PqCH8}?|7G*rmD3z-lelh#V$J%xm(B9;OU~=D0E6jbn1?5`t<2^EIDEC<) zsi#l?a{W}plcM=Bo~gm9r%Vl@vf1tra}%F+FQ9%8j7~L1Opb6qh1&*t;oA&)rY4gG zv$*(SpSJ5~YuBceEGn=MOLK2NW`X2<@Qt@dnO81BiC?rJqY@vogN(BSo~Ch&0W^o* zG3EACt&eG;GfVFT2tVF@qXn$C+Qgbitxi9Riid5E?3-2mv4^7`lrnFzVZKx1u_Ueu z3pB%UdPM}CEiL}(tm6SfI;3)v)A@Srp;6K`JWQ%-J5%Kx_nB0Z+eMNO%djaO>`j`g z^aZMVDth`P$O7ua@&QX2x*P4Q6yOMlP>lLqfxr7^H_a|=q-0xul~-}?19&ihGVeG% zqCo8z^@J_G7HN`wwjj>=@nv&=#VRhKch{@8;4;?|>cNIy8=F7b^Zzxsy$g4(*T^xIX zKt=o;HN%dmr1**76*}wJZB{;w?}h2A&4mMRV`~gWGPd7H=euvF_Hwnuh>d-?5R^?) z7*A1V+ukZmSyeHs#L1qh`F|ujYSRXpGhs<|>%p!>WzqR_JkQ0T#~RA7y7#>wBH4WH zl*z3hBWEX`E;vc*EpYPd+dOxiu+e5YmXQ>4Cjm zWztCs5CSyc+NT@!Pc-|I)l0Ez#>z`9121;{5KBnJOUIK;irwk?1>E@{qQ+>FvdgdL3TJ9$<8$q;aX$MT``!W*#m61iwUhA%_M4L}=Kcth z{C{Le$~$rJ+qkwA9g^DIFQeE-Rs(?~Pzl8Cn)X@uIHqiq_n&n6P6Tb{{S6ZD41Oo~ z0hc!R2h&BGwzV0vi@RZZhC9Y$x+6v$uFD|JL$0<`%fxNIjS%GIl;PS3L7_W#c1 zB>LHkKZNlVf3bn@^q(*iNsu!;0TJiM#Af;XIh>h0dTKt(~Mgqo9ciLqX0OP4w{)c^C z`Wuq#?WcDdd0?=K2q#vOx^v_pmTxa)?B+`w8{c`_|Fj0_h>d6TtA_3L1}(2d3#DKG z2Wtocd{O<>H#j(mrpxV6@foM;))m2{(OeH=iFfDN9cU%lWk>tL^Y@8TR>`0I-s(T- z3)JI0^=dEg&Xo{^`>@@Nj^5jleSa{F%>$!f2?9?_htUnk!5d^IDCLqlL-)q^iaf2` zAiq@wN$AMnV`E$8g|yji?Bz*T(Eiszh$)HdVvP?on^y(g&|XzW#n4*{(qtyJuvX{7 zP?pj|AgpQdCC@TdR%|pQ5`bon=a#3Q{1-!)-eD}XM8N%i^&*a6E%+UyWRUTLU%#1Z zJkABfmafuPw1D_7SIDX;i!TA$|4SAD68#s+!%+^Dmk2}G1>wTz3<;qu=3}*WAh5-6 zh%~9?lrplO*swlO{ahhpir&iNSNnf+*Zl^NQ>bdd$%;lc=ZhtQusR$_O+7;J4{q8r z)Z4Mm-7OXvT$W9e0SQUw+fv|O9gk!jhJTnlPV%E#UwlI~FMociX0%xd1jioi5li9k zOg~@0u%32zge7au|D`6YzXYy{`D~adZc-sF%R)}A1yLojB7TF)!`MYXjplwlDy}<9 zwHbpzpQ+cX(e+EMvW+0nmROE4Q$Vz4x$J4;oYdHlVj(sSS2;k8 zk7&x=J#vgoHrK}ve`4ha$7OeOy!Q5zLfOD}aojdKxEQvOTv;~JOPuOYbD0WhiXs0> zld-lY#a7w7Syr4UsD>SarXN%}w%>{cG|b%1*~``?@;_|TLuyTXqml2_51Q4v{^g(h z_>zwx8cu7B*4_YY;f|MmOER1svr2BRp(EqOMrw^BteMZb>sAE?eFkm`UZxFvE_xlk z*S+2=2^{eA0+d?W*%CW{ACGghl&Ckhk;(^Joz|-!Ay`$LyOoM7qDN%W#C)opV;GOf z4+7x`hcH@q56d#TP%|r6@BPbohYcDOYQ}a6@)%U^!xN}1rrTgmt7>4MtTkO~Wo@WM zAA@K-78LTlB4o<2%&`>f*V1E}QE~NYN=aK~_xsLP4M}Yo66R?VY^?5QS-YIeWS5{q zbFTK*%W5|i2P@nY;oZE)RKsz*M4g!7XCWM^x2V%AFl&=zW9K`}+?$^L_;rM6_%nY! z(uhE2RjI|?5Y%3q!xueYR{!3r)%V*5a4X2U5Cp@qG5mOfPBgqBAq`i6p{wr3|IDsP+O zzCPo5yoW$vdR4`r_np`HQntYt8nws-s5Z`gj^ig zy??o8miRGap@u|vR@2K8l7@aAyvi>4lZHw)zb5~hSA>H*y?q;`L ziXOcbE8Qd^R9gBwe5ksZgGw%7eQ*kVh6)T&a-CE?}(=H{36@&WzuJty9P^ zU666@3fAW{ve#c&t5DKJ zSK}1^8ta-@9gLDcsL~(gWI_<|Isd6yOyLW0O8O_q+tSPntU`@dp3ub?)d|eU1%7q7 zy=(zlq6&Ba?wFXIQ5F$NNUbe2*muoqU?KEdS<5VU{nu3m0N+}^UDHS3vY+?xj^&h;W=Vc^hc0Q#J3q2@ zVb-ye{F-9)nqFg@)0Hbx@BA$7wyqXN&&Li*BU--eV&Ed$=FF+y+D1>r4LZ=~Y}j|X z#K!!#6mhKOSqw%H(PnNtGzh_|0E&G9) z=L3oxDHMn1S&_1)bB2yv?Ka23%{2Llqm3MpZooDkIiJ2A`UE6I#%h1T%)hZL6}JmJ zzG$YnCrpvSwFCF>T16$ebXkgJ9q)=8Yb%13=JJz&w1nFuY zQW4%Bv!rb}0`X3d<>GFCmy6eZD{#{_@t1gI9Kvw%t2RCd*%2F+ufP>{2!*xSKI%Ll z{W%JSk=Cv+(c=))){A(ZCZ%CpQ^TsKTWSJj{E1hcaz{Ci9s|XmIn2)ciu^5VMIGU8 z59r~DwArfu67a^)PqIH}D5ZTujSmCSn5mk|lw#BA+>yGa%0Hnc+a?irUS#|8sevNb zG%)jn)TGFx6U{ksBBnyQnHwy9OqHrcb|eE|XNdz#0$CFXG+X>=t zr(2Er1(XF?fv;Ui&wr}ZNA2L(KURABn(wD*o+D+H-Xy3M>2WX~VtS1)?uqE{6iXWP z^nT~Jy3ecmy|=@njJtmr_aGj%ecMmlH3DWg2_B%K!xfo%oDFOH*g|Cgo0qR^9w%{U z)jWvEB-d>99E2xF2W#p-#R1M#K5{Z#oUt&~Ad44@1fzO+YxQEXG_8JN*Hzs^bd^RY zN=Ge}9Pvl^@JmFw*q(!|n&6#umT&pVs%3QaZ$sQ$`YlkcE#lq$M^hTd*!yb;spM9h8xe}xVM0CId0KD;Pn#V{MVRH|gPV&y&B*MV*l*_jK+v$%9 z)GfCpZp_p7U^wHJU~r!y0lhOnTSXmn{nD-P7S$ce4ILJz#l)1&nTt~84$E}qd${gT~eq#j4DE0g(*h+Q?sR*pv8fYRD z-ipQF=0nQ6YdEBHZ+2WR459-HgXZN6MLZ&VF~JSOULBVQ?ktQsPy!9c-ylrizBcAv90-(1{xLp zTa5h8g-PfVC}Gwwzy`F@j=TKnve_KMofoWMTCqs;pr4@)KgT)D$9YeNX{SkNh^&R6 z=S%5RB9TT3)j@RIfekAN{syN}Tr?)Q99*&O>($HN`X&XG&Z2U}O?m#PZ1lFdfx6HwKccVNDUSGd&<(zm$ogQNyFFkZ7203)v_FDg^kX%h z$V~n;yR-d3!{VT5VeL(0tT%<4!`xu0N-j=VLAznefVQxqKh-1QgpY(PnpJHyhLH&p zd3Y?$JsmyY6*z^&1f_5Tp1cC9FPs=y6@n-7iLivG?VM-r;S!k%H;mj`ej`Zk78{ln zV{~hkP0U!3lPJ^p>s0ypP8gt}pGDTIS=LU-knb-YIkz24vZjV+`VbWb0c+A~&aGYp zYg^dobV%K*nAcoepTjD@0{;Jg>VwsdK{o^f(xq9=;@YAII2!Eb0BO3BzmcD)mVOmr+@3ZAbZSk;0nUnD+1vx&HtBI}-mV8m2zyq8J^Swt z{I0LY$C3yaNZW+svMDMlcAGhlob1uYg4Q*s>}k{Cl&L#L>p(~XVoqDSgVIE34A`dlMhyIY2*ztbg*QFckJIZ&6ucR5v| zhT)co&0obKOnzW!Sxu8@E3xE>;Oc7_ujvVLtr@XJ^wD%{iBx->%?<4{8i;f`jcu>w zh06~k7A|k)ehw?czu!vvdDBg7cd6pQz?YJc-wSxm` z$P}M&LSJZdYB;O0<(vhZ%p*=}msm-YYW>2|^K-`)H|8EsWtpSmA=VUYJKygbC>yyj zc)cZ)27$-5Gu@-Nh-)rWkE%Z=iR1$ma2=LXoZBnD@P^}1Rw2?sSIhr7*N3O#Di%-f z(iij;uFfg8>GkD~iV6dI*+wqzbEjPNjqLpS*@}}-^gwG{1wL-oMC38&nt{+gTEq~b zz9!QdvV1umwa%Gf8 z#a@ph2b@L*6zuU;Y_39Fk)!sH#Z|Kv(NimAHb&nKTz9moMlDX;ONum{0xyH9iNPTE zJCU^&EtQuO8pe(oU!EYUv=fvt1_jRU1264?YsDYtXNv<_u4vW7N%lCRfk!FZ4`<>8 z>tm>DG? zDK6@nF6Y8dvz9-%-kt2)dNZbat)Ob=S@6aIrs`1IqeH26B|LKmuU-t<4JM{Lq+y_~ zT!?Ch7p0&oUB5^($!V^4>Rqc(b(Z2{r7z-=X@X+D>w0E}{*RJ3Wnr32YKrs({n*sW zcIOXE@YGJ*{IR%-@(|Z@>Cs=VgHKQpQ_QT(a&ux@^{DBhGPHH|Th{BxG2O&-Zu!s| zmBd2a%_VQj3%oDFb7--lgT*UerQn<2gP*p|81{Jyknz)=-vNYAu9;G1SnmZ#wYAA# zq^h4i>$}{QW-uNlvRpE0CotS(Xb4sDWmb-z8=$TKB!kT5k`$O#dfArLqOG-PEItEH z*Pr>RN{FXCE=e2=H+=86qGVd{0JXbb5U5&;RXjn71 z7vM76=b*<^(sa2}gtslLu^QUHC$Re=%rlFXH%uv_O!Fhrw1e+tIv$HHG2(lV6DcD=jLVbR z_05u()e3p?Klvyoc6EeawYFK5fsA)Y1^SWc;xAWLW^mpPCl|2wCT%4G@^SjLDjJv4 zGST%&L__j|^8MAHW?pb7jo3NJ%PlR()3Db^S@^0A%VO_g`BY#_tyuhwJ}5Sq)whu% zpDf{Sd$toxb~^f`bjcEZ|0z*Rypob)u=pkayA$yK z)9OZ{{4sBswWj=OA-or6TCNjP8qeWy45PtGH_rI7+v#T6ZjnQh)iJ#i{o@*v_L91d zO*Wr9GV3K<{W?udtQr`7wBHrO>sFrH;EV<3UsUfOxGN~SQo?FCMDDgFLHQ-Z#Ye6k zvn+CYrK&d475Qq@sz|h4go+2mKRQ*R;l1+$ko8LHm)B6GiLW7opcw{qB1Q3$aF!Gz z1NZvFsTgPSoOA(5Th;gMgnUxp5NWJd0gl)O((7LSvGNjf_=nMWP#e@1utuLEV?YXt zoyOTx^R`e4esIl`>2gzzz_{KHn+ugM@q2;vEP;WIt^6q)7V;x*UXqxr%Dirz_33_9 z{f0Y1<+6;0k9n9&!9# zpQ?E3iZOnI7{Hu*lYXp}^__dZHN>yR`+c$kyvE*2aYtldPW3VpS%+un_u%r$-xmAU z=qCzX)8%olW@U@gN(s2zQJpD)a!QVuO{xI;?G^VFGKr!B?)3}GzI1!$r=SWVAi?|- z)iO`s%$CK@`PTU;>7L2*J<{b`zobzG*!OduKfqbxi!s$v9jvfE>1=i-DM$&;QqB|l`)WOAH2kPxht{EAB7Tw1R;VP=29oo9u?OL;8JYK;z2 zfsGecPZGL^qxBUx?(G}(uq)x{-L&WI!+0$|bal&m zYSP2IKJpBcy}t%gYU8F6_6Qub6pFDq%Oh5_gkC>K{4yJ8oAdeNVmr^_)Wsb6@)rxn zezM+ZB&Tb$hTW@54Rq7&VDEFpy9iwMx$=h8k zZ@X$_C3y6bTYYh9bkR<2ex`ooZ%6k?2xZdEkYj!VtI}R|lA9+?NIAn9SF;53|r2$*> zi1JBt9ad*FjDszWGYE{vh_)~GXf|ij@vCnwGJQl_&N#_JtM#_JX5G?lCM(S3Rlu`1;lax~3Q0^pE@7v^7b7wc(EU9X&nPbJlaH19 zT5u?9`Hb5zQx&zKIPOy}dk?atHN7&k{fTR?ZQTITs*TjNimCU^9j1Iem9{+`(Iq1C zVQS1pp!s`~SaP&+Dl0Tq14d*L#5JtT?i$x4?ewsAp=?l88R_bH{l0*l?4kfgR#)%u z;~Oj@bO8S*;;G$!A*YWC2w?CH6%DGvfWsDRRZh=(jbqMrYP?g~WTnd+<>25SvbDj4 z3l=NCL5@u>a4ziVc;#mq6!zQ)RkVD<5=b&bSk;CDx$%NqkXo~_Jb%t>@Ny_mP!^*( zeaX@RqRmm1B+wtIxQh+toZTtmgUP0Ivx1gd^_JaTT!+&an zA#RQT_9Q$nAS`_B{~-?b2jutR`k#EuL&X)#LvImO?c>nicO$tszrhN#SiNGOklGh2 zC#G*M&Tvk~8WNia%?ZmKf)y#C?Tm%sL2laT=7BN_lxwQO`NN-rZyn!GS+wi+j!7r; zsQ4V`2cPupydbFV`H5R?%9EDQ&y*En_{a3d1YLWIYfB#NE`R4Ysb{LOWm_oX6)CI% zt@;RGbSJQdIbm@2*m72?DIuCX(rI;?|L+WYcHg(H%h*L3Sg-Zcr{F37uOLop6BwdH zebn~!PT;{h?yc;+1)NjAo4W)0?J|`a3T1t2T$2+lP!G#+NahBRbq6*|XHDr#j5xh~ zgGCL9;bP9v)W|69t*k|Z_?9gPD7LZAo#{E*TLkc<)rt4XZSQvBc^ z$jzPq5+P1BO5kA^d%yZy#=RMkZ+I$Xg%Wu(etzHB~+#mIOqk%84;q6aVJqYYjxC%5Re!oXI76q$K#}x{cyS zj&#{s^{xW3h-AQ%;236y?;c5BJD`t?E;oW8OY)G4IGw5YVc) zwT{_P8(4~m`_C`F{)E4NcFfS_IN$!;=yeXbB`6M(eS36OkJ|Gt`W}>yb~X`nY-kq8 zti}&}S8ZKN63x{1E(+WXcJ@I;^o>$VT|2cfDVse#r?Rl}KiP>DPXp`Ak#!5V)-JD5 z2Hy6`0PnydOz`Ad_d*}yK9j*;rTiq;9gYHNv+20}&&Wz1(lR=QQ$;0#2&;(;O5>LX zpsT_Io6jSs-m+jgD^YqZY>Z`W4E@_F;@c#Epx4)tvTycgj!`6nydEa-1A05Kx;1$e ztH-2A!sCJ4TfqaA#K@@(fsPcAc8w_p0KHCyU8=A+X`w*Y$w?9r-Q45sCqfE2ww6jjmQf z38SGrHl89%Q~NvHKfDa2SS$Caf(Rw*sy4Z*gFkLD6yE-k*oC!YA`^MOkF{T|Iy69i z9oIQKt=6c(8n|T_;P_YLb=4~fNAfBpb06EjBmZ_Y%B7(^zpkTN#j#crkMaRB`3&dq zOqDf3wV#*VS``KR++=yOPJt0viXd%j5kDovD%rA7EML67lttg5`5-4c>Ii~;`zr!% zcWrLM{pRkKq)$8Sx<#tCGjYS^)5PiY_<(p_v$`d8njNh?e zBQsYj$8xHJ7JZd9JJlRBh)a8u)kbSwm8Ljn{jdN9%zUCS$it2DW0>MNH2doVz6|kB zIG?*3gz*BW10F|Uh&};?wUs6JJgp0;Sp@1f6b5Yy(hd5*xO?xgrn2_$7j?vfh!sRi zR8&NyORtV1A_yudA|*J8NR8AGNQi>sj7X7=Ku|g;MOr9Uay9YRQgQbG#>0t6Bg z-W^7L=Jz<)d!6&1=Q-E8PX0|`@3r>YYu)R217eFGJEs`2wM^DNt>yOB^ubnA;Nrn-8E@f8|a zmlLv{nq|*LAS&1cPUW>l>&Y(QiFHa@eUr2%TC6-ZtYuHj2LU2Ukzz)tst&Y4A!9a1 zG!8GdaFNwGr;6xBn2bZG+gaKpdEL*8u}Q-ijr?h!mF1GjBV_f5OC`H_*qb_|A8X3e z??$93s_eSU>Uf8x+zzM0Cf|vT1U!~lNS^IKcCpDA6Z}Vs&8kZD!rmt?P9o;2IF)@Z8!`zOUKX)=~IDD?v~9kpN<>pD^)mz)3vgmUh>FM7Z~+QZ2siR zA&#-;@c9zrd|+75Vh8w!=2bSyqg*7;3HPqYwq3?_V7%47Y?=ll_ABC+m#0mtZ&yb z5=} z0rY#!N}NV5-nW4jTHuj5^QBzsP(1Olu2&@ixXSY>i)6V&6e&V@+Icjo*deRjDA}|f zjlG53$XH1VdDnPKj_;N<(b#N9~AHZi{|bKkDjlJyOAne z-hlqLi0)J=J*M=>iuUZu+J{?Fze|!=j?|3&CWI$`UK#dGDK1Sb&*;{2IA5}_Ysq85 z_lVw3aTV-?MW#56VyWa*OTULJ;s2UI{g5T#yyfhz0z)$gm{5cHU0ZDQ`E0HH8_30> z`-f7zWTGcx#@UjLk_NfpwjzE!=wzO{5wm7GDK{KcrL+464i0{43mc!=~RemEx0S+5LSg!i5f+}yfJ$h_RVt35VP>luOEAa<25}V;sjjlNFGegphexZ=bjRtS5DPBx>vhk{l>2$9w*-$rAU{! z>EJX~bJ7X>8P^z^>asur>Ye%Ps66zA*Lc~0{L)XJT2mplI{FMt{K_Q8ccD3pH9Y{I zs$C3ip_3p_(maRV=$L!Lz!Vo`f0X?)=~?~BnhQjI3fba$7Aq&Un&1#9d6*q2zm`|t z6A&hne30yQ4K3-@{`EXrp|&&)_~W)UIcEb;jAVWesTg5YZiF&zx43y{DLrDZWBKmmjq2Q)?s~4pLlid>-j5QI14-{qdQsg z#ZY15dG;gvsYXF-kWEPSBL~nk>J5h^m-jtNFtJ*1Xet^L#90k@sSsPIRfl}Rzpks?YCOs(X~9U zZR#=I3-gTFRl#_gguaLU3g0CeVRf5?xHTBg>oDOKub%oEUkGwXja3A>!95z;B3hLE6R^Ck}^r|;iqUIKOItPG2BmZ7C)%O>kv3WM> z5;<)X3_9|r?UHucmB(>rrPn{5*!`lkjlI9L?)@&N;6xW=K48c2HlEI{J5`qQa7Yec zF~L1`3y)eG&45h1X3vO*T;^M_k>H|RKp|>d)hkOLYfy~_J}Ep-uV~AyvKIFKT4n-S znazuZegyIsHP0h%Oe3ry+U6@mE)cwXbqWtp_~p8gfZJ>_89TilB7Tk+8ATwYy5BC$ zNDMyC^QUvJ=rB(Q&KHDmg2|dA;s9_(x7E3vJHTZ);J=74c>$yXaP0#=yjns0H6Z{J zF$SpaRQF7NgCl?m=k69@W7T9(^cFIb^5fTej75d3HR0cXzt8peRMm0b{q&TKm9A5% zLOR#Ro;+6Fbk}=~(kps90Pwk+7Yx}V=9)9h`)Y@HXsYQ!OZQYL?_V^lp4=1xl>uYd)jNtZ3V4;uPkfX94|x#F%7PTtrS9W5N`c-Tg@BVFCy;Ias%O! zJvB}?pL;*lh_c_XFpPzckX4u&pkTnX-h<6!cWL~xkp$IBfNH$F1wrg2uU$VuxfTSq@z~E$B=r2QW+^vAD z+~(*x-;D;6sYRg7^ZFsr2@;t$J3y}TpIQhYaS+I*ECSGV9VeA5vNScO$A_wpVl>l$ zx+`14ee0L4CCxK)e2d~*mVzf))F99G7b|`ny*JbwPk;4&nHPTxd?}(k%fx2iY(y)g zA8%cofg@?va+28Y*cYu$iax9!r6=1Q?_ph08o0G47pz1fYM5ES(Oxa|wOHxNFWF>% z`d>{eyDPI!j<+=iiamNC6C^$@zT6NmIn#l@5>7fj8^MjVv!OKU88$nF_NDz9(L*wKi-Qy!(K)ThLyQ*D%yC0`Jp4Ua)=qb}|8F zP;@+y6a!2bdaDJ&tM`I;hIP9YeT>fnJxC3d4CcRdKRk41Q`7$@Mn}}O^TN}IuXOhw z8RU6vv_qt~5wJ^m@}<_k&efpqceTeOy-7m9H}T0oytYH+)AlbpGRx2INg}5YvlU8K zMQJ!$HA?Tep%Pzbmo&|&-|`gL9d)dI013vvXlau7fnF>+-p6ZION0AOuZUVn0UXq8 zr|Q3&1O?=ZCR_O@Dw9Zn2iC;4y$=@1Fvp7rc3vk4piXUw#n>RTgXNIN_FNw<2Vi!T zy?@u^y?=QJJm$P47?AQD?JtyefNl*m18Vi9k^j|{G7#tdX5roISA`)B?kb*BdlSa?yL{FdjFVtY-hU>5Z%j?K3w zU`PDsw;x$|SP}f!ih$7(cg?{{)U9ovAo_#uTw&DQDaW z_qp9pX^-yNNDf?U{k`X=F%cFzx8+u#`$i26u0@d;wtzT(S%58%Q3og=0}wvgZO_)8 zpay`=SiKg!u{ZJa#sT1QHcH^vr}^I2)4Z;OPt>&UUvwbh_i@+J%@=f14)E4PuWy0ZV# z!Z@K^S2S>?`6TT;4`GDKrZ0qCMUmtG*^jw;c2lV&fKh^vTTi(>O zjQk^KEy+!H!~fo*{c{;4A?rs|>)e(-jsWiKJ9(`0DW#Vc#3!4Q${hmp+>O8wEl@?Y zKlo8yCY{?N9vyD*`>id^om>r#Y=w6qx63{n{1ZiQ3Or?=)qhk4{Ag4jAhxK*Yz#QG z-9kooAu&dQ-rc#y?b&Gh#AY|Vk@NTlxBqAo7wsDiAhYlHwjEwXk@KVKEb?Qhg9p$rFz65zPaZxx5kY03noEd2oSo=@Qw5}1cx-T=3rL(o z6#{VigXTGhT#I}J#d}5oY5S5WcR=hpwBT2Xcu`8>6}g;uo<6KPA@@^MNV5UPI{&%V z$@QB3u7k*pLZGxOIKt9&+bjzx4qiuJTO5Y}(rt+*$?RyyAFDPFFw{|BhOKqxyNt6{ z0ZC59y|8rxzsoq{Nqg9g{en((`R6c~@olPwfWBu1PS>L2k-uEu3QU~i;R`8N#JD-F!jzQziKA|s63pb~WI-nbzX+d! zj9XZLnTg86G_5)9XdT`0j%$vzuwpJi<*W*nT4573c*o#in^AA5cbl44$=@T;XY}pd z=##Pc+e^qH?NmudasWT^H``Y^CRW@qR98TyH>`5~uCU6?Df|~xK=!1nExF-Ayb8mW zNsrlB6h_ur0P-4)e=KJ7HD&8NB@&G4EWA$WTf|{mp{^@PKtojsmIhTjA_o{IyoEbXq+dvInk- zM_CD2I>+Os@J3bxrsGo}UdySgBCUq_GT*J{^XQ|$#kZbHqrlS6Pyf+*ft!`J&3S6u ziT|E@3QC_2o}T?wCcK_^UUe{X*ZG{%k=1qmwIw~BQ^MB=W=ygVizZd?OPsUsF7VqL zb#CNc9j1^PlIZO%JNSkfYy4OXajjPpG(MwjoS0&n;1fY}h27$R>Oaal>G-RtEn~%S zp=QkZa-X&x&7S|Vk?;T>EnWij5+Gdcw4ZvY6{lMs4s55auxsEjurapB zM-2$mu+X2Td@fCw?AyLv%s>Fe{(-_68Cmc-A>3VAqk)>UF5tk6{h0NtHk-95SK--0 zo5j-J)A^A@nbKZv-cu!u;mx>~YA)kc;a=wI)p}IjyMfPhQULiizZzD%-9!55Q%NU4 zRVKPelkZL3?n}L`Vp49op1mg}N(ybE(}%SUQQj`Y43r#rsh~M=p)x7uN+rtqo`_Yy03D)Mp7!NDjJcI$ zu4l3$k!qf}g7S<|NN%>vbDQ!)mA=cFZ3B?98HY;)89Dh?8y>agOvGKREi9R-$X0g6 zPV2g?h#OCbBULcVuVv+Gc8*K|seBSAPTO&s(psdutwUkTmtyS*EaYiz6?$QH z7<*G(@`=dmNU(~j^PVWBhCOt@X=r##f|;p1+ucgZ4ay|SBWeGz+JIaK{b$_9+bVHQ z9q7WijVk9{_ySsNvA4=%yb!W(Ls}>3{#sbO5C@dJGJ9pM&z6Ozz8X5!x=q^CZMghb zl^WMf*TBBBYm=bct#bgmIN2IFlaGZh>sR;k;IGImZFg$+)si>mukGWE45oedLvCVa z;pNM}rfu9wvbc8SW--FJWfw2Q%_MR8e1u;SxEEWs6!b7f^VLF`E^)$h*~ueO1?iXn zbuq=z$o~dDFLmSkgPR%QT@IRnMWWB^xpb0mcaTaIEM6!@DIjd-Xbe@; zU``>II$6O*%vzIY+LmP_*NrP12U%}x$)tmZnxCo`nPBg0p3F4bL9Y&t>F9@Mm#0Pj zSuWiz$x1RC3Fg&41R#b=wQCeu_IPTdiez{-7khM@q&wF*5=c zTEAXX|MM!0D7weM!EG<=GK2Og)0`{3KZwHUCt#dd)o8t7pMkGsH#KRya#OIz&O$me zWud4)tP_;a%&&yTw^qx6kApD2v9ph6j;fFM?--9xeUCiuis2ZbEaN|{W9lYQ|~`fq}_p3p9h2bbXeMZJrb7#DW+;Km==`&cOAE; z)d&_FFgI@|Ht22&2GH4=-5cemyBbiag}bXzTN+NpAV$~)umKs? z>bmpp%ixJZl%m_qnJW#thNR@cfWJ+kKSIE1MqNlv(34)d2`#x)CfgH6E);5Ul*C2* z6k=ZOQn5Zyx6Dncx#A4)K0|*xJjrHvgu22xJ&OfE-$qJXv~-GWoEV);paR3ycfXQ$ zMS)z!j@frkpDvW%-DBMH`=!tfkH9j&JD_8YAl9gn(I$dM(AJ3*wSru%$>;cvFW(pt zF6K+hLri%oN+s^>o#~C<7N2c<%duwGAB#$)8a>O9UOUrkb)v>Ql9d8CSs zl`4QMQ3u`bQkI$x&FvZETa>&IatxA(N#n%5myCTd^O8X!8NC|T0mMFnhawy?g}tgs z%JQAMwdzzQLHA@yQ(x=-)X;FbbHMV1VSl+JDXZE8MU$U=`p035EV@UNYsNY1%B5DU z2;g#=gw}V6k+D>Y_CV9!!Tg2g2D%ut_u0aeO zBnM92^3?$R5Sj3kjn63ZHV?{m-8Gj2&9d3#+0Yh7{Egp`S)g02RH$|M$m)u@$dV6w zb{V~V z`dYLOB`yUS9_5w4$Ry;kXeiaPe4##Nv|FuKv-arlLH&UX(p9JR9iDP2wd^<8Ok5%p z_Gp_OjM~q5MWJwQC2fA*r0`xYy4SCPpUwk>l50lyqj!lZPR77)YQhS7O?JQBg^EB} z19gMQp;bVpDHat~&!*hCgq{1Mboh=)6VN|eX#B8E95-bxX zCWpA%KwgYQ2QKM9xS?`atzAIeTe~EIL|uNy*Op&(WL7o?S}3kg2JT8v$m^#~9|u?# z9l>k(HlM`>>G;+M2UBCNnScS+=xe=C@Z2z%785xeq)~$CPG7`H;^Ic#t4Eu~so4-2}3#jJ$L~P>?8FJCr!joi!P2rb9UItkEt}Y)C zMjosS;iLmu#T5ycDwkI_$V)L@Eqt@utPU=e^G43|r+xt_Of^nj+2TSgDz3fb?un$A zRwsim%S8^I$-n3<0g4~`0~vQgH|4mHDOiyAur0ELbE>;vz%jVF4c<#Y9zgru{mDN+D~`o9j;ERc zkm1s(`c2K1{kdXFR?Y^vsG(N^hSd3hOy4Fk&)G42F{ca*+qUQRn)q{e$7?C8aKAgg zx{xQyCfo!EFblPRpM|Vs0KYcAYFh2{Sqxov@tGX_!d|WUkMqcq8iITEem~$5KIi5` z*gAtnqaD@FPg$r~8{BN9Kv=RgZLRAZjP**O`Hh5lA2N&AL`$ zYP&v@1W(}3Ey^A)4H@v`>0RIJ@wxS`^? zSlg2{TW5W31TsFiFq;6ZJ)L*Izcm3$gGKEs7spFd0jreNuc02RZz>z#71katMWz;s zg4Y^~%J(YAK_?2@kLqO70cmWf0rw@^2ACs?(px}gs$9)A&vk)FaA<-;Vi`!1G!#%L<(c(pN6<+$=e(Kt=jZHi061P zBd1EZcD1Ya30k;ok0gMj;mjCydu@AsCgCWkLxM$*AMJ@Vr8HkA(8u(c^E*5k2O-?* z?KdYYjVnjXby0Sq3qtq#09Nm%DyUXt9XRt zMT{H3SknE_!-}K6JUn-wY)*%O<)W*E6@*G4Z)@9Xr6{iu>Qwt#Cn<|p?T?4@(R+$N zZP&EA9CKUuvfS|YpbJr0Ej0fP;tjF0#687|!Py1Qj@pp(cHV^h^eM%o+5#sLzAN#v z7W^mqSZ^F!uPmxlI`{Y!w|F^rINkJBJGLS-QZ-zzVr82SZ@0Cm&hH4g0S+p~(8oaY z*%1PSj6%p_3I0l#ayO-cp%q!G>F&}qaLsE*>TKcc^3IkkDM*TVhg1*LlsTj1lsDJA z_B$iV|KSYv7~)3Zg{=|*C$f-(b7%|?;YHj-7Q3~r zs23Vv%pTW{7+a|;3humaEBwGprk5#*uKt>wRZc9`t1pj}DLe-5n;FV!%OHM83()ZD zFn+CLn>n5WO&!KmQ5}Zz=|ZBZxsunJfuZPYB30x5W5dz<%dzSp4^%IWTk{F*_`Vpc z2J;8ZRc31eZ=A>AF?Q#c=NP?U*g0^mwJ-h@a2{Ij=M4;dVO337cSyle3oJj{Yc6~i zdHNd2rIF~NUT)%*jBrQS+i&4{cww_76cT4I*q!#|%4^3iMbY6HLTdF$vc=^WUN2v1 z$R?qVcKY1Xl$Hq^g4*SV zS->byZ)5bkffE6|55c0M8}9|r*7J%k0+sQ5LoSQ=TcCpL`Dd=|ivR^2AAHlgE3*L$ z*ih`(9`qtd4HE!Sx znB>#15@4GNSRzZ2=4UX)YL_VtC@|n? zCt84G`d8tXrm>K9?)jnYXWs&7Bm_8U3IE{XAS&(o+SbXmMpGoShr3+nehk73MG&JE z958N;&0VWKVi9>C-szB}q+W>m;;UHk`;jSP10hsN6ux5V3@^_ZKo5DF^nKIyIeeCFL zE?XT@UU+*UHF%wwCI<}3|h8ISbu`kn8DB zTWp!&8?zlK^=2K7v7APk3>+j$eEF!G6!#@ARr8*_Lmyoqm2{>Xd=X8>gW>@ft7bp#Kh+NNbXNUUHq{GN z4DR+H`R(yep?IH2?qQ^lC%%uaB%q>>k*qF&gk38SkqHWB<1^O0#(z|%keIEGf z$#4HdS}}BP+dQ@G%ez27D+S<-$ky*KAsQ$s1HS6#c8GajpNrS87Kp};kv$qyYQ|%^ zh4shQe^p!GWDZ8u3ZH=#-U2Nj{A$BR!}Rw5*y8L|ITA3pW{F2Iw8vgZ7reZcL&@5=W9T3V? zaYN~!s?1p6SVCYbW^5MY=LMOg%!I(%#@J71&Guhc3-$Ae=Kw z-yym*g}H~5+G!S$k;%?Vn@{4@Po%VoWU|vgI{}K+h0t-p zBJscZ4UQ}M`~NSZ@qedFe?SxeXXAwF0Jtf$kTwn0%R7YGjn!iYk|}2iaDPpR&gL7# zJn$2BG7aVKn~0{D+pUk&)`(|?;eA)}9A@ld=pWHFu+70gY4m-@`;jHiBX1YEoUDAK zykb!*_nP&&GB3fF28wJ9h)gftL-aL(1pals-QQEDjU=LR^E@BG2Z&+>+~Vv zIU}X7p9elEcqe{ig`cAhSV~^6k^?Cdv{T=c8uzZK$ z_!Ldfr{7VY7$EA96`gf`HQUdYn?&q38AP63MQEjc>WjyX)fs-hFcMp|5;xD=O)qna) zWy7*_V`TR`(lkC-cWY26Be7J*-C7z{0R!BD$|(*rQgTfN49po__?W)> zL`Kgogw5O9BdDXjS-j_`0DB}SmBD?Tg-3G_xB_B4ajh)d+ykYRHmNl6Xw>?7oUnH_ zds;!JJA9rrQ_$>sZn@;)j3>=0n?HA#DSpLgg>>c};5C!$3)HFR>^Ue(n|j-wD~Q=J z;pf>CG7fa_XBdCa83E=kHSi-A@yx%9){xJ37d;Wi;sMmzBN~I@6CUdJu8gCn>>;tb z;`Ne@=Nhs^UYw=~ys^Qpv@rzc)j~~K1I`9+-yF%XqPOmC;{z>4mU7K|k#{9Ql~d|# z5i_@20t#%7)2jTR?Wtv2!YF<3#>57+gzls)bhucwj*tuDkQ^fTe#wTF1fXfSyEZ7} z;?M~6rQIETd^L(WO&>b(P zp7pP&m{#932Wh$nOb0VF!aCk`xuuGgIt+a)-K~bpPtZ1%S5<19V==5D%KavvcjNW|7iE)O5Z#k46Zw|lP6PN2 z=i@p`Xa3mnP({X4D?(@`$}PWkG9G&-RlF(t~&ny|M9dQK#%1c+s`p(jy%H|uA`FKCW=Z>Q$f*V@ zmtSq&^EwK#T!jc$LA)0;AD5f1M!b*z%POgYy=Mh~0i_vfBw*V!O|^!PSs41n7J&KrwWqUcCyl56jBX7Vx@Ka+i%%w}w3 zv9$|O7#*^)vxeDV<;iJ_y;=Pzw^ugsvUB`7f%i>i9)#WarQ+D)wOjAT(vf#$s!eSq z)8$zCXwF2R5JfTgfO5s&o+zrHT|u|R@|J3`$@{#&RXVwAe!XC$CwGJyQ3mUKdi)fj zF(cW|4nGlNX-a>@>siaOtlCTdQz|xFiSkJjdh_d^s(P0rsj5nMZZ&2K#pe546)#_M zYF4Onm6UO=Ht88KfoM8i5FvUU=sIXXALc+-^eH{ItSVjyR>!LVV`oi`f}D-!wcV1u zYYhD+6Y*r0B2fT`E7X>K-x`g&f>sRx$e7<+EZFj(YY$pg95OoIqOqs*+thm&BMi%| z(A-G&kfupagphaD8VjYQ;>oEEh>(kx)Q^za`sGMQ-C$eOyX-MeTRGu>%rJ$(p}tz9 zZSzBp^bFgjs7i}zr!PL`tfk}*Yt1BaUDM{_9nM&t+p!J~D~%3`EZNw-JbIzuAIX0h zrCAGoPiD%9!O=3S;K~Ik--IjM`+B34MW@@iV6x2m#8MpqwmNf%Pd0s!MJ$nwHM5^a1oOD zm@18D7rGtc!ZPnAM=d8%d;bK~6N7W>sX*PUGxQVoi&>CDmZP;GM|*%pZ{TxiIHX}J zu4;OouEO+~K zXwsKscV(ET94@t;xEBSMInckOX#p@(?v5&^>L_*kMhKj2>?d4@yu;eS5WPMjvs<&A z!OUditEab)3o>VxkLCvc;)90bE1MvWaj1_+rVL+B#Y?qvQg!;TfKpq8$~6%>aK2u= z=denyn`t6`RBQoem}+V&oY~q>$*C;&Jv+EBF1x@DBgWS1r{!m)s;33?nZ-*LKI+mb zl*8qp$WiHN61KtiYSjlyhz)QuxVX2f%kWdz>t1 zC97#&@@oTGrN;_MpAL-+be>Pz10Q`-dGxwV*T@jq{rtzXIZ9=BgsDaXOFhw#ODh0x zr(1|Q9-QpekfzId*m(6c`iho*BA5epTPSjj0{?5|4 z8ONrW7|A_0U|$L6!?{%%GpZa2Zy8A%)QtyV^IGi711u$W&d>&Cba8M-B=*%^Jv0Mc zzw2Rd*192r^p7A-4-l&Fn`3~Y-Jv#v4Q*w>Rz$DlUt6S0_Rxkb9-a(tf@a&X!J@+T z*)J4_ckck`Lg-xFH4~&c$#?Ob`pEQ}XM=X@*s4{06YF$a?9l4kO~4Y_8S8(d=#sPJ zFB_>J)P{r-n;4?R?Wcr)A__p}{GXc4Xs~R%5vJ6$- z%m!royHpglgjSAb4~>tRGLx-*IqFvnVgV!VcXQYPQg6AfIc^@UdDpIx7BRTk(|Cv^ zpg)i?c{k)6O5=XlU3;43`;Dd99nRg$X?))+ZUJ%C61Lp1;^R)0w!4y6Hf7c-n#ey~ z72jUuO;I#;mc+#l89q^{qkjNQvU|7R6v19`&feZ27c*3`H#cQ5!BupOHL&0{4mL>< z5oJ6q-RRXqaTt(*H5#+ZIZ z=Vl4b$A0Gbb%EnZYZprr0h@6P+4%O;I#Nl=wSeQ1@{^})H$~`vOB;jReIBqf48~JU zQ8JChitBT*-@@9E?iVM+!}~nv%lPh(ei3NBEZG)UhfS8eXhMq=leIiM3OPOa*c||9 zDgo0cjlWEvod0I}^pFeW|4;GBe-WVJ^*+UVrA#bqnhP8@la?!D%xZG?eUEQ{Pv8Da z&;an>|7rgx%1^z%O=58VKV$->`BVDi=#jdRDVO{2j8IXGYj;l4MEi~h2P%M_B@=iq z&Qc<-QY!$kg}Ft2tXsW!B~}%4ftNZyuyMLNRpp|Nyl43=ob;kWrraehvgF1t7vX%E0x z%Z2QA5xdSdhT1H&-x%-tXMHgX#y95u-6QMg2k~DAV*Vvl0+v%_yyjX+BIA=Qam7H zkG4ZNoZ2mUf2mUFyI<0~@2Q-BC1wF|e*L#km)0H-e#3KOV8!D&@i>M!JJQvE)f+}Q z-gCbbnq>&MH5pQ^poY}Pni(t0Gvkq7S_nq@U3H`ce{=`Rh&V{%&^ zu!RD2uE#krWn28Li~?Y=L7xa*%Tt0-^MBkQYK{h>_X9gy?xnwH&S_M;`uUaR;#x$8 zYQ8e}^k1=VmA3eP=qj#c%Bo2BEz7Apt7+bxqvO6BlatClqeZUf5FO}%oJCx z6hW2?qjk+QHK_&Ynvqr8DN|DyIH$@y8|T!w;_ImEgFieY#irxy0n;Wl$F=kdXxq09 z+mYwuUYP%)B^9yyiVl5k&I#%jEbnE{T!a$&TkSFd?My^LwTYCCVukbeQ#c%txG|vq zwX^%4DyQ|?1q&NNjbE*V-xepV1b=|e!Aux#cdVYj)&#tq&Lblxe5myZr}js*=Q+b9 z{)9@mvtL^YZ41Qwe|J7guwM++Aud*xfb{)~Nu+6euCczd9RLC;U_~Xdm<2<;<0t!K z`lNdC@-Bf^1Fjdk*2?j;VoTE{2rw4z{)kI&tn;G)Tf*~v%9JFUY-rxxY!Or3yH zssQ>@N~0sg=krM3;TUyYn?5kQAZNH%DFH|g>LI_s*8t1Hs{}qTH}(OreOjum)m`b( z#no1Z6IF05tD?8k1?kzz^V=WRzzSYZCCNc7Qj!WfMckY+Iq^5jYKVAK8{b&}7vUZ~ z|JLv~K*LqBUJ1$2nR?G*T3nMw2i#f*I<1AbZ?ncPXQ&qg_ObUVH%c5R;Et%2%WaV< zA-aeBuKNlvyqQ(-51G|=FNf3D8}HA$8I4;E3?B;!bO%AiY~HS2xC6J}t9oGzJ8w== z)pb;B&+V$Kq)K-<|N4vYcXuC1pY4e+m@dpt)=~bqh6dDyY|Ct~^^<`sBsr3X1hHo7 zk*WeF2rc8)pkrnQ^fU>2IHhR)^7!S(q0*d}t4Ea39ij&_%g|8ZsM@s)Y8H=LuaA%Q z5hp9|&lYkmqhi%?q~dn^Y;t2VZMZyfvEF4LAm%+4kkY%bozkBwIB=L|+EJ5D^0!ne zJeysoj*BxheV3|qk?f_RO=FJDa!sQk285HQNsFsPAewa5-e>F&#p4ND4~R|TW^n%Y z1z*j5WXX0i;5c)nR%cSD!R^d@&FkJACH4!3EQScz|p4v8nx9+LT!}fkVnxF6W?o6x;V<1>rW4NO2O5{Ah|>sxhk-|_#u#2@P4&%d4WR4QZKDP%hO zN`9pMF^()SQsKAW)Y;1O-YGX~Gfc>9E!WK0^3;cUw$(Q9{FYezOAHgXcxX?}K=!JR z^Z?46^w52-Tq>fFeFL_}@VkLL4_413!p0k%Zs>qn=TatmV>Q69b+RUhleCx`RmDCm z9JWK~3iB|85;MM0)0tB<=LNnSO+CM?%1nuBa(Qm%dluQHJ*U|aFBsdyb)s6Q6y}J* z7_o(7Q=_|Emu&61PE>sXo7qEQKwd3^_je(4#O>T~2BbUk?tm@2cLyb@t{r!MWF}ND ziE!NzfBz7r;)i^7$%zAAxods-f^OBckarK|{A`4VY>)1(=h_jcO`Bb;ms%@Hx2;U^ zb&rX%R<#px_LKg%s^+qzRjquLsD_kwEChH6@}1vTQO50}w>knjkb%bTxg*2G+n?61 z$b)xHEtI}a&zQ(i9}gK_xXh1`J&rU7S#O)j+|cG%up-fKGp&tE7`! zPYCu+5XyW;@OS&@=5>srkx3tCSY>`@7sLzgWR2yje#ubw0gSEgnvf9#jb;!MXLuz6 z)2vN3lpAZEnd>1;R~G6@>5r=sJS;N{?)5NCI)HOyb!1!Ndtg}**!Wn=#&94WW5O}k z9WGuZSWT}!APCdsm?_<>fK}5A7}JdESYW4F2(H4MCcH|W=(j32cB~#_6RrLt1Y~mj z9z`~K(W|f(UdWxH5NBTLtGOyUZhZsBcZ+dYU1I;->UOb*<+^jeB-lBxu%u~=QmWXV zlgai~vlwB7lRV()`k2%99V&nZ@juX-X%qR?7d{&f^=iQ8DwK(*4^Z-_)?6-B!bVp< zm8&_mzg!UMwVAF^gQ!Ugbbw>PO0Xn#+-pO6ia-D=c$Zxv4`OkMWE@~jYN1~Q+d_eZ7ANFt$%`!><@Ao=0OPaZ<^V}r z!Y7Sj7(BADw*2s!vy!xRO8?ub@@n=Ov)cz7EnFSvlug9qq1wYLpS65$vl68T-8&1f zn^CVbZbdK0F~w}B;VZI#VL7b|s&~3Zjzt964}l^vNq42Iaz)CryFS!1X9dh7N%6eW znS?jxW{Ab{b^>^~Uv_P~O(xJ?bFk9!?J#@9OWue;NpP zJ5#@x(iwoS1g1$!xmLM1SyGAAF1#`A`Dvj{GX!;@P4`7h7vTmQFQjx>Y}BX(u`{D# zJ*=f!(f3jNJw3zo!KrDi_pSlmu=KU;Ym5Of#fT9Lwni}mrO!12OQn%@ z{aJ>|oI)6~p*>m45ON*LS!49Qozj`6%=spg4fV00v~qK|Y{pR`6kvNLLqzo|w`w=U z-!2vF>snnGD!V8K9&B)}8D|2{X(>2T=3lgrGPA)*4QqU0E^KZ%;Y*my)@>T5>0s;d z5ULdOiM-Rwa~YZSQ~d)#uOg);sX0J#ryDz~oUM&2k7J$6s&wk`&E!-NCMs!KaWxI? z8=fZoyf@*E-hD8$@i9@66;)S@E}plEoPK&1O$5nh@{b((F|kSvMyiGgG**(J36wBc z`Ul_Dtrf<0$MYoFUo!yPENTVj{<@9#%Ot^_jmzaPFZ<+Dhex+QD0t8lq;Bq+OiFD# zHT_x_7^$ss!n^-voRtReg7)c8&1to!3SLPm)XU|lIb{~cKIk^DNrX=i4$nl(N@z=# zvP4P^0Bp$wn^rEubC zWNuAhZ0ViJ044b#&9?@4T-%sclVkytySct9n1;^-60G4HQ;r=qlEat|-+DT2qB$;> z5s+OZmdVOOwXPk8ONWop%$k>3RK?x}SvLi`7}FBaHCBIElwP;dPr( z)RXTDOYFvWky*|1L{^#31T7~|l_s#9yYD!uuM;^R(D>Kj(*rfyaSPucKmN#hrs{G{ zsm-%D2PgL?fTL!=htmH?GyxWv11BWhmmN`se{C`*T2lhnB_7drd{y0DSXXd?I$1&h zIQxx1+PC-SEKgQwZK=aQ5=gWfyI|WKoK+{yP`@r@@6_ZS+WT!WyHsMDd&nMpdEOLu zb1T{oMQ@3G5$+Qg@Wkv-R0cTWDBVYxTrl6sNihvFRDgx3H zlqMxaIsroL4G|S-27*eL-fI%kK}sk}fB=C+fe=CqB!rNJg#Du9eBb`&+dt0P=j^r4 zI`1D?E=k_!e%f8G`?`Sg|E-9<+KnR!y6K}3kbi3yBmC09t%pPZ04~8(-9F|5xT5iq zb^1m_ik%sp2FakMX+6Xd)Mc-zD2q}P0~GT*`elU*1Rk5(LV~`ZT-;|@a+sP$B7Z}X z%&Uy=-jYcK7j`Zl9_p&Jp>@VwwmT&cvo-XrZyDq~X>^aK?`1~*D&`co7I+*5cw$yE z*8e(#QzCved5AmCHuMsGwLmTBW=W_?Jzb+`5PQAlXBI3;2E0CaV&*jRrH(=JQ`wq? z4Kp{c4)<@%?u$Q1kCUAl0S`vkKQZrK*Jel^%(#S=iNwCemBMR6#ZJ;VFKw?pFruew zvH6aoov5!H#6J73c_aYF>g?M&jM(j%4JTO^^=ImNnKM=uC=d59icH-gMH17*x(X;T z16ShjKlgyM>cDHc#xeQ=;Jm}7xr5FA%@x!}(7sp62BEJS!{YXi`z3gz%CBHxnHah^ zE33V-C8S*D9(k}n0br86Sf83Pe`CtvolO7M9J1W(y<=DW*)VmzSfMk2nhlU(DOc%2T)dCNYZ&?^x$0NzBA+TI(%|vI{%;HT z&uZio6JBN<&izB)I`xrx%VcFepVQI3PHfT$2(9f_CD$ltfD6**?+X%A`bP9?<4pLW zyk>f@B!97Bis+do*EJjCTI_ww88}se_V6_@sC?SStSPBKsg`b(W3Ow7`s4?1XD1Ph z_Xeza>G>bY-9SCh-sx+4*0@(*GC4HlLwu=(O)W zG5^380^IYcwSWA)rULZ;G*!+2K^)ZoIsTphe+&xMEdg4CUvkQ0kPj-QYy}ShoD-)> z^u03)e;cUQu}GGH-n3}(mWH8;tw${&M%NAfruTz!5RjsqU%HN(P#^%jI5A>7zy5U; z{jW90fdlAo1|qx;W$@@4&Earg?4LMfVzcCzk@}4k<>p(MA1I`q`8^%M)L)~2K&e*v zR$uove6XpvGX8wE)mD}NnL&p6 zBoM&zn22owoCu)*PZIn@*Y7Q$k!5=cU3&$nc)g74c2zOK(S3H+&pxHBeBY*vJAPhi zSa!P9QYLWS5$)!i#19k#-RlbrWl4^A!>EH>;m)Z-d3jC{%r#(_@NvT=Bze;w-8D+M z-qd>iEbK^QaC5&;2`UR&8+HER-^7DyeyKMAN{oMhBSY@~!RXPkOCSRa-?52S^W0U( zr$VA_`N)z1g3T-^W6DX!f~0w~!7zhgW!K*DCgtx20B7h{nCz2;dtMcjAj5dsXJcP) zxi(h+U>WRDM31fdi)tH}>(sj)N?bj<;0Z?>ppy)@JI3)BU%%-+-woIdJ{XMp zm;|4?WTFb-HbyY+?4Pg=jGO6IV5JZIwf>Gu>#wNn5(*kTmZh{kd(9kLr443x&r!n~ zwPE_vMclWSQ7d1f`_8BU0u_N)<* z2&-K~dvm*84?R%i#-1NZcK~QmJ;klmwI;2O`zI4El=IvqqOI0od-qz7>(;oQXjSQ3 zo8Q-ce~x+L&V$eSH9HtLBt+_ze$8Zaol2W9^rv49AFNui4;ieoG4#f|M8|{GY~2J| zI9h8XczH5o(}Rky7{>ukwDNONYHSzEdZhT@s0sg?U4V*gs`?xT9e+}Py1#+;S`Xxr z?4#MH3~1~r)`u1~uI8LzDe4qHM>Sk6YSb*P3q-0>ynCu^1B;@+PK2_alL39T+{V2P z6!zPd`R8)9lmu%BPUt=U?TD%P8s35H;%0ANpz@+(@L^UyUV+buR3&%&sX9`|A|8F@+6Z6qcRJN zi*s{_fLcI(G7n9$l}tznvsS2J5{WY_|>n$=8fX|al*IFsGw2LX5y zcajn?v9A`_$UoM-fB#)9iS1@Ol0qm+yjlTyAup=5HVz^NvlK~1FS#xarzxSF&AXyG z6|AlThc<&^ghr|USURZ~s2#ehudX!UTfJ6m%tdT|6|2m-zx^p77wPU@oVZTfpxxQ~ z1gH5NH@-|E-FG{2b~bYuhJg#LwI_3s(fdDdGSNO5eYs`PSV8 z(0n(005sork0JSa8IKiub&U1m-h+7Dg)W2!Rn}A{pwa4^o>X_PwpRzbWk@(RoJ)5J#MKUu*DG_* zXbIBGqEhCPzLeCO5W8w!(@Xf>PN{~*U4d;lK1apSc#RZ^KQEq1|-Yb>3-uMQ7-$OUFyzA<2)ZdX;|IG`;t!ZXr{} z{Ks9vyy~4fbATqz{itYfLV@qzUJ%s)*K|GF%mLBh z=k3~%z)v_vSuC)OiA~~00Z%_ccw>`iAx3o^^lIOg=0yY72mdoB5PR25$Al3CT+5UmNYjqL*L5}rvc8&rTsCSW?$rHgMIGxYPhEQ zWm;w=hM$iCD6Rpnr1zl(o@nR();HRaXQrs-ZSav!w)#WoFMAEo1p{;FHsBwnB<_2w z5fZTS&Mz_Pyq1<=Dl`}JC85{R1iVRhTtmP9e?spaisdh49SVNpfTe6fx@4}|<+Y3R z79#WY({FQCkA_lT&8O;X)0$hz!7~#VNuSRq=Jx0hDgET9s$N|K@0~hdrH+(;5w;^S zUC+V`g?%zuPh3cS*VQ&vQ*@1v^t-6d`CQBoHM+#UZ9gzlHG^_Z= zff`}il~H%eR}SF%Y3@Ed`+|0BM;$o8c^?{5eF=>BO^jZYDe&axPkStS;yL})M*gp^ zzBm1|WI4AhJq#35a>c2>s2l!Yeys(dUk`RgSdb&Xd`1;^7AS2wJZP*$TYpCY&h3bN z^KeIFp<(pg=L)n5VGZQHWDgkIfT33cT#~e(pVK_z%<+3s*?G?L2S-&Dz)=^lK*<>_U zNe}sj`$*4Qp7W^c@PSZitnh@7XIE+x>ioNrdKh41+CbT9WXEe_Cv`b(;W=A}5(W5o zha;nZ+t{-2&?j#M=jCw5V3^>4O#vp<&Pe_ekK1e7*|1x~Rn3CZcou`AbsqfeHc}N) z)Y}zx+Lx}GFm%txa`vtFemO6x&5goQ8f%W=&?-^lddOh7-rTy5bBZ^vJPkeT>MDAh zwdEL&wEl$=ib)LT#;s3cY_B&X}k^;B@&ox$T+3v+inv`<;KgMIQ@AqKuP0SCb8kLB>_KaO@SKI!~` zXGEeXDQ`MzPt)7RCLVnnl4|)4FsqNdi$!F#S5Gj17ASK4%mVyd~h!? zz7qSS!M*xBZnTW^wH_y?P+F3+N#cv|H_gKbepi3^iPv}fD)QzY6u?HTewaL`y$;4* zJ{jbXmEu(M$%!NjSMOGhYwwn_yW6rn_J8)jx%aR|U+RqN#f&8V?xwK1>Yb6SYgx)i zbN1Ncg(k?t=f=i~nkSkbFU#gt&}w0q$QZ4P7K72?R9uOn=SvOejLeB4C#bHbE@0W; zUD=a6qp(C|CD&AlvFt&19J?30gg_pnp`%ZU4z_L`w@P$aLo8+idWj37;i$te^Z-K8 z*Jgi?Z8xYndkjV7)V}9|m}rxi?Fq^T_a82qwa9%}OBCI3*_MoEG?1Amul@_M_%9@l z5P%kOlmGWL6FpX7+E|WG2~L-h`=9JovBWiYDz*P)r#e$3ned1wK`|ibh`KiM0Nh z>0ZqAU1dKd{I-XR@in~xn-%($)NJ$LOrFW2S=Q(v0UYS>v``b{eC$ZfmP8}mq+w!S z%i_l+onfYl(#TwWY23LeV7Y$Y^)p`j7o0}ywv?c~TOkIxt0L?C!tlAWtwN#+|t7TT0`TXjx*i7U;wxQdS1mjK@AlPRV zp6?$>`F${uMbr`{&B=g0WT$7|=DH3C%htttd^;+O-A~S#%gL5In`vgoO}Wq`ec-w! z%M%yaFdR0a#FE^yEoXZF%beUMR zA?y)CKu0mW8$Hw?JN1eT)`<_c$>J9+S%i#;v>xO44(S=64`xppZV;*?yL!9tfB^Z# z@Ts)kI|l#*PVV`yfG5XkkAi|Ef)`!$xs_5#>ucMIbF<@hmkbUcRf;fP3d~QhKj}LL zsB>1nU%qzBdhtK6|k)ijN%Ivduwcz73z zU`pZN>N**eqnp-0Gj4w;rr-U)NlSH7PWzM?wP2>< z%JSSzg}mHXm@VLx`iKo{=|3m*kM@w&46H$XdEq-5qZrT@@zFn?oMLBXK2nO!JEn5v zj_#j<&2p|^wmR=Qb`oQ5W@e`D^eW%SC$V+>a@_A`=T7gJzIhql`uT~JRO{u>9hql$ zZv~w?70u*!8IQqNyC`%_)RcW6){bgoTrizhK@Wb%qzJ;9>gkxZ&e4ru*!PNMElR(= z6^sum4{MMWLKFO}d^6Sb#+?}QX{snp66JA0cKG{vTAPHcTCPiA%`or1Cano-lq5nv zfF4qa9M=izz7ya2Dj1$vv}9mY%^S}y525vy6O#j8+ED=|j;n`p(FN^F0ULH~{PNdu# zs$5Khs$*?LSCXQ~_w!hYnw(SjEMi=RzcD^!K9W!n0ex zzOlFi!1?6j4V5&7i)O4v8pwl`S(0E!ZPB8XPHPjOQwM;vu#OUD-WqRrDGiBSL-_4I(=Au;G~aYTJskO{ z(3q}VPNwJ3QVE*L)YY%eEN5ojg`}Ba+~QJLR{{kB=uvlTXLFZ`G^v8_=MpizX}6oF zqL!aiTb~lTpL@$kJZj|EPJ3|LR(BLCu8ryD)v~{)g??h7)IaV#oT7%_X1ndyG;9a$ zyt)ijaUdwwR|S5py$VruetmIVbtAO>$tP^ZZ$1|M;q!#?^7kjiZt4J&*(3ooc(I^9(FUg7s&3jB`LEy=jrDb@%p` zd%?xL{FN^_@3yB8AWEq46{+&kys_J%--{qe)&5$b!f0_2fS683cM=n#))UuT6>@a) z9_;aN9qQYD{<^gJ6Ae;8RUPK(SoC_U^9BY;3hjw%(d&sK$7gYqw6kgihgrqlB?%HB+#ifv4-^tf z3iZ~a=v6k22DstU!f*1d`yjA=T##FhD zY=?d6KDa|h`6o6}Dy#xo2IB*3=;o>Kt91WBDVYG-SAZ(L2&v*98NAB#WVI+lG|4z_ zG~xq<-abaHvR2?et?(q&q#t%}^epl(6L39abufT1=A7Z+;@6T!8{OIrW-WIgf|yHc z%Ro-54bq-TvAEDd6$_z&(G>X|kGfN-Ax~z5oLYJe<+c>!=6mGZeS(9xhSqnP)Ln=k zdb7O#uB68HTlZf`ZU*edV9Zb#y>~RW5>#Jiyj5W(_P5{lb$F#T^4i|#@!q?lkb6GN zNiFnj@knG^M??e^mQ`$JQ+$y&AQc@vljykK`!rhQ_$+siX$cG=rlOfE>#;e}=nZ+g zJ%nDMRW2A<>zgth@!;%<6Jf<~rV`hVKeN)NLH0Vu#Q3!6gvM)qiL8}%;MR8V7X#TV>?zR-Y{v+FjoNM#~o)@HmhfQ z^NTu#nk}kWs+mB{?OV^9@vKUlc<*!V+jDVI35{VKyYlJB1dj*t(EVgo1lG`?Fo;{jBQEvqtLTrM@lxfoj z%iN3%w>zuWoJ(b={J~6T2~cC_S2dLT&?x%!t054AAykbBs}eg zd-77ZE75(tvu<{U-2Xhz^)S{#DtKvy!sDuzmYJL!^fcjDpXgc5i9TW^rgr@MnY|gT z3y+~gf);&}TVqzkG8;v~4uk?}{Aoy7E3KS-D!DDz!R1JULhY$AVQ+Ie;TO3!jcsyD z8qeeuZ`^A1Z#HASN(7B}wm=R&NRO$AUUH^BPsUGGHR<1Pvbr(+XU?+DORu^0kSp%n zZt87|D(miUh`(U2Y;nxWhu=%OGN-$f6Em>XS&6lxnYD){xG_`9>#KcokYDaQ+PC^A z!>jV)#cPyqNAW*T6~rd+c2F}dx_+baIJ@?@jo^ssxyHuJ&-()&$8SIt&&+CO2{CbQ zrq?rGU+oHLIbm-;U0&g>stPZy1{%dn2N-LYp~k@1nfXR}8Y~KLA*~k{SID2(^{A@6 zM^{Y`HSnm5k)PPM(2n9g-7}wxO2Wr{1hLD*7Te_&xTARxF_4a~F#O@N=I2{%UhH`9 zOLQYmvk)>=z3t9`WJH#cX+Z93`g5l~S)Ge{N>6fRs*D?^?F`z_`Mnxn=luhh`a7!>x{)o;9JUJ^=GJp-M&zW*NtJJma`F& zvJ>qGQLwd{ja12yt@qZ?IDB9r-q4C5$U8wHc5z$U2p6~?IYrcl50CvQY|3RQF(S+7 z+UhE>rquTQeQT@-Jx092Y>T8n?nG9|9-asWFJI`AjMal-pviW! zR7*wPepB4=$Q5# z*Q8++&v|Lb(5Ck6nGl84!drk5jHw~R(VFR|6D9@aE4p|wvqMr99BrTfNr1V~ANM=$t#$}X%3(*jl z;Hvu5nuP&cSJG@rY*ynrQt1%GYNNa6(nKSA{opyX%I-2Pd8GPMu+or33wrTW`;C_7 z+Jh$OM>l+Tx=+cI{T|_9FlUHqeKEHRn(EmQY}FkC>$nEHz|vnlSPjR$$F@yI!}wRH zAB~svC0rM-&{~S6DuPh^yRX|KVV@4zX!B!%;Nj-n&uz-%ulthO*?BmY=P9g5m+;}I zojiUGUjA~G1W4W$FGjyn1)4vUGsb(@Lk8S6mv6D-(91|=satYzO{g$(scUQ1E!`(! zVMB`@lzrMK-gH?A_NWpevBH_{oDIbYj`{5q2&CEjHgU`!A9oUSVk&+8$gJHxb&(Tp z+6`UZ%=q9!DO}{%QXAa$h7cTBSjsuWB1ad^u>?b+XAKAjAO608a@(oc^mwmJCLd%vk4=@#~o-J`60 zge!wAFS_W@j4wGF}?xWih$PbvJcIS1X0N?tST+Qw6M>Cs8dY zk=p9i5XqPOOpJ)s_XTI+7uyK*7(Nw3d3 z-tSdbJ!dnB;r@AJe-u8Qq0e(ohpO{7q2jFpj*|~DnjruRW9P*HV(n%Z zwciakP>8i|A%CpD(bhTwPvmBKkwJ?k@o2yEAjo= z!?-E0vglcYE>cbf_+Lj?{}FfW2RizJ=Y2})f5vV?D>YQi{r%+r|nc{K7j|2$V>Mpg>s0HLGq`VWYYxX0hC9(cp=@!ROK zhbarbYokh+RD_Y#?z_cxK&C}0ml#4&BDIf~PetRSzYnJJ>OcPU_7-rjvCnJpvg^sc zg1e`bjHsiLvS&8B2naN->@Lp?j{#Gq+=Q`4(GBB1ukRn?^wWxT<$-4<=-3mH`yBl$ z%rRCHgall4tRZK|8@~!SZg5u3YLl@0YDD{j9hbj@`mBh~!i9gU5|mA9mkW(+&X&x? zT1GuxITs|dS+Zf(Mt|xTG-B<=Z!Q57TX0VZ^gD@;pHk4>xfmI#al;Dr_zf^f1opaR z-udsZ@o#3ha{d*7jdHRb0#h_>Ts_$jx~@hnE_W@8I{;0Bf;o`L-tf(@IWVx8fAC|7!o!W@#MvLvwGBU=;jG(I zDV|oKvZbLdIdpey^9Nw-TUbi8daJK?)MvAHu5;4?n98WoJ%-6KSQ|T=(4sfRPPl6odfgQ^JxNXq z+%DdJ?7Lj3u<56#*(i2ndP`OJ z&O;w&!&aw`1@mIr_0TXEPN2N<;CItJ@iw_aJDS{vQW%xE#2^sZEta6 z;KKUNzG$J@$)=vB`-Ty(33)immBhizNyWiUrJ%B~F>siAh(GvFRZwRAH<$XdIrw%5 z+KJx8i6}K>c=X|sKLf7AF}YDf{(y_*OWSmrDq@b$pm=h%?B|OlKX(Nm_WRiroZj}$ zYp-^}DK~#a!-hhiudU61LH(nC>A3sj5L`3p&0|0JVtiz2lPK=v^{pR#0GF7Qo;CL< zr(E`6!*6~%0q&&JErSN*6;f_R%k^kudQT_48ZCTRd=hsaza*O>{xDg4#mzJ?y?rn2 zhEHbG{m&^DLxwk^tPFfxge`XLo+Vg#%m^~welVP@>Fj_-iq)@0sDB~Jjw=1U^2-Ff zvsrMp!o){f)K@dp&-oibm)_0Eda4(mygTx+3RF|(*IX9qT~E0j27PYpFW$g@6FyH4 zMj!@rf-yP6x&e6vi0Zwef_UwK3IhW9GJbPFtvd-nsq5+Uah5^q$^j2Vu2fP;DrPM@ zy%VXGIw5IgV@{gN&#K`K+ZY4Wti@1n@G;1}8v?PA%>TCN*t`8N^jIMqnxNwAF28!u zW5JIdUk@tB54!c#sQ4`azlQfEL!)dBx_Z1&rMd=8*e80Ec+GPcO!vL6?4C|e31HqG1g;so-GY?uE=rx|5 zud)2vzS;@e`kp0t&DiY&`mbM?G-LqDx{9@o%s*Bba3tPbAbM@nIX8yvvzc)*@!!c! z447<)ThbUa*!-z#LPF>VM}m|8L!i|KIu|AVB{ZlseSt2nMa? z;Ci0kd{nit#PG9XqT6TiuG=mD>ku0Kcy*X>A7ah7<(5h z7|prz3))jUan^@CEl|q89A8U${HN*o2r2OSlINbhx}eO`PV;EoFM~c6*w{wg*ZbMH z{+C^;OgB>}{Lr%1Q)Ligr`Jul>f2I8EhG6OI;c+*jY6Keuy^Pnmf{hg9zQ=9S4;*d zt4MTmA`_GTmCe8a0&#(L=eLKFy_INFQx0(aQl6Ft;>1LqZ+-aJw z6jEg<69JP9*zW9zKJ9}Iv}!s2S+tl&xj||f><$>iulR!cG8kss8s zIrscLs<1#OJyyyO4~W?mXf=i<-zquZpVL^lX(Tu2SPg6{9Ct1;xBA=Mr@WcijB5R4 z%bxq>Lh$lzl@req)F^nlyAc2F`s9ux_Gv);M zn^)X&76+{az3cSv^BQ^ZR zPGRa|?vtSjOZ(et5VVPh7Z2R%-T{iIcr{oi!)r+caXp5vb+>j6d?ta?=X8Gz zug(!f@Fpj$x+G*(35()vMt6%`8;97Zdq0Md1=U=C$I1v;K1NZ%nhQp?0ERc%@Shy4z)qjIZW2=|b5gx;Lr)AmSm@2>0N*VmDA z3gn&VA`8<#BM6GgFQUA9%_EU%A7~9B3-s(dE4z86o}9~M(f#(6SR9!9+sTwQck}CQ zDMCE|tsyT3sIm=s_BbO_{R97u_oS}k%pl8eU2ij>D=(4>s!&0_tjGy6fBG+sEYJO~ z$RHkQdy~}2LdVbPl5d8fA4DF&>1ya~CrVvQhQO4p3pc?b?;^25kNtQPIWz> zF!*BboZpu&K$<>ZCT2CZY*R0^f7iFTbN1GQbEA-~Sru@ME19EoBPn+ew{q^vP$AW9 zs8EK-xsK#505SFB0AIhHuAwV=)9aSXj4^LS40nR73@o6f3sj8t&BVT}#ZOE#n?S4FzC(Ea*h2l{QC080-4t*){1dW8$hB7EBJGiA6~5-NZkVvojvP@hUOMF@~%3@eEb;no+HzCW;EfqIsP{tLzRN= z1}9_0_%YUK@=BrubKXNUWNt&nCy_;}$YQ4W1omOa9o%dWvM~3sl6{p(`l9259^dln zXHTZsK-h4Tml*K%9{70>PaHb7r{3z-)rWn=3#hVu7qo;0U*5{Z=I5>lj{1nxV;Qb! zOh0;72~~E^vDka_EkS0xS|U14G}MqslmuTl!>u?CfjPmPG+l)AWAJ@e{n&luWtykB zG+BnI?d>-gdJ(D=Sx*K2m= zs<~Yw-edX9MNY0-1snI&d@|a6qS*6m?)G;&{Yj4tG?(^F-Mom!oo-k&Z33=7Xim7fYHmzn(2yt9sn!I?#EKFX9U)C%qYBXfiq4OWo$4g6)mi8{IuN&uj9*iH7GgL7s zBC8o-guw`kqe-%S*Gk<=HapWLW~R%;q)zml%Yfok#zidPHJ^PfW8#5%`Y`da`RT`TgmK)+Q!R_Yp7PE*kY!l$1cccclBEg66>k zM4cMN&$Xx*D^$H!%=ynp+b2H=GrEi=KHpp0OF;CVNKZ6rC^@Oms???xB^#+|Lb$<* zIT>cMHzusaa5f4(o=+V-h+0mMq8)yuY>+$73Z5GZqhcK}BB5Zs+ZqoIyCTpYoGp|X z7bY9;Rn(o_Xh%JI8U2lN_pDhsi0XNKv$z!a;9@K++0B$}ICszJa;_g6s9XKQldNf~ zTj>+#jU?!4@1H%(Y?-loxLA=&Lzjh{5*`;gN|PEMN_0!SD#?NOgY!bmy1z_8O2CB zH~h`I*MJkCKJR8qdsdQukf0%|n^~lCfm$3hm(`oVd_gr~SR+2@FkixOyluQ$I7K~v zwclnNw|8af9Jdv`8X3PbhZn~C3l!1g@z!+?n9wO~IU6t-l0;R74^Cs7^cx+6z8_T}>N_y%rYZyMMJVZR>K=o&2^K^uW7#o`qegShjy= z>Hv9Z2O3iqrhGf$+zGHy2|e3#I=53WJ+iLi3gcyoLfS%$eX`%&y_#dgqsAOWOms0N zXIhxD!Z___tOU`XwQvY_gvT)5V`5tLHaGxBe@e!R+gR0wp1cSrHiEYmC_AC}C4$A%kN{YK2Y*CIxbibA zvI*>Vd^>)u1OJQL<-ZD}^EpNo4G9;H_8f)et?84@k_0 zHk?Y@gKALYO}Unj-_1BUS3J0Xp|*so?wk=(W4Mr&(cY85eC~~eC(JB;6pTL%CAB}* zjJQ}j*JPJ4#lPEX71LK0RrI9lPP5~tBL78SRDP! z@0-DQhvt^iONn1Sv@))PZM?)mu}7|4$~q?t<_{c}vv*}T4_#Kf`ztqRJEV{|WMmTh z-HHP%+Ug|*%@^P?s|4>qf@dGPXvbEJhHF<2a69d&_g%ZAjQ}2)jCA)4=;+v?X< z2gL-pyOxiTijy^=(GP~BsUL%wnhJ?l_=qza_-Q)p$#^tVwdNeWGrh+^RGg>&boG&; z=x#>qHk(FgObpA2)o82JhLIDVUy5FRa3@rzcxZdEjYq|~2t=nisHd&eKw51}uX&K& zTu|y_`^DHNLv`;0nDR!R>2{QBrdqg*Fu|3?*>)AS9A_X#iQX<|io2dq+mtu71EQNP zE-&efdb^Wa9(QYRvl~wh)p(j_&uumaNpbTTyiGwIC4}<3g*N`P5o*xN9*3|Z6NG4V zSD8<5?Q|H~Po$`z?K$LL((yI}9Pch$3n9Owll&ZP2dj<+eXlF`|5aBGwIPI=r}B4C zoueVuG0)5AR(5Ubm%}QHPPk&C`u&7Gh{f9V0&-YKFEi101(Ra<#pGJ@XU|{6j*5o; z@?Mp!r~7=wOKj3L5QXkyiE9YlgX{E37Z8s`!|h)og{BMV+7-enS@b9y*&# zBWH6H4So+dhv^}5{e^uE<88_mb@D3Rh}EM=QC7wAOPcr+-a4awC*Zbfb@mGMZNYF> zQI|4;5Jww2y+5q8`lD52g*%oZ+vS{PCn;`&jrY`R8;|LE>F6risw}V)s!r;kmQ>d4 zeC>XfdC!0@djR4HtlE~ns=27TZ!xKY-uacgG+wM0aRs4Jv6ZcpC~4PVTXhdJYx6~6 z)u|uA>eV{JXkDRqINurr<}9Y%9p6@rKG~!>NnxkN=r`F4&;xAHjfP zo$OtB+Jj$^@4hWh(~PK86|TwRwASYf%U?L$KbVuCtnitl9>E7#k6?5*RkK(9+)95k z^OdC92-3*kiaU$-KYC$6tp-2$8EFSVfEeyXD*7!(j*vw##bIfSyls|eJY;I#ZSyA9e&$P2>B4 zmI{~EZ44pXoFN}tvG}y^M4ISG5ak}=MK{3u@~_uuN}%p7T6UNzC4R9`Og=2A$l4TbGq?728f zSdeoojt<8^MhP3AW>77YtEu?pH&RoPdev4_x);2R@XzDXCdIe3oRW)Lu9sHZgXmJc z%Y3ClY+qd~|_HM^6b&j70`uH&p>FbP3629!o6{9jXqj9fhO@BC< zdB&a3`2jO=-@w? z8F1hyd~Qs3a5T@^6FnA?u*&H(@W_Ra94@eiDi)${@UrkJpZoLL;Cc(4$`ih)i#!vi zZ`2JcT{BIm+=qsBWlRcU#h?@ovb9~t$XyNa(k0K4vZeg8qevS;`vp_ut+X zTqkZ}Biyff&?*ntG$VbZV@{WA5u#H1eK6gNBSG)r3P53x*y1=CgL+&p^c!5>&MOx< zOjruWlYKJkA`KQ14&DnT2FZzv8kx;JCzJXmlO)akZ5iT3RtC{Kb4iex$b=Vr&$jmP z87ulLOMGrtzNVv-fOF2)wwZKc2M_%z-^^%EPlb=h$j}$auII>C8fpL!_rDwrW?TIu z(xYcy2xxlLM$>AexyZq(hpMzd38)&JH*tcMS`m;Kj|s~(^yJWboI~Rl?cpye-te=- zD%kFWt1%P&8JBip+6{Wbw54%)fPJ5T~uu_3_y3oxOR#7l;Tn*gvE4rEX z$yp=HB6ztTJAn^YCcmseT=FzX%TlK)e(WTB0_2-lsP7A=^1-G;!$a(@{Uf5vVJEDV zXX^2F@n(4%+m26SY-&i^6|_pV8pGs_8g70wdn}k!ANTFZ&TGWnv7JbXD@oA!sQSTa z*k(D{0+CVOFWOl)*V5~>`pO}vVQ$Di4tg~*2%$gG%w|*(vP$9+#cBq|=Ct4(AocQ2 z0B^C9Z@@+2b@a&9POr{p_##5F=0qc(BL}cv#Hr&_!Ya~RP9Ak1NG!I>*~Fqr4De6h z|C^r%1ZqF=n;JM9Oz^0D`D~d^yL~~fXWJV9wfU8h9e!c-**N`UaT4Vl=?{3HIRmL* zHl@!fN=u@=acE0ASFc=D?-IkDCnQ8Wc+q93Dwg&TW%8cLI5pvn`53XHC-gn*<0V(B}>C0Sqk^4@~le5-`be z<>N=?&eoboK5$7FMh#T$2#@}p$ZC*)KNI4j($hjeD%DNo%H(vV;4COHJyqqjKM$2R zx9GHjGwM$#DI8NbK+?P%Imp{mH(NIRa++DLSiVU7CHR7kPVg@O8=(u*K!xz;y{>=U zM9shX*G?KEmNwN8sx3wiIu{M8SW4H~M@rD!Gjg+QMtvFb^X>@NDLKMnhhg<_<9SQO zyhD68iu%pr(E|lu+%Ws=f%1uAXU_s*R|3o!k+0^8sG3>K26^^zfy`&>sH&frU&2U? zRKE(mw0l;0t{ke4$pM}Ki*LMG?NoO$sjwD!5np1Db4=BuG0)wR z^*nZ*Kp%M}zC(c*%CX3jj8{^KSSGQ`5&*^5c*sFZnYz=6-AfyYIyN4<4;%0Q46i%0zm9=>Fz#^cBmnr@6EL(WiRg)$ z_F&vfCTO(yAN*10P5B8)7B;R2_24UVOw!XP`F45R{D><++WBrtcZwjS90pxrOpV(1 zA$Di-1wL=ICO$T?vNWmtZIEV@Lwg2=dl<8*8>_+nsffU@*wy_p4%ZmBeqa&-NOq$YE3IUBzl|gI zVvOLuk0a}mUlun|&aHJ8mA$ysa6S^i)louMJcF2~@#1&33~4GL&O`W&*l`9yr?uDu zs$75zKUEs@-E#Mve?r|zZl+~CjKmhOjk-)OT9hcK<_jb3<9OV zlXHv9J@0pMvgKoLBmgHyivxqudKEOH+lHBCMTr&?xnZ%Oq;QJZ_By6^Rn z1^x`|^p-*HU8!=OL;MCnp;ura4c86A8jjtx9M86Zs#inl`(sLH7lBJYSfn6otmTtLQ zr=wzGxLL>o+&(hbs-$x?;@YY2DA4HAt8`cB{(^4aqnV{TAH)QUK;xQ3l&$nfX}mln zNYlh()7e=yq`DsR8kkrZg7`QUJbnu*(%G{L}iUc6itHB***q`?%EJO8U=dWojl8}%U;@HSQX4L zhhG%w^oskGIr@@^N?>NVx6;Vrm))Bezoa|Na=Iki!$>Zu*ve`Rl-l`**$DG#PRYHN z(zAX~v0Aa6Vd$2&PjCJ4q-yY(NBf+>@Lkj%e?PZjZeYgxwqmK{UHOGFj=PaYJvC3e zKP4=Mcb_(v{SuX92je9OL-FSoecfk^9cELgHZZs0wNM)H*S_vS42dIX^pZqx#fZ9< zZx1w+V3gon{&gfoPZYN<)rfSCneP1uv&iI`GLB6S8Z(l~K_&0o?xB(v+-XCLUox@` zGhEc-HH^WZ?XZxSEBt;E{0lyAF#!iaifmfdsO>M0_J4ks>j>6MSIEscGY_6&TOpm3 z3^Zb-qwC!wGW_$FJ>xC`m2O}60fDbV#noMg_6VX>=Ssb5OMn~TJ!+sXI?TA(Ta+C} z!#d<(pJ%_AIPk=o$!)oUmpSSwot#TaRI1C?pWt~sJ=x3_EjwoTyLa$gWT795E0-k8$w%HZ9CJ@@RcvIQ9*ktN#{-#8%9fNm>qzEV z(K^Kc9$@@AfX&G8t8%+3y?;_luwUisA;x1dv+M4tT4?5F7u%*6S4sh5MX8qiR_(Z! z!G;2&R8Po(6P4fIy+mWA=q`FyVrF^BJC&i&{NCB{0*}mQP+mXHC}HXJfpm-G;_CyZ zecsh@cD@>`JTHG)1v~>&XolgG8SM+3D+~0g#WVI}q+S3UJ6G$IMhMopc_FQW^NQYB z)~!yS>@jDi+A}R-d)VOWU4E~n))JW)*Mq{M230*cGT`0kP`}3-`J!5MM6V=03asCUR5@^_#bY--{N+NfY1eYz(H9iREFY5Cu zWYP~c>?uDb5~+e{c_d=*?ts5Yv~FE&P@TB#XPej6SX3qYvnT#JVwZQW&rEL>!jHN!-%8YQ zMXY8HBtM>D}Kdi{*Vp6?4jntp=te`SO!F` znhuEHy3PIcwA_F5zwuaS3$0x$vf(OJe9vZIW8dPwI~yLg^6QotO^r%y$4RH2zQxX^ z9WhCsP|SBTUFw>i@^hF8Oacoss|OJ%qP~r@f8IN{J7mKmFkJgh@L@Z9qN0v;m3qHs z36i0RUm|Q-I()?oa^Kn^U`byJc3oFG{+?9^{PeV#bt&ac$QGfW=*q!Htr5;O|Gv6E z`vrSuV5L#BT8xilFDJR@5A9|KJ_wAD55D+Ry`UD8L3DQzmwtTQZr2?X&?Eb91PBlnq(kT}gvcN*kzN7;5|t8qG$B9&3Gap(X6Cxa`#qlbe%|Xo zo(~US31P3b_S$=$=lT2p&!TJhh(Y)pHcRDM!?&M84=8I6x%W=JB@flgD@9w2_V33B zj^FJaei(KS{cMYvL_?Tp*;$*CZwHZ0BoWq#9qvtyA<p6dMx%bl>s?uH&zUGqV6$KPkx`FyCnz5|6FV4spxoeAgp$_ ze_3Loy?XMnc_dPEngN;(yX~7?NIZ<4+kG7r^`qVm8ou&5u<*Qb_;R9rWPPA89crranEL!ea2N* z)oT&{c?U3LaSy}KN#*@}np2pc9725c_5HUp1aWIm>{)rv3xlZ0q{+Msql^1oF+TPW ztKmJhvNyT1Z+#T|zL3GQonED1ZTdz(xBG#f9zz?!x20m9>1p?>2B{QY1!3pMN=mru zuZz^P>JuVjFyC3bsyB;WJW6&VWq~aRdUo5dufKtg9EMBcZOd=`DB$N$`{Y1C@ucg# zm%-Z26ADRVt+A)AoF%U;I zI@W#2(v|~87<+8><_HK8;a1*iaXc9|a@X$6f?$C)0To|NlXR##6Mmpt*tT<(E$_?g zh)nZbr(EWgrnY{I#+i4yX_#n-n=wgSV5fzf@jhCPpy#QE=#riWgD2EoXP_<@yZg*4 z#Wd_YeC@%*S8|6zS;L|5^r`B~k}5wCzcGhEbDw-rf0Z`-;_xJ{MfPEf4IpZ#DUJ@hghIW_-x7skk*-%C@(JAaNThspPYMMyt}uF=xbm>J0yr>^fD z8wUlw>{}$$;0e)|L5%!k^?i?Fxhj2T3b_+S!6hlf6=$CtiaAF#PFRNxyTO$Nip^v0 z@hqw*hnMBnNhv(l7hz4XV(z`neoD@b@Io)x;%Z61WYvAtN_Ki%uUxv1L5)2P#k85} zca$ial37iaIn=zsjdu0xQs4p8ckRotS>AO?apUUwKsH~4wpQ$F}Y)Y zyrOp*i|1Xpe&%MR45S+J>l4U~>>n0)A2Ln|UUYVByXo@5*>XXP=v1y(r9=#Y)Q6;m zYx|%`ziO*gGd{%tNXWx;tFN2CGvQ~=DtdF)8C$EuPZV;u0_(H&r!@AXcAr5?ReRQ! z6{7ZI-e2X+zwq+2hN$qDb6sjGzq2})hbS@iDQPf5 zOJ9~JZax&I&47o88_~L{{MfI)RR77B#DJHJ&kU14P}bS0-v1p%Si(+X6Pdjy!)7$& zX>Q1QV%qEFAUiKa#O2Y6Vy!J221i&%8U_ZtBQcASqKdf^VlSww{43ALR0}>QgpK@u zKX}$(-bye2I;WXXb;6P;vn6c#qR;5`evw_TxBu-KwJ#jcgF7om0-MV>J-b#4 zbI&nQ%4(h?uSCsfL93U09}rTh4M7&18S2+7cbt%htq|^~JDly0^VR6`u5UP6K>fYU zC$10jrL}wmT)a-zp}Wp|+rsv59QoFmnU1Z3RCfRKaHGG+{d(=|nfcd1V??Cf((FS2 zP5KMOp9T2gaE$%JfE@q*0x@k_neyPAFz5mH^mWi8Piph<;diwhgze}%R6sXEpVQ7-n#0toGyh{4G1TGmkd-x zWGg51EYI5m?;_x_zkl_`dQ0FCLey`@^+DzronLeG1M;6?bH9c;BXtlV#QugkUF|kz zB%pmgpbTY7q;OP=K3vY~+bP&}X-Nmo@s2@Qh+|w_RC4BGz6F#x?UPnU>mvY2BO!Y_xb6y2m%4^n_`pi@ z#CLLR;~=Pg-Grl~%g2ubyJ-n;>kUCZ@cSe`JU`_-#yz9l2@ zu8BdOobZG5Am#((D)BR|pxN3Mp7Hi)Beo$#Z{=DYx7O|ZTdPAKM4TOlk9$yK?zn17VPkP>GSIq6!(Cj6S{V`qYr=>Gojqh?5ySPtU(`=bT`0WqB+q|=@udG8qRIk?R`;UsPwW&Hj zm9QT40BK;GVwZOSk`=P$3Ret*l9t5Z$ykZx3w=(YFuOmXp^!F_)q8EEauA%xpbIzDBH%GvZ`+-7gg|n{#RUG~1 zBkhY>nN1L%#S}i@=YXgjq_0W>^cz=|qO1({YU;X!8v}QdOX>rWf;~@JdgI$(!egpn z5lVfutb(L$CIsaQ`aaCLuPy>Fp4xnw`2jCIWqQK|5Qye^Ya7S(_S7Hf-=YkEtLRid zJs{A4uw&8OwM5;$Xp<$E%*wXFqAOx|arJd7`%R26U@Dz_3Z6pOKn?a(|CFLd%SD`Rd{XY(9eQ65B$JpO zj9wA&Jy87^C1D8UCRmPtFXZ)d-IJ9E9=3FdV^9wUd*M|)%%iU!8J@{Eirb_`6~m|f z*=4@`YfE2X^A>Cw9dG2|pk#ALtE}8jA75DEV)X(V@hO#IdW6rWai&;62V|`ZMI>#; zMlDKZ^vu}F7Hozu@s^FIenn33sSEZ|UR%G~%rA@l0$mTbeJg(^5TsaX=R@WmKXAUS zk2!tdoldz2(sE%qqDxE@S1&7rem>=M@KqIZ()Ra|VP$vk5nFvFF0KbT*xm;*_ZZwa z&JRBf{oyiK=dtB%MZObQlX2DD2L{8_h~}+M3F4zF9h>ahr0mp=0xz)$W* zyZC{rg-r|pEbxkG=3`pievij~`f(sT4Qz$0O~YxD!a4prOnSgev4?TLhY2fJ`8Soy z@|Jw4{!>i-@8`+oKl@8w^XCcdANRje-kpbLk{dNqOh#AG@L zqJ+*6@Abo$qL*f`i3-L3lzv~nvXh|s=}3{~vVsIR@(@A~Wq$_PuM8f;>e8=V z^4a-WSTM~P!Ym+@4sL(tLX{XRa2AAC1u07JZ|Fd;5#H^4v_q}SxnrbW>0vvQsT+mr za1s)V0Zw15X4`*?nSevAL1(Ie1rkU5ij~ef`#92t+855B6xB#uBWxL3)xB$bLkAUS z=w5&O?b~;>)B<*(Z}sE*^yiDe7h`{NkL1P9V`gX$LHk{|lZ*o(ZJ$F#rhC>4-PA*h zaAp-XMXe4G*HSkLjV`~{exhnokcD7orXAVO9-a!B>qrpi_Epq4WZhOhB;%IX7@OCzwYqAUA|!O~=ZmnWY5d$ep^x8|vCE&Sf?fb& zf`Z9^^|wGU^YQLaReJxu{QyLoE0#lBPW60OL-bp3Q~#lBUt1bnq9Ubs3R_u`z@YR6YS?(W!w_=%X z&6-Qnhx$-_@i8e6!i-K(A`IE-#d=rh5-A1E^Ejcs{Fg$5|A+~fB?xALof5;1^;U|6 z8bF0lBHs%;6sWKum{Vl^CNZ_sVCfsW(#nQQr4)asupUY5%v<@Y&ocjr3dODRz9MKd z;@o=U+-Uf=s>^{BZ^MR+BC4#maqE1B%3E^UD+SD$#;z zs?cWJ;x}T0A$~F@DX4mj&zqX6Sw(X!t)%UlPMJ|PtzOyaENJqYv_Zc>W zg>H^;=jLlJEe`R*nrRBk>OO^wQ?;U`9ca3uKXdMx#efQBJFFoZ)UoAY&#M!drNl=f zdWj-Pk~?9;#odQpGa*Zv;Zx$5;EzNCt*H&`K^89#0pk(R4A`bs42~-W0zDT)ODhlE zrz1)5_N^DPlYIF~uGg{j`UNRNUBEzuCMrIG#z9H(C;)SdM9QWzB4U+@yE+ zI+oy?%q!7-o{uvTdk$jy#}n1VnWrQ-gb0In`V|tc<-NO|bzHdD16rXJpBZ z*Hs5x-%h_n%c|=huMVBHV=7aHO~XxH4MslyT6Bmo^5U_)kWh!km5b+Y`Kat|5wBdj zFz#i~>-yDMTk73QHuRZADe7=oeBeZAZv{(Keo3|tDT?A%*e+wJC&a@Q&8@K&O zqNDgDK&D@)@_wU$pJxmXdEJ#J2TVp3M{lIMQt%xjll zL&4~#k@A6Co%UxcUXBk|t0U~%05p?rEXHja{?sQbZ2+0=n=wHpC{c|e<79CBGXfl% zf6U6+=U`=Q8>*psw^#Jk(Q$?PHt4-t>K8P;F8Q)CtY_R~P1D=~vC4>9WNXVKNpf67 z@*qz$GTb~;R|?bc$lIi;NXEL!jleCG2%Mf_OIBu!fW;7oqv(qx#oz@eSg@dAo0w*x^x20W4Ukqa4WB$5XLEh?bo6wElQHM|)_ppQTa*J2XBM}8VmwLC06 z$HCiAh%sa2l4gv}{Q&GZn+bn25a^5Dmp^UM`(uOh=5Fn>2RE(hqh66vuN)_j@>}qv zTD3_2v&yuD>!w}{6iJr8+VrAM%oIst|FC{V{O70C=`y`b3lR6N?#f&RM0@ilF)PHZDuP96N5_ z-M*kxEE_bt7PGmQdH%xQs`Bx^yoorR;~Jrd$m_5wM!k#97sLpjQv0!T|4DzqZF%nV zNbQgE`>Dox=Lj@>;DT4|-w9z}xhS~El3}Zqu8yb(Oqk#z`qbyjio}G(#s4U;>hN=n zhg2E;$}Q@{O1;z06f#=5>ViuZ^G^AkLN{bn@u3?PP$bh*+O(TQ?u@f?4|jsENzSPD51hIlw7<}F0^OWkf248 z;9J&`B!Fgi$(q8~j=*0Wp^yd_^;$6jS?nvcRLIbG)xK?WcQH~E;iay$gYe1Pa$0>s z!aa9gCsKCqhM`D)ssP!)_COOx;o$QhBy#%Ntz`Br&;mO<|>cFF{0Iedb7C_3Y+QtP3=gWevAZCe2G&N8Pt z5Jn|TntI=V0bAiII+xrl+ZmS?f4%VBQ4w*;ErK7ot0fL`qQdc8ob6^_Nm1sh zyV-%Q;hW2ThI*%eN^SO&iX+eHJeCye6K>#GP(b@YL5CA&sI^1s6+@?#7t%RUtCk-2%)Iq;Rp?Q_N_=%JbYt%SU}_Lj)Y;)?L;3wF9)Wkw97@I=kq!BIN>c>0&z-Cl=vH*w+_D+dt#6hmLf$o|&()f~5Kd+@3H?m*9+Yfx=O{8R>)B<6HkZtf~ z=$1$%;ysvn1uDVe_Dr~7C=Q9Tb*YUbvM6Yn9jD02&gPOj^k|1ZPs-p=p#$&rW^l?} zW^CG#&c$pC;;l^ed=2JEx_5KfbIHMZxtcJ}*;K#gLyRU=R=cCUUr^8y6C&QxPe&(n z${@GASnq~@u`H?Y_QGL*qO;J6ia)lLul=dlyifLXWT;?4iWQ}7Er&bvb#poM-6iMh zQ03gEXEOq1^32N$bHDoj_%g-G(a$mUG+Cp0mQ=yagiqs3I$y>dSr=-{56QAQ%n5s~ z)^^;cpCnN;u6JD?y1i?N+@_odpy!WsGbE^!3I6#-TrqoErpRr}nyKfUHpBI#XHOqs zb!DANPtM*>v)T7}K5{!Qyd;p2Pc0ScnvbGP?UA+j<=;lWpF4V8UlHOaLoUiSN`;|a z(g$OWl1K}faOXBz)0bJv^OTl}c`m`zNq3QV0o2KHhhaM1-hTEAJS3yMWV4=h-?~IP zxteMLPf!~I7SRbA%($sbA7<*^!11+jR0eD^wOPT+bGMaSSfDpLg|+4Xpn z&Nys@&b7F2bMFEG2-fU6i{igPO>)PD@rt`D4fRJ-_#K}*p-&4vxQoddzk}udKLN`l z0NRb^A7j&@D@S==oP$UAwu~T8j@@VojCf8DhAoEW8xwt879X_u%m!UV8SqaP(sx*= zHg{YnW_4iZBJOLFS9e#>i^Ko>=fiN)dtLl>FEZvp3SbL?E97%Ldgz?9& z4QEXt=Bo4?Q9120G9J-0I;A)KmI5Myc*R4ev_bsR!12C)KA1%uj&?v3HDPG(#p3~M zNS5PIKRI?85=efA5WJ5z%FCYIpkn1R`;B@%4f(@|L6UIxs8T|p;c1yIhQB;-bj-}w z?AB2gPh~ol7yGUA@(tTWlqSOY)YcQ*^mGr-)jL&O%FwafF^=eJ;gv1$Uf8)cMRH0j zle*zaO*4t^f*w<=KomsA(S9NFd{9D7G=4z&MWJ=ZZg(7GEn6FYg3$o&%I}wt_#)6B4k? zjQaw~iyI|8b$qNAtx^=p=Mq7E?uug@FL>2&l*-hS6AzYDLGbd5kc(7haQzlou< zcve6gi?F(R{_|}6!DwP@jIwud6KvtGi2?OIWlA;3u8m;U%}ybCpSfa5ir73dRWNl6 zm2I>w%P&*JTi2)&7BD`WHrzelH0Jg^<_;?*p8B@V@n*!+r2h1(=yA?z`cv2VnWRo$ z)`UE@?ascg;^#na|M+pa`RonUq(;_6Ra#i+x1>JD$Pj+?Q^CfnE7r|)x&2KSo`1ZldA_izRl659?=DeZ32vwf39V%JYQ0(5%? zdQ24qZYNUn%f)jpX|usX;?AXkFW=h%HI5Uj8ZO?A&EQ=g-)7EWL&~vX?AprP8$s;_ zGXaVk8}&6V&DN#e(MkMXr=AGiIPzOCdpgj5PWHs5h;G~qcTIwOKlNaDE^J_j0v#_b zlzeTuSg@};G>jR^Nbr3M^Pdba=o)u2cOTu|t~u{MCKYw&FqfML$j#@A+bxT4_+bsR zD-8YQ(?0l&-Y&3zGkqmX2~F2rWF4kQ_i}60NN=Jkjo_y?hpy@3Z+Pnn#Ld0NpH%aL zYct~;n%?((azETDS6k?0Shq}OvR4UhnMB(QKT==8nzM0FI(oUe7zE(&vOB$%a{!cl zx4*zG93ErpG*d>Y?^N!9M)sb5~q8QuiXJOtFq3dS#a}L@3SX>xN2%*$ZmoO zH2LFi>z$3o+{-!yCcUY#+W%E@Ov>GBmU)L_(*NU}DTygvd2V)L+RMFL1Tz$m)rr-x z<4GeIu24uZV(12O8T$Udfbq@dexHgaXQD9x(+(s=j;O`4XnRj-w)fdHtSB2lNd6Gvsn>NgG;&~Pt6g5h}L!J&3{tMo-Sj5rSZW6WGJje7_e5w)f z_MfXo5=J&<2jAu3*?x*@nNzo_m6jsg&G&vaDp0dt_pog_0i^iSwnlHRvCyc@xSpLnWoE;1$g%JG|_zT;JSG8su5JO2D zWnQZ?XvZz5e3D@7#%uOtq%8Y1<$4?2Z+BGg-K|LD8&FxAWZnk>;T-y#@?7fiiYBbmqpH^6SnRpwc^ zg&!FgLz4?qxTfR&I)Gbg+41(bL&RLcubyAqKCKg7ve5Bn6~WkTVfV%km^L&+%T|i` zz+HR|K_{jCpQU&veY z@wa8KqUD)KVusYA<$6g4e#wio!5sxdVwB6wL8Xtc`j)PaP5H#l(0r;YC+5jRw5QS{ z!h4ILTCCX0nGT7@4m9GGbwrh+35xm1cDXMu06&DSPU}l(GT@);u;NzsALZZwq9err z^VIrZzy%;L6=pPafE(m>gL1SMK9Yw;bp!HY?4&LC$OOO!9N*G3xr`@k8T%pl!CF5O z=`kQC@&q5h_1*ppUlW`Ij|UYo%?k$Kp&RK^K_S^^#>;F6{&f!j=A!<;$U>jFW7^+alUdXYfGagKsH$>N#gz0;xAFolZ=vA-^@>ww|aEVt`J**t$*iE{46e92O<3)zX$nYF;yT?gsi$AQEZBPEwg}-e* zC_yyf!H0lxyjr8PDf9?Fx6HtMe*Q=S|0#v6GE2CgB&cE!J2AD5Vgy)^dt*9|ByWHI z^%DO%(R*WfqsTOVjT7rdkk$pkB7aE{aF4b^a^ImFFhtkzfU#`nVFa3>YDRfcE0<*COY9;l|_mmxQpPvP{fea`DPjS1srE?R=) zv@U6+n{>2>7HRFaz5G&+z;N3hGFwdU`RuBtU85j>uY_Dm?Np<{f%kY=+&{FcVaI#E zRB{vj8{5*X4=(Wg!dbi)Yek&K;$~#;0W{fVqqVHEja$Z~-xuLtjJokZ zw^t(DR$Eb;k!5;1O15ze~K zA#`wTuY>u4LhGuo)T(k9G2p%T`JdYI|1QUx9&T88Ow!OY;C7gwT9D+{UlLl>YEx5F zvvA%&+C$g=@-Dn6o0e?_s7jI!lzRBNL+*Lt9)vAqCLF(R$GbjJh_@+D<~ql&)y03& zPoi{ZEW8R-HOX1Abbc{f=T&+#F(JE(Cg+@`3H_ihK!unPalz_$BtaM|_#?`F*4 zLQ<1G;n)Lvj0H!e308yaAG)em|NK;e^KEIum6%vF(6N@116lZz8-RnW{)a@QW>!(J z@O!xNYeRG!bJdp_lVy9>PNv9juf2v!D2J&0#Z#uE0A_0BTG&|tmns6BZG{QA+I-2m z)T+aj^K)+{DTh%u&7W>1wM3iJLd=kO8qI(^Y|BclmA!ks zGxa0aeGo^h^TXQNYw#>483sk7hE;3gpGALt*_E13VfLS>>(nfv>_qhe-o2ro)=X#O zDY7@?JW;o{@E!PR7+ss`CLDKn13pNQ+p&RTRmn}W_49#QS#(Q&--@NW2sV_d{ z9C|}EF?!Eoo(-I&o{(?3T!Z?xi{hZgI!z-0n|IH_pN(i=sgpgp@Mf^cZy!U=Cj&fd z)J;O`hukUyI1|12;04(GZPaE%(92qUbr<(OFaWA^K30l9?cvdsrM5G>abk;$9{{)h zt6%aV^`kidgL?;I0W1$==UYK+k7^bSG((vCKooT`*0^qhx2+jT$&nrHsj$A9r3J5R z@7~dfU?YcB`P*W_FKz)1%47YPHu^0W=N#((y>JBBJSW!?CMbQQ?sqYMM|$zFXl)SU-I;^$2%6D zNAW`8ZaK!9w|fOL@U9grWf9V4?7;lQ?@r2-5t@!ja#a`gCd@^y^6dGtCMU~BbCq9M z!^2~KmJu02lXC=!C5GgTQQASk=@3yU(&U1HV>{!w^>dE~r4^L3R05fB-gWa5TwqnU z)+(e+)65CnR!EU9&K0WwpA$O&k4O1Witc|DR(?{=U;YLlQwH2z%U||0&q}~P z_W#-||4W0RJ3$5SY<0}92NbvMr*wD(JM)aU*2;lMKS3M+b^wIty_@e0792hwHesH( zKOpCfQuA%>z1Ze;JAwl$P%E=vWc$aWw;1JBFX=!0?I>!TXjb1s^3$fP?fD>BE40E3 zZBhQiKzr@#P152Fe0k>JMXoF%V_0@-aoO40Z1QL88hGoGNSB(>F8VJghz_d2$3E`) zDcsYx3A`RWkIs4RgwDO5qO7y`L~%()E_pl>!JhFv0p*Tu{s6bUYflXrOIy~s-nH+; zVI*H00pk$g_%jqqrq=dC$tG;tF>p_q2-3&O<+~%-FNioPt+bfyaXdy* z;)Vf1XhGPbVGh8rJH-8rAc>&SVP^7Tjc%UFv^ET9+6)jh41rGfv=$p!>mQ9U6jQML z0O>Wl3}kV_{;P?sbqFdSvsu5L_2q(}%U#H9^!ZxhfFycfCn6HZOxynaWBq560=svA z@MiGo80(QuK|9wAJ?u^S8CwE4>V97@0G})0foLD~S&(2g2~4=>gM$H8qVN6Tw4E9; zPV5g1ue^8fU_|ZUA8Ukc#ec>_@t4uD2AA%2LJ!~l%v$)bXz%~SQ$mKk49CZM{TJBF zACSuOa}W6WlVbb3G-Bm9e|c8_{SmqRtN%4RY?X%jKk*+Ol3CFms3BsSo@G26U;GrM zk=DK|RSr!aDhkRyP0)`icbDyscXqaapY9{BfJrc%XycC_v}%?{2Bq1%$7w)cK-w5_ z4fW0dL-L)Y9yPEen9G7|2(8HN|ABQy0EJ*g?8Xq0*CL#|9<;v0$}9s#LGZz&0b#4;|?IP@u*$Pf)Z$J&mLx-=7LFU};wEMOylML9qDJtf$DVxKH9+sa*e$@e?|QTa zFwVu$4lenUiVK>mB^nV@p05|j+R)?!^!b;7q1Ekp%F@6o^+n6216bm?p*fOWQ?MBN zbok+f%$sMQaORV+Y7+=RW*2nQvN$vR=f8^KN^f95(}s)d7^VsRf}lF3aD zr2`*JF=MrE7NJy61l0_&dFMs)mYEM3`+O$Jl*C$!;qdV3m*-%M&#QJ*ZfU|Xq*E5a z=BP}v2#VxPK0dBOj5z?j5k1Z=r`TPAWLS!UQ&57ZU%7U=TBC^XESEABY zPJPmu4kS)>kK*t%2at^$L`jAdVDd}HC95Q#?iTx}DnUv?V~a=8GI(~2Q7&-o(p+YF0s@dLvZ(*$tpdp(}54(~V1yiNi+a5iK)CWXku2Oy17ip$nrDYl8JSLpl0FnoTCqk6_B$j;MU6wjNl7x%>}_)(+i6(WVlJe9j-1~Lhj zc12NQ-Qc;D!k%vkp<1SXn)kMMf|Aect#oDLN~U77z@qph^?lC3OuoT*s5YJJ-}iye z%Y?+2|7o=gXb12cdnvyiR*oW`^G^Q014vEb{_t8fy6 z5;}`IJzV}nshzWT@anux4E%C^3r@}|$uQDNUdO^BB`e$-6L^{eitpLRFwgJz>Z6~# zH|#&>xZiq0<}_B4>7E@!zNenbJyRUNtHtv(RnlAAqss*_4mxv>Esu~M1#3C84b?U} z#oKZ4(?wTQeEuxEEJ+?YeU);T#@izF@aytH6zJCX72;oMy{&BAv0x$y{zjKSn+TXr zBBDyMe#zNdbKcx9K?BFH5xqkc)CJz=tS>L2Ne9 zebrFqgNVrJ*U_e_$H>pSu#_DNWSp9~R{FiMG&#yW?xV3V$0IDO4DWlx=?i;c$<{Ci zzZ2Q+J;ad9Vi+tfwWdnfZ!sJb==Du4xr`P7Zv(0aJNm95YH_k@chjQsRrHPy*{1%V z0MHvj-aTJ8u*%5@v~`ec#A4$!H8ON*qNv}UUc(|^TEZ>z@|Vzl^KJBo{0Zh8nuJRX z4UIt4Os6mR?=2o4N-W@*GT+DncCWA{>|DdOAYW|u{5Vl~LdL3ilPPA8Kgg%Cn01E! zQ3Vt1XQy7~maqj{IOQrfIcbL28p;-T>eBzPo4RN+r96J$IB@Q?BJa${Jrph|^9W8P z6CUb(A#b1!yc_P+7NNuOqud@ytUr0&$TA9DqJl{F*YB+J8clp)9*7bxU@ZntxVeRW z`bIyF$`e%CkNFL@htC%+R_0pdz5D8;GhBKIzfqBzUa!pUMJdB>RLR%iO^;je{@#j$ zrOQDqe`Wlj!l!{G#aYwZ%G7vz8QUdrcYN04GW9Lh!qz|+BeR56SmytKlgp|fvGDG4 zXYMEUf$zrkvO(`+1JVG%#!WULXF%Bf1R~E`hnd)VfUAEG!_rK1>Zo5+cLtwY%d|f> z(BI{>2riPOTYxNQl8p#TM(NYdgL{VC$Md^ZEG`55y~GU!MOeZ4)-5-O__@@s>p^zr zoPcvqC_N3d++GPpq_!^Qv`Ma%4gK~21b&MvoBC#Ex&t(pkB3%3Dr9(N_S(~C+D0^w z-k#}DI#5M@SkkUuPra9$K62c}WoLk0NaDgS#*9`PB%Zq(YyXu8egwEDo{GDS`c|+o zth~hEh9seLBTW-rpY+eQCKub*NKlq8`Yl!7@VhjiK(KRx^ox6YI*{!Uk#YgtZRR`< z{$4#(5Rt8AcOg$u{X{LD46#XzJ;MNbcR8fCz=dPRx*TkaGvb26Z;o1+d%HD5^5BAt zaitF~balJpJ1<5ep9ve)M3DW5~jP%c~v^=C33lDc(zBaN_P+7`T0`<524${G965Y3FFcd~86M%rH|ZTI>l+F}akll2S^AJNJ185M_urbmsj}r`vRF;DQD~Qbo(Wv5 z8)qLm9jZZqG)^WEPnsp-j4>~HXYzt=j(3Q2ijER*$G=4FyY~Waaodq@d*SSafHFQs zj}UNV4WN#K?8o<<$Um<1IVJs=NqnG3S6FTxM`ABnM=^ljU1334GhcZN(Yu|E4NPpx zw=30|D!!;$^u9OOOr(4TJ2*{IaCS6rQAMfO@9d+ zR-{+z>v(kMu-ZCYqSCUfto*WaU(?2c37!;r2Kq1;H$-o-!X{oT$BIn{xT6{wE^l%o zD$GkXd4N^o<37MDu}}tdstM^D^6PM8wPx^RGH^5f+A#okj{E0~UM z38s|noHh=1ZBNOS;LOPb3gJ&5SEidPUM;*7h!23kUEgd9%SkgqgasC5sJ*MdHusKQ zcyqA5Lgf4L`8(_sFFiS;xggs}9Z|U`5Jz2>>l|ZK(ge2Vaqe5K`uaWEe0IEwxJE<6 z*o32aTi(s@vSpB<{Tipm!8wL@^+o_y+UD0;cyHZ>d~cu?sYuweK%;Xt>zoXNwR#u? zO>61GFpKPt#(h0d{HJzjRQr&J3tl#E5}!NX(4{6{{URXzM0B?3dp1f@_NU`gti*~4>34j+d=rkg>C2?UuEt2A>wT3ebiroP4{F~1}qZ+ zbg~%mol}D`Ox{9x0bI4V&)mB3!PL{e76>KEZb8}$YkrM1fH!X7ccT8p6-LTK_LT1IR61mA z042#)Reo4|esvkABOS4>c$j}Dq@XJkF!p@q{K3aHcM)mfCl6SSuF;K=lkvN_a$hA9T~|_i%eFCpE}DGLTeS{0 zmJLB5Jk+!$dh$dPa8+x&I>*}mF#LDm4b5oK7Y^7D#0G~0rPZ`M9mv+I^%FRj1M)6s zH$w2>zLES74ToK@?>y5e8Oe~d1HqMmR;gme16?hZ(xMKuzy8nRLrFn}?*a$l>+vFk zo`nQAyI$R88F$r5U7$^BJylGx;}~g^^|h^3v@JiLY}c{tT&NjnWn>1zme!Mg)F`Gx*P|aIO^!(|Y5~%e^ zWZDc-**6jQfUVwl7>Brsajz5^9nrAv?MlTPg6?0l$fc#UkT#drcW1mDkS(GxFW#KJ zxA*#XF-KP(D0gM{a}O|^o?G85ZoOIJkNNzWddX~U_pl);D)y!2QpggO2N z0T$Y`V>$ct{ul5**Y1IW!IKnn`vqJKojV_4A0`T9s!+<&w2k1)Oq9emrnbaY1heqN zT&~JJx)``&1gb5*^~C*Y=9$W`4II{sbxN<%(x{w$=kiv@bb`FP586b)qYxOu$;zsw zWf;WTi+^Vc_~eiz0G}Mcb!XB@vrA`-3_%U(?;&EFaI}j8L+?4K$AG1FOjDZL`;BAQ zQUY@}ch57P))&i{j3r$kw1#&BW7@eVWX4KWIGk(8`P4k|BPToukX*;mqGig|gigr( zao+7gQqT?K9)Sn(C>M*Q@Ad5hCnd1#-_Y-XJ(g~CtYh=9&aD= z3!1o1cu&z&8E*Jw+2THAcQCpkD*_MX0v4hFSg+F|BC!%K>k@prT9S3JvTOd>l=HSB z9z#x`p=d18BnkCpnDtbq%B%$0vY}wuhH-^Cz;Kwgk|X#e9V3(q&L+(_Bgo-%TCoD_ z(Ce?>xi+inM(a}N6ncNOl82ZtiRVoAd0FKF;WqH|rx7gBI8~8R*tKGj|z1{zR%YpK* zI>Yk69I%<)Y|ak}XJvMoHf|#;N9qc``}NKe7-X?o4O8i*TrXCcPlBRl)s#W+H$`Xr zumPg7W#x~1NTwU3F?D0mFzUBzen34zv=O62FceH)aBNh13i$pmk@VHa=|-a6%lxCz z#}ofNb^@GUbCS$_dce>SW@vX-ZWKc%@0?%;@Rq~}Rh$*rV%bA;f;)*?cUlUEeP}-p z^^W8?+fUf1k-CGH;K&hLTEituHhRzy6JuwT7&902<6y4@)$p}I1hvyZcUATfLUk7}*OX&}+K z4r_4?%bSL$H5Eqw*H4N5sGjR-s{sQxXHAC?&V%qv>r3I=R+qo!#5 zb>_V3jv=Qd*LbZ#nU+_&K}$ho_MEP%P2lJe)`+szA*jZL1-H*0A=0gSXiWdRLtOm# z7RdNuv%XUyI-F7Awl^|&YA9*hKS<@D=J4@cDC>orTu{o$<(%A-`l%$9z?T}cE#WAQO-k&# zY4aOsRNY-h#S3&$PWiG&n(gf0&$1Jb1Df73#yy%#8g+H8{UdD^05Gi1Dhy^bM@f@% zu~w0Xb|m1!Q@dq3_0%b%ku zea;J81QU*DWk~xdV)q#s9O051Q~cx}nL?GMwir7qBxWAUBGS5hxDT{;=DS72NG+(l zYDF^AJ(eV2V#coq&dR=j=h6IjQ1x;mu!bKM{%h#Umw}2{~p369+b}CkG=lRm+EKm#|U_I@gmTV`}b=0Avym$F9`d) zW%rLgKJ@+kbRtFUc??%uC0oVf=`mB8jjW>0;Pjc@{?_4ccG`L}Zx+WcegryESSQ`R z+ggIi_pHdim8}e4u&vX!@Sb$^*eBmNve$2kT9VNW**2BZt|>8LY;397xd|BeXW`Y& zJsBcW-NaDHDZP^5&h6CFPNYb1?ol$e0;#ZgniqA>^qO&gg*!yD?~ zL#1AOh09;#=tjRpKQPWk4v66nRlL@~)xEMR&Hqocn!ON;NL%{}Iw5{{ zt}O1ho@uOTfQHHqz29?JXE}RxP5T(_(FQI@LS1S|g++ecrS7S~=58bZI&1!6#4E{` z>7171;j6@ z0rUu8<(kpU$kvQHD|1DzMfd|unvf^U9KUV5b#JgAukeR0*KQ1kZS zU4J7kUK+_ObG?Qqgf_SRJ)p;)6Rx!J_FvDo;jo>|L3`J2p?e*^Y-tvx0Y}gnH0VE@*?)0P@{6NdbtRDJZr%HMv1+(s zzi#}4S&?b-%A*9=S4a$uw%lerqw zX#jm90W8#I+z(?zN~7?HedQavusKsPmH|=RwavHh3)r?lk%%1Rz2E#jr_>kZl>g1@JtA7J+%dM3fWuFSIq33DO;l;=XJA zThG2$;fVuRtWx#@$%A2viBmu6DUeR|xz>X?xy?<%bHXs?9fd*rrw#aDeS;L{Yc_Kb z#Oew2S@GM2KP4TlKh>P>o!TB!cy_Q$5R@VE0A}o8g;zs>Qspr9iv&E$T zdlDaB1-iO@@%x}(a6a1HeFt4ojH&pSVoZUHAPxO@egfR(}ZVKgKkKq(Ps;p z+!o%*V@{|GDgSZxo}tN;caId(5lwU#@hcDU3mnThBxgV9-MLXS%}*S}cd(uw0W
~p2)L2YXmDS>jXzj;9(rS^ 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 diff --git a/docs/guides/getting_started/images/intro-client-id.png b/docs/guides/getting_started/images/intro-client-id.png deleted file mode 100644 index e370aa2ecbb1c2e9b98b7f7cd35fa664e1e3bc28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5109 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 RUQVJ2(AP21F4ue#^It;zlF|SG diff --git a/docs/guides/getting_started/images/intro-create-app.png b/docs/guides/getting_started/images/intro-create-app.png index 7aceb84b41cf809f45b591c6a5a1e6f4ccbbe2b1..94f229a73bccbbc7edfce76b29b12853a184ede7 100644 GIT binary patch literal 23986 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 literal 52035 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 G(EkD3K*%=$ diff --git a/docs/guides/getting_started/images/intro-create-bot.png b/docs/guides/getting_started/images/intro-create-bot.png index 0522358cfcb35a17632b03394333891988fc0c0e..cc4e55ec39cdd8618832e5c639959f958dd5f727 100644 GIT binary patch literal 20044 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�za0T3WZx8>F( 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 QJMf${G&R7Sxqkn@0M Date: Thu, 3 May 2018 22:48:33 +0800 Subject: [PATCH 151/183] Fix sample link & add missing pictures --- docs/guides/getting_started/first-bot.md | 4 ++-- .../getting_started/images/intro-copy-oauth.png | Bin 0 -> 20319 bytes .../images/intro-generate-oauth.png | Bin 0 -> 9008 bytes docs/guides/introduction/intro.md | 4 ++-- 4 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 docs/guides/getting_started/images/intro-copy-oauth.png create mode 100644 docs/guides/getting_started/images/intro-generate-oauth.png diff --git a/docs/guides/getting_started/first-bot.md b/docs/guides/getting_started/first-bot.md index 84d5da915..a5a83aa16 100644 --- a/docs/guides/getting_started/first-bot.md +++ b/docs/guides/getting_started/first-bot.md @@ -27,8 +27,8 @@ Discord Applications Portal first. ![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!** +7. (Optional) If this bot will be public, check "Public Bot." + * **Do not tick any other options!** [Discord Applications Portal]: https://discordapp.com/developers/applications/me diff --git a/docs/guides/getting_started/images/intro-copy-oauth.png b/docs/guides/getting_started/images/intro-copy-oauth.png new file mode 100644 index 0000000000000000000000000000000000000000..31dc6f74320a628ed367803b1b44738fcb2bfebd GIT binary patch literal 20319 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 literal 0 HcmV?d00001 diff --git a/docs/guides/introduction/intro.md b/docs/guides/introduction/intro.md index 165a7949a..c22edd1f7 100644 --- a/docs/guides/introduction/intro.md +++ b/docs/guides/introduction/intro.md @@ -17,7 +17,7 @@ understand these topics to some extent before proceeding. Here are some examples: -1. [Official quick start guide] +1. [Official samples] 2. [Official template] > [!NOTE] @@ -26,7 +26,7 @@ Here are some examples: > 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 +[Official samples]: https://github.com/RogueException/Discord.Net/tree/dev/samples [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/ From 157acc46955d52809b959b52fb1a04404a6df3a5 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Thu, 3 May 2018 23:03:35 +0800 Subject: [PATCH 152/183] Add tag examples --- .../Entities/Messages/TagHandling.cs | 48 +++++++++++++------ src/Discord.Net.Core/Utils/MentionUtils.cs | 9 ++++ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Messages/TagHandling.cs b/src/Discord.Net.Core/Entities/Messages/TagHandling.cs index 667f7241b..eaadd6400 100644 --- a/src/Discord.Net.Core/Entities/Messages/TagHandling.cs +++ b/src/Discord.Net.Core/Entities/Messages/TagHandling.cs @@ -1,21 +1,39 @@ namespace Discord { - /// Specifies the handling type the tag should use. + /// + /// Specifies the handling type the tag should use. + /// + /// + /// public enum TagHandling { - /// Tag handling is ignored. - Ignore = 0, //<@53905483156684800> -> <@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) + /// + /// Tag handling is ignored (e.g. <@53905483156684800> -> <@53905483156684800>). + /// + Ignore = 0, + /// + /// Removes the tag entirely. + /// + Remove, + /// + /// Resolves to username (e.g. <@53905483156684800> -> @Voltana). + /// + Name, + /// + /// Resolves to username without mention prefix (e.g. <@53905483156684800> -> Voltana). + /// + NameNoPrefix, + /// + /// Resolves to username with discriminator value. (e.g. <@53905483156684800> -> @Voltana#8252). + /// + FullName, + /// + /// Resolves to username with discriminator value without mention prefix. (e.g. <@53905483156684800> -> Voltana#8252). + /// + FullNameNoPrefix, + /// + /// Sanitizes the tag (e.g. <@53905483156684800> -> <@53905483156684800> (w/ nbsp)). + /// + Sanitize } } diff --git a/src/Discord.Net.Core/Utils/MentionUtils.cs b/src/Discord.Net.Core/Utils/MentionUtils.cs index edfd3b12c..ae506e142 100644 --- a/src/Discord.Net.Core/Utils/MentionUtils.cs +++ b/src/Discord.Net.Core/Utils/MentionUtils.cs @@ -16,16 +16,25 @@ namespace Discord /// /// Returns a mention string based on the user ID. /// + /// + /// A user mention string (e.g. <@80351110224678912>). + /// 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. /// + /// + /// A channel mention string (e.g. <#103735883630395392>). + /// public static string MentionChannel(ulong id) => MentionChannel(id.ToString()); internal static string MentionRole(string id) => $"<@&{id}>"; /// /// Returns a mention string based on the role ID. /// + /// + /// A role mention string (e.g. <@&165511591545143296>). + /// public static string MentionRole(ulong id) => MentionRole(id.ToString()); /// From f197174fcc2df1e092b26cd2c8087b602ffb1960 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Fri, 4 May 2018 09:36:53 +0800 Subject: [PATCH 153/183] Fix embed docs consistency --- src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs | 2 +- src/Discord.Net.Core/Entities/Messages/EmbedField.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs index ab1360ce3..e596c0707 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs @@ -3,7 +3,7 @@ using System.Diagnostics; namespace Discord { /// - /// Represents a author field of an . + /// A author field of an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedAuthor diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs index 3ae000022..5d8fd3c6b 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs @@ -3,7 +3,7 @@ using System.Diagnostics; namespace Discord { /// - /// Represents a field for an . + /// A field for an . /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public struct EmbedField From 089f97a0101a4ef9194f65b2d4435c0c69dfa87c Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 5 May 2018 13:43:04 +0800 Subject: [PATCH 154/183] Add details regarding userbot support --- docs/faq/basics/client-basics.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/faq/basics/client-basics.md b/docs/faq/basics/client-basics.md index 376667ca0..ad55c9722 100644 --- a/docs/faq/basics/client-basics.md +++ b/docs/faq/basics/client-basics.md @@ -8,11 +8,12 @@ title: Basic Questions about Client ## My client keeps returning 401 upon logging in! > [!WARNING] -> Userbot/selfbot (logging in with a user token) is not -> officially supported with this library. +> Userbot/selfbot (logging in with a user token) is no +> longer supported with this library starting from 2.0, as +> logging in under a user account may result in account termination. > -> Logging in under a user account may result in account -> termination! +> For more information, see issue [827] & [958], as well as the official +> [Discord API Terms of Service]. There are few possible reasons why this may occur. @@ -20,9 +21,12 @@ There are few possible reasons why this may occur. 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*. + mind that tokens is **different** from a *client secret*. [TokenType]: xref:Discord.TokenType +[827]: https://github.com/RogueException/Discord.Net/issues/827 +[958]: https://github.com/RogueException/Discord.Net/issues/958 +[Discord API Terms of Service]: https://discordapp.com/developers/docs/legal ## 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? From baa8beb3824ed59fb50d269b9e13721fa9f5a04e Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 5 May 2018 13:49:42 +0800 Subject: [PATCH 155/183] Add XML Docs --- .../Entities/Channels/IMessageChannel.cs | 4 +- src/Discord.Net.Core/Entities/Emotes/Emote.cs | 4 +- src/Discord.Net.Core/Entities/IMentionable.cs | 2 +- src/Discord.Net.Core/Entities/Users/IUser.cs | 11 ++++ .../Extensions/UserExtensions.cs | 60 ++++++++++++++++--- 5 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index 9837a3048..51a16c74a 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -20,7 +20,7 @@ namespace Discord Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); #if FILESYSTEM /// - /// Sends a file to this message channel, with an optional caption. + /// Sends a file to this message channel with an optional caption. /// /// The file path of the file. /// The message to be sent. @@ -35,7 +35,7 @@ namespace Discord Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); #endif /// - /// Sends a file to this message channel, with an optional caption. + /// Sends a file to this message channel with an optional caption. /// /// The of the file to be sent. /// The name of the attachment. diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs index 32d49fede..68156016b 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -59,7 +59,7 @@ namespace Discord } /// Parses an from its raw format. - /// The raw encoding of an emote; for example, <:dab:277855270321782784>. + /// The raw encoding of an emote; for example, <:dab:277855270321782784>. /// An emote. /// Invalid emote format. public static Emote Parse(string text) @@ -70,7 +70,7 @@ namespace Discord } /// Tries to parse an from its raw format. - /// The raw encoding of an emote; for example, <:dab:277855270321782784>. + /// The raw encoding of an emote; for example, <:dab:277855270321782784>. /// An emote. public static bool TryParse(string text, out Emote result) { diff --git a/src/Discord.Net.Core/Entities/IMentionable.cs b/src/Discord.Net.Core/Entities/IMentionable.cs index 225806723..cf4a8662b 100644 --- a/src/Discord.Net.Core/Entities/IMentionable.cs +++ b/src/Discord.Net.Core/Entities/IMentionable.cs @@ -9,7 +9,7 @@ namespace Discord /// Returns a special string used to mention this object. /// /// - /// A string that is recognized by Discord as a mention (e.g. <@168693960628371456>). + /// A string that is recognized by Discord as a mention (e.g. <@168693960628371456>). /// string Mention { get; } } diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index f651a23f3..929824ced 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -14,6 +14,13 @@ namespace Discord /// /// Gets the URL to this user's avatar. /// + /// The format to return. + /// + /// The size of the image to return in. Image size can be any power of two between 16 and 2048. + /// + /// + /// User's avatar URL. + /// string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); /// /// Gets the URL to this user's default avatar. @@ -43,6 +50,10 @@ namespace Discord /// /// Returns a direct message channel to this user, or create one if it does not already exist. /// + /// The options to be used when sending the request. + /// + /// An awaitable Task containing the DM channel. + /// Task GetOrCreateDMChannelAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Core/Extensions/UserExtensions.cs b/src/Discord.Net.Core/Extensions/UserExtensions.cs index ad00296f7..41e84c3cb 100644 --- a/src/Discord.Net.Core/Extensions/UserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/UserExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using System.IO; @@ -6,7 +7,16 @@ 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 via DM. + /// + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// An awaitable Task containing the message sent to the channel. + /// public static async Task SendMessageAsync(this IUser user, string text, bool isTTS = false, @@ -16,7 +26,23 @@ 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 this message channel with an optional caption. + /// + /// The of the file to be sent. + /// The name of the attachment. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// If you wish to upload an image and have it embedded in a embed, you may + /// upload the file and refer to the file with "attachment://filename.ext" in the + /// . + /// + /// + /// An awaitable Task containing the message sent to the channel. + /// public static async Task SendFileAsync(this IUser user, Stream stream, string filename, @@ -30,7 +56,22 @@ namespace Discord } #if FILESYSTEM - /// Sends a file to the user via DM. + /// + /// Sends a file via DM with an optional caption. + /// + /// The file path of the file. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// If you wish to upload an image and have it embedded in a embed, you may + /// upload the file and refer to the file with "attachment://filename.ext" in the + /// . + /// + /// + /// An awaitable Task containing the message sent to the channel. + /// public static async Task SendFileAsync(this IUser user, string filePath, string text = null, @@ -41,10 +82,15 @@ 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. + /// + /// 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. + /// is not between 0 to 7. public static Task BanAsync(this IGuildUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) => user.Guild.AddBanAsync(user, pruneDays, reason, options); } From 27dc4831e88c952f3ea1f68b18434e16197fdfa9 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 5 May 2018 13:50:27 +0800 Subject: [PATCH 156/183] Add XML Docs --- .../Entities/Channels/IMessageChannel.cs | 9 +++++++++ src/Discord.Net.Rest/Entities/RestApplication.cs | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index 51a16c74a..da8b0a637 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -17,6 +17,9 @@ namespace Discord /// Whether the message should be read aloud by Discord or not. /// The to be sent. /// The options to be used when sending the request. + /// + /// An awaitable Task containing the message sent to the channel. + /// Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); #if FILESYSTEM /// @@ -32,6 +35,9 @@ namespace Discord /// upload the file and refer to the file with "attachment://filename.ext" in the /// . /// + /// + /// An awaitable Task containing the message sent to the channel. + /// Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); #endif /// @@ -48,6 +54,9 @@ namespace Discord /// upload the file and refer to the file with "attachment://filename.ext" in the /// . /// + /// + /// An awaitable Task containing the message sent to the channel. + /// Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); /// diff --git a/src/Discord.Net.Rest/Entities/RestApplication.cs b/src/Discord.Net.Rest/Entities/RestApplication.cs index 198ce1a61..d033978d0 100644 --- a/src/Discord.Net.Rest/Entities/RestApplication.cs +++ b/src/Discord.Net.Rest/Entities/RestApplication.cs @@ -6,7 +6,7 @@ using Model = Discord.API.Application; namespace Discord.Rest { /// - /// Represents a REST entity that contains information about a Discord application created via the developer portal. + /// Represents a REST-based entity that contains information about a Discord application created via the developer portal. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestApplication : RestEntity, IApplication From 45839bd982a822dd9da10ceb48989fdb2cd8cd85 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 5 May 2018 15:29:47 +0800 Subject: [PATCH 157/183] Add XML Docs --- .../Entities/Channels/IMessageChannel.cs | 2 +- .../Entities/Messages/IMessage.cs | 25 ++++++- .../Entities/Messages/ISystemMessage.cs | 2 +- .../Entities/Messages/IUserMessage.cs | 2 +- .../Entities/Channels/IRestMessageChannel.cs | 67 ++++++++++++++++++- .../Channels/ISocketMessageChannel.cs | 52 ++++++++++++-- .../Channels/SocketCategoryChannel.cs | 1 + .../Entities/Channels/SocketDMChannel.cs | 2 +- .../Entities/Channels/SocketGroupChannel.cs | 3 +- .../Entities/Channels/SocketGuildChannel.cs | 6 +- .../Entities/Channels/SocketTextChannel.cs | 2 +- .../Entities/Guilds/SocketGuild.cs | 39 +++++++---- .../Entities/Messages/MessageCache.cs | 8 ++- .../Entities/Messages/SocketMessage.cs | 50 +++++++++++++- .../Entities/Messages/SocketReaction.cs | 33 +++++++++ .../Entities/Messages/SocketSystemMessage.cs | 3 + .../Entities/Messages/SocketUserMessage.cs | 3 + .../Entities/Users/SocketGlobalUser.cs | 5 +- .../Entities/Users/SocketGroupUser.cs | 1 + .../Entities/Users/SocketGuildUser.cs | 1 + .../Entities/Users/SocketSelfUser.cs | 1 + .../Entities/Users/SocketUnknownUser.cs | 1 + .../Entities/Users/SocketWebhookUser.cs | 1 + .../Entities/Voice/SocketVoiceServer.cs | 27 +++++++- 24 files changed, 300 insertions(+), 37 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index da8b0a637..7abecf54d 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -117,7 +117,7 @@ namespace Discord /// /// The options to be used when sending the request. /// - /// A collection of messages. + /// An awaitable Task containing a collection of messages. /// Task> GetPinnedMessagesAsync(RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index d66a6b883..c8ed14f0c 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -31,14 +31,20 @@ namespace Discord /// /// Gets the time this message was sent. /// + /// + /// Time of when the message was sent. + /// DateTimeOffset Timestamp { get; } /// - /// Gets the time of this message's last edit, or if none is set. + /// Gets the time of this message's last edit. /// + /// + /// Time of when the message was last edited; when the message is never edited. + /// DateTimeOffset? EditedTimestamp { get; } /// - /// Gets the channel this message was sent to. + /// Gets the source channel of the message. /// IMessageChannel Channel { get; } /// @@ -49,10 +55,16 @@ namespace Discord /// /// Returns all attachments included in this message. /// + /// + /// Collection of attachments. + /// IReadOnlyCollection Attachments { get; } /// /// Returns all embeds included in this message. /// + /// + /// Collection of embed objects. + /// IReadOnlyCollection Embeds { get; } /// /// Returns all tags included in this message's content. @@ -61,14 +73,23 @@ namespace Discord /// /// Returns the IDs of channels mentioned in this message. /// + /// + /// Collection of channel IDs. + /// IReadOnlyCollection MentionedChannelIds { get; } /// /// Returns the IDs of roles mentioned in this message. /// + /// + /// Collection of role IDs. + /// IReadOnlyCollection MentionedRoleIds { get; } /// /// Returns the IDs of users mentioned in this message. /// + /// + /// Collection of user IDs. + /// IReadOnlyCollection MentionedUserIds { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs b/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs index 0f5a171d1..89cd17a35 100644 --- a/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/ISystemMessage.cs @@ -1,7 +1,7 @@ namespace Discord { /// - /// Represents a message sent by the system. + /// Represents a generic 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 1afb3a3b2..18ef93266 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents a Discord message object. + /// Represents a generic message sent by a user. /// public interface IUserMessage : IMessage { diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index 2895dc17d..37a5ef9cf 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -12,37 +12,102 @@ namespace Discord.Rest /// /// Sends a message to this message channel. /// + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// An awaitable Task containing the message sent to the channel. + /// new Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); #if FILESYSTEM /// /// Sends a file to this message channel, with an optional caption. /// + /// The file path of the file. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// If you wish to upload an image and have it embedded in a embed, you may + /// upload the file and refer to the file with "attachment://filename.ext" in the + /// . + /// + /// + /// An awaitable Task containing the message sent to the channel. + /// new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); #endif /// /// Sends a file to this message channel, with an optional caption. /// + /// The of the file to be sent. + /// The name of the attachment. + /// The message to be sent. + /// Whether the message should be read aloud by Discord or not. + /// The to be sent. + /// The options to be used when sending the request. + /// + /// If you wish to upload an image and have it embedded in a embed, you may + /// upload the file and refer to the file with "attachment://filename.ext" in the + /// . + /// + /// + /// An awaitable Task containing the message sent to the channel. + /// 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 if not found. + /// Gets a message from this message channel with the given id, or if not found. /// + /// The ID of the message. + /// The options to be used when sending the request. + /// + /// The message gotten from either the cache or the download, or if none is found. + /// Task GetMessageAsync(ulong id, RequestOptions options = null); /// /// Gets the last N messages from this message channel. /// + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. Flattening the paginated response into a collection of messages with + /// is required if you wish to access the messages. + /// IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); /// /// Gets a collection of messages in this channel. /// + /// The ID of the starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. Flattening the paginated response into a collection of messages with + /// is required if you wish to access the messages. + /// IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); /// /// Gets a collection of messages in this channel. /// + /// The starting message to get the messages from. + /// The direction of the messages to be gotten from. + /// The numbers of message to be gotten from. + /// The options to be used when sending the request. + /// + /// Paged collection of messages. Flattening the paginated response into a collection of messages with + /// is required if you wish to access the messages. + /// IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null); /// /// Gets a collection of pinned messages in this channel. /// + /// The options to be used when sending the request. + /// + /// An awaitable Task containing a collection of messages. + /// new Task> GetPinnedMessagesAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index 6d769b9c4..59c382184 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -10,7 +10,12 @@ namespace Discord.WebSocket /// public interface ISocketMessageChannel : IMessageChannel { - /// Gets all messages in this channel's cache. + /// + /// Gets all messages in this channel's cache. + /// + /// + /// A collection of WebSocket-based messages. + /// IReadOnlyCollection CachedMessages { get; } /// @@ -20,6 +25,9 @@ namespace Discord.WebSocket /// Whether the message should be read aloud by Discord or not. /// The to be sent. /// The options to be used when sending the request. + /// + /// An awaitable Task containing the message sent to the channel. + /// new Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null); #if FILESYSTEM /// @@ -35,6 +43,9 @@ namespace Discord.WebSocket /// upload the file and refer to the file with "attachment://filename.ext" in the /// . /// + /// + /// An awaitable Task containing the message sent to the channel. + /// new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); #endif /// @@ -51,14 +62,47 @@ namespace Discord.WebSocket /// upload the file and refer to the file with "attachment://filename.ext" in the /// . /// + /// + /// An awaitable Task containing the message sent to the channel. + /// new Task SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null); + /// + /// Gets the cached message if one exists. + /// + /// The ID of the message. + /// + /// Cached message object; if it doesn't exist in the cache. + /// SocketMessage GetCachedMessage(ulong id); - /// Gets the last N messages from this message channel. + /// + /// Gets the last N messages from this message channel. + /// + /// The number of messages to get. + /// + /// A collection of WebSocket-based messages. + /// IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch); - /// Gets a collection of messages in this channel. + + /// + /// Gets a collection of messages in this channel. + /// + /// The message ID to start the fetching from. + /// The direction of which the message should be gotten from. + /// The number of messages to get. + /// + /// A collection of WebSocket-based messages. + /// IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); - /// Gets a collection of messages in this channel. + /// + /// Gets a collection of messages in this channel. + /// + /// The message to start the fetching from. + /// The direction of which the message should be gotten from. + /// The number of messages to get. + /// + /// A collection of WebSocket-based messages. + /// IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch); /// /// Gets a collection of pinned messages in this channel. diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index 1305233e4..37e6afef1 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -14,6 +14,7 @@ namespace Discord.WebSocket [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketCategoryChannel : SocketGuildChannel, ICategoryChannel { + /// public override IReadOnlyCollection Users => Guild.Users.Where(x => Permissions.GetValue( Permissions.ResolveChannel(Guild, x, this, Permissions.ResolveGuild(Guild, x)), diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 8008d434a..eab60f6c9 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -30,7 +30,7 @@ namespace Discord.WebSocket Recipient = recipient; recipient.GlobalUser.AddRef(); if (Discord.MessageCacheSize > 0) - _messages = new MessageCache(Discord, this); + _messages = new MessageCache(Discord); } internal static SocketDMChannel Create(DiscordSocketClient discord, ClientState state, Model model) { diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 94bf70493..57fcc51a2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -39,7 +39,7 @@ namespace Discord.WebSocket : base(discord, id) { if (Discord.MessageCacheSize > 0) - _messages = new MessageCache(Discord, this); + _messages = new MessageCache(Discord); _voiceStates = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, 5); _users = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, 5); } @@ -108,6 +108,7 @@ namespace Discord.WebSocket => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); /// + /// Message content is too long, length must be less or equal to . public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index 8d6f22133..ecbbbcd49 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -21,7 +21,7 @@ namespace Discord.WebSocket /// Gets the guild associated with this channel. /// /// - /// The guild that this channel belongs to. + /// A guild that this channel belongs to. /// public SocketGuild Guild { get; } /// @@ -34,7 +34,7 @@ namespace Discord.WebSocket /// Gets the parent category of this channel. /// /// - /// The parent category ID associated with this channel, or if none is set. + /// A parent category ID associated with this channel, or if none is set. /// public ICategoryChannel Category => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; @@ -42,7 +42,7 @@ namespace Discord.WebSocket /// public IReadOnlyCollection PermissionOverwrites => _overwrites; /// - /// Returns a collection of users that are able to view the channel. + /// Gets a collection of users that are able to view the channel. /// /// /// A collection of users that can access the channel (i.e. the users seen in the user list). diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index ae8ab54da..eed8f45b9 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -39,7 +39,7 @@ namespace Discord.WebSocket : base(discord, id, guild) { if (Discord.MessageCacheSize > 0) - _messages = new MessageCache(Discord, this); + _messages = new MessageCache(Discord); } internal new static SocketTextChannel Create(SocketGuild guild, ClientState state, Model model) { diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 1f027e321..029e600fc 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -91,11 +91,11 @@ namespace Discord.WebSocket public Task SyncPromise => _syncPromise.Task; public Task DownloaderPromise => _downloaderPromise.Task; /// - /// Returns the associated with this guild. + /// Gets the associated with this guild. /// public IAudioClient AudioClient => _audioClient; /// - /// Returns the first viewable text channel. + /// Gets the first viewable text channel. /// /// /// This property does not guarantee the user can send message to it. @@ -105,7 +105,7 @@ namespace Discord.WebSocket .OrderBy(c => c.Position) .FirstOrDefault(); /// - /// Returns the AFK voice channel, or if none is set. + /// Gets the AFK voice channel, or if none is set. /// public SocketVoiceChannel AFKChannel { @@ -138,31 +138,34 @@ namespace Discord.WebSocket } } /// - /// Returns a collection of text channels present in this guild. + /// Gets a collection of text channels present in this guild. /// public IReadOnlyCollection TextChannels => Channels.Select(x => x as SocketTextChannel).Where(x => x != null).ToImmutableArray(); /// - /// Returns a collection of voice channels present in this guild. + /// Gets a collection of voice channels present in this guild. /// public IReadOnlyCollection VoiceChannels => Channels.Select(x => x as SocketVoiceChannel).Where(x => x != null).ToImmutableArray(); /// - /// Returns a collection of category channels present in this guild. + /// Gets a collection of category channels present in this guild. /// public IReadOnlyCollection CategoryChannels => Channels.Select(x => x as SocketCategoryChannel).Where(x => x != null).ToImmutableArray(); /// - /// Returns the current logged-in user. + /// Gets the current logged-in user. /// public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; /// - /// Returns the @everyone role in this guild. + /// Gets the @everyone role in this guild. /// public SocketRole EveryoneRole => GetRole(Id); /// - /// Returns a collection of channels present in this guild. + /// Gets a collection of channels present in this guild. /// + /// + /// Collection of channels. + /// public IReadOnlyCollection Channels { get @@ -175,10 +178,16 @@ namespace Discord.WebSocket /// /// Gets a collection of emotes created in this guild. /// + /// + /// Collection of emotes. + /// public IReadOnlyCollection Emotes => _emotes; /// /// Gets a collection of features enabled in this guild. /// + /// + /// Collection of features in string. + /// public IReadOnlyCollection Features => _features; /// /// Gets a collection of users in this guild. @@ -188,10 +197,16 @@ namespace Discord.WebSocket /// You may need to enable to fetch the full user list /// upon startup, or use to manually download the users. /// + /// + /// Collection of users. + /// public IReadOnlyCollection Users => _members.ToReadOnlyCollection(); /// /// Gets a collection of roles in this guild. /// + /// + /// Collection of roles. + /// public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); internal SocketGuild(DiscordSocketClient client, ulong id) @@ -378,7 +393,7 @@ namespace Discord.WebSocket //Bans /// - /// Gets a collection of the banned users in this guild. + /// Returns a collection of the banned users in this guild. /// /// The options to be used when sending the request. /// @@ -634,7 +649,7 @@ namespace Discord.WebSocket /// The ID of the webhook. /// The options to be used when sending the request. /// - /// A webhook associated with the ID. + /// An awaitable Task containing the webhook associated with the ID. /// public Task GetWebhookAsync(ulong id, RequestOptions options = null) => GuildHelper.GetWebhookAsync(this, Discord, id, options); @@ -643,7 +658,7 @@ namespace Discord.WebSocket /// /// The options to be used when sending the request. /// - /// A collection of webhooks. + /// An awaitable Task containing a collection of webhooks. /// public Task> GetWebhooksAsync(RequestOptions options = null) => GuildHelper.GetWebhooksAsync(this, Discord, options); diff --git a/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs b/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs index c2cad4d86..8cac95cd3 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/MessageCache.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -14,7 +14,7 @@ namespace Discord.WebSocket public IReadOnlyCollection Messages => _messages.ToReadOnlyCollection(); - public MessageCache(DiscordSocketClient discord, IChannel channel) + public MessageCache(DiscordSocketClient discord) { _size = discord.MessageCacheSize; _messages = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(_size * 1.05)); @@ -28,7 +28,7 @@ namespace Discord.WebSocket _orderedMessages.Enqueue(message.Id); while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out ulong msgId)) - _messages.TryRemove(msgId, out SocketMessage msg); + _messages.TryRemove(msgId, out SocketMessage _); } } @@ -44,6 +44,8 @@ namespace Discord.WebSocket return result; return null; } + + /// is less than 0. public IReadOnlyCollection GetMany(ulong? fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) { if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs index d339a20ed..0767f2ad7 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketMessage.cs @@ -14,8 +14,20 @@ namespace Discord.WebSocket public abstract class SocketMessage : SocketEntity, IMessage { private long _timestampTicks; - + + /// + /// Gets the author of this message. + /// + /// + /// A WebSocket-based user object. + /// public SocketUser Author { get; } + /// + /// Gets the source channel of the message. + /// + /// + /// A WebSocket-based message channel. + /// public ISocketMessageChannel Channel { get; } /// public MessageSource Source { get; } @@ -31,10 +43,40 @@ namespace Discord.WebSocket public virtual bool IsPinned => false; /// public virtual DateTimeOffset? EditedTimestamp => null; + /// + /// Returns all attachments included in this message. + /// + /// + /// Collection of attachments. + /// public virtual IReadOnlyCollection Attachments => ImmutableArray.Create(); + /// + /// Returns all embeds included in this message. + /// + /// + /// Collection of embed objects. + /// public virtual IReadOnlyCollection Embeds => ImmutableArray.Create(); + /// + /// Returns the channels mentioned in this message. + /// + /// + /// Collection of WebSocket-based guild channels. + /// public virtual IReadOnlyCollection MentionedChannels => ImmutableArray.Create(); + /// + /// Returns the roles mentioned in this message. + /// + /// + /// Collection of WebSocket-based roles. + /// public virtual IReadOnlyCollection MentionedRoles => ImmutableArray.Create(); + /// + /// Returns the users mentioned in this message. + /// + /// + /// Collection of WebSocket-based users. + /// public virtual IReadOnlyCollection MentionedUsers => ImmutableArray.Create(); /// public virtual IReadOnlyCollection Tags => ImmutableArray.Create(); @@ -69,6 +111,12 @@ namespace Discord.WebSocket public Task DeleteAsync(RequestOptions options = null) => MessageHelper.DeleteAsync(this, Discord, options); + /// + /// Gets the content of the message. + /// + /// + /// Content of the message. + /// public override string ToString() => Content; internal SocketMessage Clone() => MemberwiseClone() as SocketMessage; diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs index bfd6aa042..8df6d51b4 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketReaction.cs @@ -2,12 +2,45 @@ using Model = Discord.API.Gateway.Reaction; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based reaction object. + /// public class SocketReaction : IReaction { + /// + /// Gets the ID of the user who added the reaction. + /// + /// + /// A user snowflake ID. + /// public ulong UserId { get; } + /// + /// Gets the user who added the reaction if possible. + /// + /// + /// A user object where possible. This value is not always returned. + /// public Optional User { get; } + /// + /// Gets the ID of the message that has been reacted to. + /// + /// + /// A message snowflake ID. + /// public ulong MessageId { get; } + /// + /// Gets the message that has been reacted to if possible. + /// + /// + /// A WebSocket-based message where possible. This value is not always returned. + /// public Optional Message { get; } + /// + /// Gets the channel where the reaction takes place in. + /// + /// + /// A WebSocket-based message channel. + /// public ISocketMessageChannel Channel { get; } /// public IEmote Emote { get; } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs index c37f04124..d0ce5025b 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketSystemMessage.cs @@ -3,6 +3,9 @@ using Model = Discord.API.Message; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based message sent by the system. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketSystemMessage : SocketMessage, ISystemMessage { diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index 58e87017c..d22464652 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -9,6 +9,9 @@ using Model = Discord.API.Message; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based message sent by a user. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketUserMessage : SocketMessage, IUserMessage { diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs index 3117eb14c..48de7552a 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGlobalUser.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Linq; using Model = Discord.API.User; using PresenceModel = Discord.API.Presence; @@ -54,7 +54,8 @@ namespace Discord.WebSocket Presence = SocketPresence.Create(model); DMChannel = state.DMChannels.FirstOrDefault(x => x.Recipient.Id == Id); } - + + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Global)"; internal new SocketGlobalUser Clone() => MemberwiseClone() as SocketGlobalUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs index 10701b418..601677e2e 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGroupUser.cs @@ -35,6 +35,7 @@ namespace Discord.WebSocket return entity; } + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Group)"; internal new SocketGroupUser Clone() => MemberwiseClone() as SocketGroupUser; //IVoiceState diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 73f5f0b8a..588ed554d 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -173,6 +173,7 @@ namespace Discord.WebSocket public ChannelPermissions GetPermissions(IGuildChannel channel) => new ChannelPermissions(Permissions.ResolveChannel(Guild, this, channel, GuildPermissions.RawValue)); + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Guild)"; internal new SocketGuildUser Clone() => MemberwiseClone() as SocketGuildUser; //IGuildUser diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs index 972ba6ea0..af7710629 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketSelfUser.cs @@ -67,6 +67,7 @@ namespace Discord.WebSocket public Task ModifyAsync(Action func, RequestOptions options = null) => UserHelper.ModifyAsync(this, Discord, func, options); + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Self)"; internal new SocketSelfUser Clone() => MemberwiseClone() as SocketSelfUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs index 4cfaa686d..b3eb08f6d 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUnknownUser.cs @@ -29,6 +29,7 @@ namespace Discord.WebSocket return entity; } + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Unknown)"; internal new SocketUnknownUser Clone() => MemberwiseClone() as SocketUnknownUser; } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs index b0374c85d..b66f14e7d 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketWebhookUser.cs @@ -44,6 +44,7 @@ namespace Discord.WebSocket return entity; } + private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")}, Webhook)"; internal new SocketWebhookUser Clone() => MemberwiseClone() as SocketWebhookUser; diff --git a/src/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs b/src/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs index 57abf1d03..c5f13b1a9 100644 --- a/src/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs +++ b/src/Discord.Net.WebSocket/Entities/Voice/SocketVoiceServer.cs @@ -2,12 +2,33 @@ using System.Diagnostics; namespace Discord.WebSocket { + /// + /// Represents a WebSocket-based voice server. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketVoiceServer { - public Cacheable Guild { get; private set; } - public string Endpoint { get; private set; } - public string Token { get; private set; } + /// + /// Gets the guild associated with the voice server. + /// + /// + /// A cached entity of the guild. + /// + public Cacheable Guild { get; } + /// + /// Gets the endpoint URL of the voice server host. + /// + /// + /// An URL representing the voice server host. + /// + public string Endpoint { get; } + /// + /// Gets the voice connection token. + /// + /// + /// A voice connection token. + /// + public string Token { get; } internal SocketVoiceServer(Cacheable guild, string endpoint, string token) { From 3a7d7ee9552a2c6fea1c32b6f3cd91f6efdd29f5 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 5 May 2018 15:36:22 +0800 Subject: [PATCH 158/183] Minor fixes in documentations + Fix unescaped '<' + Fix typo --- .../Commands/PreconditionAttribute.Overwrites.md | 4 ++-- src/Discord.Net.Core/Entities/Emotes/Emote.cs | 4 ++-- src/Discord.Net.Core/Entities/IMentionable.cs | 2 +- src/Discord.Net.Core/Entities/Users/IUser.cs | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md b/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md index 8117b2c3f..9af953e72 100644 --- a/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md +++ b/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md @@ -31,11 +31,11 @@ The following example creates a precondition to see if the user has sufficient role required to access the command. ```cs -public class RequireRoleAtribute : PreconditionAttribute +public class RequireRoleAttribute : PreconditionAttribute { private readonly ulong _roleId; - public RequireRoleAtribute(ulong roleId) + public RequireRoleAttribute(ulong roleId) { _roleId = roleId; } diff --git a/src/Discord.Net.Core/Entities/Emotes/Emote.cs b/src/Discord.Net.Core/Entities/Emotes/Emote.cs index 68156016b..32d49fede 100644 --- a/src/Discord.Net.Core/Entities/Emotes/Emote.cs +++ b/src/Discord.Net.Core/Entities/Emotes/Emote.cs @@ -59,7 +59,7 @@ namespace Discord } /// Parses an from its raw format. - /// The raw encoding of an emote; for example, <:dab:277855270321782784>. + /// The raw encoding of an emote; for example, <:dab:277855270321782784>. /// An emote. /// Invalid emote format. public static Emote Parse(string text) @@ -70,7 +70,7 @@ namespace Discord } /// Tries to parse an from its raw format. - /// The raw encoding of an emote; for example, <:dab:277855270321782784>. + /// The raw encoding of an emote; for example, <:dab:277855270321782784>. /// An emote. public static bool TryParse(string text, out Emote result) { diff --git a/src/Discord.Net.Core/Entities/IMentionable.cs b/src/Discord.Net.Core/Entities/IMentionable.cs index cf4a8662b..225806723 100644 --- a/src/Discord.Net.Core/Entities/IMentionable.cs +++ b/src/Discord.Net.Core/Entities/IMentionable.cs @@ -9,7 +9,7 @@ namespace Discord /// Returns a special string used to mention this object. /// /// - /// A string that is recognized by Discord as a mention (e.g. <@168693960628371456>). + /// A string that is recognized by Discord as a mention (e.g. <@168693960628371456>). /// string Mention { get; } } diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index 929824ced..aabe4fdb4 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -12,7 +12,7 @@ namespace Discord /// string AvatarId { get; } /// - /// Gets the URL to this user's avatar. + /// Returns the URL to this user's avatar. /// /// The format to return. /// @@ -23,7 +23,7 @@ namespace Discord /// string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128); /// - /// Gets the URL to this user's default avatar. + /// Returns the URL to this user's default avatar. /// string GetDefaultAvatarUrl(); /// @@ -35,11 +35,11 @@ namespace Discord /// ushort DiscriminatorValue { get; } /// - /// Returns if this user is a bot user. + /// Gets if this user is a bot user. /// bool IsBot { get; } /// - /// Returns if this user is a webhook user. + /// Gets if this user is a webhook user. /// bool IsWebhook { get; } /// From c2de0c055f3894c795ea7d7da774fb006c752898 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 5 May 2018 15:40:16 +0800 Subject: [PATCH 159/183] Fix seealso for preconditions and add missing descriptions --- .../Commands/PreconditionAttribute.Overwrites.md | 4 ---- .../Attributes/ParameterPreconditionAttribute.cs | 1 + .../Attributes/PreconditionAttribute.cs | 13 +++++++++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md b/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md index 9af953e72..75b9f93a5 100644 --- a/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md +++ b/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md @@ -1,7 +1,5 @@ --- uid: Discord.Commands.PreconditionAttribute -seealso: - - linkId: Discord.Commands.ParameterPreconditionAttribute remarks: *content --- @@ -12,8 +10,6 @@ method-level for a command. --- uid: Discord.Commands.ParameterPreconditionAttribute -seealso: - - linkId: Discord.Commands.PreconditionAttribute remarks: *content --- diff --git a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs index efdb2c5b2..9b750809b 100644 --- a/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs @@ -6,6 +6,7 @@ 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 { diff --git a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs index 58d9c8ba4..1d791c92e 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -3,12 +3,15 @@ using System.Threading.Tasks; namespace Discord.Commands { - /// Requires the module or class to pass the specified precondition before execution can begin. + /// + /// 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. + /// Specifies a group that this precondition belongs to. /// /// /// of the same group require only one of the preconditions to pass in order to @@ -17,6 +20,12 @@ namespace Discord.Commands /// public string Group { get; set; } = null; + /// + /// Checks if the has the sufficient permission to be executed. + /// + /// The context of the command. + /// The command being executed. + /// The service collection used for dependency injection. public abstract Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services); } } From 5a824a5695d72a62e28e0a307a5be446557291be Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 5 May 2018 15:44:15 +0800 Subject: [PATCH 160/183] Add missing exceptions --- .../Entities/Channels/RestGroupChannel.cs | 26 +++++++++++++++++++ .../Entities/Channels/SocketDMChannel.cs | 2 ++ 2 files changed, 28 insertions(+) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index d9a6c1a31..34346555f 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -84,14 +84,40 @@ namespace Discord.Rest => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); /// + /// Message content is too long, length must be less or equal to . public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM /// + /// + /// is a zero-length string, contains only white space, or contains one or more + /// invalid characters as defined by . + /// + /// + /// is . + /// + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// + /// The specified path is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// + /// The file specified in was not found. + /// + /// is in an invalid format. + /// An I/O error occurred while opening the file. + /// Message content is too long, length must be less or equal to . 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 /// + /// Message content is too long, length must be less or equal to . 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); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index eab60f6c9..763296590 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -78,6 +78,7 @@ namespace Discord.WebSocket => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); /// + /// Message content is too long, length must be less or equal to . public Task SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); #if FILESYSTEM @@ -86,6 +87,7 @@ namespace Discord.WebSocket => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, embed, options); #endif /// + /// Message content is too long, length must be less or equal to . 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); From f27d659ebeb88348e24e759308933e3983831ad7 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 5 May 2018 15:50:00 +0800 Subject: [PATCH 161/183] Document exposed TypeReaders --- .../Readers/ChannelTypeReader.cs | 8 ++++++-- .../Readers/MessageTypeReader.cs | 9 ++++++--- src/Discord.Net.Commands/Readers/RoleTypeReader.cs | 5 +++++ src/Discord.Net.Commands/Readers/TypeReader.cs | 14 +++++++++++++- src/Discord.Net.Commands/Readers/UserTypeReader.cs | 9 +++++++-- 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs index cd7a9d744..6cb9ca6e6 100644 --- a/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/ChannelTypeReader.cs @@ -6,19 +6,23 @@ using System.Threading.Tasks; namespace Discord.Commands { + /// + /// A for parsing objects implementing . + /// + /// The type to be checked; must implement . public class ChannelTypeReader : TypeReader where T : class, IChannel { + /// public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { if (context.Guild != null) { var results = new Dictionary(); var channels = await context.Guild.GetChannelsAsync(CacheMode.CacheOnly).ConfigureAwait(false); - ulong id; //By Mention (1.0) - if (MentionUtils.TryParseChannel(input, out id)) + if (MentionUtils.TryParseChannel(input, out ulong id)) AddResult(results, await context.Guild.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f); //By Id (0.9) diff --git a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs index a87cfbe43..acec2f12d 100644 --- a/src/Discord.Net.Commands/Readers/MessageTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/MessageTypeReader.cs @@ -4,15 +4,18 @@ using System.Threading.Tasks; namespace Discord.Commands { + /// + /// A for parsing objects implementing . + /// + /// The type to be checked; must implement . public class MessageTypeReader : TypeReader where T : class, IMessage { + /// public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { - ulong id; - //By Id (1.0) - if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) { if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg) return TypeReaderResult.FromSuccess(msg); diff --git a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs index c199033fa..4c9aaf4d8 100644 --- a/src/Discord.Net.Commands/Readers/RoleTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/RoleTypeReader.cs @@ -6,9 +6,14 @@ using System.Threading.Tasks; namespace Discord.Commands { + /// + /// A for parsing objects implementing . + /// + /// The type to be checked; must implement . public class RoleTypeReader : TypeReader where T : class, IRole { + /// public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { if (context.Guild != null) diff --git a/src/Discord.Net.Commands/Readers/TypeReader.cs b/src/Discord.Net.Commands/Readers/TypeReader.cs index af45a0aac..037213ae9 100644 --- a/src/Discord.Net.Commands/Readers/TypeReader.cs +++ b/src/Discord.Net.Commands/Readers/TypeReader.cs @@ -1,10 +1,22 @@ -using System; +using System; using System.Threading.Tasks; namespace Discord.Commands { + /// + /// Defines a reader class that parses user input into a specified type. + /// public abstract class TypeReader { + /// + /// Attempts to parse the into the desired type. + /// + /// The context of the command. + /// The raw input of the command. + /// The service collection used for dependency injection. + /// + /// An awaitable Task containing the result of the type reading process. + /// public abstract Task ReadAsync(ICommandContext context, string input, IServiceProvider services); } } diff --git a/src/Discord.Net.Commands/Readers/UserTypeReader.cs b/src/Discord.Net.Commands/Readers/UserTypeReader.cs index 7feab50ad..6d9f1dd8c 100644 --- a/src/Discord.Net.Commands/Readers/UserTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/UserTypeReader.cs @@ -7,9 +7,14 @@ using System.Threading.Tasks; namespace Discord.Commands { + /// + /// A for parsing objects implementing . + /// + /// The type to be checked; must implement . public class UserTypeReader : TypeReader where T : class, IUser { + /// public override async Task ReadAsync(ICommandContext context, string input, IServiceProvider services) { var results = new Dictionary(); @@ -72,8 +77,8 @@ namespace Discord.Commands .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); + foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Nickname, StringComparison.OrdinalIgnoreCase))) + AddResult(results, guildUser as T, guildUser.Nickname == input ? 0.60f : 0.50f); } if (results.Count > 0) From 2014870dc05dedb6171338d8ae4329eaa4c73f90 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 5 May 2018 15:57:40 +0800 Subject: [PATCH 162/183] Fix letter-casing for files --- docs/faq/commands/{Commands.md => commands.md} | 0 docs/faq/misc/{Glossary.md => glossary.md} | 0 docs/faq/misc/{Legacy.md => legacy.md} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename docs/faq/commands/{Commands.md => commands.md} (100%) rename docs/faq/misc/{Glossary.md => glossary.md} (100%) rename docs/faq/misc/{Legacy.md => legacy.md} (100%) diff --git a/docs/faq/commands/Commands.md b/docs/faq/commands/commands.md similarity index 100% rename from docs/faq/commands/Commands.md rename to docs/faq/commands/commands.md diff --git a/docs/faq/misc/Glossary.md b/docs/faq/misc/glossary.md similarity index 100% rename from docs/faq/misc/Glossary.md rename to docs/faq/misc/glossary.md diff --git a/docs/faq/misc/Legacy.md b/docs/faq/misc/legacy.md similarity index 100% rename from docs/faq/misc/Legacy.md rename to docs/faq/misc/legacy.md From 3aa5d363de335444558fdb4bf1bf48a5743bc917 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 5 May 2018 18:22:46 +0800 Subject: [PATCH 163/183] Add 'last modified' plugin Source: https://github.com/Still34/DocFx.Plugin.LastModified Licensed under MIT License --- .../plugins/LastModifiedPostProcessor.dll | Bin 0 -> 7168 bytes docs/docfx.json | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 docs/_template/lastmodified/plugins/LastModifiedPostProcessor.dll diff --git a/docs/_template/lastmodified/plugins/LastModifiedPostProcessor.dll b/docs/_template/lastmodified/plugins/LastModifiedPostProcessor.dll new file mode 100644 index 0000000000000000000000000000000000000000..c527ec698d2621ceccdec139a60ba2e4e9114250 GIT binary patch literal 7168 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 literal 0 HcmV?d00001 diff --git a/docs/docfx.json b/docs/docfx.json index ccd271999..cb6a36360 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -35,9 +35,10 @@ "dest": "_site", "template": [ "default", - "_template/light-dark-theme" + "_template/light-dark-theme", + "_template/lastmodified" ], - "postProcessors": [ "ExtractSearchIndex" ], + "postProcessors": [ "ExtractSearchIndex", "LastModifiedPostProcessor" ], "overwrite": "_overwrites/**/**.md", "globalMetadata": { "_appTitle": "Discord.Net Documentation", From 124efdf7e66ee070fbcdde07ed4f20c717c9c1c0 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sun, 6 May 2018 04:40:14 +0800 Subject: [PATCH 164/183] XML Docs --- src/Discord.Net.Core/CDN.cs | 70 ++++++++++++++++--- src/Discord.Net.Core/DiscordConfig.cs | 51 +++++++++++++- .../Entities/Messages/IAttachment.cs | 35 ++++++++-- .../Entities/Messages/IEmbed.cs | 64 +++++++++++++---- src/Discord.Net.Core/Entities/Roles/Color.cs | 15 ++-- src/Discord.Net.Core/Entities/Users/IUser.cs | 4 +- .../Entities/Messages/Attachment.cs | 11 ++- 7 files changed, 211 insertions(+), 39 deletions(-) diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index 1b75d0633..0fefc9a0d 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -8,13 +8,26 @@ namespace Discord public static class CDN { /// - /// Returns the Discord developer application icon. + /// Returns an application icon URL. /// + /// The application identifier. + /// The icon identifier. + /// + /// A URL pointing to the application's 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 . + /// Returns a user avatar URL. /// + /// The user snowflake identifier. + /// The avatar identifier. + /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The format to return. + /// + /// A URL pointing to the user's avatar in the specified size. + /// public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, ImageFormat format) { if (avatarId == null) @@ -26,34 +39,64 @@ namespace Discord /// Returns the default user avatar URL. /// /// The discriminator value of a user. + /// + /// A URL pointing to the user's default avatar when one isn't set. + /// public static string GetDefaultUserAvatarUrl(ushort discriminator) { return $"{DiscordConfig.CDNUrl}embed/avatars/{discriminator % 5}.png"; } /// - /// Returns the icon URL associated with the given guild ID. + /// Returns an icon URL. /// + /// The guild snowflake identifier. + /// The icon identifier. + /// + /// A URL pointing to the guild's icon. + /// 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. + /// Returns a guild splash URL. /// + /// The guild snowflake identifier. + /// The splash icon identifier. + /// + /// A URL pointing to the guild's icon. + /// 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. + /// Returns a channel icon URL. /// + /// The channel snowflake identifier. + /// The icon identifier. + /// + /// A URL pointing to the channel's icon. + /// 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. + /// Returns an emoji URL. /// + /// The emoji snowflake identifier. + /// Whether this emoji is animated. + /// + /// A URL pointing to the custom emote. + /// 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 . + /// Returns a Rich Presence asset URL. /// + /// The application identifier. + /// The asset identifier. + /// The size of the image to return in. This can be any power of two between 16 and 2048. + /// The format to return. + /// + /// A URL pointing to the asset image in the specified size. + /// public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format) { string extension = FormatToExtension(format, ""); @@ -61,10 +104,21 @@ namespace Discord } /// - /// Returns the Spotify album URL based on the album art ID. + /// Returns a Spotify album URL. /// + /// The identifier for the album art (e.g. 6be8f4c8614ecf4f1dd3ebba8d8692d8ce4951ac). + /// + /// A URL pointing to the Spotify album art. + /// public static string GetSpotifyAlbumArtUrl(string albumArtId) => $"https://i.scdn.co/image/{albumArtId}"; + /// + /// Returns a Spotify direct URL for a track. + /// + /// The identifier for the track (e.g. 4uLU6hMCjMI75M1A2tKUQC). + /// + /// A URL pointing to the Spotify track. + /// public static string GetSpotifyDirectUrl(string trackId) => $"https://open.spotify.com/track/{trackId}"; diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index b3cdddfa5..3dad011f5 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -8,12 +8,22 @@ namespace Discord public class DiscordConfig { /// - /// Returns the gateway version Discord.Net uses. + /// Returns the API version Discord.Net uses. /// + /// + /// A 32-bit integer representing the API version that Discord.Net uses to communicate with Discord. + /// A list of available API version can be seen on the official + /// Discord API documentation + /// . + /// public const int APIVersion = 6; /// /// Gets the Discord.Net version, including the build number. /// + /// + /// A string containing the detailed version information, including its build number; Unknown when + /// the version fails to be fetched. + /// public static string Version { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute()?.InformationalVersion ?? typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ?? @@ -22,54 +32,91 @@ namespace Discord /// /// Gets the user agent that Discord.Net uses in its clients. /// + /// + /// The user agent used in each Discord.Net request. + /// public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; /// /// Returns the base Discord API URL. /// + /// + /// The Discord API URL using . + /// public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/"; /// /// Returns the base Discord CDN URL. /// + /// + /// The base Discord Content Delivery Network (CDN) URL. + /// public const string CDNUrl = "https://cdn.discordapp.com/"; /// - /// Returns the base Discord invite URL. + /// Returns the base Discord invite URL. /// + /// + /// The base Discord invite URL. + /// public const string InviteUrl = "https://discord.gg/"; /// /// Returns the default timeout for requests. /// + /// + /// The amount of time it takes in milliseconds before a request is timed out. + /// public const int DefaultRequestTimeout = 15000; /// /// Returns the max length for a Discord message. /// + /// + /// The maximum length of a message allowed by Discord. + /// public const int MaxMessageSize = 2000; /// /// Returns the max messages allowed to be in a request. /// + /// + /// The maximum number of messages that can be gotten per-batch. + /// public const int MaxMessagesPerBatch = 100; /// /// Returns the max users allowed to be in a request. /// + /// + /// The maximum number of users that can be gotten per-batch. + /// public const int MaxUsersPerBatch = 1000; /// /// Returns the max guilds allowed to be in a request. /// + /// + /// The maximum number of guilds that can be gotten per-batch. + /// public const int MaxGuildsPerBatch = 100; /// /// Gets or sets how a request should act in the case of an error, by default. /// + /// + /// The currently set . + /// public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; /// /// Gets or sets the minimum log level severity that will be sent to the Log event. /// + /// + /// The currently set for logging level. + /// public LogSeverity LogLevel { get; set; } = LogSeverity.Info; /// /// Gets or sets whether the initial log entry should be printed. /// + /// + /// If set to true, the library will attempt to print the current version of the library, as well as + /// the API version it uses on startup. + /// internal bool DisplayInitialLog { get; set; } = true; } } diff --git a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs index f01876186..c61d8177c 100644 --- a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs +++ b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs @@ -6,33 +6,54 @@ namespace Discord public interface IAttachment { /// - /// Gets the snowflake ID of the attachment. + /// Gets the ID of this attachment. /// + /// + /// A snowflake ID associated with this attachment. + /// ulong Id { get; } /// - /// Gets the filename of the attachment. + /// Gets the filename of this attachment. /// + /// + /// A string containing the full filename of this attachment (e.g. textFile.txt). + /// string Filename { get; } /// - /// Gets the URL of the attachment. + /// Gets the URL of this attachment. /// + /// + /// A string containing the URL of this attachment. + /// string Url { get; } /// - /// Gets the proxied URL of the attachment. + /// Gets a proxied URL of this attachment. /// + /// + /// A string containing the proxied URL of this attachment. + /// string ProxyUrl { get; } /// - /// Gets the file size of the attachment. + /// Gets the file size of this attachment. /// + /// + /// The size of this attachment in bytes. + /// int Size { get; } /// - /// Gets the height of the attachment if it is an image, or return when it is not. + /// Gets the height of this attachment. /// + /// + /// The height of this attachment if it is a picture; otherwise . + /// int? Height { get; } /// - /// Gets the width of the attachment if it is an image, or return when it is not. + /// Gets the width of this attachment. /// + /// + /// The width of this attachment if it is a picture; otherwise . + /// int? Width { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs index 473a61ed5..1482de20a 100644 --- a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs +++ b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs @@ -9,56 +9,96 @@ namespace Discord public interface IEmbed { /// - /// Gets the title URL of the embed. + /// Gets the title URL of this embed. /// + /// + /// A string containing the URL set in a title of the embed. + /// string Url { get; } /// - /// Gets the title of the embed. + /// Gets the title of this embed. /// + /// + /// The title of the embed. + /// string Title { get; } /// - /// Gets the description of the embed. + /// Gets the description of this embed. /// + /// + /// The description field of the embed. + /// string Description { get; } /// - /// Gets the type of the embed. + /// Gets the type of this embed. /// + /// + /// The type of the embed. + /// EmbedType Type { get; } /// - /// Gets the timestamp of the embed, or if none is set. + /// Gets the timestamp of this embed. /// + /// + /// A based on the timestamp present at the bottom left of the embed, or + /// if none is set. + /// DateTimeOffset? Timestamp { get; } /// - /// Gets the sidebar color of the embed, or if none is set. + /// Gets the color of this embed. /// + /// + /// The color of the embed present on the side of the embed, or if none is set. + /// Color? Color { get; } /// - /// Gets the image of the embed, or if none is set. + /// Gets the image of this embed. /// + /// + /// The image of the embed, or if none is set. + /// EmbedImage? Image { get; } /// - /// Gets the video of the embed, or if none is set. + /// Gets the video of this embed. /// + /// + /// 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. + /// Gets the author field of this embed. /// + /// + /// 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. + /// Gets the footer field of this embed. /// + /// + /// The author field of the embed, or if none is set. + /// EmbedFooter? Footer { get; } /// - /// Gets the provider of the embed, or if none is set. + /// Gets the provider of this embed. /// + /// + /// The source of the embed, or if none is set. + /// EmbedProvider? Provider { get; } /// - /// Gets the thumbnail featured in the embed, or if none is set. + /// Gets the thumbnail featured in this embed. /// + /// + /// The thumbnail featured in the embed, or if none is set. + /// EmbedThumbnail? Thumbnail { get; } /// /// Gets the fields of the embed. /// + /// + /// An array of the fields of the embed. + /// ImmutableArray Fields { get; } } } diff --git a/src/Discord.Net.Core/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs index cdee5284d..fa7624a57 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -4,7 +4,7 @@ using System.Diagnostics; namespace Discord { /// - /// Represents a Discord color. + /// Represents a color used in Discord. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public struct Color @@ -65,7 +65,7 @@ namespace Discord /// /// Initializes a struct with the given raw value. /// - /// A raw value for the color (e.g. 0x607D8B). + /// The raw value of the color (e.g. 0x607D8B). public Color(uint rawValue) { RawValue = rawValue; @@ -73,9 +73,9 @@ namespace Discord /// /// 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. + /// The byte that represents the red color. + /// The byte that represents the green color. + /// The byte that represents the blue color. public Color(byte r, byte g, byte b) { RawValue = @@ -126,8 +126,11 @@ namespace Discord } /// - /// Gets the hexadecimal representation of the color (e.g. #000ccc). + /// Gets the hexadecimal representation of the color (e.g. #000ccc). /// + /// + /// A hexadecimal string of the color. + /// public override string ToString() => $"#{Convert.ToString(RawValue, 16)}"; private string DebuggerDisplay => diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index aabe4fdb4..e1e6c258f 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -12,11 +12,11 @@ namespace Discord /// string AvatarId { get; } /// - /// Returns the URL to this user's avatar. + /// Returns a URL to this user's avatar. /// /// The format to return. /// - /// The size of the image to return in. Image size can be any power of two between 16 and 2048. + /// The size of the image to return in. This can be any power of two between 16 and 2048. /// /// /// User's avatar URL. diff --git a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs index a51ac8e09..0f5aaf438 100644 --- a/src/Discord.Net.Rest/Entities/Messages/Attachment.cs +++ b/src/Discord.Net.Rest/Entities/Messages/Attachment.cs @@ -3,7 +3,9 @@ using Model = Discord.API.Attachment; namespace Discord { - /// A Discord attachment. + /// + /// An attachment file seen in a . + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class Attachment : IAttachment { @@ -39,7 +41,12 @@ namespace Discord model.Width.IsSpecified ? model.Width.Value : (int?)null); } - /// Returns the filename of the attachment. + /// + /// Returns the filename of this attachment. + /// + /// + /// A string containing the filename of this attachment. + /// public override string ToString() => Filename; private string DebuggerDisplay => $"{Filename} ({Size} bytes)"; } From 0ad66f6765addf6d4d25dfbddadeaa542d461403 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sun, 6 May 2018 04:55:15 +0800 Subject: [PATCH 165/183] Fix minor consistencies & redundant impl --- .../Preconditions/RequireUserPermissionAttribute.cs | 6 +++--- src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs | 2 +- src/Discord.Net.WebSocket/Audio/AudioClient.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 072f10e0f..f2bd717e4 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -54,9 +54,9 @@ namespace Discord.Commands if (GuildPermission.HasValue) { if (guildUser == null) - return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel")); + return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel.")); if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) - return Task.FromResult(PreconditionResult.FromError($"User requires guild permission {GuildPermission.Value}")); + return Task.FromResult(PreconditionResult.FromError($"User requires guild permission {GuildPermission.Value}.")); } if (ChannelPermission.HasValue) @@ -68,7 +68,7 @@ namespace Discord.Commands perms = ChannelPermissions.All(context.Channel); if (!perms.Has(ChannelPermission.Value)) - return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}")); + return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}.")); } return Task.FromResult(PreconditionResult.FromSuccess()); diff --git a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs index fdfe9b5a1..68930e74c 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs @@ -9,7 +9,7 @@ using Model = Discord.API.GuildMember; namespace Discord.Rest { [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class RestGuildUser : RestUser, IGuildUser, IUpdateable + public class RestGuildUser : RestUser, IGuildUser { private long? _joinedAtTicks; private ImmutableArray _roleIds; diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index c3cbc9ca7..c3960fa67 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -16,7 +16,7 @@ using System.Collections.Generic; namespace Discord.Audio { //TODO: Add audio reconnecting - internal partial class AudioClient : IAudioClient, IDisposable + internal partial class AudioClient : IAudioClient { internal struct StreamPair { From 3e591972cafdffb6692a1b94cf9a63251188df8b Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sun, 6 May 2018 06:01:34 +0800 Subject: [PATCH 166/183] Add properties examples to overwrite --- .../Common/ObjectProperties.Overwrites.md | 174 ++++++++++++++++++ .../Channels/GuildChannelProperties.cs | 9 +- .../Channels/TextChannelProperties.cs | 3 + .../Entities/Emotes/EmoteProperties.cs | 1 + .../Entities/Guilds/GuildProperties.cs | 17 +- .../Entities/Messages/MessageProperties.cs | 18 +- .../Entities/Roles/RoleProperties.cs | 11 +- .../Entities/Users/GuildUserProperties.cs | 10 +- .../Entities/Users/SelfUserProperties.cs | 10 +- .../Entities/Webhooks/WebhookProperties.cs | 11 +- 10 files changed, 189 insertions(+), 75 deletions(-) create mode 100644 docs/_overwrites/Common/ObjectProperties.Overwrites.md diff --git a/docs/_overwrites/Common/ObjectProperties.Overwrites.md b/docs/_overwrites/Common/ObjectProperties.Overwrites.md new file mode 100644 index 000000000..e9c365d39 --- /dev/null +++ b/docs/_overwrites/Common/ObjectProperties.Overwrites.md @@ -0,0 +1,174 @@ +--- +uid: Discord.GuildChannelProperties +example: [*content] +--- + +The following example uses @Discord.IGuildChannel.ModifyAsync* to +apply changes specified in the properties, + +```cs +var channel = _client.GetChannel(id) as IGuildChannel; +if (channel == null) return; + +await channel.ModifyAsync(x => +{ + x.Name = "new-name"; + x.Position = channel.Position - 1; +}); +``` + +--- +uid: Discord.TextChannelProperties +example: [*content] +--- + +The following example uses @Discord.ITextChannel.ModifyAsync* to +apply changes specified in the properties, + +```cs +var channel = _client.GetChannel(id) as ITextChannel; +if (channel == null) return; + +await channel.ModifyAsync(x => +{ + x.Name = "cool-guys-only"; + x.Topic = "This channel is only for cool guys and adults!!!"; + x.Position = channel.Position - 1; + x.IsNsfw = true; +}); +``` + +--- +uid: Discord.VoiceChannelProperties +example: [*content] +--- + +The following example uses @Discord.IVoiceChannel.ModifyAsync* to +apply changes specified in the properties, + +```cs +var channel = _client.GetChannel(id) as IVoiceChannel; +if (channel == null) return; + +await channel.ModifyAsync(x => +{ + x.UserLimit = 5; +}); +``` + +--- +uid: Discord.EmoteProperties +example: [*content] +--- + +The following example uses @Discord.IGuild.ModifyEmoteAsync* to +apply changes specified in the properties, + +```cs +await guild.ModifyEmoteAsync(x => +{ + x.Name = "blobo"; +}); +``` + +--- +uid: Discord.MessageProperties +example: [*content] +--- + +The following example uses @Discord.IUserMessage.ModifyAsync* to +apply changes specified in the properties, + +```cs +var message = await channel.SendMessageAsync("boo"); +await Task.Delay(TimeSpan.FromSeconds(1)); +await message.ModifyAsync(x => x.Content = "boi"); +``` + +--- +uid: Discord.GuildProperties +example: [*content] +--- + +The following example uses @Discord.IGuild.ModifyAsync* to +apply changes specified in the properties, + +```cs +var guild = _client.GetGuild(id); +if (guild == null) return; + +await guild.ModifyAsync(x => +{ + x.Name = "VERY Fast Discord Running at Incredible Hihg Speed"; +}); +``` + +--- +uid: Discord.RoleProperties +example: [*content] +--- + +The following example uses @Discord.IRole.ModifyAsync* to +apply changes specified in the properties, + +```cs +var role = guild.GetRole(id); +if (role == null) return; + +await role.ModifyAsync(x => +{ + x.Name = "cool boi"; + x.Color = Color.Gold; + x.Hoist = true; + x.Mentionable = true; +}); +``` + +--- +uid: Discord.GuildUserProperties +example: [*content] +--- + +The following example uses @Discord.IGuildUser.ModifyAsync* to +apply changes specified in the properties, + +```cs +var user = guild.GetUser(id); +if (user == null) return; + +await user.ModifyAsync(x => +{ + x.Nickname = "I need healing"; +}); +``` + +--- +uid: Discord.SelfUserProperties +example: [*content] +--- + +The following example uses @Discord.ISelfUser.ModifyAsync* to +apply changes specified in the properties, + +```cs +await selfUser.ModifyAsync(x => +{ + x.Username = "Mercy"; +}); +``` + +--- +uid: Discord.WebhookProperties +example: [*content] +--- + +The following example uses @Discord.IWebhook.ModifyAsync* to +apply changes specified in the properties, + +```cs +await webhook.ModifyAsync(x => +{ + x.Name = "very fast fox"; + x.ChannelId = newChannelId; +}); +``` \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs index fdbd0447c..a0edfc796 100644 --- a/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs @@ -3,14 +3,7 @@ namespace Discord /// /// Properties that are used to modify an with the specified changes. /// - /// - /// - /// await (Context.Channel as ITextChannel)?.ModifyAsync(x => - /// { - /// x.Name = "do-not-enter"; - /// }); - /// - /// + /// public class GuildChannelProperties { /// diff --git a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs index b68c416b7..03b56b26c 100644 --- a/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs @@ -1,8 +1,11 @@ +using System; + namespace Discord { /// /// Properties that are used to modify an with the specified changes. /// + /// public class TextChannelProperties : GuildChannelProperties { /// diff --git a/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs b/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs index 721345afe..de457a0dc 100644 --- a/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs +++ b/src/Discord.Net.Core/Entities/Emotes/EmoteProperties.cs @@ -5,6 +5,7 @@ namespace Discord /// /// Properties that are used to modify an with the specified changes. /// + /// public class EmoteProperties { /// diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs index 3c136b579..7e23f3b1d 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs @@ -3,28 +3,19 @@ namespace Discord /// /// Properties that are used to modify an with the specified changes. /// - /// - /// - /// await Context.Guild.ModifyAsync(async x => - /// { - /// x.Name = "aaaaaah"; - /// }); - /// - /// - /// + /// public class GuildProperties { - public Optional Username { get; set; } /// - /// Gets or sets the name of the Guild. + /// Gets or sets the name of the guild. Must be within 100 characters. /// public Optional Name { get; set; } /// - /// Gets or sets the region for the Guild's voice connections. + /// Gets or sets the region for the guild's voice connections. /// public Optional Region { get; set; } /// - /// Gets or sets 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; } /// diff --git a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs index c2892117a..2cc0eab8e 100644 --- a/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs +++ b/src/Discord.Net.Core/Entities/Messages/MessageProperties.cs @@ -4,23 +4,9 @@ namespace Discord /// Properties that are used to modify an with the specified changes. /// /// - /// The content of a message can be cleared with if and only if an is present. + /// The content of a message can be cleared with if and only if an 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.") - /// .Build(); - /// }); - /// - /// + /// public class MessageProperties { /// diff --git a/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs b/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs index 79372b86d..54fa27bfd 100644 --- a/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs +++ b/src/Discord.Net.Core/Entities/Roles/RoleProperties.cs @@ -3,16 +3,7 @@ namespace Discord /// /// 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; - /// }); - /// - /// - /// + /// public class RoleProperties { /// diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs index beb2c392f..401b0ec7a 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -5,15 +5,7 @@ namespace Discord /// /// Properties that are used to modify an with the following parameters. /// - /// - /// - /// await guildUser.ModifyAsync(x => - /// { - /// x.Nickname = $"festive {guildUser.Username}"; - /// }); - /// - /// - /// + /// public class GuildUserProperties { /// diff --git a/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs b/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs index d79da0265..e2ae12ba4 100644 --- a/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/SelfUserProperties.cs @@ -3,15 +3,7 @@ namespace Discord /// /// 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")); - /// }); - /// - /// - /// + /// public class SelfUserProperties { /// diff --git a/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs index 387ee6106..00af9f9ef 100644 --- a/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs +++ b/src/Discord.Net.Core/Entities/Webhooks/WebhookProperties.cs @@ -3,16 +3,7 @@ namespace Discord /// /// Properties used to modify an with the specified changes. /// - /// - /// - /// await webhook.ModifyAsync(x => - /// { - /// x.Name = "Bob"; - /// x.Avatar = new Image("avatar.jpg"); - /// }); - /// - /// - /// + /// public class WebhookProperties { /// From adae5ffc9e42343f170d6c869644637587330b59 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sun, 6 May 2018 06:07:28 +0800 Subject: [PATCH 167/183] Fix missing Username prop --- src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs | 2 +- src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs index 7e23f3b1d..eccd852dd 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs @@ -3,7 +3,7 @@ namespace Discord /// /// Properties that are used to modify an with the specified changes. /// - /// + /// public class GuildProperties { /// diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index b279f06a3..6408ead2f 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -32,7 +32,6 @@ namespace Discord.Rest Icon = args.Icon.IsSpecified ? args.Icon.Value?.ToModel() : Optional.Create(), Name = args.Name, Splash = args.Splash.IsSpecified ? args.Splash.Value?.ToModel() : Optional.Create(), - Username = args.Username, VerificationLevel = args.VerificationLevel }; From d8bb9e7aaa1e908ceb9227285c0cebec9add5280 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sun, 6 May 2018 06:31:50 +0800 Subject: [PATCH 168/183] Add warning for bulk-delete endpoint --- src/Discord.Net.Core/Entities/Channels/ITextChannel.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index d1b2465ad..243eb4be3 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -31,12 +31,20 @@ namespace Discord /// /// The messages to be bulk-deleted. /// The options to be used when sending the request. + /// + /// > [!IMPORTANT] + /// > This method can only remove messages that are posted within 14 days! + /// Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); /// /// Bulk-deletes multiple messages. /// /// The IDs of the messages to be bulk-deleted. /// The options to be used when sending the request. + /// + /// > [!IMPORTANT] + /// > This method can only remove messages that are posted within 14 days! + /// Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null); /// From 8f64c045999c1bb0440bdc818f082e7459756b36 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sun, 6 May 2018 06:37:55 +0800 Subject: [PATCH 169/183] Replace note block --- .../Entities/Channels/ITextChannel.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index 243eb4be3..099822fb0 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -29,22 +29,24 @@ namespace Discord /// /// Bulk-deletes multiple messages. /// - /// The messages to be bulk-deleted. - /// The options to be used when sending the request. /// - /// > [!IMPORTANT] - /// > This method can only remove messages that are posted within 14 days! + /// + /// This method can only remove messages that are posted within 14 days! + /// /// + /// The messages to be bulk-deleted. + /// The options to be used when sending the request. Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null); /// /// Bulk-deletes multiple messages. /// - /// The IDs of the messages to be bulk-deleted. - /// The options to be used when sending the request. /// - /// > [!IMPORTANT] - /// > This method can only remove messages that are posted within 14 days! + /// + /// This method can only remove messages that are posted within 14 days! + /// /// + /// The IDs of the messages to be bulk-deleted. + /// The options to be used when sending the request. Task DeleteMessagesAsync(IEnumerable messageIds, RequestOptions options = null); /// From 65d4e4360eb88e3bc5cc8f0da6135e5ee997af6c Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sun, 6 May 2018 06:57:53 +0800 Subject: [PATCH 170/183] Add BaseSocketClient docs --- src/Discord.Net.WebSocket/BaseSocketClient.cs | 57 +++++++++++++------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 81e573e1d..7dec313cf 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -50,30 +50,48 @@ namespace Discord.WebSocket /// /// Gets a Discord application information for the logged-in user. /// + /// + /// This method reflects your application information you submitted when creating a Discord application via + /// the Developer Portal. + /// /// The options to be used when sending the request. /// - /// Application information. This reflects your application information you submitted when creating a - /// Discord application via the Developer Portal. + /// An awaitable containing the application information. /// public abstract Task GetApplicationInfoAsync(RequestOptions options = null); /// /// Gets a user who shares a mutual guild with logged-in user with the provided snowflake ID. /// /// The user snowflake ID. + /// + /// This method gets the user present in the WebSocket cache with the given condition. + /// + /// Sometimes a user may return because Discord does not send offline users in + /// large guilds (i.e. guild with 100+ members) actively. To download more users on startup, please + /// enable . + /// + /// /// - /// A user who shares a mutual guild with the logged-in user and who is also present in the WebSocket cache; - /// or when the user cannot be found. + /// A WebSocket-based generic user; when the user cannot be found. /// public abstract SocketUser GetUser(ulong id); /// - /// Gets a user who shares a mutual guild with the logged-in user with the provided username and discriminator value combo. + /// Gets a user who shares a mutual guild with the logged-in user with the provided username and + /// discriminator value combo. /// /// The name of the user. /// The discriminator value of the user. + /// + /// This method gets the user present in the WebSocket cache with the given condition. + /// + /// Sometimes a user may return because Discord does not send offline users in + /// large guilds (i.e. guild with 100+ members) actively. To download more users on startup, please + /// enable . + /// + /// /// - /// A user who shares a mutual guild with the logged-in user and who is also present in the WebSocket cache; - /// or when the user cannot be found. + /// A WebSocket-based generic user; when the user cannot be found. /// public abstract SocketUser GetUser(string username, string discriminator); /// @@ -81,8 +99,8 @@ namespace Discord.WebSocket /// /// The channel snowflake ID. /// - /// A generic channel object (voice, text, category, etc.); or when the channel - /// cannot be found. + /// A generic WebSocket-based channel object (voice, text, category, etc.); when the + /// channel cannot be found. /// public abstract SocketChannel GetChannel(ulong id); /// @@ -90,7 +108,7 @@ namespace Discord.WebSocket /// /// The guild snowflake ID. /// - /// A guild; or when the guild cannot be found. + /// A WebSocket-based guild; when the guild cannot be found. /// public abstract SocketGuild GetGuild(ulong id); /// @@ -98,7 +116,7 @@ namespace Discord.WebSocket /// /// The unique identifier of the voice region. /// - /// A voice region; or if none can be found. + /// A REST-based voice region; if none can be found. /// public abstract RestVoiceRegion GetVoiceRegion(string id); /// @@ -127,9 +145,14 @@ namespace Discord.WebSocket /// Sets the of the logged-in user. /// /// - /// This method sets the of the user. Please note that Rich Presence cannot be - /// set via this method or client. Rich Presence is strictly limited to RPC clients only. Furthermore, - /// Discord will only accept setting of name and the type of activity. + /// This method sets the of the user. + /// + /// Discord will only accept setting of name and the type of activity. + /// + /// + /// Rich Presence cannot be set via this method or client. Rich Presence is strictly limited to RPC + /// clients only. + /// /// /// The activty to be set. /// @@ -149,8 +172,10 @@ namespace Discord.WebSocket /// Creates a guild for the logged-in user who is in less than 10 active guilds. /// /// - /// This method creates a new guild on behalf of the logged-in user. Note that due to Discord's limitation, - /// this method will only work for users that are in less than 10 guilds. + /// This method creates a new guild on behalf of the logged-in user. + /// + /// Due to Discord's limitation, this method will only work for users that are in less than 10 guilds. + /// /// /// The name of the new guild. /// The voice region to create the guild with. From 0b15bbc54d15c036f770bab818712956c527b4ee Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sun, 6 May 2018 15:20:34 +0800 Subject: [PATCH 171/183] Add XML docs --- src/Discord.Net.Core/IDiscordClient.cs | 31 +++++++++++++++- src/Discord.Net.WebSocket/BaseSocketClient.cs | 37 +++++++++++-------- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/Discord.Net.Core/IDiscordClient.cs b/src/Discord.Net.Core/IDiscordClient.cs index 92aee2b08..f5bbd0a28 100644 --- a/src/Discord.Net.Core/IDiscordClient.cs +++ b/src/Discord.Net.Core/IDiscordClient.cs @@ -10,16 +10,33 @@ namespace Discord /// public interface IDiscordClient : IDisposable { + /// + /// Gets the current state of connection. + /// ConnectionState ConnectionState { get; } + /// + /// Gets the currently logged-in user. + /// ISelfUser CurrentUser { get; } + /// + /// Gets the token type of the logged-in user. + /// TokenType TokenType { get; } Task StartAsync(); Task StopAsync(); /// - /// Gets the application information associated with this account. + /// Gets a Discord application information for the logged-in user. /// + /// + /// This method reflects your application information you submitted when creating a Discord application via + /// the Developer Portal. + /// + /// The options to be used when sending the request. + /// + /// An awaitable containing the application information. + /// Task GetApplicationInfoAsync(RequestOptions options = null); /// @@ -36,11 +53,21 @@ namespace Discord /// Task> GetPrivateChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// - /// Gets a list of direct message channels. + /// Returns a collection of direct message channels. /// + /// + /// This method returns a collection of currently opened direct message channels. + /// + /// This method will not return previously opened DM channels outside of the current session! If you + /// have just started the client, this may return an empty collection. + /// + /// /// /// The that determines whether the object should be fetched from cache. /// + /// + /// An awaitable containing a collection of DM channels. + /// Task> GetDMChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// /// Gets a list of group channels. diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 7dec313cf..b3702a811 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -60,15 +60,19 @@ namespace Discord.WebSocket /// public abstract Task GetApplicationInfoAsync(RequestOptions options = null); /// - /// Gets a user who shares a mutual guild with logged-in user with the provided snowflake ID. + /// Gets a user. /// /// The user snowflake ID. /// /// This method gets the user present in the WebSocket cache with the given condition. - /// - /// Sometimes a user may return because Discord does not send offline users in - /// large guilds (i.e. guild with 100+ members) actively. To download more users on startup, please - /// enable . + /// + /// Sometimes a user may return due to Discord not sending offline users in large + /// guilds (i.e. guild with 100+ members) actively. To download users on startup, consider enabling + /// . + /// + /// + /// This method does not attempt to fetch users that the logged-in user does not have access to (i.e. + /// users who don't share mutual guild(s) with the current user). /// /// /// @@ -77,17 +81,20 @@ namespace Discord.WebSocket public abstract SocketUser GetUser(ulong id); /// - /// Gets a user who shares a mutual guild with the logged-in user with the provided username and - /// discriminator value combo. + /// Gets a user. /// /// The name of the user. /// The discriminator value of the user. /// /// This method gets the user present in the WebSocket cache with the given condition. - /// - /// Sometimes a user may return because Discord does not send offline users in - /// large guilds (i.e. guild with 100+ members) actively. To download more users on startup, please - /// enable . + /// + /// Sometimes a user may return due to Discord not sending offline users in large + /// guilds (i.e. guild with 100+ members) actively. To download users on startup, consider enabling + /// . + /// + /// + /// This method does not attempt to fetch users that the logged-in user does not have access to (i.e. + /// users who don't share mutual guild(s) with the current user). /// /// /// @@ -95,7 +102,7 @@ namespace Discord.WebSocket /// public abstract SocketUser GetUser(string username, string discriminator); /// - /// Gets a channel that the logged-in user is accessible to with the provided ID. + /// Gets a channel. /// /// The channel snowflake ID. /// @@ -104,7 +111,7 @@ namespace Discord.WebSocket /// public abstract SocketChannel GetChannel(ulong id); /// - /// Gets a guild that the logged-in user is accessible to with the provided ID. + /// Gets a guild. /// /// The guild snowflake ID. /// @@ -112,7 +119,7 @@ namespace Discord.WebSocket /// public abstract SocketGuild GetGuild(ulong id); /// - /// Gets a voice region with the provided ID. + /// Gets a voice region. /// /// The unique identifier of the voice region. /// @@ -149,7 +156,7 @@ namespace Discord.WebSocket /// /// Discord will only accept setting of name and the type of activity. /// - /// + /// /// Rich Presence cannot be set via this method or client. Rich Presence is strictly limited to RPC /// clients only. /// From ac47d84ea74c81e136434dcc33967fe4cd95c1e6 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sun, 6 May 2018 15:22:17 +0800 Subject: [PATCH 172/183] Replace langword null to code block null instead - Because DocFX sucks at rendering langword --- .../Entities/Activities/GameAsset.cs | 2 +- .../Entities/Channels/IGuildChannel.cs | 10 ++--- .../Entities/Channels/IMessageChannel.cs | 4 +- .../Entities/Channels/ITextChannel.cs | 4 +- .../Entities/Channels/IVoiceChannel.cs | 2 +- .../Channels/VoiceChannelProperties.cs | 2 +- .../Entities/Guilds/GuildEmbedProperties.cs | 4 +- .../Entities/Guilds/IGuild.cs | 38 +++++++++---------- .../Entities/Guilds/IUserGuild.cs | 2 +- .../Entities/Invites/IInviteMetadata.cs | 4 +- .../Entities/Messages/EmbedBuilder.cs | 2 +- .../Entities/Messages/EmbedVideo.cs | 4 +- .../Entities/Messages/IAttachment.cs | 4 +- .../Entities/Messages/IMessage.cs | 2 +- .../Entities/Users/IVoiceState.cs | 2 +- src/Discord.Net.Core/Net/HttpException.cs | 2 +- src/Discord.Net.Core/RequestOptions.cs | 2 +- src/Discord.Net.Core/Utils/Cacheable.cs | 2 +- .../Utils/ConcurrentHashSet.cs | 8 ++-- .../Entities/Channels/IRestMessageChannel.cs | 4 +- .../Entities/Guilds/GuildHelper.cs | 14 +++---- .../Entities/Guilds/RestGuild.cs | 4 +- src/Discord.Net.WebSocket/BaseSocketClient.cs | 12 +++--- .../DiscordSocketConfig.cs | 2 +- .../Channels/ISocketMessageChannel.cs | 2 +- .../Entities/Channels/SocketGuildChannel.cs | 2 +- .../Entities/Channels/SocketTextChannel.cs | 2 +- .../Entities/Guilds/SocketGuild.cs | 10 ++--- .../Entities/Users/SocketVoiceState.cs | 2 +- 29 files changed, 77 insertions(+), 77 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs index b0c8ea975..467b2885f 100644 --- a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs +++ b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs @@ -19,7 +19,7 @@ namespace Discord public string ImageId { get; internal set; } /// - /// Returns the image URL of the asset, or when the application ID does not exist. + /// Returns the image URL of the asset, or null when the application ID does not exist. /// public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) => ApplicationId.HasValue ? CDN.GetRichAssetUrl(ApplicationId.Value, ImageId, size, format) : null; diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index fdbe77653..5028bd388 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -24,7 +24,7 @@ namespace Discord /// Gets the parent ID (category) of this channel in the guild's channel list. /// /// - /// The parent category ID associated with this channel, or if none is set. + /// The parent category ID associated with this channel, or null if none is set. /// ulong? CategoryId { get; } /// @@ -57,10 +57,10 @@ namespace Discord /// Creates a new invite to this channel. /// /// - /// The time (in seconds) until the invite expires. Set to to never expire. + /// 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 to have unlimited uses. + /// The max amount of times this invite may be used. Set to null to have unlimited uses. /// /// /// If , a user accepting this invite will be kicked from the guild after closing their client. @@ -86,12 +86,12 @@ namespace Discord Task ModifyAsync(Action func, RequestOptions options = null); /// - /// Gets the permission overwrite for a specific role, or if one does not exist. + /// Gets the permission overwrite for a specific role, or null if one does not exist. /// /// The role to get the overwrite from. OverwritePermissions? GetPermissionOverwrite(IRole role); /// - /// Gets the permission overwrite for a specific user, or if one does not exist. + /// Gets the permission overwrite for a specific user, or null if one does not exist. /// /// The user to get the overwrite from. OverwritePermissions? GetPermissionOverwrite(IUser user); diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index 7abecf54d..837e90604 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -60,13 +60,13 @@ namespace Discord 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 if not found. + /// Gets a message from this message channel with the given id, or null if not found. /// /// The ID of the message. /// The that determines whether the object should be fetched from cache. /// The options to be used when sending the request. /// - /// The message gotten from either the cache or the download, or if none is found. + /// The message gotten from either the cache or the download, or null if none is found. /// Task GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index 099822fb0..ac4bd2f9d 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -22,7 +22,7 @@ namespace Discord /// Gets the current topic for this text channel. /// /// - /// The topic set in the channel, or if none is set. + /// The topic set in the channel, or null if none is set. /// string Topic { get; } @@ -72,7 +72,7 @@ namespace Discord /// The ID of the webhook. /// The options to be used when sending the request. /// - /// A webhook associated with the , or if not found. + /// A webhook associated with the , or null if not found. /// Task GetWebhookAsync(ulong id, RequestOptions options = null); /// diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index f69e6e22e..e1efb1a86 100644 --- a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -14,7 +14,7 @@ namespace Discord int Bitrate { get; } /// /// Gets the max amount of users allowed to be connected to this channel at one time, or - /// if none is set. + /// null if none is set. /// int? UserLimit { get; } diff --git a/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs b/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs index c285560df..46e8f8550 100644 --- a/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs +++ b/src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs @@ -10,7 +10,7 @@ namespace Discord /// public Optional Bitrate { get; set; } /// - /// Gets or sets the maximum number of users that can be present in a channel, or if none. + /// Gets or sets the maximum number of users that can be present in a channel, or null if none. /// public Optional UserLimit { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs b/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs index 68925b103..2977cd10c 100644 --- a/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs +++ b/src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs @@ -10,11 +10,11 @@ namespace Discord /// public Optional Enabled { get; set; } /// - /// Sets the channel that the invite should place its users in, if not . + /// Sets the channel that the invite should place its users in, if not null. /// public Optional Channel { get; set; } /// - /// Sets the channel the invite should place its users in, if not . + /// Sets the channel the invite should place its users in, if not null. /// public Optional ChannelId { get; set; } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 057b94788..fa4af045a 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -40,19 +40,19 @@ namespace Discord /// VerificationLevel VerificationLevel { get; } /// - /// Returns the ID of this guild's icon, or if none is set. + /// Returns the ID of this guild's icon, or null if none is set. /// string IconId { get; } /// - /// Returns the URL of this guild's icon, or if none is set. + /// Returns the URL of this guild's icon, or null if none is set. /// string IconUrl { get; } /// - /// Returns the ID of this guild's splash image, or if none is set. + /// Returns the ID of this guild's splash image, or null if none is set. /// string SplashId { get; } /// - /// Returns the URL of this guild's splash image, or if none is set. + /// Returns the URL of this guild's splash image, or null if none is set. /// string SplashUrl { get; } /// @@ -65,7 +65,7 @@ namespace Discord bool Available { get; } /// - /// Gets the ID of the AFK voice channel for this guild, or if none is set. + /// Gets the ID of the AFK voice channel for this guild, or null if none is set. /// ulong? AFKChannelId { get; } /// @@ -73,11 +73,11 @@ namespace Discord /// ulong DefaultChannelId { get; } /// - /// Gets the ID of the embed channel set in the widget settings of this guild, or if none is set. + /// Gets the ID of the embed channel set in the widget settings of this guild, or null if none is set. /// ulong? EmbedChannelId { get; } /// - /// Gets the ID of the channel where randomized welcome messages are sent, or if none is set. + /// Gets the ID of the channel where randomized welcome messages are sent, or null if none is set. /// ulong? SystemChannelId { get; } /// @@ -245,7 +245,7 @@ namespace Discord /// The options to be used when sending the request. /// /// An awaitable containing the generic channel with the specified ID, or - /// if none is found. + /// null if none is found. /// Task GetChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// @@ -269,7 +269,7 @@ namespace Discord /// The options to be used when sending the request. /// /// An awaitable containing the text channel with the specified ID, or - /// if none is found. + /// null if none is found. /// Task GetTextChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// @@ -304,7 +304,7 @@ namespace Discord /// The options to be used when sending the request. /// /// An awaitable containing the voice channel with the specified ID, or - /// if none is found. + /// null if none is found. /// Task GetVoiceChannelAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// @@ -316,7 +316,7 @@ namespace Discord /// The options to be used when sending the request. /// /// An awaitable containing the AFK voice channel set within this guild, or - /// if none is set. + /// null if none is set. /// Task GetAFKChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// @@ -340,7 +340,7 @@ namespace Discord /// The options to be used when sending the request. /// /// An awaitable containing the first viewable text channel in this guild, or - /// if none is found. + /// null if none is found. /// Task GetDefaultChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// @@ -352,7 +352,7 @@ namespace Discord /// The options to be used when sending the request. /// /// An awaitable containing the embed channel set within the server's widget settings, or - /// if none is set. + /// null if none is set. /// Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// @@ -392,7 +392,7 @@ namespace Discord Task> GetInvitesAsync(RequestOptions options = null); /// - /// Gets the role in this guild with the provided ID, or if not found. + /// Gets the role in this guild with the provided ID, or null if not found. /// /// The role ID. IRole GetRole(ulong id); @@ -424,13 +424,13 @@ namespace Discord /// Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// - /// Gets the user in this guild with the provided ID, or if not found. + /// Gets the user in this guild with the provided ID, or null if not found. /// /// The user ID. /// The that determines whether the object should be fetched from cache. /// The options to be used when sending the request. /// - /// An awaitable containing the guild user with the specified ID, otherwise . + /// An awaitable containing the guild user with the specified ID, otherwise null. /// Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// @@ -472,12 +472,12 @@ namespace Discord Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); /// - /// Gets the webhook in this guild with the provided ID, or if not found. + /// Gets the webhook in this guild with the provided ID, or null if not found. /// /// The webhook ID. /// The options to be used when sending the request. /// - /// An awaitable containing the webhook with the specified ID, otherwise . + /// An awaitable containing the webhook with the specified ID, otherwise null. /// Task GetWebhookAsync(ulong id, RequestOptions options = null); /// @@ -495,7 +495,7 @@ namespace Discord /// The guild emote ID. /// The options to be used when sending the request. /// - /// An awaitable containing the emote found with the specified ID, or if not found. + /// An awaitable containing the emote found with the specified ID, or null if not found. /// Task GetEmoteAsync(ulong id, RequestOptions options = null); /// diff --git a/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs index 5da2ce5da..809ed7d66 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs @@ -7,7 +7,7 @@ namespace Discord /// string Name { get; } /// - /// Gets the icon URL associated with this guild, or if one is not set. + /// Gets the icon URL associated with this guild, or null if one is not set. /// string IconUrl { get; } /// diff --git a/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs index dcd3de997..4247c403b 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs @@ -19,11 +19,11 @@ namespace Discord /// bool IsTemporary { get; } /// - /// Gets the time (in seconds) until the invite expires, or if it never expires. + /// Gets the time (in seconds) until the invite expires, or null if it never expires. /// int? MaxAge { get; } /// - /// Gets the max amount of times this invite may be used, or if there is no limit. + /// Gets the max amount of times this invite may be used, or null if there is no limit. /// int? MaxUses { get; } /// diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index 087b30993..c54b2ffcf 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -106,7 +106,7 @@ namespace Discord /// Gets or sets the list of of an . /// An embed builder's fields collection is set to - /// . + /// null. /// Description length exceeds . /// /// The list of existing . diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs index 4368f74a4..d02b2cdc3 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs @@ -13,11 +13,11 @@ namespace Discord /// public string Url { get; } /// - /// Gets the height of the video, or if none. + /// Gets the height of the video, or null if none. /// public int? Height { get; } /// - /// Gets the weight of the video, or if none. + /// Gets the weight of the video, or null if none. /// public int? Width { get; } diff --git a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs index c61d8177c..5d4d32cfa 100644 --- a/src/Discord.Net.Core/Entities/Messages/IAttachment.cs +++ b/src/Discord.Net.Core/Entities/Messages/IAttachment.cs @@ -45,14 +45,14 @@ namespace Discord /// Gets the height of this attachment. /// /// - /// The height of this attachment if it is a picture; otherwise . + /// The height of this attachment if it is a picture; otherwise null. /// int? Height { get; } /// /// Gets the width of this attachment. /// /// - /// The width of this attachment if it is a picture; otherwise . + /// The width of this attachment if it is a picture; otherwise null. /// int? Width { get; } } diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index c8ed14f0c..b2eb65439 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -39,7 +39,7 @@ namespace Discord /// Gets the time of this message's last edit. /// /// - /// Time of when the message was last edited; when the message is never edited. + /// Time of when the message was last edited; null when the message is never edited. /// DateTimeOffset? EditedTimestamp { get; } diff --git a/src/Discord.Net.Core/Entities/Users/IVoiceState.cs b/src/Discord.Net.Core/Entities/Users/IVoiceState.cs index 725ef2870..c22f094b4 100644 --- a/src/Discord.Net.Core/Entities/Users/IVoiceState.cs +++ b/src/Discord.Net.Core/Entities/Users/IVoiceState.cs @@ -26,7 +26,7 @@ namespace Discord /// bool IsSuppressed { get; } /// - /// Gets the voice channel this user is currently in, or if none. + /// Gets the voice channel this user is currently in, or null if none. /// IVoiceChannel VoiceChannel { get; } /// diff --git a/src/Discord.Net.Core/Net/HttpException.cs b/src/Discord.Net.Core/Net/HttpException.cs index c49273451..aa440bf1c 100644 --- a/src/Discord.Net.Core/Net/HttpException.cs +++ b/src/Discord.Net.Core/Net/HttpException.cs @@ -23,7 +23,7 @@ namespace Discord.Net /// /// A /// JSON error code - /// from Discord, or if none. + /// from Discord, or null if none. /// public int? DiscordCode { get; } /// diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 2318f3f98..c2d1f6549 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -14,7 +14,7 @@ namespace Discord /// /// 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 + /// 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. /// public int? Timeout { get; set; } diff --git a/src/Discord.Net.Core/Utils/Cacheable.cs b/src/Discord.Net.Core/Utils/Cacheable.cs index 15358dda0..02da682ae 100644 --- a/src/Discord.Net.Core/Utils/Cacheable.cs +++ b/src/Discord.Net.Core/Utils/Cacheable.cs @@ -25,7 +25,7 @@ namespace Discord /// /// /// 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; } diff --git a/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs index 233d1b0b0..e8441d7bb 100644 --- a/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs +++ b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs @@ -157,7 +157,7 @@ namespace Discord : this(collection, EqualityComparer.Default) { } public ConcurrentHashSet(IEqualityComparer comparer) : this(DefaultConcurrencyLevel, DefaultCapacity, true, comparer) { } - /// is + /// is null public ConcurrentHashSet(IEnumerable collection, IEqualityComparer comparer) : this(comparer) { @@ -210,7 +210,7 @@ namespace Discord if (_budget == 0) _budget = _tables._buckets.Length / _tables._locks.Length; } - /// is + /// is null public bool ContainsKey(T value) { if (value == null) throw new ArgumentNullException(nameof(value)); @@ -234,7 +234,7 @@ namespace Discord return false; } - /// is + /// is null public bool TryAdd(T value) { if (value == null) throw new ArgumentNullException(nameof(value)); @@ -284,7 +284,7 @@ namespace Discord } } - /// is + /// is null public bool TryRemove(T value) { if (value == null) throw new ArgumentNullException(nameof(value)); diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index 37a5ef9cf..b0eed8b25 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -59,12 +59,12 @@ namespace Discord.Rest 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 if not found. + /// Gets a message from this message channel with the given id, or null if not found. /// /// The ID of the message. /// The options to be used when sending the request. /// - /// The message gotten from either the cache or the download, or if none is found. + /// The message gotten from either the cache or the download, or null if none is found. /// Task GetMessageAsync(ulong id, RequestOptions options = null); /// diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index 6408ead2f..53cc20024 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -14,7 +14,7 @@ namespace Discord.Rest internal static class GuildHelper { //General - /// is . + /// is null. public static async Task ModifyAsync(IGuild guild, BaseDiscordClient client, Action func, RequestOptions options) { @@ -62,7 +62,7 @@ namespace Discord.Rest return await client.ApiClient.ModifyGuildAsync(guild.Id, apiArgs, options).ConfigureAwait(false); } - /// is . + /// is null. public static async Task ModifyEmbedAsync(IGuild guild, BaseDiscordClient client, Action func, RequestOptions options) { @@ -140,7 +140,7 @@ namespace Discord.Rest var models = await client.ApiClient.GetGuildChannelsAsync(guild.Id, options).ConfigureAwait(false); return models.Select(x => RestGuildChannel.Create(client, guild, x)).ToImmutableArray(); } - /// is . + /// is null. public static async Task CreateTextChannelAsync(IGuild guild, BaseDiscordClient client, string name, RequestOptions options) { @@ -150,7 +150,7 @@ namespace Discord.Rest var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestTextChannel.Create(client, guild, model); } - /// is . + /// is null. public static async Task CreateVoiceChannelAsync(IGuild guild, BaseDiscordClient client, string name, RequestOptions options) { @@ -160,7 +160,7 @@ namespace Discord.Rest var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestVoiceChannel.Create(client, guild, model); } - /// is . + /// is null. public static async Task CreateCategoryChannelAsync(IGuild guild, BaseDiscordClient client, string name, RequestOptions options) { @@ -195,7 +195,7 @@ namespace Discord.Rest } //Roles - /// is . + /// is null. public static async Task CreateRoleAsync(IGuild guild, BaseDiscordClient client, string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) { @@ -302,7 +302,7 @@ namespace Discord.Rest var emote = await client.ApiClient.CreateGuildEmoteAsync(guild.Id, apiargs, options).ConfigureAwait(false); return emote.ToEntity(); } - /// is . + /// is null. public static async Task ModifyEmoteAsync(IGuild guild, BaseDiscordClient client, ulong id, Action func, RequestOptions options) { diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 5a79565ba..22ec20f03 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -132,7 +132,7 @@ namespace Discord.Rest => GuildHelper.DeleteAsync(this, Discord, options); /// - /// is . + /// is null. public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await GuildHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); @@ -140,7 +140,7 @@ namespace Discord.Rest } /// - /// is . + /// is null. public async Task ModifyEmbedAsync(Action func, RequestOptions options = null) { var model = await GuildHelper.ModifyEmbedAsync(this, Discord, func, options).ConfigureAwait(false); diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index b3702a811..69a557da9 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -66,7 +66,7 @@ namespace Discord.WebSocket /// /// This method gets the user present in the WebSocket cache with the given condition. /// - /// Sometimes a user may return due to Discord not sending offline users in large + /// Sometimes a user may return null due to Discord not sending offline users in large /// guilds (i.e. guild with 100+ members) actively. To download users on startup, consider enabling /// . /// @@ -76,7 +76,7 @@ namespace Discord.WebSocket /// /// /// - /// A WebSocket-based generic user; when the user cannot be found. + /// A WebSocket-based generic user; null when the user cannot be found. /// public abstract SocketUser GetUser(ulong id); @@ -88,7 +88,7 @@ namespace Discord.WebSocket /// /// This method gets the user present in the WebSocket cache with the given condition. /// - /// Sometimes a user may return due to Discord not sending offline users in large + /// Sometimes a user may return null due to Discord not sending offline users in large /// guilds (i.e. guild with 100+ members) actively. To download users on startup, consider enabling /// . /// @@ -98,7 +98,7 @@ namespace Discord.WebSocket /// /// /// - /// A WebSocket-based generic user; when the user cannot be found. + /// A WebSocket-based generic user; null when the user cannot be found. /// public abstract SocketUser GetUser(string username, string discriminator); /// @@ -115,7 +115,7 @@ namespace Discord.WebSocket /// /// The guild snowflake ID. /// - /// A WebSocket-based guild; when the guild cannot be found. + /// A WebSocket-based guild; null when the guild cannot be found. /// public abstract SocketGuild GetGuild(ulong id); /// @@ -123,7 +123,7 @@ namespace Discord.WebSocket /// /// The unique identifier of the voice region. /// - /// A REST-based voice region; if none can be found. + /// A REST-based voice region; null if none can be found. /// public abstract RestVoiceRegion GetVoiceRegion(string id); /// diff --git a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs index 17f200c08..d85230fec 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketConfig.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketConfig.cs @@ -15,7 +15,7 @@ namespace Discord.WebSocket public const string GatewayEncoding = "json"; /// - /// Gets or sets the WebSocket host to connect to. If , the client will use the + /// Gets or sets the WebSocket host to connect to. If null, the client will use the /// /gateway endpoint. /// public string GatewayHost { get; set; } = null; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index 59c382184..c37311a41 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -72,7 +72,7 @@ namespace Discord.WebSocket /// /// The ID of the message. /// - /// Cached message object; if it doesn't exist in the cache. + /// Cached message object; null if it doesn't exist in the cache. /// SocketMessage GetCachedMessage(ulong id); /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index ecbbbcd49..1151f0141 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -34,7 +34,7 @@ namespace Discord.WebSocket /// Gets the parent category of this channel. /// /// - /// A parent category ID associated with this channel, or if none is set. + /// A parent category ID associated with this channel, or null if none is set. /// public ICategoryChannel Category => CategoryId.HasValue ? Guild.GetChannel(CategoryId.Value) as ICategoryChannel : null; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index eed8f45b9..92bc07c60 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -152,7 +152,7 @@ namespace Discord.WebSocket /// The ID of the webhook. /// The options to be used when sending the request. /// - /// A webhook associated with the , or if not found. + /// A webhook associated with the , or null if not found. /// public Task GetWebhookAsync(ulong id, RequestOptions options = null) => ChannelHelper.GetWebhookAsync(this, Discord, id, options); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 029e600fc..8c930e79a 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -116,7 +116,7 @@ namespace Discord.WebSocket } } /// - /// Gets the embed channel set in the widget settings of this guild, or if none is set. + /// Gets the embed channel set in the widget settings of this guild, or null if none is set. /// public SocketGuildChannel EmbedChannel { @@ -127,7 +127,7 @@ namespace Discord.WebSocket } } /// - /// Gets the channel where randomized welcome messages are sent, or if none is set. + /// Gets the channel where randomized welcome messages are sent, or null if none is set. /// public SocketTextChannel SystemChannel { @@ -372,12 +372,12 @@ namespace Discord.WebSocket => GuildHelper.DeleteAsync(this, Discord, options); /// - /// is . + /// is null. public Task ModifyAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyAsync(this, Discord, func, options); /// - /// is . + /// is null. public Task ModifyEmbedAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyEmbedAsync(this, Discord, func, options); /// @@ -671,7 +671,7 @@ namespace Discord.WebSocket public Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null) => GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options); /// - /// is . + /// is null. public Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null) => GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options); /// diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs index 428405431..d5f0433ad 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketVoiceState.cs @@ -23,7 +23,7 @@ namespace Discord.WebSocket private readonly Flags _voiceStates; /// - /// Gets the voice channel that the user is currently in; or if none. + /// Gets the voice channel that the user is currently in; or null if none. /// public SocketVoiceChannel VoiceChannel { get; } /// From 1bb06cc37b7c2312b0ab111da63026a01da01e42 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sun, 6 May 2018 15:40:31 +0800 Subject: [PATCH 173/183] Replace all langword placements with code block --- .../Attributes/PreconditionAttribute.cs | 2 +- src/Discord.Net.Commands/CommandService.cs | 6 +- .../Results/RuntimeResult.cs | 2 +- .../Entities/Channels/IGuildChannel.cs | 4 +- .../Entities/Channels/ITextChannel.cs | 2 +- .../Entities/Guilds/IGuild.cs | 24 ++++---- .../Entities/Guilds/IUserGuild.cs | 2 +- .../Entities/Guilds/IVoiceRegion.cs | 8 +-- src/Discord.Net.Core/Entities/Image.cs | 2 +- .../Entities/Invites/IInviteMetadata.cs | 4 +- .../Entities/Messages/EmbedBuilder.cs | 14 ++--- .../Entities/Messages/IEmbed.cs | 16 +++--- .../Entities/Messages/IMessage.cs | 4 +- .../Entities/Messages/ReactionMetadata.cs | 2 +- .../Permissions/ChannelPermissions.cs | 42 +++++++------- .../Entities/Permissions/GuildPermissions.cs | 56 +++++++++---------- src/Discord.Net.Core/Entities/Roles/IRole.cs | 12 ++-- .../Entities/Users/GuildUserProperties.cs | 6 +- .../Entities/Users/ISelfUser.cs | 4 +- src/Discord.Net.Core/Entities/Users/IUser.cs | 4 +- .../Entities/Users/IVoiceState.cs | 10 ++-- .../Utils/ConcurrentHashSet.cs | 2 +- .../Entities/Channels/ChannelHelper.cs | 2 +- .../Entities/Channels/RestGroupChannel.cs | 2 +- .../Entities/Guilds/RestGuild.cs | 4 +- src/Discord.Net.WebSocket/BaseSocketClient.cs | 2 +- .../Entities/Guilds/SocketGuild.cs | 14 ++--- .../Entities/Users/SocketGuildUser.cs | 2 +- 28 files changed, 129 insertions(+), 125 deletions(-) diff --git a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs index 1d791c92e..316b2729e 100644 --- a/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs @@ -15,7 +15,7 @@ namespace Discord.Commands /// /// /// 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 + /// be successful (A || B). Specifying = null or not at all will /// require *all* preconditions to pass, just like normal (A && B). /// public string Group { get; set; } = null; diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index 39cae845e..24db6e9b5 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -129,7 +129,7 @@ namespace Discord.Commands /// The type of module. /// /// The for your dependency injection solution, if using one - otherwise, pass - /// . + /// null. /// /// /// A built module. @@ -144,7 +144,7 @@ namespace Discord.Commands /// The type of module. /// /// The for your dependency injection solution, if using one - otherwise, pass - /// . + /// null. /// /// /// A built module. @@ -183,7 +183,7 @@ namespace Discord.Commands /// The containing command modules. /// /// An for your dependency injection solution, if using one - otherwise, pass - /// . + /// null. /// /// /// A collection of built modules. diff --git a/src/Discord.Net.Commands/Results/RuntimeResult.cs b/src/Discord.Net.Commands/Results/RuntimeResult.cs index a7febd68e..e4c86fc23 100644 --- a/src/Discord.Net.Commands/Results/RuntimeResult.cs +++ b/src/Discord.Net.Commands/Results/RuntimeResult.cs @@ -8,7 +8,7 @@ namespace Discord.Commands /// /// Initializes a new class with the type of error and reason. /// - /// The type of failure, or if none. + /// The type of failure, or null if none. /// The reason of failure. protected RuntimeResult(CommandError? error, string reason) { diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index 5028bd388..3d11e2c6f 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -63,10 +63,10 @@ namespace Discord /// The max amount of times this invite may be used. Set to null to have unlimited uses. /// /// - /// If , a user accepting this invite will be kicked from the guild after closing their client. + /// If true, 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). + /// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites). /// /// /// The options to be used when sending the request. diff --git a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs index ac4bd2f9d..29c67bf6b 100644 --- a/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/ITextChannel.cs @@ -14,7 +14,7 @@ namespace Discord /// Determines whether the channel is NSFW. /// /// - /// if the channel has the NSFW flag enabled; otherwise, . + /// true if the channel has the NSFW flag enabled; otherwise, false. /// bool IsNsfw { get; } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index fa4af045a..17f5ac328 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -23,7 +23,7 @@ namespace Discord /// Determines if this guild is embeddable (i.e. can use widget). /// /// - /// if this guild can be embedded via widgets; otherwise . + /// true if this guild can be embedded via widgets; otherwise false. /// bool IsEmbeddable { get; } /// @@ -59,7 +59,7 @@ namespace Discord /// Determines if this guild is currently connected and ready to be used. /// /// - /// Returns if this guild is currently connected and ready to be used. Only applies + /// Returns true if this guild is currently connected and ready to be used. Only applies /// to the WebSocket client. /// bool Available { get; } @@ -260,7 +260,7 @@ namespace Discord /// Task> GetTextChannelsAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// - /// Gets a text channel in this guild with the provided ID, or if not found. + /// Gets a text channel in this guild with the provided ID, or null if not found. /// /// The text channel ID. /// @@ -328,7 +328,7 @@ namespace Discord /// The options to be used when sending the request. /// /// An awaitable containing the system channel within this guild, or - /// if none is set. + /// null if none is set. /// Task GetSystemChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// @@ -459,25 +459,29 @@ namespace Discord /// Task DownloadUsersAsync(); /// - /// Removes all users from this guild if they have not logged on in a provided number of - /// or, if is , returns the - /// number of users that would be removed. + /// Prunes inactive users. /// + /// + /// This method removes all users that have not logged on in the provided number of days 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 options to be used when sending the request. /// - /// An awaitable containing the number of users to be or has been removed from this guild. + /// An awaitable containing the number of users to be or has been 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 a webhook found within this guild. /// /// The webhook ID. /// The options to be used when sending the request. /// - /// An awaitable containing the webhook with the specified ID, otherwise null. + /// An awaitable containing the webhook with the specified ID; null if none is + /// found. /// Task GetWebhookAsync(ulong id, RequestOptions options = null); /// diff --git a/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs index 809ed7d66..b6685edf6 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IUserGuild.cs @@ -11,7 +11,7 @@ namespace Discord /// string IconUrl { get; } /// - /// Returns if the current user owns this guild. + /// Returns true if the current user owns this guild. /// bool IsOwner { get; } /// diff --git a/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs b/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs index eef208905..8516036f1 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IVoiceRegion.cs @@ -14,19 +14,19 @@ namespace Discord /// string Name { get; } /// - /// Returns if this voice region is exclusive to VIP accounts. + /// Returns true if this voice region is exclusive to VIP accounts. /// bool IsVip { get; } /// - /// Returns if this voice region is the closest to your machine. + /// Returns true if this voice region is the closest to your machine. /// bool IsOptimal { get; } /// - /// Returns if this is a deprecated voice region (avoid switching to these). + /// Returns true if this is a deprecated voice region (avoid switching to these). /// bool IsDeprecated { get; } /// - /// Returns if this is a custom voice region (used for events/etc). + /// Returns true if this is a custom voice region (used for events/etc). /// bool IsCustom { get; } } diff --git a/src/Discord.Net.Core/Entities/Image.cs b/src/Discord.Net.Core/Entities/Image.cs index dd77ec6ae..5453027ac 100644 --- a/src/Discord.Net.Core/Entities/Image.cs +++ b/src/Discord.Net.Core/Entities/Image.cs @@ -35,7 +35,7 @@ namespace Discord /// is a zero-length string, contains only white space, or contains one or more invalid /// characters as defined by . /// - /// is . + /// is null. /// /// The specified path, file name, or both exceed the system-defined maximum length. For example, on /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 diff --git a/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs index 4247c403b..1c6b1dd79 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs @@ -10,11 +10,11 @@ namespace Discord /// IUser Inviter { get; } /// - /// Returns if this invite was revoked. + /// Returns true if this invite was revoked. /// bool IsRevoked { get; } /// - /// Returns if users accepting this invite will be removed from the guild when they + /// Returns true if users accepting this invite will be removed from the guild when they /// log off. /// bool IsTemporary { get; } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs index c54b2ffcf..6bd259195 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -125,28 +125,28 @@ namespace Discord /// Gets or sets the timestamp of an . /// /// - /// The timestamp of the embed, or if none is set. + /// The timestamp of the embed, or null if none is set. /// public DateTimeOffset? Timestamp { get; set; } /// /// Gets or sets the sidebar color of an . /// /// - /// The color of the embed, or if none is set. + /// The color of the embed, or null if none is set. /// public Color? Color { get; set; } /// /// Gets or sets the of an . /// /// - /// The author field builder of the embed, or if none is set. + /// The author field builder of the embed, or null if none is set. /// public EmbedAuthorBuilder Author { get; set; } /// /// Gets or sets the of an . /// /// - /// The footer field builder of the embed, or if none is set. + /// The footer field builder of the embed, or null if none is set. /// public EmbedFooterBuilder Footer { get; set; } @@ -452,7 +452,7 @@ namespace Discord /// Gets or sets the field name. /// /// - /// Field name is , empty or entirely whitespace. + /// Field name is null, empty or entirely whitespace. /// - or - /// Field name length exceeds . /// @@ -474,7 +474,7 @@ namespace Discord /// Gets or sets the field value. /// /// - /// Field value is , empty or entirely whitespace. + /// Field value is null, empty or entirely whitespace. /// - or - /// Field value length exceeds . /// @@ -540,7 +540,7 @@ namespace Discord /// The current builder. /// /// - /// or is , empty or entirely whitespace. + /// or is null, empty or entirely whitespace. /// - or - /// or exceeds the maximum length allowed by Discord. /// diff --git a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs index 1482de20a..4c1029a10 100644 --- a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs +++ b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs @@ -41,56 +41,56 @@ namespace Discord /// /// /// A based on the timestamp present at the bottom left of the embed, or - /// if none is set. + /// null if none is set. /// DateTimeOffset? Timestamp { get; } /// /// Gets the color of this embed. /// /// - /// The color of the embed present on the side of the embed, or if none is set. + /// The color of the embed present on the side of the embed, or null if none is set. /// Color? Color { get; } /// /// Gets the image of this embed. /// /// - /// The image of the embed, or if none is set. + /// The image of the embed, or null if none is set. /// EmbedImage? Image { get; } /// /// Gets the video of this embed. /// /// - /// The video of the embed, or if none is set. + /// The video of the embed, or null if none is set. /// EmbedVideo? Video { get; } /// /// Gets the author field of this embed. /// /// - /// The author field of the embed, or if none is set. + /// The author field of the embed, or null if none is set. /// EmbedAuthor? Author { get; } /// /// Gets the footer field of this embed. /// /// - /// The author field of the embed, or if none is set. + /// The author field of the embed, or null if none is set. /// EmbedFooter? Footer { get; } /// /// Gets the provider of this embed. /// /// - /// The source of the embed, or if none is set. + /// The source of the embed, or null if none is set. /// EmbedProvider? Provider { get; } /// /// Gets the thumbnail featured in this embed. /// /// - /// The thumbnail featured in the embed, or if none is set. + /// The thumbnail featured in the embed, or null if none is set. /// EmbedThumbnail? Thumbnail { get; } /// diff --git a/src/Discord.Net.Core/Entities/Messages/IMessage.cs b/src/Discord.Net.Core/Entities/Messages/IMessage.cs index b2eb65439..edbe4f4b6 100644 --- a/src/Discord.Net.Core/Entities/Messages/IMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IMessage.cs @@ -17,11 +17,11 @@ namespace Discord /// MessageSource Source { get; } /// - /// Returns if this message was sent as a text-to-speech message. + /// Returns true if this message was sent as a text-to-speech message. /// bool IsTTS { get; } /// - /// Returns if this message was added to its channel's pinned messages. + /// Returns true if this message was added to its channel's pinned messages. /// bool IsPinned { get; } /// diff --git a/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs b/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs index 8ef11bc47..8f2678cd9 100644 --- a/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs +++ b/src/Discord.Net.Core/Entities/Messages/ReactionMetadata.cs @@ -11,7 +11,7 @@ namespace Discord public int ReactionCount { get; internal set; } /// - /// Returns if the current user has used this reaction. + /// Returns true if the current user has used this reaction. /// public bool IsMe { get; internal set; } } diff --git a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs index 99134bb90..a30e51f5f 100644 --- a/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/ChannelPermissions.cs @@ -37,52 +37,52 @@ namespace Discord /// Gets a packed value representing all the permissions in this . public ulong RawValue { get; } - /// If , a user may create invites. + /// If true, a user may create invites. public bool CreateInstantInvite => Permissions.GetValue(RawValue, ChannelPermission.CreateInstantInvite); - /// If , a user may create, delete and modify this channel. + /// If true, a user may create, delete and modify this channel. public bool ManageChannel => Permissions.GetValue(RawValue, ChannelPermission.ManageChannels); - /// If , a user may add reactions. + /// If true, a user may add reactions. public bool AddReactions => Permissions.GetValue(RawValue, ChannelPermission.AddReactions); - /// If , a user may join channels. + /// If true, a user may join channels. [Obsolete("Use ViewChannel instead.")] public bool ReadMessages => ViewChannel; - /// If , a user may view channels. + /// If true, a user may view channels. public bool ViewChannel => Permissions.GetValue(RawValue, ChannelPermission.ViewChannel); - /// If , a user may send messages. + /// If true, a user may send messages. public bool SendMessages => Permissions.GetValue(RawValue, ChannelPermission.SendMessages); - /// If , a user may send text-to-speech messages. + /// If true, a user may send text-to-speech messages. public bool SendTTSMessages => Permissions.GetValue(RawValue, ChannelPermission.SendTTSMessages); - /// If , a user may delete messages. + /// If true, a user may delete messages. public bool ManageMessages => Permissions.GetValue(RawValue, ChannelPermission.ManageMessages); - /// If , Discord will auto-embed links sent by this user. + /// If true, Discord will auto-embed links sent by this user. public bool EmbedLinks => Permissions.GetValue(RawValue, ChannelPermission.EmbedLinks); - /// If , a user may send files. + /// If true, a user may send files. public bool AttachFiles => Permissions.GetValue(RawValue, ChannelPermission.AttachFiles); - /// If , a user may read previous messages. + /// If true, a user may read previous messages. public bool ReadMessageHistory => Permissions.GetValue(RawValue, ChannelPermission.ReadMessageHistory); - /// If , a user may mention @everyone. + /// If true, a user may mention @everyone. public bool MentionEveryone => Permissions.GetValue(RawValue, ChannelPermission.MentionEveryone); - /// If , a user may use custom emoji from other guilds. + /// If true, a user may use custom emoji from other guilds. public bool UseExternalEmojis => Permissions.GetValue(RawValue, ChannelPermission.UseExternalEmojis); - /// If , a user may connect to a voice channel. + /// If true, a user may connect to a voice channel. public bool Connect => Permissions.GetValue(RawValue, ChannelPermission.Connect); - /// If , a user may speak in a voice channel. + /// If true, a user may speak in a voice channel. public bool Speak => Permissions.GetValue(RawValue, ChannelPermission.Speak); - /// If , a user may mute users. + /// If true, a user may mute users. public bool MuteMembers => Permissions.GetValue(RawValue, ChannelPermission.MuteMembers); - /// If , a user may deafen users. + /// If true, a user may deafen users. public bool DeafenMembers => Permissions.GetValue(RawValue, ChannelPermission.DeafenMembers); - /// If , a user may move other users between voice channels. + /// If true, a user may move other users between voice channels. public bool MoveMembers => Permissions.GetValue(RawValue, ChannelPermission.MoveMembers); - /// If , a user may use voice-activity-detection rather than push-to-talk. + /// If true, a user may use voice-activity-detection rather than push-to-talk. public bool UseVAD => Permissions.GetValue(RawValue, ChannelPermission.UseVAD); - /// If , a user may adjust role permissions. This also implictly grants all other permissions. + /// If true, a user may adjust role permissions. This also implictly grants all other permissions. public bool ManageRoles => Permissions.GetValue(RawValue, ChannelPermission.ManageRoles); - /// If , a user may edit the webhooks for this channel. + /// If true, a user may edit the webhooks for this channel. public bool ManageWebhooks => Permissions.GetValue(RawValue, ChannelPermission.ManageWebhooks); /// Creates a new with the provided packed value. diff --git a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs index e1dbb08fd..9a0cb2919 100644 --- a/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs +++ b/src/Discord.Net.Core/Entities/Permissions/GuildPermissions.cs @@ -16,65 +16,65 @@ namespace Discord /// Gets a packed value representing all the permissions in this . public ulong RawValue { get; } - /// If , a user may create invites. + /// If true, a user may create invites. public bool CreateInstantInvite => Permissions.GetValue(RawValue, GuildPermission.CreateInstantInvite); - /// If , a user may ban users from the guild. + /// If true, a user may ban users from the guild. public bool BanMembers => Permissions.GetValue(RawValue, GuildPermission.BanMembers); - /// If , a user may kick users from the guild. + /// If true, a user may kick users from the guild. public bool KickMembers => Permissions.GetValue(RawValue, GuildPermission.KickMembers); - /// If , a user is granted all permissions, and cannot have them revoked via channel permissions. + /// If true, a user is granted all permissions, and cannot have them revoked via channel permissions. public bool Administrator => Permissions.GetValue(RawValue, GuildPermission.Administrator); - /// If , a user may create, delete and modify channels. + /// If true, a user may create, delete and modify channels. public bool ManageChannels => Permissions.GetValue(RawValue, GuildPermission.ManageChannels); - /// If , a user may adjust guild properties. + /// If true, a user may adjust guild properties. public bool ManageGuild => Permissions.GetValue(RawValue, GuildPermission.ManageGuild); - /// If , a user may add reactions. + /// If true, a user may add reactions. public bool AddReactions => Permissions.GetValue(RawValue, GuildPermission.AddReactions); - /// If , a user may view the audit log. + /// If true, a user may view the audit log. public bool ViewAuditLog => Permissions.GetValue(RawValue, GuildPermission.ViewAuditLog); - /// If , a user may join channels. + /// If true, a user may join channels. public bool ReadMessages => Permissions.GetValue(RawValue, GuildPermission.ReadMessages); - /// If , a user may send messages. + /// If true, a user may send messages. public bool SendMessages => Permissions.GetValue(RawValue, GuildPermission.SendMessages); - /// If , a user may send text-to-speech messages. + /// If true, a user may send text-to-speech messages. public bool SendTTSMessages => Permissions.GetValue(RawValue, GuildPermission.SendTTSMessages); - /// If , a user may delete messages. + /// If true, a user may delete messages. public bool ManageMessages => Permissions.GetValue(RawValue, GuildPermission.ManageMessages); - /// If , Discord will auto-embed links sent by this user. + /// If true, Discord will auto-embed links sent by this user. public bool EmbedLinks => Permissions.GetValue(RawValue, GuildPermission.EmbedLinks); - /// If , a user may send files. + /// If true, a user may send files. public bool AttachFiles => Permissions.GetValue(RawValue, GuildPermission.AttachFiles); - /// If , a user may read previous messages. + /// If true, a user may read previous messages. public bool ReadMessageHistory => Permissions.GetValue(RawValue, GuildPermission.ReadMessageHistory); - /// If , a user may mention @everyone. + /// If true, a user may mention @everyone. public bool MentionEveryone => Permissions.GetValue(RawValue, GuildPermission.MentionEveryone); - /// If , a user may use custom emoji from other guilds. + /// If true, a user may use custom emoji from other guilds. public bool UseExternalEmojis => Permissions.GetValue(RawValue, GuildPermission.UseExternalEmojis); - /// If , a user may connect to a voice channel. + /// If true, a user may connect to a voice channel. public bool Connect => Permissions.GetValue(RawValue, GuildPermission.Connect); - /// If , a user may speak in a voice channel. + /// If true, a user may speak in a voice channel. public bool Speak => Permissions.GetValue(RawValue, GuildPermission.Speak); - /// If , a user may mute users. + /// If true, a user may mute users. public bool MuteMembers => Permissions.GetValue(RawValue, GuildPermission.MuteMembers); - /// If , a user may deafen users. + /// If true, a user may deafen users. public bool DeafenMembers => Permissions.GetValue(RawValue, GuildPermission.DeafenMembers); - /// If , a user may move other users between voice channels. + /// If true, a user may move other users between voice channels. public bool MoveMembers => Permissions.GetValue(RawValue, GuildPermission.MoveMembers); - /// If , a user may use voice-activity-detection rather than push-to-talk. + /// If true, a user may use voice-activity-detection rather than push-to-talk. public bool UseVAD => Permissions.GetValue(RawValue, GuildPermission.UseVAD); - /// If , a user may change their own nickname. + /// If true, a user may change their own nickname. public bool ChangeNickname => Permissions.GetValue(RawValue, GuildPermission.ChangeNickname); - /// If , a user may change the nickname of other users. + /// If true, a user may change the nickname of other users. public bool ManageNicknames => Permissions.GetValue(RawValue, GuildPermission.ManageNicknames); - /// If , a user may adjust roles. + /// If true, a user may adjust roles. public bool ManageRoles => Permissions.GetValue(RawValue, GuildPermission.ManageRoles); - /// If , a user may edit the webhooks for this guild. + /// If true, a user may edit the webhooks for this guild. public bool ManageWebhooks => Permissions.GetValue(RawValue, GuildPermission.ManageWebhooks); - /// If , a user may edit the emojis for this guild. + /// If true, a user may edit the emojis for this guild. public bool ManageEmojis => Permissions.GetValue(RawValue, GuildPermission.ManageEmojis); /// Creates a new with the provided packed value. diff --git a/src/Discord.Net.Core/Entities/Roles/IRole.cs b/src/Discord.Net.Core/Entities/Roles/IRole.cs index f4cb4c64d..c0f4e9942 100644 --- a/src/Discord.Net.Core/Entities/Roles/IRole.cs +++ b/src/Discord.Net.Core/Entities/Roles/IRole.cs @@ -21,24 +21,24 @@ namespace Discord /// Determines whether the role can be separated in the user list. /// /// - /// Returns if users of this role are separated in the user list; otherwise, returns - /// . + /// Returns true if users of this role are separated in the user list; otherwise, returns + /// false. /// bool IsHoisted { get; } /// /// Determines whether the role is managed by Discord. /// /// - /// Returns if this role is automatically managed by Discord; otherwise, returns - /// . + /// Returns true if this role is automatically managed by Discord; otherwise, returns + /// false. /// bool IsManaged { get; } /// /// Determines whether the role is mentionable. /// /// - /// Returns if this role may be mentioned in messages; otherwise, returns - /// . + /// Returns true if this role may be mentioned in messages; otherwise, returns + /// false. /// bool IsMentionable { get; } /// diff --git a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs index 401b0ec7a..27a8be351 100644 --- a/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs +++ b/src/Discord.Net.Core/Entities/Users/GuildUserProperties.cs @@ -12,21 +12,21 @@ namespace Discord /// Gets or sets whether the user should be muted in a voice channel. /// /// - /// If this value is set to , no user will be able to hear this user speak in the guild. + /// If this value is set to true, no user will be able to hear this user speak in the guild. /// public Optional Mute { get; set; } /// /// Gets or sets whether the user should be deafened in a voice channel. /// /// - /// If this value is set to , this user will not be able to hear anyone speak in the guild. + /// If this value is set to true, this user will not be able to hear anyone speak in the guild. /// public Optional Deaf { get; 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 null or /// . /// public Optional Nickname { get; set; } diff --git a/src/Discord.Net.Core/Entities/Users/ISelfUser.cs b/src/Discord.Net.Core/Entities/Users/ISelfUser.cs index 4a97c86ef..1ead0cbba 100644 --- a/src/Discord.Net.Core/Entities/Users/ISelfUser.cs +++ b/src/Discord.Net.Core/Entities/Users/ISelfUser.cs @@ -13,11 +13,11 @@ namespace Discord /// string Email { get; } /// - /// Returns if this user's email has been verified. + /// Returns true if this user's email has been verified. /// bool IsVerified { get; } /// - /// Returns if this user has enabled MFA on their account. + /// Returns true if this user has enabled MFA on their account. /// bool IsMfaEnabled { get; } diff --git a/src/Discord.Net.Core/Entities/Users/IUser.cs b/src/Discord.Net.Core/Entities/Users/IUser.cs index e1e6c258f..96b9ae7ee 100644 --- a/src/Discord.Net.Core/Entities/Users/IUser.cs +++ b/src/Discord.Net.Core/Entities/Users/IUser.cs @@ -35,11 +35,11 @@ namespace Discord /// ushort DiscriminatorValue { get; } /// - /// Gets if this user is a bot user. + /// Gets true if this user is a bot user. /// bool IsBot { get; } /// - /// Gets if this user is a webhook user. + /// Gets true if this user is a webhook user. /// bool IsWebhook { get; } /// diff --git a/src/Discord.Net.Core/Entities/Users/IVoiceState.cs b/src/Discord.Net.Core/Entities/Users/IVoiceState.cs index c22f094b4..abe06c3b3 100644 --- a/src/Discord.Net.Core/Entities/Users/IVoiceState.cs +++ b/src/Discord.Net.Core/Entities/Users/IVoiceState.cs @@ -6,23 +6,23 @@ namespace Discord public interface IVoiceState { /// - /// Returns if the guild has deafened this user. + /// Returns true if the guild has deafened this user. /// bool IsDeafened { get; } /// - /// Returns if the guild has muted this user. + /// Returns true if the guild has muted this user. /// bool IsMuted { get; } /// - /// Returns if this user has marked themselves as deafened. + /// Returns true if this user has marked themselves as deafened. /// bool IsSelfDeafened { get; } /// - /// Returns if this user has marked themselves as muted. + /// Returns true if this user has marked themselves as muted. /// bool IsSelfMuted { get; } /// - /// Returns if the guild is temporarily blocking audio to/from this user. + /// Returns true if the guild is temporarily blocking audio to/from this user. /// bool IsSuppressed { get; } /// diff --git a/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs index e8441d7bb..bbdc59087 100644 --- a/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs +++ b/src/Discord.Net.Core/Utils/ConcurrentHashSet.cs @@ -165,7 +165,7 @@ namespace Discord InitializeFromCollection(collection); } /// - /// or is + /// or is null /// public ConcurrentHashSet(int concurrencyLevel, IEnumerable collection, IEqualityComparer comparer) : this(concurrencyLevel, DefaultCapacity, false, comparer) diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 8976a8557..6a3521bff 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -168,7 +168,7 @@ namespace Discord.Rest /// invalid characters as defined by . /// /// - /// is . + /// is null. /// /// /// The specified path, file name, or both exceed the system-defined maximum length. For example, on diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index 34346555f..4b4ccb057 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -94,7 +94,7 @@ namespace Discord.Rest /// invalid characters as defined by . /// /// - /// is . + /// is null. /// /// /// The specified path, file name, or both exceed the system-defined maximum length. For example, on diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index 22ec20f03..91e097218 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -148,7 +148,7 @@ namespace Discord.Rest } /// - /// is . + /// is null. public async Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null) { var arr = args.ToArray(); @@ -324,7 +324,7 @@ namespace Discord.Rest public Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null) => GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options); /// - /// is . + /// is null. public Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null) => GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options); /// diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index 69a557da9..858fec7fe 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -106,7 +106,7 @@ namespace Discord.WebSocket /// /// The channel snowflake ID. /// - /// A generic WebSocket-based channel object (voice, text, category, etc.); when the + /// A generic WebSocket-based channel object (voice, text, category, etc.); null when the /// channel cannot be found. /// public abstract SocketChannel GetChannel(ulong id); diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 8c930e79a..6eb01b295 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -105,7 +105,7 @@ namespace Discord.WebSocket .OrderBy(c => c.Position) .FirstOrDefault(); /// - /// Gets the AFK voice channel, or if none is set. + /// Gets the AFK voice channel, or null if none is set. /// public SocketVoiceChannel AFKChannel { @@ -455,7 +455,7 @@ namespace Discord.WebSocket /// /// The name of the new channel. /// The options to be used when sending the request. - /// is . + /// is null. /// /// The created text channel. /// @@ -467,7 +467,7 @@ namespace Discord.WebSocket /// /// The name of the new channel. /// The options to be used when sending the request. - /// is . + /// is null. /// /// The created voice channel. /// @@ -479,7 +479,7 @@ namespace Discord.WebSocket /// /// The name of the new channel. /// The options to be used when sending the request. - /// is . + /// is null. /// /// The created category channel. /// @@ -537,12 +537,12 @@ namespace Discord.WebSocket /// /// The name of the new role. /// - /// The permissions that the new role possesses. Set to to use the default permissions. + /// The permissions that the new role possesses. Set to null to use the default permissions. /// - /// The color of the role. Set to to use the default color. + /// The color of the role. Set to null to use the default color. /// Used to determine if users of this role are separated in the user list. /// The options to be used when sending the request. - /// is . + /// is null. /// /// The created role. /// diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 588ed554d..8721e722b 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -61,7 +61,7 @@ namespace Discord.WebSocket public IReadOnlyCollection Roles => _roleIds.Select(id => Guild.GetRole(id)).Where(x => x != null).ToReadOnlyCollection(() => _roleIds.Length); /// - /// Returns the voice channel the user is in, or if none. + /// Returns the voice channel the user is in, or null if none. /// public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; /// From c7b236ddf5d7324dd8c1513ccd528a7a45d0031f Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sun, 6 May 2018 15:58:23 +0800 Subject: [PATCH 174/183] Add more IGuild docs --- .../Entities/Guilds/IGuild.cs | 98 ++++++++++++++----- 1 file changed, 72 insertions(+), 26 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 17f5ac328..064a9997f 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -13,11 +13,17 @@ namespace Discord /// /// Gets the name of this guild. /// + /// + /// A string containing 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. + /// automatically moved to the AFK voice channel. /// + /// + /// The amount of time in seconds for a user to be marked as inactive and moved into the AFK voice channel. + /// int AFKTimeout { get; } /// /// Determines if this guild is embeddable (i.e. can use widget). @@ -34,33 +40,56 @@ namespace Discord /// Gets the level of Multi-Factor Authentication requirements a user must fulfill before being allowed to /// perform administrative actions in this guild. /// + /// + /// The level of MFA requirement. + /// MfaLevel MfaLevel { get; } /// /// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild. /// + /// + /// The level of requirements. + /// VerificationLevel VerificationLevel { get; } /// - /// Returns the ID of this guild's icon, or null if none is set. + /// Gets the ID of this guild's icon. /// + /// + /// An identifier for the splash image; null if none is set. + /// string IconId { get; } /// - /// Returns the URL of this guild's icon, or null if none is set. + /// Gets the URL of this guild's icon. /// + /// + /// A URL pointing to the guild's icon; null if none is set. + /// string IconUrl { get; } /// - /// Returns the ID of this guild's splash image, or null if none is set. + /// Gets the ID of this guild's splash image. /// + /// + /// An identifier for the splash image; null if none is set. + /// string SplashId { get; } /// - /// Returns the URL of this guild's splash image, or null if none is set. + /// Gets the URL of this guild's splash image. /// + /// + /// A URL pointing to the guild's splash image; null if none is set. + /// string SplashUrl { get; } /// /// Determines if this guild is currently connected and ready to be used. /// + /// + /// + /// This property only applies to a WebSocket-based client. + /// + /// This boolean is used to determine if the guild is currently connected to the WebSocket and is ready to be used/accessed. + /// /// - /// Returns true if this guild is currently connected and ready to be used. Only applies - /// to the WebSocket client. + /// true if this guild is currently connected and ready to be used; otherwise false. /// bool Available { get; } @@ -143,7 +172,7 @@ namespace Discord /// Task ModifyEmbedAsync(Action func, RequestOptions options = null); /// - /// Bulk modifies the order of channels in this guild. + /// Bulk-modifies the order of channels in this guild. /// /// The properties used to modify the channel positions with. /// The options to be used when sending the request. @@ -152,17 +181,21 @@ namespace Discord /// Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null); /// - /// Bulk modifies the order of roles in this guild. + /// Bulk-modifies the order of roles in this guild. /// /// The properties used to modify the role positions with. /// The options to be used when sending the request. - Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null); /// /// An awaitable . /// + Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null); /// - /// Leaves this guild. If you are the owner, use instead. + /// Leaves this guild. /// + /// + /// This method will make the currently logged-in user leave the guild. If the user is the owner, use + /// instead. + /// /// The options to be used when sending the request. /// /// An awaitable . @@ -178,7 +211,7 @@ namespace Discord /// Task> GetBansAsync(RequestOptions options = null); /// - /// Bans the provided user from this guild and optionally prunes their recent messages. + /// Bans the user from this guild and optionally prunes their recent messages. /// /// The user to ban. /// @@ -192,9 +225,9 @@ namespace Discord /// 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. + /// Bans the user from this guild and optionally prunes their recent messages. /// - /// The ID of the user to ban. + /// The snowflake ID of the user to ban. /// /// The number of days to remove messages from this user for - must be between [0, 7]. /// @@ -389,12 +422,19 @@ namespace Discord /// /// Gets a collection of all invites to this guild. /// + /// The options to be used when sending the request. + /// + /// An awaitable containing a collection of invites found within this guild. + /// Task> GetInvitesAsync(RequestOptions options = null); /// - /// Gets the role in this guild with the provided ID, or null if not found. + /// Gets a role in this guild. /// /// The role ID. + /// + /// A role that matches the provided snowflake identifier; null if none is found. + /// IRole GetRole(ulong id); /// /// Creates a new role with the provided name. @@ -412,25 +452,30 @@ namespace Discord /// /// Gets a collection of all users in this guild. /// - /// The that determines whether the object should be fetched from - /// cache. + /// + /// This method retrieves all users found within this guild. + /// + /// This may return an incomplete list on the WebSocket implementation. + /// + /// + /// + /// The that determines whether the object should be fetched from cache. + /// /// The options to be used when sending the request. /// /// An awaitable containing a collection of users found within this guild. /// - /// - /// This may return an incomplete list on the WebSocket implementation because Discord only sends offline - /// users on large guilds. - /// Task> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// - /// Gets the user in this guild with the provided ID, or null if not found. + /// Gets a user found in this guild. /// - /// The user ID. - /// The that determines whether the object should be fetched from cache. + /// The user ID to search for. + /// The that determines whether the object should be fetched from + /// cache. /// The options to be used when sending the request. /// - /// An awaitable containing the guild user with the specified ID, otherwise null. + /// An awaitable containing the guild user with the specified ID; null if none is + /// found. /// Task GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); /// @@ -499,7 +544,8 @@ namespace Discord /// The guild emote ID. /// The options to be used when sending the request. /// - /// An awaitable containing the emote found with the specified ID, or null if not found. + /// An awaitable containing the emote found with the specified ID; null if none is + /// found. /// Task GetEmoteAsync(ulong id, RequestOptions options = null); /// From 25557218dbc9390dbeac108afc153776f52dc26f Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sun, 6 May 2018 16:11:19 +0800 Subject: [PATCH 175/183] Add details to SpotifyGame --- .../Entities/Activities/SpotifyGame.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs index beec4eebc..23f88687d 100644 --- a/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs +++ b/src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs @@ -13,36 +13,65 @@ namespace Discord /// /// Gets the song's artist(s). /// + /// + /// A collection of string containing all artists featured in the track (e.g. Avicii; Rita Ora). + /// public IReadOnlyCollection Artists { get; internal set; } /// /// Gets the Spotify album title of the song. /// + /// + /// A string containing the name of the album (e.g. AVĪCI (01)). + /// public string AlbumTitle { get; internal set; } /// /// Gets the track title of the song. /// + /// + /// A string containing the name of the song (e.g. Lonely Together (feat. Rita Ora)). + /// public string TrackTitle { get; internal set; } /// /// Gets the duration of the song. /// + /// + /// A containing the duration of the song. + /// public TimeSpan? Duration { get; internal set; } /// /// Gets the track ID of the song. /// + /// + /// A string containing the Spotify ID of the track (e.g. 7DoN0sCGIT9IcLrtBDm4f0). + /// public string TrackId { get; internal set; } /// /// Gets the session ID of the song. /// + /// + /// The purpose of this property is currently unknown. + /// + /// + /// A string containing the session ID. + /// public string SessionId { get; internal set; } /// /// Gets the URL of the album art. /// + /// + /// A URL pointing to the album art of the track (e.g. + /// https://i.scdn.co/image/ba2fd8823d42802c2f8738db0b33a4597f2f39e7). + /// public string AlbumArtUrl { get; internal set; } /// /// Gets the direct Spotify URL of the track. /// + /// + /// A URL pointing directly to the track on Spotify. (e.g. + /// https://open.spotify.com/track/7DoN0sCGIT9IcLrtBDm4f0). + /// public string TrackUrl { get; internal set; } internal SpotifyGame() { } @@ -50,6 +79,10 @@ namespace Discord /// /// Gets the full information of the song. /// + /// + /// A string containing the full information of the song (e.g. + /// Avicii, Rita Ora - Lonely Together (feat. Rita Ora) (3:08) + /// public override string ToString() => $"{string.Join(", ", Artists)} - {TrackTitle} ({Duration})"; private string DebuggerDisplay => $"{Name} (Spotify)"; } From ea82c2537e64d4b75f85d9369e90cb55cfcadcc7 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Tue, 8 May 2018 16:30:48 +0800 Subject: [PATCH 176/183] Initial proofread of the articles --- docs/faq/basics/basic-operations.md | 8 +- docs/faq/basics/client-basics.md | 8 +- docs/faq/basics/getting-started.md | 28 +++---- docs/faq/commands/commands.md | 85 ++++++++++++---------- docs/faq/commands/samples/Remainder.cs | 2 +- docs/faq/misc/legacy.md | 4 +- docs/guides/commands/intro.md | 2 +- docs/guides/commands/post-execution.md | 36 ++++----- docs/guides/concepts/connections.md | 14 ++-- docs/guides/concepts/deployment.md | 4 +- docs/guides/concepts/entities.md | 2 +- docs/guides/concepts/events.md | 2 +- docs/guides/concepts/logging.md | 2 +- docs/guides/getting_started/terminology.md | 6 +- 14 files changed, 105 insertions(+), 98 deletions(-) diff --git a/docs/faq/basics/basic-operations.md b/docs/faq/basics/basic-operations.md index 518a7426d..aef28a683 100644 --- a/docs/faq/basics/basic-operations.md +++ b/docs/faq/basics/basic-operations.md @@ -8,7 +8,7 @@ title: Questions about Basic Operations ## How should I safely check a type? > [!WARNING] -> Direct casting (e.g. `(Type)type`) is **the least recommended** +> 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. > @@ -28,9 +28,9 @@ A good and safe casting example: ## 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 +> The [GetChannel] method by default returns an [IChannel], allowing +> channel types such as [IVoiceChannel], [ICategoryChannel] +> to be returned; consequently, you cannot send a message > to channels like those. Any implementation of [IMessageChannel] has a [SendMessageAsync] diff --git a/docs/faq/basics/client-basics.md b/docs/faq/basics/client-basics.md index ad55c9722..48386206c 100644 --- a/docs/faq/basics/client-basics.md +++ b/docs/faq/basics/client-basics.md @@ -21,7 +21,7 @@ There are few possible reasons why this may occur. 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*. + mind that a token is **different** from a *client secret*. [TokenType]: xref:Discord.TokenType [827]: https://github.com/RogueException/Discord.Net/issues/827 @@ -33,11 +33,11 @@ There are few possible reasons why this may occur. 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 +Discord 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 +particular guild has downloaded; however, it is 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. @@ -46,7 +46,7 @@ event is triggered, then you can proceed to do whatever you like. ## 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, +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). diff --git a/docs/faq/basics/getting-started.md b/docs/faq/basics/getting-started.md index 08972ba2e..6e39aeed0 100644 --- a/docs/faq/basics/getting-started.md +++ b/docs/faq/basics/getting-started.md @@ -8,25 +8,27 @@ title: Beginner Questions / How to Get 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. - +by [FiniteReality]. +This tool allows you to set permissions that the bot will be assigned +with, and invite the bot into your guild. With this method, bots will +also be assigned a unique role that a regular user cannot use; this +is what we call a `Managed` role. Because you cannot assign this +role to any other users, it is much safer than creating a single +role which, intentionally or not, can be applied to other users +to escalate their privilege. + +[FiniteReality]: https://github.com/FiniteReality/permissions-calculator [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. +token can log into your account. This risk applies to both user +and bot accounts. That also means that you should **never** 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? diff --git a/docs/faq/commands/commands.md b/docs/faq/commands/commands.md index 4811b02be..11c28f6ec 100644 --- a/docs/faq/commands/commands.md +++ b/docs/faq/commands/commands.md @@ -5,37 +5,35 @@ title: Questions about Commands # Command-related Questions -## How can I restrict some of my commands so only certain users can execute them? +## How can I restrict some of my commands so only specific 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.*). +guild or channel (*e.g., `GuildPermission.Administrator`, +`ChannelPermission.ManageMessages`*). If, however, you wish to restrict the commands based on the user's -role, you can either create your own custom precondition or use +role, you can either create your 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 +Its source can also be used as an example for creating your 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`. +## I am 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* +You may be confusing @Discord.Commands.CommandService.AddModulesAsync* +with @Discord.Commands.CommandService.AddModuleAsync*. The former +is used to add modules via the assembly, while the latter is used to +add a single module. ## 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 +do not 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. @@ -47,13 +45,14 @@ the last parameter. ## 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 +that they have a transient nature; modules are spawned whenever a +request is received, and are killed from memory when the execution +finishes. In other words, you cannot store persistent +data inside a module. Consider using a service if you wish to +workaround this. + +Service is often used to hold data externally so that they 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, @@ -68,15 +67,23 @@ A brief example of service and dependency injection can be seen below. ## 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**. +By default, the library warns the user about any long-running event +handler that persists for **more than 3 seconds**. Any event +handlers that are run on the same thread as the gateway task, the task +in charge of keeping the connection alive, may block the processing of +heartbeat, and thus terminating the connection. + +In this case, the library detects that a `MessageReceived` +event handler is blocking the gateway thread. This warning is +typically associated with the command handler as it listens for that +particular event. If the command handler is blocking the thread, then +this **might** mean that you have a long-running command (in rare +cases, runtime errors can also cause blockage, usually associated +with Mono, which is not supported by this library). -To resolve this, the library has designed a flag called [RunMode]. +To prevent a long-running command from blocking the gateway +thread, a flag called [RunMode] is explicitly designed to resolve +this issue. There are 2 main `RunMode`s. @@ -84,7 +91,7 @@ There are 2 main `RunMode`s. 2. `RunMode.Async` You can set the `RunMode` either by specifying it individually via -the `CommandAttribute`, or by setting the global default with +the `CommandAttribute` or by setting the global default with the [DefaultRunMode] flag under `CommandServiceConfig`. # [CommandAttribute](#tab/cmdattrib) @@ -101,10 +108,9 @@ the [DefaultRunMode] flag under `CommandServiceConfig`. > [!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. +> to a different thread, keep in mind that by doing so, there will be +> **potentially unwanted consequences**. Before applying this flag, +> please consider whether it is necessary to do so. > > Further details regarding `RunMode.Async` can be found below. @@ -115,16 +121,15 @@ the [DefaultRunMode] flag under `CommandServiceConfig`. ## 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. +[Task.Run], essentially making the task that is used to invoke the +command task to be finished on a different thread. This design 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]. +1. You can potentially introduce a race condition. +2. Unnecessary overhead caused by the [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). diff --git a/docs/faq/commands/samples/Remainder.cs b/docs/faq/commands/samples/Remainder.cs index a28c782e0..337fb6e45 100644 --- a/docs/faq/commands/samples/Remainder.cs +++ b/docs/faq/commands/samples/Remainder.cs @@ -16,5 +16,5 @@ public Task EchoAsync(string text) => ReplyAsync(text); // 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. +// e.g., // !echo "Coffee Cake" \ No newline at end of file diff --git a/docs/faq/misc/legacy.md b/docs/faq/misc/legacy.md index ef4caa1cd..fec6ba24d 100644 --- a/docs/faq/misc/legacy.md +++ b/docs/faq/misc/legacy.md @@ -7,8 +7,8 @@ title: Questions about Legacy Versions ## 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 +If you are currently using an older version of the 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. diff --git a/docs/guides/commands/intro.md b/docs/guides/commands/intro.md index 8036ed09e..e2fad73e8 100644 --- a/docs/guides/commands/intro.md +++ b/docs/guides/commands/intro.md @@ -107,7 +107,7 @@ be found in @Guides.Commands.TypeReaders. #### Optional Parameters Parameters, by default, are always required. To make a parameter -optional, give it a default value (i.e. `int num = 0`). +optional, give it a default value (i.e., `int num = 0`). #### Parameters with Spaces diff --git a/docs/guides/commands/post-execution.md b/docs/guides/commands/post-execution.md index 5f19147cb..267f84b8f 100644 --- a/docs/guides/commands/post-execution.md +++ b/docs/guides/commands/post-execution.md @@ -6,18 +6,18 @@ 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 +post-execution handling system so you can have 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 +If you recall, in the [Command Guide], we have 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. +result and print it to the chat, essentially creating the most +fundamental form of a post-execution handler. With this in mind, we could start doing things like the following, @@ -25,8 +25,8 @@ With this in mind, we could start doing things like the following, 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). +handler. This design 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 @@ -37,8 +37,8 @@ about the impact in the [FAQ](xref:FAQ.Commands). 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 +successfully executed **without any run-time exceptions** or **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. @@ -52,7 +52,7 @@ next? We can take this further by using [RuntimeResult]. ### RuntimeResult -`RuntimeResult` was originally introduced in 1.0 to allow +`RuntimeResult` was initially 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. @@ -62,7 +62,7 @@ However, it wasn't widely adopted due to the aforementioned 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 +The best way to make use of it is to create your version of `RuntimeResult`. You can achieve this by inheriting the `RuntimeResult` class. @@ -71,16 +71,16 @@ 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. +The sky is the limit from here. You can add any additional information +you would like regarding the execution result. -For example, you may want to add your own result type or other +For example, you may want to add your 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 +After you're done creating your [RuntimeResult], you can implement it in your command by marking the command return type to `Task`. @@ -100,12 +100,12 @@ And now we can check for it in our [CommandExecuted] handler: ## 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 +have not 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 +All exceptions thrown during a command execution are caught and 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. diff --git a/docs/guides/concepts/connections.md b/docs/guides/concepts/connections.md index 324b67566..99ea45756 100644 --- a/docs/guides/concepts/connections.md +++ b/docs/guides/concepts/connections.md @@ -11,14 +11,14 @@ stopped. To start a connection, invoke the `StartAsync` method on a client that supports a WebSocket connection; to end a connection, invoke the -`StopAsync` method. This will gracefully close any open WebSocket or +`StopAsync` method, which gracefully closes 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 -connection is actually made.** +connection is made.** -As a result, you will need to hook into one of the connection-state +As a result, you need to hook into one of the connection-state based events to have an accurate representation of when a client is ready for use. @@ -29,7 +29,7 @@ ready to be used. 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. +sync and has a completed guild cache. [DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient @@ -41,8 +41,8 @@ sync, and has a complete guild cache. > [!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. +> gateway (as explained in [events]), the connection manager will +> **NOT** be able 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 @@ -50,6 +50,6 @@ and resume a connection. 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! +your client does not manage to reconnect, you have found a bug! [events]: xref:Guides.Concepts.Events diff --git a/docs/guides/concepts/deployment.md b/docs/guides/concepts/deployment.md index d26abee34..eea747817 100644 --- a/docs/guides/concepts/deployment.md +++ b/docs/guides/concepts/deployment.md @@ -68,7 +68,7 @@ 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` +* `dotnet publish -c Release` Additionally, you may want to target a specific platform when publishing the application so you may use the application without @@ -80,7 +80,7 @@ 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` +* `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 7c66c7a57..7415f043a 100644 --- a/docs/guides/concepts/entities.md +++ b/docs/guides/concepts/entities.md @@ -56,7 +56,7 @@ DiscordSocketClient. > [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 +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. diff --git a/docs/guides/concepts/events.md b/docs/guides/concepts/events.md index d8e586681..293b5dc72 100644 --- a/docs/guides/concepts/events.md +++ b/docs/guides/concepts/events.md @@ -74,7 +74,7 @@ object. [Cacheable]: xref:Discord.Cacheable`2 > [!NOTE] -> Many events relating to a Message entity (i.e. `MessageUpdated` and +> 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. diff --git a/docs/guides/concepts/logging.md b/docs/guides/concepts/logging.md index dba78006f..b92d2bd53 100644 --- a/docs/guides/concepts/logging.md +++ b/docs/guides/concepts/logging.md @@ -16,7 +16,7 @@ section. > [!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 +> 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. diff --git a/docs/guides/getting_started/terminology.md b/docs/guides/getting_started/terminology.md index a03dc8fbf..61a226dcf 100644 --- a/docs/guides/getting_started/terminology.md +++ b/docs/guides/getting_started/terminology.md @@ -28,13 +28,13 @@ 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 -implementation are prefixed with `Rest` (e.g. `RestChannel`). +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 -with `Rpc` (e.g. `RpcChannel`). +with `Rpc` (e.g., `RpcChannel`). `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. -All entities are prefixed with `Socket` (e.g. `SocketChannel`). \ No newline at end of file +All entities are prefixed with `Socket` (e.g., `SocketChannel`). \ No newline at end of file From fdaa689ae8be8e9d2ff5190b6cfed5af244b805b Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Wed, 9 May 2018 06:04:59 +0800 Subject: [PATCH 177/183] Add explanation for RunMode --- docs/faq/commands/commands.md | 38 ++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/docs/faq/commands/commands.md b/docs/faq/commands/commands.md index 11c28f6ec..2c905eaad 100644 --- a/docs/faq/commands/commands.md +++ b/docs/faq/commands/commands.md @@ -23,7 +23,7 @@ custom preconditions. [RequireUserPermission]: xref:Discord.Commands.RequireUserPermissionAttribute [Preconditions Addons]: https://github.com/Joe4evr/Discord.Addons/tree/master/src/Discord.Addons.Preconditions -## I am getting an error about `Assembly.GetEntryAssembly`. +## Why am I getting an error about `Assembly.GetEntryAssembly`? You may be confusing @Discord.Commands.CommandService.AddModulesAsync* with @Discord.Commands.CommandService.AddModuleAsync*. The former @@ -65,7 +65,7 @@ A brief example of service and dependency injection can be seen below. [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? +## Discord.Net keeps saying that a `MessageReceived` handler is blocking the gateway, what should I do? By default, the library warns the user about any long-running event handler that persists for **more than 3 seconds**. Any event @@ -77,9 +77,11 @@ In this case, the library detects that a `MessageReceived` event handler is blocking the gateway thread. This warning is typically associated with the command handler as it listens for that particular event. If the command handler is blocking the thread, then -this **might** mean that you have a long-running command (in rare -cases, runtime errors can also cause blockage, usually associated -with Mono, which is not supported by this library). +this **might** mean that you have a long-running command. + +> [!NOTE] +> In rare cases, runtime errors can also cause blockage, usually +> associated with Mono, which is not supported by this library. To prevent a long-running command from blocking the gateway thread, a flag called [RunMode] is explicitly designed to resolve @@ -87,9 +89,21 @@ this issue. There are 2 main `RunMode`s. -1. `RunMode.Sync` (default) +1. `RunMode.Sync` 2. `RunMode.Async` +`Sync` is the default behavior and makes the command to be run on the +same thread as the gateway one. `Async` will spin the task off to a +different thread from the gateway one. + +> [!IMPORTANT] +> While specifying `RunMode.Async` allows the command to be spun off +> to a different thread, keep in mind that by doing so, there will be +> **potentially unwanted consequences**. Before applying this flag, +> please consider whether it is necessary to do so. +> +> Further details regarding `RunMode.Async` can be found below. + 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`. @@ -106,14 +120,6 @@ the [DefaultRunMode] flag under `CommandServiceConfig`. *** -> [!IMPORTANT] -> While specifying `RunMode.Async` allows the command to be spun off -> to a different thread, keep in mind that by doing so, there will be -> **potentially unwanted consequences**. 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 @@ -142,7 +148,7 @@ For #3, in Discord.Net 2.0, the library introduces a new event called **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 +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 @@ -151,6 +157,6 @@ For #4, exceptions are caught in [CommandService#Log] event under [ExecuteResult]: xref:Discord.Commands.ExecuteResult [RuntimeResult]: xref:Discord.Commands.RuntimeResult [CommandExecuted]: xref:Discord.Commands.CommandService.CommandExecuted -[CommandService#Log]: xref:Discord.Commands.CommandService.Log +[CommandService.Log]: xref:Discord.Commands.CommandService.Log [LogMessage.Exception]: xref:Discord.LogMessage.Exception* [CommandException]: xref:Discord.Commands.CommandException \ No newline at end of file From a9983026f067effeac7108c710ba6917162b7995 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Tue, 22 May 2018 10:15:49 +0800 Subject: [PATCH 178/183] Add event docs - MessageReceived - ChannelUpdated/Destroyed/Created --- docs/docfx.json | 2 +- .../BaseSocketClient.Events.cs | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/docs/docfx.json b/docs/docfx.json index cb6a36360..663a49cda 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -43,7 +43,7 @@ "globalMetadata": { "_appTitle": "Discord.Net Documentation", "_appFooter": "Discord.Net (c) 2015-2018 2.0.0-beta", - "_enableSearch": true, + "_enableSearch": true }, "noLangKeyword": false, "xrefService": [ diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs index c236b1045..9a94cbf23 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.Events.cs @@ -7,6 +7,17 @@ namespace Discord.WebSocket { //Channels /// Fired when a channel is created. + /// + /// + /// This event is fired when a generic channel has been created. The event handler must return a + /// . + /// + /// + /// The newly created channel is passed into the event handler parameter. The given channel type may + /// include, but not limited to, Private Channels (DM, Group), Guild Channels (Text, Voice, Category); + /// see the derived classes of for more details. + /// + /// public event Func ChannelCreated { add { _channelCreatedEvent.Add(value); } @@ -14,12 +25,35 @@ namespace Discord.WebSocket } internal readonly AsyncEvent> _channelCreatedEvent = new AsyncEvent>(); /// Fired when a channel is destroyed. + /// + /// + /// This event is fired when a generic channel has been destroyed. The event handler must return a + /// . + /// + /// + /// The destroyed channel is passed into the event handler parameter. The given channel type may + /// include, but not limited to, Private Channels (DM, Group), Guild Channels (Text, Voice, Category); + /// see the derived classes of for more details. + /// + /// public event Func ChannelDestroyed { add { _channelDestroyedEvent.Add(value); } remove { _channelDestroyedEvent.Remove(value); } } internal readonly AsyncEvent> _channelDestroyedEvent = new AsyncEvent>(); /// Fired when a channel is updated. + /// + /// + /// This event is fired when a generic channel has been destroyed. The event handler must return a + /// . + /// + /// + /// The original (prior to update) channel is passed into the first , while + /// the updated channel is passed into the second. The given channel type may include, but not limited + /// to, Private Channels (DM, Group), Guild Channels (Text, Voice, Category); see the derived classes of + /// for more details. + /// + /// public event Func ChannelUpdated { add { _channelUpdatedEvent.Add(value); } remove { _channelUpdatedEvent.Remove(value); } @@ -28,12 +62,46 @@ namespace Discord.WebSocket //Messages /// Fired when a message is received. + /// + /// + /// This event is fired when a message is received. The event handler must return a + /// . + /// + /// + /// The message that is sent to the client is passed into the event handler parameter as + /// . This message may be a system message (i.e. + /// ) or a user message (i.e. . See + /// the derived clsases of for more details. + /// + /// public event Func MessageReceived { add { _messageReceivedEvent.Add(value); } remove { _messageReceivedEvent.Remove(value); } } internal readonly AsyncEvent> _messageReceivedEvent = new AsyncEvent>(); /// Fired when a message is deleted. + /// + /// + /// This event is fired when a message is deleted. The event handler must return a + /// and accept a and + /// as its parameters. + /// + /// + /// + /// It is not possible to retrieve the message via + /// ; the message cannot be retrieved by Discord + /// after the message has been deleted. + /// + /// If caching is enabled via , the + /// entity will contain the deleted message; otherwise, in event + /// that the message cannot be retrieved, the snowflake ID of the message is preserved in the + /// . + /// + /// + /// The source channel of the removed message will be passed into the + /// parameter. + /// + /// public event Func, ISocketMessageChannel, Task> MessageDeleted { add { _messageDeletedEvent.Add(value); } remove { _messageDeletedEvent.Remove(value); } From 73ebc0258031ff74770b1f4b4a8c4be9071f703c Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 26 May 2018 15:32:23 +0800 Subject: [PATCH 179/183] Fix light theme link color --- .../_template/light-dark-theme/styles/light.css | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/_template/light-dark-theme/styles/light.css b/docs/_template/light-dark-theme/styles/light.css index 18660dfe6..a602274a2 100644 --- a/docs/_template/light-dark-theme/styles/light.css +++ b/docs/_template/light-dark-theme/styles/light.css @@ -10,11 +10,24 @@ body { overflow: visible; } +/* links */ + +a:active, a:hover, a:visited { + color: #0078d7; +} + +a { + color: #0050c5; + cursor: pointer; + text-decoration: none; + word-wrap: break-word; +} + /* code */ code { - color: #222f3d; - background-color: #f9f9f9; + color: #3b5269; + background-color: #ececec; border-radius: 4px; padding: 3px 7px; } From 5d1621a9c5ea27d8758e6a6e6dc4b4d6c093ff7a Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 26 May 2018 16:06:20 +0800 Subject: [PATCH 180/183] Fix xml docs error --- .../Entities/AuditLogs/IAuditLogEntry.cs | 28 +++++++++++-------- .../Entities/Guilds/IGuild.cs | 11 ++++++-- src/Discord.Net.Core/Entities/Image.cs | 8 +++--- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs index b85730a1d..c4a6be702 100644 --- a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs +++ b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs @@ -1,34 +1,40 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Discord { /// - /// Represents an entry in an audit log + /// Represents an entry in an audit log. /// public interface IAuditLogEntry : IEntity { /// - /// The action which occured to create this entry + /// Gets the action which occurred to create this entry. /// + /// + /// The type of action for this audit log entry. + /// ActionType Action { get; } /// - /// The data for this entry. May be if no data was available. + /// Gets the data for this entry. /// + /// + /// An for this audit log entry; null if no data is available. + /// IAuditLogData Data { get; } /// - /// The user responsible for causing the changes + /// Gets the user responsible for causing the changes. /// + /// + /// A user object. + /// IUser User { get; } /// - /// The reason behind the change. May be if no reason was provided. + /// Gets the reason behind the change. /// + /// + /// A string containing the reason for the change; null if none is provided. + /// string Reason { get; } } } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index e38c01c6d..2b18726e2 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -214,18 +214,20 @@ namespace Discord /// Gets a ban object for a banned user. /// /// The banned user. + /// The options to be used when sending the request. /// /// An awaitable containing the ban object, which contains the user information and the - /// reason for the ban; null if the ban entry cannot be found. + /// reason for the ban; null if the ban entry cannot be found. /// Task GetBanAsync(IUser user, RequestOptions options = null); /// /// Gets a ban object for a banned user. /// /// The snowflake identifier for the banned user. + /// The options to be used when sending the request. /// /// An awaitable containing the ban object, which contains the user information and the - /// reason for the ban; null if the ban entry cannot be found. + /// reason for the ban; null if the ban entry cannot be found. /// Task GetBanAsync(ulong userId, RequestOptions options = null); /// @@ -406,10 +408,12 @@ namespace Discord /// null if none is set. /// Task GetEmbedChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); + /// /// Creates a new text channel. /// /// The new name for the text channel. + /// The delegate containing the properties to be applied to the channel upon its creation. /// The options to be used when sending the request. /// /// An awaitable containing the newly created text channel. @@ -419,6 +423,7 @@ namespace Discord /// Creates a new voice channel. /// /// The new name for the voice channel. + /// The delegate containing the properties to be applied to the channel upon its creation. /// The options to be used when sending the request. /// /// An awaitable containing the newly created voice channel. @@ -428,6 +433,8 @@ namespace Discord /// Creates a new channel category. /// /// The new name for the category. + /// The options to be used when sending the request. + /// /// An awaitable containing the newly created category channel. /// Task CreateCategoryAsync(string name, RequestOptions options = null); diff --git a/src/Discord.Net.Core/Entities/Image.cs b/src/Discord.Net.Core/Entities/Image.cs index 2fc0e50b9..5c775f9f4 100644 --- a/src/Discord.Net.Core/Entities/Image.cs +++ b/src/Discord.Net.Core/Entities/Image.cs @@ -30,21 +30,21 @@ namespace Discord /// . /// /// The path to the file. - /// + /// /// is a zero-length string, contains only white space, or contains one or more invalid /// characters as defined by . /// - /// is null. + /// is null. /// /// The specified path, file name, or both exceed the system-defined maximum length. For example, on /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 /// characters. /// - /// is in an invalid format. + /// is in an invalid format. /// /// The specified is invalid, (for example, it is on an unmapped drive). /// - /// + /// /// specified a directory.-or- The caller does not have the required permission. /// /// The file specified in was not found. From 098ead93d98ea9e629f9c4d8681a38ad8c1e9a82 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 26 May 2018 16:33:38 +0800 Subject: [PATCH 181/183] Add partial documentation for audit log impl --- src/Discord.Net.Core/DiscordConfig.cs | 12 ++++ .../Entities/AuditLogs/IAuditLogData.cs | 8 +-- .../Entities/Guilds/IGuild.cs | 21 ++++++- src/Discord.Net.Core/RequestOptions.cs | 7 ++- .../AuditLogs/DataTypes/BanAuditLogData.cs | 8 ++- .../DataTypes/ChannelCreateAuditLogData.cs | 28 +++++++++- .../DataTypes/ChannelDeleteAuditLogData.cs | 30 +++++++++- .../AuditLogs/DataTypes/ChannelInfo.cs | 29 ++++++++++ .../DataTypes/ChannelUpdateAuditLogData.cs | 11 +++- .../DataTypes/EmoteCreateAuditLogData.cs | 19 +++++-- .../DataTypes/EmoteDeleteAuditLogData.cs | 17 +++++- .../DataTypes/EmoteUpdateAuditLogData.cs | 23 +++++++- .../Entities/AuditLogs/DataTypes/GuildInfo.cs | 55 +++++++++++++++++++ 13 files changed, 244 insertions(+), 24 deletions(-) diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index c65810d4e..489219161 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -93,7 +93,19 @@ namespace Discord /// The maximum number of guilds that can be gotten per-batch. /// public const int MaxGuildsPerBatch = 100; + /// + /// Returns the max user reactions allowed to be in a request. + /// + /// + /// The maximum number of user reactions that can be gotten per-batch. + /// public const int MaxUserReactionsPerBatch = 100; + /// + /// Returns the max audit log entries allowed to be in a request. + /// + /// + /// The maximum number of audit log entries that can be gotten per-batch. + /// public const int MaxAuditLogEntriesPerBatch = 100; /// diff --git a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs index 47aaffb26..da28b53a5 100644 --- a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs +++ b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs @@ -1,13 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Discord { /// - /// Represents data applied to an + /// Represents data applied to an . /// public interface IAuditLogData { } diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index 2b18726e2..8d6c090b5 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -22,7 +22,8 @@ namespace Discord /// automatically moved to the AFK voice channel. /// /// - /// The amount of time in seconds for a user to be marked as inactive and moved into the AFK voice channel. + /// An representing the amount of time in seconds for a user to be marked as inactive + /// and moved into the AFK voice channel. /// int AFKTimeout { get; } /// @@ -94,8 +95,12 @@ namespace Discord bool Available { get; } /// - /// Gets the ID of the AFK voice channel for this guild, or null if none is set. + /// Gets the ID of the AFK voice channel for this guild. /// + /// + /// An representing the snowflake identifier of the AFK voice channel; null if + /// none is set. + /// ulong? AFKChannelId { get; } /// /// Gets the ID of the the default channel for this guild. @@ -542,7 +547,17 @@ namespace Discord /// Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); - /// Gets the specified number of audit log entries for this guild. + /// + /// Gets the specified number of audit log entries for this guild. + /// + /// The number of audit log entries to fetch. + /// + /// The that determines whether the object should be fetched from cache. + /// + /// The options to be used when sending the request. + /// + /// An awaitable containing a collection of requested audit log entries. + /// Task> GetAuditLogAsync(int limit = DiscordConfig.MaxAuditLogEntriesPerBatch, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index c2d1f6549..785b518f3 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -25,9 +25,12 @@ namespace Discord public RetryMode? RetryMode { get; set; } public bool HeaderOnly { get; internal set; } /// - /// Gets or sets the reason for this action in the guild's audit log. Note that this property may not apply - /// to every action. + /// Gets or sets the reason for this action in the guild's audit log. /// + /// + /// Gets or sets the reason that will be written to the guild's audit log if applicable. This may not apply + /// to all actions. + /// public string AuditLogReason { get; set; } internal bool IgnoreState { get; set; } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs index 4b9d5875f..b249486aa 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Represents an audit log data for a ban action. + /// public class BanAuditLogData : IAuditLogData { private BanAuditLogData(IUser user) @@ -18,6 +21,9 @@ namespace Discord.Rest return new BanAuditLogData(RestUser.Create(discord, userInfo)); } + /// + /// Gets the user that was banned. + /// public IUser Target { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs index ef4787295..6539519a1 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json.Linq; using System.Collections.Generic; using System.Linq; @@ -7,6 +6,9 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Represents an audit log data for a channel creation. + /// public class ChannelCreateAuditLogData : IAuditLogData { private ChannelCreateAuditLogData(ulong id, string name, ChannelType type, IReadOnlyCollection overwrites) @@ -44,9 +46,33 @@ namespace Discord.Rest return new ChannelCreateAuditLogData(entry.TargetId.Value, name, type, overwrites.ToReadOnlyCollection()); } + /// + /// Gets the snowflake ID of the created channel. + /// + /// + /// An representing the snowflake identifier for the created channel. + /// public ulong ChannelId { get; } + /// + /// Gets the name of the created channel. + /// + /// + /// A string containing the name of the created channel. + /// public string ChannelName { get; } + /// + /// Gets the type of the created channel. + /// + /// + /// The type of channel that was created. + /// public ChannelType ChannelType { get; } + /// + /// Gets a collection of permission overwrites that was assigned to the created channel. + /// + /// + /// A collection of permission . + /// public IReadOnlyCollection Overwrites { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs index 4816ce770..260de5509 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs @@ -1,14 +1,14 @@ -using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Represents an audit log data for a channel deletion. + /// public class ChannelDeleteAuditLogData : IAuditLogData { private ChannelDeleteAuditLogData(ulong id, string name, ChannelType type, IReadOnlyCollection overwrites) @@ -37,9 +37,33 @@ namespace Discord.Rest return new ChannelDeleteAuditLogData(id, name, type, overwrites.ToReadOnlyCollection()); } + /// + /// Gets the snowflake ID of the deleted channel. + /// + /// + /// An representing the snowflake identifier for the deleted channel. + /// public ulong ChannelId { get; } + /// + /// Gets the name of the deleted channel. + /// + /// + /// A string containing the name of the deleted channel. + /// public string ChannelName { get; } + /// + /// Gets the type of the deleted channel. + /// + /// + /// The type of channel that was deleted. + /// public ChannelType ChannelType { get; } + /// + /// Gets a collection of permission overwrites that was assigned to the deleted channel. + /// + /// + /// A collection of permission . + /// public IReadOnlyCollection Overwrites { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs index e2d6064a9..753e1d3a5 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs @@ -1,5 +1,8 @@ namespace Discord.Rest { + /// + /// Represents information for a channel. + /// public struct ChannelInfo { internal ChannelInfo(string name, string topic, int? bitrate, int? limit) @@ -10,9 +13,35 @@ namespace Discord.Rest UserLimit = limit; } + /// + /// Gets the name of this channel. + /// + /// + /// A string containing the name of this channel. + /// public string Name { get; } + /// + /// Gets the topic of this channel. + /// + /// + /// A string containing the topic of this channel, if any. + /// public string Topic { get; } + /// + /// Gets the bitrate of this channel if applicable. + /// + /// + /// An representing the bitrate set for the voice channel; null if not + /// applicable. + /// public int? Bitrate { get; } + /// + /// Gets the number of users allowed to be in this channel if applicable. + /// + /// + /// An representing the number of users allowed to be in this voice channel; + /// null if not applicable. + /// public int? UserLimit { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs index f3403138d..4e88a6f00 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Represents an audit log data for a channel update. + /// public class ChannelUpdateAuditLogData : IAuditLogData { private ChannelUpdateAuditLogData(ulong id, ChannelInfo before, ChannelInfo after) @@ -38,6 +41,12 @@ namespace Discord.Rest return new ChannelUpdateAuditLogData(entry.TargetId.Value, before, after); } + /// + /// Gets the snowflake ID of the updated channel. + /// + /// + /// An representing the snowflake identifier for the updated channel. + /// public ulong ChannelId { get; } public ChannelInfo Before { get; set; } public ChannelInfo After { get; set; } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs index 5d1ef8463..1e490b42d 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs @@ -1,14 +1,13 @@ -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Represents an audit log data for an emoji creation. + /// public class EmoteCreateAuditLogData : IAuditLogData { private EmoteCreateAuditLogData(ulong id, string name) @@ -25,7 +24,19 @@ namespace Discord.Rest return new EmoteCreateAuditLogData(entry.TargetId.Value, emoteName); } + /// + /// Gets the snowflake ID of the created emoji. + /// + /// + /// An representing the snowflake identifier for the created emoji. + /// public ulong EmoteId { get; } + /// + /// Gets the name of the created emoji. + /// + /// + /// A string containing the name of the created emoji. + /// public string Name { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs index d0a11191f..2ce6acfbf 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Represents an audit log data for an emoji deletion. + /// public class EmoteDeleteAuditLogData : IAuditLogData { private EmoteDeleteAuditLogData(ulong id, string name) @@ -22,7 +25,19 @@ namespace Discord.Rest return new EmoteDeleteAuditLogData(entry.TargetId.Value, emoteName); } + /// + /// Gets the snowflake ID of the deleted emoji. + /// + /// + /// An representing the snowflake identifier for the deleted emoji. + /// public ulong EmoteId { get; } + /// + /// Gets the name of the deleted emoji. + /// + /// + /// A string containing the name of the deleted emoji. + /// public string Name { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs index 60020bcaa..506797707 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Represents an audit log data for an emoji update. + /// public class EmoteUpdateAuditLogData : IAuditLogData { private EmoteUpdateAuditLogData(ulong id, string oldName, string newName) @@ -24,8 +27,26 @@ namespace Discord.Rest return new EmoteUpdateAuditLogData(entry.TargetId.Value, oldName, newName); } + /// + /// Gets the snowflake ID of the updated emoji. + /// + /// + /// An representing the snowflake identifier of the updated emoji. + /// public ulong EmoteId { get; } + /// + /// Gets the new name of the updated emoji. + /// + /// + /// A string containing the new name of the updated emoji. + /// public string NewName { get; } + /// + /// Gets the old name of the updated emoji. + /// + /// + /// A string containing the old name of the updated emoji. + /// public string OldName { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs index 90865ef72..4e830240a 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs @@ -1,5 +1,8 @@ namespace Discord.Rest { + /// + /// Represents information for a guild. + /// public struct GuildInfo { internal GuildInfo(int? afkTimeout, DefaultMessageNotifications? defaultNotifs, @@ -18,14 +21,66 @@ namespace Discord.Rest ContentFilterLevel = filter; } + /// + /// 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. + /// + /// + /// An representing the amount of time in seconds for a user to be marked as inactive + /// and moved into the AFK voice channel. + /// public int? AfkTimeout { get; } + /// + /// Gets the default message notifications for users who haven't explicitly set their notification settings. + /// public DefaultMessageNotifications? DefaultMessageNotifications { get; } + /// + /// Gets the ID of the AFK voice channel for this guild. + /// + /// + /// An representing the snowflake identifier of the AFK voice channel; null if + /// none is set. + /// public ulong? AfkChannelId { get; } + /// + /// Gets the name of this guild. + /// + /// + /// A string containing the name of this guild. + /// public string Name { get; } + /// + /// Gets the ID of the region hosting this guild's voice channels. + /// public string RegionId { get; } + /// + /// Gets the ID of this guild's icon. + /// + /// + /// An identifier for the splash image; null if none is set. + /// public string IconHash { get; } + /// + /// Gets the level of requirements a user must fulfill before being allowed to post messages in this guild. + /// + /// + /// The level of requirements. + /// public VerificationLevel? VerificationLevel { get; } + /// + /// Gets the owner of this guild. + /// + /// + /// An object representing the owner of this guild. + /// public IUser Owner { get; } + /// + /// Gets the level of Multi-Factor Authentication requirements a user must fulfill before being allowed to + /// perform administrative actions in this guild. + /// + /// + /// The level of MFA requirement. + /// public MfaLevel? MfaLevel { get; } public int? ContentFilterLevel { get; } } From afda0cd172a98154bc33b4c27eed65580189785d Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 26 May 2018 20:47:21 +0800 Subject: [PATCH 182/183] Add documentation for some REST-based objects --- .../Entities/Channels/IVoiceChannel.cs | 2 +- src/Discord.Net.Core/Entities/Guilds/IBan.cs | 8 ++- .../Entities/Invites/IInvite.cs | 36 ++++++++++- .../Entities/Invites/IInviteMetadata.cs | 40 ++++++++++-- .../Entities/Channels/RestChannel.cs | 3 + .../Entities/Channels/RestDMChannel.cs | 30 ++++++++- .../Entities/Channels/RestGroupChannel.cs | 2 + .../Entities/Channels/RestGuildChannel.cs | 2 +- .../Entities/Channels/RestTextChannel.cs | 64 ++++++++++++++++++- .../Entities/Channels/RestVoiceChannel.cs | 9 +++ .../Entities/Guilds/RestBan.cs | 15 +++++ 11 files changed, 196 insertions(+), 15 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs index e1efb1a86..4dc3a6086 100644 --- a/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord { /// - /// Represents a voice channel in a guild. + /// Represents a generic voice channel in a guild. /// public interface IVoiceChannel : IGuildChannel, IAudioChannel { diff --git a/src/Discord.Net.Core/Entities/Guilds/IBan.cs b/src/Discord.Net.Core/Entities/Guilds/IBan.cs index 3ce76d29b..103cfcfd9 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IBan.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IBan.cs @@ -8,10 +8,16 @@ namespace Discord /// /// Gets the banned user. /// + /// + /// A generic object that was banned. + /// IUser User { get; } /// - /// Gets the reason why the user is banned. + /// Gets the reason why the user is banned if specified. /// + /// + /// A string containing the reason behind the ban; null if none is specified. + /// string Reason { get; } } } diff --git a/src/Discord.Net.Core/Entities/Invites/IInvite.cs b/src/Discord.Net.Core/Entities/Invites/IInvite.cs index 62e4a4bfc..0902d437c 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInvite.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInvite.cs @@ -1,5 +1,3 @@ -using System.Threading.Tasks; - namespace Discord { /// @@ -10,44 +8,76 @@ namespace Discord /// /// Gets the unique identifier for this invite. /// + /// + /// A string containing the invite code (e.g. FTqNnyS). + /// string Code { get; } /// - /// Gets the URL used to accept this invite, using Code. + /// Gets the URL used to accept this invite using . /// + /// + /// A string containing the full invite URL (e.g. https://discord.gg/FTqNnyS). + /// string Url { get; } /// /// Gets the channel this invite is linked to. /// + /// + /// A generic channel that the invite points to. + /// IChannel Channel { get; } /// /// Gets the ID of the channel this invite is linked to. /// + /// + /// An representing the channel snowflake identifier that the invite points to. + /// ulong ChannelId { get; } /// /// Gets the name of the channel this invite is linked to. /// + /// + /// A string containing the name of the channel that the invite points to. + /// string ChannelName { get; } /// /// Gets the guild this invite is linked to. /// + /// + /// A generic representing the guild that the invite points to. + /// IGuild Guild { get; } /// /// Gets the ID of the guild this invite is linked to. /// + /// + /// An representing the guild snowflake identifier that the invite points to. + /// ulong GuildId { get; } /// /// Gets the name of the guild this invite is linked to. /// + /// + /// A string containing the name of the guild that the invite points to. + /// string GuildName { get; } /// /// Gets the approximated count of online members in the guild. /// + /// + /// An representing the approximated online member count of the guild that the + /// invite points to; null if one cannot be obtained. + /// int? PresenceCount { get; } /// /// Gets the approximated count of total members in the guild. /// + /// + /// An representing the approximated total member count of the guild that the + /// invite points to; null if one cannot be obtained. + /// int? MemberCount { get; } } } diff --git a/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs index 1c6b1dd79..c3c4c857d 100644 --- a/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs +++ b/src/Discord.Net.Core/Entities/Invites/IInviteMetadata.cs @@ -2,37 +2,63 @@ using System; namespace Discord { - /// Represents additional information regarding the generic invite object. + /// + /// Represents additional information regarding the generic invite object. + /// public interface IInviteMetadata : IInvite { /// /// Gets the user that created this invite. /// + /// + /// A generic that created this invite. + /// IUser Inviter { get; } /// - /// Returns true if this invite was revoked. + /// Determines whether the invite has been revoked. /// + /// + /// true if this invite was revoked; otherwise false. + /// bool IsRevoked { get; } /// - /// Returns true if users accepting this invite will be removed from the guild when they - /// log off. + /// Determines whether the invite is a temporary one (i.e. whether the invite will be removed from the guild + /// when the user logs off). /// + /// + /// true if users accepting this invite will be removed from the guild when they log off; otherwise + /// false. + /// 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. /// + /// + /// An representing the time in seconds until this invite expires; null if this + /// invite 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 number of uses this invite may have. /// + /// + /// An representing the number of uses this invite may be accepted until it is removed + /// from the guild; null if none is set. + /// int? MaxUses { get; } /// - /// Gets the amount of times this invite has been used. + /// Gets the number of times this invite has been used. /// + /// + /// An representing the number of times this invite has been used. + /// int Uses { get; } /// /// Gets when this invite was created. /// + /// + /// A representing the time of which the invite was first created. + /// DateTimeOffset CreatedAt { get; } } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs index c6d765787..09c2e75a1 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestChannel.cs @@ -6,6 +6,9 @@ using Model = Discord.API.Channel; namespace Discord.Rest { + /// + /// Represents a generic REST-based channel. + /// public class RestChannel : RestEntity, IChannel, IUpdateable { /// diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 7af228654..eb6fe9105 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -10,7 +10,7 @@ using Model = Discord.API.Channel; namespace Discord.Rest { /// - /// Represents a REST-based DM channel. + /// Represents a REST-based direct-message channel. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestDMChannel : RestChannel, IDMChannel, IRestPrivateChannel, IRestMessageChannel @@ -74,18 +74,46 @@ namespace Discord.Rest => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); /// + /// Message content is too long, length must be less or equal to . public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); /// + /// + /// is a zero-length string, contains only white space, or contains one or more + /// invalid characters as defined by . + /// + /// + /// is null. + /// + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// + /// The specified path is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// + /// The file specified in was not found. + /// + /// is in an invalid format. + /// An I/O error occurred while opening the file. + /// Message content is too long, length must be less or equal to . 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); /// + /// Message content is too long, length must be less or equal to . 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 DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index f69b048d8..2f14dc94f 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -83,8 +83,10 @@ namespace Discord.Rest public Task> GetPinnedMessagesAsync(RequestOptions options = null) => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); + /// public Task DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index 7c3cc53c9..f0ff4d4c4 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -8,7 +8,7 @@ using Model = Discord.API.Channel; namespace Discord.Rest { /// - /// Represents a private REST group channel. + /// Represents a private REST-based group channel. /// public class RestGuildChannel : RestChannel, IGuildChannel { diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 332fb0679..f93757c31 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -8,14 +8,20 @@ using Model = Discord.API.Channel; namespace Discord.Rest { + /// + /// Represents a REST-based channel in a guild that can send and receive messages. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestTextChannel : RestGuildChannel, IRestMessageChannel, ITextChannel { + /// public string Topic { get; private set; } + /// public string Mention => MentionUtils.MentionChannel(Id); private bool _nsfw; + /// public bool IsNsfw => _nsfw || ChannelHelper.IsNsfw(this); internal RestTextChannel(BaseDiscordClient discord, IGuild guild, ulong id) @@ -36,6 +42,7 @@ namespace Discord.Rest _nsfw = model.Nsfw.GetValueOrDefault(); } + /// public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); @@ -47,41 +54,80 @@ namespace Discord.Rest public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) => ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options); + /// 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); + /// + /// Message content is too long, length must be less or equal to . public Task SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null) => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); + /// + /// + /// is a zero-length string, contains only white space, or contains one or more + /// invalid characters as defined by . + /// + /// + /// is null. + /// + /// + /// The specified path, file name, or both exceed the system-defined maximum length. For example, on + /// Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 + /// characters. + /// + /// + /// The specified path is invalid, (for example, it is on an unmapped drive). + /// + /// + /// specified a directory.-or- The caller does not have the required permission. + /// + /// + /// The file specified in was not found. + /// + /// is in an invalid format. + /// An I/O error occurred while opening the file. + /// Message content is too long, length must be less or equal to . 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); + /// + /// Message content is too long, length must be less or equal to . 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 DeleteMessageAsync(ulong messageId, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, messageId, Discord, options); + /// public Task DeleteMessageAsync(IMessage message, RequestOptions options = null) => ChannelHelper.DeleteMessageAsync(this, message.Id, Discord, 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) => ChannelHelper.EnterTypingState(this, Discord, options); - + public Task CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null) => ChannelHelper.CreateWebhookAsync(this, Discord, name, avatar, options); public Task GetWebhookAsync(ulong id, RequestOptions options = null) @@ -92,14 +138,18 @@ namespace Discord.Rest private string DebuggerDisplay => $"{Name} ({Id}, Text)"; //ITextChannel + /// async Task ITextChannel.CreateWebhookAsync(string name, Stream avatar, RequestOptions options) => await CreateWebhookAsync(name, avatar, options).ConfigureAwait(false); + /// async Task ITextChannel.GetWebhookAsync(ulong id, RequestOptions options) => await GetWebhookAsync(id, options).ConfigureAwait(false); + /// async Task> ITextChannel.GetWebhooksAsync(RequestOptions options) => await GetWebhooksAsync(options).ConfigureAwait(false); //IMessageChannel + /// async Task IMessageChannel.GetMessageAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -107,6 +157,7 @@ namespace Discord.Rest else return null; } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -114,6 +165,7 @@ namespace Discord.Rest else return AsyncEnumerable.Empty>(); } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -121,6 +173,7 @@ namespace Discord.Rest else return AsyncEnumerable.Empty>(); } + /// IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -128,20 +181,26 @@ namespace Discord.Rest else return AsyncEnumerable.Empty>(); } + /// async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); + /// async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, Embed embed, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, embed, options).ConfigureAwait(false); + /// 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); //IGuildChannel + /// async Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -149,6 +208,7 @@ namespace Discord.Rest else return null; } + /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -158,6 +218,7 @@ namespace Discord.Rest } //IChannel + /// async Task IChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) @@ -165,6 +226,7 @@ namespace Discord.Rest else return null; } + /// IAsyncEnumerable> IChannel.GetUsersAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index 5b44136e0..2af6c6768 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -8,10 +8,15 @@ using Model = Discord.API.Channel; namespace Discord.Rest { + /// + /// Represents a REST-based voice channel in a guild. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestVoiceChannel : RestGuildChannel, IVoiceChannel, IRestAudioChannel { + /// public int Bitrate { get; private set; } + /// public int? UserLimit { get; private set; } internal RestVoiceChannel(BaseDiscordClient discord, IGuild guild, ulong id) @@ -24,6 +29,7 @@ namespace Discord.Rest entity.Update(model); return entity; } + /// internal override void Update(Model model) { base.Update(model); @@ -32,6 +38,7 @@ namespace Discord.Rest UserLimit = model.UserLimit.Value != 0 ? model.UserLimit.Value : (int?)null; } + /// public async Task ModifyAsync(Action func, RequestOptions options = null) { var model = await ChannelHelper.ModifyAsync(this, Discord, func, options).ConfigureAwait(false); @@ -46,8 +53,10 @@ namespace Discord.Rest Task IAudioChannel.ConnectAsync(Action configAction) => throw new NotSupportedException(); //IGuildChannel + /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(null); + /// IAsyncEnumerable> IGuildChannel.GetUsersAsync(CacheMode mode, RequestOptions options) => AsyncEnumerable.Empty>(); } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs b/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs index 2c65c8b59..ec8f60ae5 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestBan.cs @@ -3,9 +3,18 @@ using Model = Discord.API.Ban; namespace Discord.Rest { + /// + /// Represents a REST-based ban object. + /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestBan : IBan { + /// + /// Gets the banned user. + /// + /// + /// A generic object that was banned. + /// public RestUser User { get; } /// public string Reason { get; } @@ -20,6 +29,12 @@ namespace Discord.Rest return new RestBan(RestUser.Create(client, model.User), model.Reason); } + /// + /// Gets the name of the banned user. + /// + /// + /// A string containing the name of the user that was banned. + /// public override string ToString() => User.ToString(); private string DebuggerDisplay => $"{User}: {Reason}"; From cb57ada8d9f4a8a8b787ba6990afb08ea81a89a1 Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Sat, 26 May 2018 21:03:07 +0800 Subject: [PATCH 183/183] Add partial documentation for audit log objects --- .../Entities/AuditLogs/IAuditLogEntry.cs | 2 +- .../AuditLogs/DataTypes/BanAuditLogData.cs | 5 +- .../DataTypes/ChannelCreateAuditLogData.cs | 2 +- .../DataTypes/ChannelDeleteAuditLogData.cs | 2 +- .../AuditLogs/DataTypes/ChannelInfo.cs | 2 +- .../DataTypes/ChannelUpdateAuditLogData.cs | 2 +- .../DataTypes/EmoteCreateAuditLogData.cs | 2 +- .../DataTypes/EmoteDeleteAuditLogData.cs | 2 +- .../DataTypes/EmoteUpdateAuditLogData.cs | 2 +- .../Entities/AuditLogs/DataTypes/GuildInfo.cs | 4 +- .../DataTypes/GuildUpdateAuditLogData.cs | 17 ++++++- .../DataTypes/InviteCreateAuditLogData.cs | 50 ++++++++++++++++++- .../DataTypes/InviteDeleteAuditLogData.cs | 50 ++++++++++++++++++- .../AuditLogs/DataTypes/InviteInfo.cs | 38 ++++++++++++++ .../DataTypes/MemberRoleAuditLogData.cs | 1 - .../DataTypes/MemberUpdateAuditLogData.cs | 3 +- .../DataTypes/OverwriteDeleteAuditLogData.cs | 6 --- .../Entities/AuditLogs/DataTypes/RoleInfo.cs | 30 +++++------ .../DataTypes/WebhookDeleteAuditLogData.cs | 4 -- .../DataTypes/WebhookUpdateAuditLogData.cs | 4 -- .../Entities/AuditLogs/RestAuditLogEntry.cs | 5 +- 21 files changed, 186 insertions(+), 47 deletions(-) diff --git a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs index c4a6be702..d8f7f1a9f 100644 --- a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs +++ b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs @@ -1,7 +1,7 @@ namespace Discord { /// - /// Represents an entry in an audit log. + /// Represents a generic audit log entry. /// public interface IAuditLogEntry : IEntity { diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs index b249486aa..51a859376 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs @@ -6,7 +6,7 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { /// - /// Represents an audit log data for a ban action. + /// Represents a piece of audit log data related to a ban. /// public class BanAuditLogData : IAuditLogData { @@ -24,6 +24,9 @@ namespace Discord.Rest /// /// Gets the user that was banned. /// + /// + /// A generic object representing the banned user. + /// public IUser Target { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs index 6539519a1..451aac373 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs @@ -7,7 +7,7 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { /// - /// Represents an audit log data for a channel creation. + /// Represents a piece of audit log data related to a channel creation. /// public class ChannelCreateAuditLogData : IAuditLogData { diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs index 260de5509..7278b7b75 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs @@ -7,7 +7,7 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { /// - /// Represents an audit log data for a channel deletion. + /// Represents a piece of audit log data related to a channel deletion. /// public class ChannelDeleteAuditLogData : IAuditLogData { diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs index 753e1d3a5..82b129656 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelInfo.cs @@ -1,7 +1,7 @@ namespace Discord.Rest { /// - /// Represents information for a channel. + /// Represents information for a channel. /// public struct ChannelInfo { diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs index 4e88a6f00..8e990f7c7 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs @@ -6,7 +6,7 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { /// - /// Represents an audit log data for a channel update. + /// Represents a piece of audit log data related to a channel update. /// public class ChannelUpdateAuditLogData : IAuditLogData { diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs index 1e490b42d..a114548cc 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs @@ -6,7 +6,7 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { /// - /// Represents an audit log data for an emoji creation. + /// Represents a piece of audit log data related to an emoji creation. /// public class EmoteCreateAuditLogData : IAuditLogData { diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs index 2ce6acfbf..e522b0ee2 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs @@ -6,7 +6,7 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { /// - /// Represents an audit log data for an emoji deletion. + /// Represents a piece of audit log data related to an emoji deletion. /// public class EmoteDeleteAuditLogData : IAuditLogData { diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs index 506797707..e8cd76513 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs @@ -6,7 +6,7 @@ using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { /// - /// Represents an audit log data for an emoji update. + /// Represents a piece of audit log data related to an emoji update. /// public class EmoteUpdateAuditLogData : IAuditLogData { diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs index 4e830240a..057930c82 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildInfo.cs @@ -57,7 +57,7 @@ namespace Discord.Rest /// Gets the ID of this guild's icon. /// /// - /// An identifier for the splash image; null if none is set. + /// A string containing the identifier for the splash image; null if none is set. /// public string IconHash { get; } /// @@ -71,7 +71,7 @@ namespace Discord.Rest /// Gets the owner of this guild. /// /// - /// An object representing the owner of this guild. + /// A generic object representing the owner of this guild. /// public IUser Owner { get; } /// diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs index 08550ed7a..b4ffd3bf1 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Represents a piece of audit log data related to a guild update. + /// public class GuildUpdateAuditLogData : IAuditLogData { private GuildUpdateAuditLogData(GuildInfo before, GuildInfo after) @@ -73,7 +76,19 @@ namespace Discord.Rest return new GuildUpdateAuditLogData(before, after); } + /// + /// Gets the guild information before the changes. + /// + /// + /// An information object containing the original guild information before the changes were made. + /// public GuildInfo Before { get; } + /// + /// Gets the guild information after the changes. + /// + /// + /// An information object containing the guild information after the changes were made. + /// public GuildInfo After { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs index 292715420..c072686f5 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Represents a piece of audit log data related to an invite creation. + /// public class InviteCreateAuditLogData : IAuditLogData { private InviteCreateAuditLogData(int maxAge, string code, bool temporary, IUser inviter, ulong channelId, int uses, int maxUses) @@ -44,12 +47,57 @@ namespace Discord.Rest return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); } + /// + /// Gets the time (in seconds) until the invite expires. + /// + /// + /// An representing the time in seconds until this invite expires. + /// public int MaxAge { get; } + /// + /// Gets the unique identifier for this invite. + /// + /// + /// A string containing the invite code (e.g. FTqNnyS). + /// public string Code { get; } + /// + /// Determines whether the invite is a temporary one (i.e. whether the invite will be removed from the guild + /// when the user logs off). + /// + /// + /// true if users accepting this invite will be removed from the guild when they log off; otherwise + /// false. + /// public bool Temporary { get; } + /// + /// Gets the user that created this invite. + /// + /// + /// A generic that created this invite. + /// public IUser Creator { get; } + /// + /// Gets the ID of the channel this invite is linked to. + /// + /// + /// An representing the channel snowflake identifier that the invite points to. + /// public ulong ChannelId { get; } + /// + /// Gets the number of times this invite has been used. + /// + /// + /// An representing the number of times this invite has been used. + /// public int Uses { get; } + /// + /// Gets the max number of uses this invite may have. + /// + /// + /// An representing the number of uses this invite may be accepted until it is removed + /// from the guild; null if none is set. + /// public int MaxUses { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs index 1dc6d518b..ef5a26c11 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Represents a piece of audit log data related to an invite removal. + /// public class InviteDeleteAuditLogData : IAuditLogData { private InviteDeleteAuditLogData(int maxAge, string code, bool temporary, IUser inviter, ulong channelId, int uses, int maxUses) @@ -44,12 +47,57 @@ namespace Discord.Rest return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses); } + /// + /// Gets the time (in seconds) until the invite expires. + /// + /// + /// An representing the time in seconds until this invite expires. + /// public int MaxAge { get; } + /// + /// Gets the unique identifier for this invite. + /// + /// + /// A string containing the invite code (e.g. FTqNnyS). + /// public string Code { get; } + /// + /// Determines whether the invite is a temporary one (i.e. whether the invite will be removed from the guild + /// when the user logs off). + /// + /// + /// true if users accepting this invite will be removed from the guild when they log off; otherwise + /// false. + /// public bool Temporary { get; } + /// + /// Gets the user that created this invite. + /// + /// + /// A generic that created this invite. + /// public IUser Creator { get; } + /// + /// Gets the ID of the channel this invite is linked to. + /// + /// + /// An representing the channel snowflake identifier that the invite points to. + /// public ulong ChannelId { get; } + /// + /// Gets the number of times this invite has been used. + /// + /// + /// An representing the number of times this invite has been used. + /// public int Uses { get; } + /// + /// Gets the max number of uses this invite may have. + /// + /// + /// An representing the number of uses this invite may be accepted until it is removed + /// from the guild; null if none is set. + /// public int MaxUses { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs index c9840f6cc..54026dbf3 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteInfo.cs @@ -1,5 +1,8 @@ namespace Discord.Rest { + /// + /// Represents information for an invite. + /// public struct InviteInfo { internal InviteInfo(int? maxAge, string code, bool? temporary, ulong? channelId, int? maxUses) @@ -11,10 +14,45 @@ namespace Discord.Rest MaxUses = maxUses; } + /// + /// Gets the time (in seconds) until the invite expires. + /// + /// + /// An representing the time in seconds until this invite expires; null if this + /// invite never expires or not specified. + /// public int? MaxAge { get; } + /// + /// Gets the unique identifier for this invite. + /// + /// + /// A string containing the invite code (e.g. FTqNnyS). + /// public string Code { get; } + /// + /// Determines whether the invite is a temporary one (i.e. whether the invite will be removed from the guild + /// when the user logs off). + /// + /// + /// true if users accepting this invite will be removed from the guild when they log off, + /// false if not; null if not specified. + /// public bool? Temporary { get; } + /// + /// Gets the ID of the channel this invite is linked to. + /// + /// + /// An representing the channel snowflake identifier that the invite points to; + /// null if not specified. + /// public ulong? ChannelId { get; } + /// + /// Gets the max number of uses this invite may have. + /// + /// + /// An representing the number of uses this invite may be accepted until it is removed + /// from the guild; null if none is specified. + /// public int? MaxUses { get; } } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs index b0f0a1fe1..e06c013ed 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs index 38f078848..7d3d3dba0 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs @@ -1,8 +1,7 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; -using ChangeModel = Discord.API.AuditLogChange; namespace Discord.Rest { diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs index 445c2e302..dc2f4a6f0 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs @@ -1,13 +1,7 @@ -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; -using ChangeModel = Discord.API.AuditLogChange; -using OptionModel = Discord.API.AuditLogOptions; namespace Discord.Rest { diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleInfo.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleInfo.cs index 2208990e6..a5c83c5eb 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleInfo.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleInfo.cs @@ -1,21 +1,21 @@ namespace Discord.Rest { public struct RoleInfo + { + internal RoleInfo(Color? color, bool? mentionable, bool? hoist, string name, + GuildPermissions? permissions) { - internal RoleInfo(Color? color, bool? mentionable, bool? hoist, string name, - GuildPermissions? permissions) - { - Color = color; - Mentionable = mentionable; - Hoist = hoist; - Name = name; - Permissions = permissions; - } - - public Color? Color { get; } - public bool? Mentionable { get; } - public bool? Hoist { get; } - public string Name { get; } - public GuildPermissions? Permissions { get; } + Color = color; + Mentionable = mentionable; + Hoist = hoist; + Name = name; + Permissions = permissions; } + + public Color? Color { get; } + public bool? Mentionable { get; } + public bool? Hoist { get; } + public string Name { get; } + public GuildPermissions? Permissions { get; } + } } diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs index 4133d5dff..2d536d868 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs index 54da42a8b..7db7aee36 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs b/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs index 9e30a5014..0cf616973 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs @@ -1,10 +1,13 @@ -using System.Linq; +using System.Linq; using Model = Discord.API.AuditLog; using EntryModel = Discord.API.AuditLogEntry; namespace Discord.Rest { + /// + /// Represents a REST-based audit log entry. + /// public class RestAuditLogEntry : RestEntity, IAuditLogEntry { private RestAuditLogEntry(BaseDiscordClient discord, Model fullLog, EntryModel model, IUser user)