diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..1d9c17c --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,10 @@ + +############### +# folder # +############### +/**/DROP/ +/**/TEMP/ +/**/packages/ +/**/bin/ +/**/obj/ +_site \ No newline at end of file diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..74290e5 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# Contributing to Docs + +First of all, thank you for your interest in contributing to our +documentation work. We really appreciate it! That being said, +there are several guidelines you should attempt to follow when adding +to/changing the documentation. + +## General Guidelines + +* Keep code samples in each section's `samples` folder +* When referencing an object in the API, link to its page in the + API documentation +* Documentation should be written in an FAQ/Wiki-style format +* 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 `foxbot#0282` on +Discord. + +## XML Docstrings Guidelines + +* When using the `` tag, use concise verbs. For example: + +```cs +/// Gets or sets the guild user in this object. +public IGuildUser GuildUser { get; set; } +``` + +* The `` tag should not be more than 3 lines long. Consider +simplifying the terms or using the `` tag instead. +* When using the `` tag, put the code sample within the +`src/Discord.Net.Examples` project under the corresponding path of +the object and surround the code with a `#region` tag. +* If the remarks you are looking to write are too long, consider +writing a shorter version in the XML docs while keeping the longer +version in the `overwrites` folder using the DocFX overwrites syntax. + * You may find an example of this in the samples provided within + the folder. + +## Docs Guide Guidelines + +* Use a ruler set at 70 characters (use the docs workspace provided +if you are using Visual Studio Code) +* Links should use long syntax +* Pages should be short and concise, not broad and long + +Example of long link syntax: + +```md +Please consult the [API Documentation] for more information. + +[API Documentation]: xref:System.String +``` + +## Recommended Reads + +* [Microsoft Docs](https://docs.microsoft.com) +* [Flask Docs](https://flask.pocoo.org/docs/1.0/) +* [DocFX Manual](https://dotnet.github.io/docfx/) +* [Sandcastle XML Guide](http://ewsoftware.github.io/XMLCommentsGuide) \ No newline at end of file diff --git a/docs/Discord.Net.Docs.code-workspace b/docs/Discord.Net.Docs.code-workspace new file mode 100644 index 0000000..d9f4428 --- /dev/null +++ b/docs/Discord.Net.Docs.code-workspace @@ -0,0 +1,21 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "editor.rulers": [ + 70 + ], + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "obj/": true, + "_site/": true, + } + } +} \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..20e41f6 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,17 @@ +# 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]. + +> [!IMPORTANT] +> You must use DocFX version **2.76.0** for everything to work correctly. + +1. Navigate to the root of the repository. +2. Build the docs using `docfx docs/docfx.json`. Add the `--serve` + parameter to preview the site locally. + +Please note that if you intend to target a specific version, ensure +that you have the correct version checked out. + +[docfx-main]: https://dotnet.github.io/docfx/ +[docfx-installing]: https://dotnet.github.io/docfx/index.html diff --git a/docs/_overwrites/Commands/CommandException.Overwrite.md b/docs/_overwrites/Commands/CommandException.Overwrite.md new file mode 100644 index 0000000..166a011 --- /dev/null +++ b/docs/_overwrites/Commands/CommandException.Overwrite.md @@ -0,0 +1,31 @@ +--- +uid: Discord.Commands.CommandException +remarks: *content +--- + +This @System.Exception class is typically used when diagnosing +an error thrown during the execution of a command. You will find the +thrown exception passed into +[LogMessage.Exception](xref:Discord.LogMessage.Exception), which is +sent to your [CommandService.Log](xref:Discord.Commands.CommandService.Log) +event handler. + +--- +uid: Discord.Commands.CommandException +example: [*content] +--- + +You may use this information to handle runtime exceptions after +execution. Below is an example of how you may use this: + +```cs +public Task LogHandlerAsync(LogMessage logMessage) +{ + // Note that this casting method requires C#7 and up. + if (logMessage?.Exception is CommandException cmdEx) + { + Console.WriteLine($"{cmdEx.GetBaseException().GetType()} was thrown while executing {cmdEx.Command.Aliases.First()} in {cmdEx.Context.Channel} by {cmdEx.Context.User}."); + } + return Task.CompletedTask; +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md b/docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md new file mode 100644 index 0000000..d47980d --- /dev/null +++ b/docs/_overwrites/Commands/DontAutoLoadAttribute.Overwrite.md @@ -0,0 +1,22 @@ +--- +uid: Discord.Commands.DontAutoLoadAttribute +remarks: *content +--- + +The attribute can be applied to a public class that inherits +@Discord.Commands.ModuleBase. By applying this attribute, +@Discord.Commands.CommandService.AddModulesAsync* will not discover and +add the marked module to the CommandService. + +--- +uid: Discord.Commands.DontAutoLoadAttribute +example: [*content] +--- + +```cs +[DontAutoLoad] +public class MyModule : ModuleBase +{ + // ... +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md b/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md new file mode 100644 index 0000000..950d299 --- /dev/null +++ b/docs/_overwrites/Commands/DontInjectAttribute.Overwrite.md @@ -0,0 +1,27 @@ +--- +uid: Discord.Commands.DontInjectAttribute +remarks: *content +--- + +The attribute can be applied to a public settable property inside a +@Discord.Commands.ModuleBase based class. By applying this attribute, +the marked property will not be automatically injected of the +dependency. See @Guides.Commands.DI to learn more. + +--- +uid: Discord.Commands.DontInjectAttribute +example: [*content] +--- + +```cs +public class MyModule : ModuleBase +{ + [DontInject] + public MyService MyService { get; set; } + + public MyModule() + { + MyService = new MyService(); + } +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Commands/ICommandContext.Inclusion.md b/docs/_overwrites/Commands/ICommandContext.Inclusion.md new file mode 100644 index 0000000..a5eaeea --- /dev/null +++ b/docs/_overwrites/Commands/ICommandContext.Inclusion.md @@ -0,0 +1,5 @@ +An example of how this class is used the command system can be seen +below: + +[!code[Sample module](../../guides/text_commands/samples/intro/empty-module.cs)] +[!code[Command handler](../../guides/text_commands/samples/intro/command_handler.cs)] diff --git a/docs/_overwrites/Commands/ICommandContext.Overwrite.md b/docs/_overwrites/Commands/ICommandContext.Overwrite.md new file mode 100644 index 0000000..d9e50b4 --- /dev/null +++ b/docs/_overwrites/Commands/ICommandContext.Overwrite.md @@ -0,0 +1,27 @@ +--- +uid: Discord.Commands.ICommandContext +example: [*content] +--- + +[!include[Example Section](ICommandContext.Inclusion.md)] + +--- +uid: Discord.Commands.CommandContext +example: [*content] +--- + +[!include[Example Section](ICommandContext.Inclusion.md)] + +--- +uid: Discord.Commands.SocketCommandContext +example: [*content] +--- + +[!include[Example Section](ICommandContext.Inclusion.md)] + +--- +uid: Discord.Commands.ShardCommandContext +example: [*content] +--- + +[!include[Example Section](ICommandContext.Inclusion.md)] \ No newline at end of file diff --git a/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md b/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md new file mode 100644 index 0000000..75b9f93 --- /dev/null +++ b/docs/_overwrites/Commands/PreconditionAttribute.Overwrites.md @@ -0,0 +1,103 @@ +--- +uid: Discord.Commands.PreconditionAttribute +remarks: *content +--- + +This precondition attribute can be applied on module-level or +method-level for a command. + +[!include[Additional Remarks](PreconditionAttribute.Remarks.Inclusion.md)] + +--- +uid: Discord.Commands.ParameterPreconditionAttribute +remarks: *content +--- + +This precondition attribute can be applied on parameter-level for a +command. + +[!include[Additional Remarks](PreconditionAttribute.Remarks.Inclusion.md)] + +--- +uid: Discord.Commands.PreconditionAttribute +example: [*content] +--- + +The following example creates a precondition to see if the user has +sufficient role required to access the command. + +```cs +public class RequireRoleAttribute : PreconditionAttribute +{ + private readonly ulong _roleId; + + public RequireRoleAttribute(ulong roleId) + { + _roleId = roleId; + } + + public override async Task CheckPermissionsAsync(ICommandContext context, + CommandInfo command, IServiceProvider services) + { + var guildUser = context.User as IGuildUser; + if (guildUser == null) + return PreconditionResult.FromError("This command cannot be executed outside of a guild."); + + var guild = guildUser.Guild; + if (guild.Roles.All(r => r.Id != _roleId)) + return PreconditionResult.FromError( + $"The guild does not have the role ({_roleId}) required to access this command."); + + return guildUser.RoleIds.Any(rId => rId == _roleId) + ? PreconditionResult.FromSuccess() + : PreconditionResult.FromError("You do not have the sufficient role required to access this command."); + } +} +``` + +--- +uid: Discord.Commands.ParameterPreconditionAttribute +example: [*content] +--- + +The following example creates a precondition on a parameter-level to +see if the targeted user has a lower hierarchy than the user who +executed the command. + +```cs +public class RequireHierarchyAttribute : ParameterPreconditionAttribute +{ + public override async Task CheckPermissionsAsync(ICommandContext context, + ParameterInfo parameter, object value, IServiceProvider services) + { + // Hierarchy is only available under the socket variant of the user. + if (!(context.User is SocketGuildUser guildUser)) + return PreconditionResult.FromError("This command cannot be used outside of a guild."); + + SocketGuildUser targetUser; + switch (value) + { + case SocketGuildUser targetGuildUser: + targetUser = targetGuildUser; + break; + case ulong userId: + targetUser = await context.Guild.GetUserAsync(userId).ConfigureAwait(false) as SocketGuildUser; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + if (targetUser == null) + return PreconditionResult.FromError("Target user not found."); + + if (guildUser.Hierarchy < targetUser.Hierarchy) + return PreconditionResult.FromError("You cannot target anyone else whose roles are higher than yours."); + + var currentUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false) as SocketGuildUser; + if (currentUser?.Hierarchy < targetUser.Hierarchy) + return PreconditionResult.FromError("The bot's role is lower than the targeted user."); + + return PreconditionResult.FromSuccess(); + } +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Commands/PreconditionAttribute.Remarks.Inclusion.md b/docs/_overwrites/Commands/PreconditionAttribute.Remarks.Inclusion.md new file mode 100644 index 0000000..daa0c33 --- /dev/null +++ b/docs/_overwrites/Commands/PreconditionAttribute.Remarks.Inclusion.md @@ -0,0 +1,6 @@ +A "precondition" in the command system is used to determine if a +condition is met before entering the command task. Using a +precondition 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/DiscordComparers.Overwrites.md b/docs/_overwrites/Common/DiscordComparers.Overwrites.md new file mode 100644 index 0000000..cbff7cf --- /dev/null +++ b/docs/_overwrites/Common/DiscordComparers.Overwrites.md @@ -0,0 +1,29 @@ +--- +uid: Discord.DiscordComparers.ChannelComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare channels. + +--- +uid: Discord.DiscordComparers.GuildComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare guilds. + +--- +uid: Discord.DiscordComparers.MessageComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare messages. + +--- +uid: Discord.DiscordComparers.RoleComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare roles. + +--- +uid: Discord.DiscordComparers.UserComparer +summary: *content +--- +Gets an [IEqualityComparer](xref:System.Collections.Generic.IEqualityComparer`1)<> to be used to compare users. diff --git a/docs/_overwrites/Common/EmbedBuilder.Overwrites.md b/docs/_overwrites/Common/EmbedBuilder.Overwrites.md new file mode 100644 index 0000000..85c292d --- /dev/null +++ b/docs/_overwrites/Common/EmbedBuilder.Overwrites.md @@ -0,0 +1,69 @@ +--- +uid: Discord.EmbedBuilder +seealso: + - linkId: Discord.EmbedFooterBuilder + - linkId: Discord.EmbedAuthorBuilder + - linkId: Discord.EmbedFieldBuilder +remarks: *content +--- + +This builder class is used to build an @Discord.Embed (rich embed) +object that will be ready to be sent via @Discord.IMessageChannel.SendMessageAsync* +after @Discord.EmbedBuilder.Build* is called. + +--- +uid: Discord.EmbedBuilder +example: [*content] +--- + +#### Basic Usage + +The example below builds an embed and sends it to the chat using the +command system. + +```cs +[Command("embed")] +public async Task SendRichEmbedAsync() +{ + var embed = new EmbedBuilder + { + // Embed property can be set within object initializer + Title = "Hello world!", + Description = "I am a description set by initializer." + }; + // Or with methods + 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.") + .WithColor(Color.Blue) + .WithTitle("I overwrote \"Hello world!\"") + .WithDescription("I am a description.") + .WithUrl("https://example.com") + .WithCurrentTimestamp(); + + //Your embed needs to be built before it is able to be sent + await ReplyAsync(embed: embed.Build()); +} +``` + +![Embed Example](images/embed-example.png) + +#### Usage with Local Images + +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); +} +``` diff --git a/docs/_overwrites/Common/EmbedObjectBuilder.Inclusion.md b/docs/_overwrites/Common/EmbedObjectBuilder.Inclusion.md new file mode 100644 index 0000000..a9d3539 --- /dev/null +++ b/docs/_overwrites/Common/EmbedObjectBuilder.Inclusion.md @@ -0,0 +1,25 @@ +The example will build a rich embed with an author field, a footer +field, and 2 normal fields using an @Discord.EmbedBuilder: + +```cs +var exampleAuthor = new EmbedAuthorBuilder() + .WithName("I am a bot") + .WithIconUrl("https://discord.com/assets/e05ead6e6ebc08df9291738d0aa6986d.png"); +var exampleFooter = new EmbedFooterBuilder() + .WithText("I am a nice footer") + .WithIconUrl("https://discord.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(); +``` diff --git a/docs/_overwrites/Common/EmbedObjectBuilder.Overwrites.md b/docs/_overwrites/Common/EmbedObjectBuilder.Overwrites.md new file mode 100644 index 0000000..c633c29 --- /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 0000000..cf93c7e --- /dev/null +++ b/docs/_overwrites/Common/IEmote.Inclusion.md @@ -0,0 +1,26 @@ +The sample below sends a message and adds an @Discord.Emoji and a custom +@Discord.Emote to the message. + +```cs +public async Task SendAndReactAsync(ISocketMessageChannel channel) +{ + var message = await channel.SendMessageAsync("I am a message."); + + // Creates a Unicode-based emoji based on the Unicode string. + // This is effectively the same as new Emoji("💕"). + var heartEmoji = new Emoji("\U0001f495"); + // Reacts to the message with the Emoji. + await message.AddReactionAsync(heartEmoji); + + // Parses a custom emote based on the provided Discord emote format. + // Please note that this does not guarantee the existence of + // the emote. + var emote = Emote.Parse("<:thonkang:282745590985523200>"); + // Reacts to the message with the Emote. + await message.AddReactionAsync(emote); +} +``` + +#### Result + +![React Example](images/react-example.png) \ No newline at end of file diff --git a/docs/_overwrites/Common/IEmote.Overwrites.md b/docs/_overwrites/Common/IEmote.Overwrites.md new file mode 100644 index 0000000..034533d --- /dev/null +++ b/docs/_overwrites/Common/IEmote.Overwrites.md @@ -0,0 +1,81 @@ +--- +uid: Discord.IEmote +seealso: + - linkId: Discord.Emote + - linkId: Discord.Emoji + - linkId: Discord.GuildEmote + - linkId: Discord.IUserMessage +remarks: *content +--- + +This interface is often used with reactions. It can represent an +unicode-based @Discord.Emoji, or a custom @Discord.Emote. + +--- +uid: Discord.Emote +seealso: + - linkId: Discord.IEmote + - linkId: Discord.GuildEmote + - linkId: Discord.Emoji + - linkId: Discord.IUserMessage +remarks: *content +--- + +> [!NOTE] +> A valid @Discord.Emote format is `<:emoteName:emoteId>`. This can be +> obtained by escaping with a `\` in front of the emote using the +> Discord chat client. + +This class represents a custom emoji. This type of emoji can be +created via the @Discord.Emote.Parse* or @Discord.Emote.TryParse* +method. + +--- +uid: Discord.Emoji +seealso: + - linkId: Discord.Emote + - linkId: Discord.GuildEmote + - linkId: Discord.Emoji + - linkId: Discord.IUserMessage +remarks: *content +--- + +> [!NOTE] +> A valid @Discord.Emoji format is Unicode-based. This means only +> something like `🙃` or `\U0001f643` would work, instead of +> `:upside_down:`. +> +> A Unicode-based emoji can be obtained by escaping with a `\` in +> front of the emote using the Discord chat client or by looking up on +> [Emojipedia](https://emojipedia.org). + +This class represents a standard Unicode-based emoji. This type of emoji +can be created by passing the Unicode into the constructor. + +--- +uid: Discord.IEmote +example: [*content] +--- + +[!include[Example Section](IEmote.Inclusion.md)] + +--- +uid: Discord.Emoji +example: [*content] +--- + +[!include[Example Section](IEmote.Inclusion.md)] + +--- +uid: Discord.Emote +example: [*content] +--- + +[!include[Example Section](IEmote.Inclusion.md)] + +--- +uid: Discord.GuildEmote +example: [*content] +--- + +[!include[Example Section](IEmote.Inclusion.md)] \ No newline at end of file diff --git a/docs/_overwrites/Common/ObjectProperties.Overwrites.md b/docs/_overwrites/Common/ObjectProperties.Overwrites.md new file mode 100644 index 0000000..e9c365d --- /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/docs/_overwrites/Common/OverrideTypeReaderAttribute.Overwrites.md b/docs/_overwrites/Common/OverrideTypeReaderAttribute.Overwrites.md new file mode 100644 index 0000000..29b547e --- /dev/null +++ b/docs/_overwrites/Common/OverrideTypeReaderAttribute.Overwrites.md @@ -0,0 +1,24 @@ +--- +uid: Discord.Commands.OverrideTypeReaderAttribute +remarks: *content +--- + +This attribute is used to override a command parameter's type reading +behaviour. This may be useful when you have multiple custom +@Discord.Commands.TypeReader and would like to specify one. + +--- +uid: Discord.Commands.OverrideTypeReaderAttribute +examples: [*content] +--- + +The following example will override the @Discord.Commands.TypeReader +of @Discord.IUser to `MyUserTypeReader`. + +```cs +public async Task PrintUserAsync( + [OverrideTypeReader(typeof(MyUserTypeReader))] IUser user) +{ + //... +} +``` \ No newline at end of file diff --git a/docs/_overwrites/Common/images/embed-example.png b/docs/_overwrites/Common/images/embed-example.png new file mode 100644 index 0000000..f23fb4d Binary files /dev/null and b/docs/_overwrites/Common/images/embed-example.png differ diff --git a/docs/_overwrites/Common/images/react-example.png b/docs/_overwrites/Common/images/react-example.png new file mode 100644 index 0000000..822857d Binary files /dev/null and b/docs/_overwrites/Common/images/react-example.png differ diff --git a/docs/_template/description-generator/plugins/DocFX.Plugin.DescriptionGenerator.dll b/docs/_template/description-generator/plugins/DocFX.Plugin.DescriptionGenerator.dll new file mode 100644 index 0000000..0eeab1b Binary files /dev/null and b/docs/_template/description-generator/plugins/DocFX.Plugin.DescriptionGenerator.dll differ diff --git a/docs/_template/description-generator/plugins/LICENSE b/docs/_template/description-generator/plugins/LICENSE new file mode 100644 index 0000000..d74703f --- /dev/null +++ b/docs/_template/description-generator/plugins/LICENSE @@ -0,0 +1,29 @@ +MIT License + +Copyright (c) 2018 Still Hsu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +============================================================================== + +Humanizer (https://github.com/Humanizr/Humanizer) +The MIT License (MIT) +Copyright (c) .NET Foundation and Contributors + +============================================================================== \ No newline at end of file diff --git a/docs/_template/material/public/main.css b/docs/_template/material/public/main.css new file mode 100644 index 0000000..3cb9283 --- /dev/null +++ b/docs/_template/material/public/main.css @@ -0,0 +1,239 @@ +@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;400;700&display=swap'); + +:root { + --bs-font-sans-serif: 'Roboto'; + --bs-border-radius: 10px; + + --border-radius-button: 40px; + --card-box-shadow: 0 1px 2px 0 #3d41440f, 0 1px 3px 1px #3d414429; + + --material-yellow-light: #e6dfbf; + --material-yellow-dark: #5a5338; + + --material-blue-light: #c4d9f1; + --material-blue-dark: #383e5a; + + --material-red-light: #f1c4c4; + --material-red-dark: #5a3838; + + --material-warning-header: #f57f171a; + --material-warning-background: #f6e8bd; + --material-warning-background-dark: #57502c; + + --material-info-header: #1976d21a; + --material-info-background: #e3f2fd; + --material-info-background-dark: #2c4557; + + --material-danger-header: #d32f2f1a; + --material-danger-background: #ffebee; + --material-danger-background-dark: #572c2c; +} + +/* HEADINGS */ + +h1 { + font-weight: 600; + font-size: 32px; +} + +h2 { + font-weight: 600; + font-size: 24px; + line-height: 1.8; +} + +h3 { + font-weight: 600; + font-size: 20px; + line-height: 1.8; +} + +h5 { + font-size: 14px; + padding: 10px 0px; +} + +article h2, +article h3, +article h4 { + margin-top: 15px; + margin-bottom: 15px; +} + +/* MAKES PROPERTIES BE SEPARATED CLEARLY */ +article h3 { + padding-bottom: 8px; + border-bottom: 2px solid #ddd; +} + +/** IMAGES **/ +img { + border-radius: var(--bs-border-radius); + box-shadow: var(--card-box-shadow); +} + +/** NAVBAR **/ +.navbar-brand > img { + box-shadow: none; + color: var(--bs-nav-link-color); + margin-right: var(--bs-navbar-brand-margin-end); +} + +[data-bs-theme='light'] nav.navbar { + background-color: var(--bs-primary-bg-subtle); +} + +[data-bs-theme='dark'] nav.navbar { + background-color: var(--bs-tertiary-bg); +} + +.navbar-nav > li > a { + border-radius: var(--border-radius-button); + transition: 200ms; +} + +.navbar-nav a.nav-link:focus, +.navbar-nav a.nav-link:hover { + background-color: var(--bs-primary-border-subtle); +} + +.navbar-nav .nav-link.active, +.navbar-nav .nav-link.show { + color: var(--bs-link-hover-color); +} + +/** SEARCH AND FILTER **/ +input.form-control { + border-radius: var(--border-radius-button); +} + +form.filter { + margin: 0.3rem; +} + +/** ALERTS **/ +.alert { + padding: 0; + border: none; + box-shadow: var(--card-box-shadow); +} + +.alert > p { + padding: 0.2rem 0.7rem 0.7rem 1rem; +} + +.alert > ul { + margin-bottom: 0; + padding: 5px 40px; +} + +.alert > h5 { + padding: 0.5rem 0.7rem 0.7rem 1rem; + border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0; + font-weight: bold; + text-transform: capitalize; +} + +.alert-info { + color: var(--material-blue-dark); + background-color: var(--material-info-background); +} + +[data-bs-theme='dark'] .alert-info { + color: var(--material-blue-light); + background-color: var(--material-info-background-dark); +} + +.alert-info > h5 { + background-color: var(--material-info-header); +} + +.alert-warning { + color: var(--material-yellow-dark); + background-color: var(--material-warning-background); +} + +[data-bs-theme='dark'] .alert-warning { + color: var(--material-yellow-light); + background-color: var(--material-warning-background-dark); +} + +.alert-warning > h5 { + background-color: var(--material-warning-header); +} + +.alert-danger { + color: var(--material-red-dark); + background-color: var(--material-danger-background); +} + +[data-bs-theme='dark'] .alert-danger { + color: var(--material-red-light); + background-color: var(--material-danger-background-dark); +} + +.alert-danger > h5 { + background-color: var(--material-danger-header); +} + +/* CODE HIGHLIGHT */ +code { + border-radius: var(--bs-border-radius); + margin: 4px 2px; + box-shadow: var(--card-box-shadow); +} + +/* MAKES PARAMETERS MORE SPACIOUS */ +:not(pre) > code { + padding: 3px; +} + +/* MAKES LIST ITEMS BE SLIGHTLY MORE SEPARATED */ +/* THIS AVOIDS CODE BLOCK OVERLAP */ +ul:not(.navbar-nav) > li:not(:last-child) { + margin-bottom: 4px; +} + +/* MAKES NAVBAR LINKS LOOK BETTER IN MOBILE */ +.navbar-expand-md .navbar-nav .nav-link { + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); +} + +/* MAKES INHERITANCE LIST READABLE */ +:is(dl.typelist.inheritedMembers, dl.typelist.extensionMethods) > dd > div::after { + content: none !important; +} + +:is(dl.typelist.inheritedMembers, dl.typelist.extensionMethods) > dd > div { + display: block !important; +} + +/* MAKES "IN THIS ARTICLE" MORE READABLE */ +.affix h5, .affix .h5 { + font-weight: normal !important; +} + +/* MAKES INDEX LOGO VISIBLE ON DIFFERENT THEMES */ +article[data-uid="Home.Landing"] img[alt="logo"] { + height: 100pt !important; + box-shadow: none; +} + +[data-bs-theme="light"] article[data-uid="Home.Landing"] img[alt="logo"] { + content: url('/marketing/logo/SVG/Combinationmark.svg') !important; +} + +[data-bs-theme="dark"] article[data-uid="Home.Landing"] img[alt="logo"] { + content: url('/marketing/logo/SVG/Combinationmark White.svg') !important; +} + +article[data-uid="Home.Landing"] img { + border-radius: 0; +} + +/* MAKES SIDEBAR LINKS A BIT MORE DISTINGUISHABLE */ +.affix ul li a:not(.link-body-emphasis) { + display: block !important; + margin-left: 8px !important; +} diff --git a/docs/_template/material/public/main.js b/docs/_template/material/public/main.js new file mode 100644 index 0000000..facbb63 --- /dev/null +++ b/docs/_template/material/public/main.js @@ -0,0 +1,72 @@ +export default +{ + iconLinks: + [ + { + icon: 'github', + href: 'https://github.com/discord-net/Discord.Net', + title: 'GitHub' + }, + { + icon: 'box-seam-fill', + href: 'https://www.nuget.org/packages/Discord.Net/', + title: 'NuGet' + }, + { + icon: 'discord', + href: 'https://discord.gg/dnet', + title: 'Discord' + } + ], + start: () => + { + // Ugly hack to improve toc filter. + let target = document.getElementById("toc"); + + if(!target) return; + + let config = { attributes: false, childList: true, subtree: true }; + let observer = new MutationObserver((list) => + { + for(const mutation of list) + { + if(mutation.type === "childList" && mutation.target == target) + { + let filter = target.getElementsByClassName("form-control")[0]; + + let filterValue = localStorage.getItem("tocFilter"); + let scrollValue = localStorage.getItem("tocScroll"); + + if(filterValue && filterValue !== "") + { + filter.value = filterValue; + + let inputEvent = new Event("input"); + filter.dispatchEvent(inputEvent); + } + + // Add event to store scroll pos. + let tocDiv = target.getElementsByClassName("flex-fill")[0]; + + tocDiv.addEventListener("scroll", (event) => + { + if (event.target.scrollTop >= 0) + { + localStorage.setItem("tocScroll", event.target.scrollTop); + } + }); + + if(scrollValue && scrollValue >= 0) + { + tocDiv.scroll(0, scrollValue); + } + + observer.disconnect(); + break; + } + } + }); + + observer.observe(target, config); + } +} diff --git a/docs/api/.gitignore b/docs/api/.gitignore new file mode 100644 index 0000000..bffdb30 --- /dev/null +++ b/docs/api/.gitignore @@ -0,0 +1,6 @@ + +############### +# temp file # +############### +*.yml +.manifest \ No newline at end of file diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..c97ecfd --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,17 @@ +--- +uid: API.Docs +--- + +# API Documentation + +This is where you will find documentation for all members and objects in Discord.Net. +This is automatically generated based on the [dev](https://github.com/discord-net/Discord.Net/tree/dev) branch. + +# 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 diff --git a/docs/docfx.json b/docs/docfx.json new file mode 100644 index 0000000..24bb29a --- /dev/null +++ b/docs/docfx.json @@ -0,0 +1,69 @@ +{ + "metadata": [ + { + "src": [ + { + "src": "../src", + "files": ["**/*.csproj"], + "exclude": ["Discord.Net.DebugTools/*.csproj"] + } + ], + "dest": "api", + "filter": "filterConfig.yml" + } + ], + "build": { + "content": [ + { + "files": ["api/**.yml", "api/index.md"] + }, + { + "files": ["toc.yml", "index.md"] + }, + { + "files": ["faq/**.md", "faq/**/toc.yml"] + }, + { + "files": ["guides/**.md", "guides/**/toc.yml"] + }, + { + "src": "../", + "files": ["CHANGELOG.md"] + } + ], + "resource": [ + { + "files": [ + "**/images/**", + "**/samples/**", + "langwordMapping.yml", + "marketing/logo/**.svg", + "marketing/logo/**.png", + "favicon.png", + "../src/Discord.Net.Examples/**.cs" + ] + } + ], + "output": "_site", + "template": [ + "default", + "modern", + "_template/material", + "_template/description-generator" + ], + "postProcessors": [ + "ExtractSearchIndex", + "DescriptionPostProcessor" + ], + "overwrite": "_overwrites/**/**.md", + "globalMetadata": { + "_appTitle": "Discord.Net Documentation", + "_appName": "Discord.Net", + "_appFooter": "Discord.Net © 2015-2024 3.14.1", + "_enableSearch": true, + "_appLogoPath": "marketing/logo/SVG/Logomark Purple.svg", + "_appFaviconPath": "favicon.png" + }, + "xref": ["https://github.com/dotnet/docfx/raw/main/.xrefmap.json"] + } +} diff --git a/docs/faq/basics/basic-operations.md b/docs/faq/basics/basic-operations.md new file mode 100644 index 0000000..dd6cbd9 --- /dev/null +++ b/docs/faq/basics/basic-operations.md @@ -0,0 +1,123 @@ +--- +uid: FAQ.Basics.BasicOp +title: Basic Operations Questions +--- + +# Basic Operations Questions + +In the following section, you will find commonly asked questions and +answers regarding basic usage of the library, as well as +language-specific tips when using this library. + +## 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/how-to/safely-cast-using-pattern-matching-is-and-as-operators + +## How do I send a message? + +> [!TIP] +> 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] +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:Guides.Entities.Glossary#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. + +# [Adding a reaction to another message](#tab/emoji-others) + +[!code-csharp[Emoji](samples/emoji-others.cs)] + +# [Adding a reaction to a sent message](#tab/emoji-self) + +[!code-csharp[Emoji](samples/emoji-self.cs)] + +--- + +[addreactionasync]: xref:Discord.IMessage.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/discord-net/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 diff --git a/docs/faq/basics/client-basics.md b/docs/faq/basics/client-basics.md new file mode 100644 index 0000000..dcc8576 --- /dev/null +++ b/docs/faq/basics/client-basics.md @@ -0,0 +1,148 @@ +--- +uid: FAQ.Basics.ClientBasics +title: Client Basics Questions +--- + +# Client Basics Questions + +In the following section, you will find commonly asked questions and +answers about common issues that you may face when utilizing the +various clients offered by the library. + +## I keep having trouble with intents! + +As Discord.NET has upgraded from Discord API v6 to API v9, +`GatewayIntents` must now be specified in the socket config, as well as on the [developer portal]. + +```cs + +// Where ever you declared your websocket client. +DiscordSocketClient _client; + +... + +var config = new DiscordSocketConfig() +{ + .. // Other config options can be presented here. + GatewayIntents = GatewayIntents.All +} + +_client = new DiscordSocketClient(config); + +``` +### Common intents: + +- AllUnprivileged: This is a group of most common intents, that do NOT require any [developer portal] intents to be enabled. +This includes intents that receive messages such as: `GatewayIntents.GuildMessages, GatewayIntents.DirectMessages` +- GuildMembers: An intent disabled by default, as you need to enable it in the [developer portal]. +- GuildPresences: Also disabled by default, this intent together with `GuildMembers` are the only intents not included in `AllUnprivileged`. +- All: All intents, it is ill advised to use this without care, as it _can_ cause a memory leak from presence. +The library will give responsive warnings if you specify unnecessary intents. + + +> [!NOTE] +> All gateway intents, their Discord API counterpart and their enum value are listed +> [HERE](xref:Discord.GatewayIntents) + +### Stacking intents: + +It is common that you require several intents together. +The example below shows how this can be done. + +```cs + +GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers | .. + +``` + +> [!NOTE] +> Further documentation on the ` | ` operator can be found +> [HERE](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/bitwise-and-shift-operators) + +[developer portal]: https://discord.com/developers/ + +## My client keeps returning 401 upon logging in! + +> [!WARNING] +> 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. +> +> 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. + +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 a token is **different** from a *client secret*. + +[TokenType]: xref:Discord.TokenType +[827]: https://github.com/discord-net/Discord.Net/issues/827 +[958]: https://github.com/discord-net/Discord.Net/issues/958 +[Discord API Terms of Service]: https://discord.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? + +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 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 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. + +[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 + +## What is a shard/sharded client, and how is it different from the `DiscordSocketClient`? +As your bot grows in popularity, it is recommended that you should section your bot off into separate processes. +The [DiscordShardedClient] is essentially a class that allows you to easily create and manage multiple [DiscordSocketClient] +instances, with each one serving a different amount of guilds. + +There are very few differences from the [DiscordSocketClient] class, and it is very straightforward +to modify your existing code to use a [DiscordShardedClient] when necessary. + +1. You can specify the total amount of shards, or shard ids, via [DiscordShardedClient]'s constructors. +If the total shards are not specified then the library will get the recommended shard count via the +[Get Gateway Bot](https://discord.com/developers/docs/topics/gateway#get-gateway-bot) route. +2. The [Connected], [Disconnected], [Ready], and [LatencyUpdated] events + are replaced with [ShardConnected], [ShardDisconnected], [ShardReady], and [ShardLatencyUpdated]. +3. Every event handler you apply/remove to the [DiscordShardedClient] is applied/removed to each shard. + If you wish to control a specific shard's events, you can access an individual shard through the `Shards` property. + +If you do not wish to use the [DiscordShardedClient] and instead reuse the same [DiscordSocketClient] code and manually shard them, +you can do so by specifying the [ShardId] for the [DiscordSocketConfig] and pass that to the [DiscordSocketClient]'s constructor. + +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient +[DiscordShardedClient]: xref:Discord.WebSocket.DiscordShardedClient +[Connected]: xref:Discord.WebSocket.DiscordSocketClient.Connected +[Disconnected]: xref:Discord.WebSocket.DiscordSocketClient.Disconnected +[LatencyUpdated]: xref:Discord.WebSocket.DiscordSocketClient.LatencyUpdated +[ShardConnected]: xref:Discord.WebSocket.DiscordShardedClient.ShardConnected +[ShardDisconnected]: xref:Discord.WebSocket.DiscordShardedClient.ShardDisconnected +[ShardReady]: xref:Discord.WebSocket.DiscordShardedClient.ShardReady +[ShardLatencyUpdated]: xref:Discord.WebSocket.DiscordShardedClient.ShardLatencyUpdated +[ShardId]: xref:Discord.WebSocket.DiscordSocketConfig.ShardId diff --git a/docs/faq/basics/dependency-injection.md b/docs/faq/basics/dependency-injection.md new file mode 100644 index 0000000..eb98c00 --- /dev/null +++ b/docs/faq/basics/dependency-injection.md @@ -0,0 +1,46 @@ +--- +uid: FAQ.Basics.DI +title: Questions about Dependency Injection +--- + +# Dependency Injection-related Questions + +In the following section, you will find common questions and answers +to utilizing dependency injection with @Discord.Commands and @Discord.Interactions, as well as +common troubleshooting steps regarding DI. + +## 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; 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. + +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 + +## Why is my Command/Interaction Service complaining about a missing dependency? + +If you encounter an error similar to `Failed to create MyModule, +dependency MyExternalDependency was not found.`, you may have +forgotten to add the external dependency to the dependency container. + +For example, if your module, `MyModule`, requests a `DatabaseService` +in its constructor, the `DatabaseService` must be present in the +[IServiceProvider] when registering `MyModule`. + +[!code-csharp[Missing Dependencies](samples/missing-dep.cs)] + +[IServiceProvider]: xref:System.IServiceProvider diff --git a/docs/faq/basics/getting-started.md b/docs/faq/basics/getting-started.md new file mode 100644 index 0000000..8315b5b --- /dev/null +++ b/docs/faq/basics/getting-started.md @@ -0,0 +1,96 @@ +--- +uid: FAQ.Basics.GetStarted +title: Getting Started +--- + +# Getting Started + +In this following section, you will find commonly asked questions and +answers about how to get started with Discord.Net, as well as basic +introduction to the Discord API ecosystem. + +## How do I add my bot to my guild? + +Inviting your bot can be done by using the OAuth2 url generator provided by the [Discord Developer Portal]. + +Permissions can be granted by selecting the `bot` scope in the scopes section. + +![Scopes](images/scopes.png) + +A permissions tab will appear below the scope selection, +from which you can pick any permissions your bot may require to function. +When invited, the role this bot is granted will include these permissions. +If you grant no permissions, no role will be created for your bot upon invitation as there is no need for one. + +![Permissions](images/permissions.png) + +When done selecting permissions, you can use the link below in your browser to invite the bot +to servers where you have the `Manage Server` permission. + +![Invite](images/link.png) + +If you are planning to play around with slash/context commands, +make sure to check the `application commands` scope before inviting your bot! + +> [!NOTE] +> You do not have to kick and reinvite your bot to update permissions/scopes later on. +> Simply reusing the invite link with provided scopes/perms will update it accordingly. + +[Discord Developer Portal]: https://discord.com/developers/applications/ + +## 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 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? + +Each user and object on Discord has its own snowflake ID generated +based on various conditions. + +![Snowflake Generation](images/snowflake.png) + +Anyone can see the ID; 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. + +There are several common ways to do this: + +1. Right click on the role either in the Server Settings + or in the user's role list (recommended). + ![Roles](images/role-copy.png) +2. Make the role mentionable and mention the role, and escape it + using the `\` character in front. +3. Inspect the roles collection within the guild via your debugger. diff --git a/docs/faq/basics/images/dev-mode.png b/docs/faq/basics/images/dev-mode.png new file mode 100644 index 0000000..2407fc5 Binary files /dev/null and b/docs/faq/basics/images/dev-mode.png differ diff --git a/docs/faq/basics/images/link.png b/docs/faq/basics/images/link.png new file mode 100644 index 0000000..dd6b520 Binary files /dev/null and b/docs/faq/basics/images/link.png differ diff --git a/docs/faq/basics/images/mention-escape.png b/docs/faq/basics/images/mention-escape.png new file mode 100644 index 0000000..9279780 Binary files /dev/null and b/docs/faq/basics/images/mention-escape.png differ diff --git a/docs/faq/basics/images/permissions.png b/docs/faq/basics/images/permissions.png new file mode 100644 index 0000000..7e9b15a Binary files /dev/null and b/docs/faq/basics/images/permissions.png differ diff --git a/docs/faq/basics/images/role-copy.png b/docs/faq/basics/images/role-copy.png new file mode 100644 index 0000000..dd92c00 Binary files /dev/null and b/docs/faq/basics/images/role-copy.png differ diff --git a/docs/faq/basics/images/scopes.png b/docs/faq/basics/images/scopes.png new file mode 100644 index 0000000..31bba27 Binary files /dev/null and b/docs/faq/basics/images/scopes.png differ diff --git a/docs/faq/basics/images/snowflake.png b/docs/faq/basics/images/snowflake.png new file mode 100644 index 0000000..816a10e Binary files /dev/null and b/docs/faq/basics/images/snowflake.png differ diff --git a/docs/faq/basics/samples/DI.cs b/docs/faq/basics/samples/DI.cs new file mode 100644 index 0000000..ce4454b --- /dev/null +++ b/docs/faq/basics/samples/DI.cs @@ -0,0 +1,28 @@ +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 the module's constructor + + // private readonly MyService _myService; + // public MyModule (MyService myService) => _myService = myService; + + [Command("string")] + public Task GetOrSetStringAsync(string input) + { + if (string.IsNullOrEmpty(_myService.MyCoolString)) _myService.MyCoolString = input; + return ReplyAsync(_myService.MyCoolString); + } +} \ No newline at end of file diff --git a/docs/faq/basics/samples/cast.cs b/docs/faq/basics/samples/cast.cs new file mode 100644 index 0000000..73ef523 --- /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-others.cs b/docs/faq/basics/samples/emoji-others.cs new file mode 100644 index 0000000..dd3e631 --- /dev/null +++ b/docs/faq/basics/samples/emoji-others.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/basics/samples/emoji-self.cs b/docs/faq/basics/samples/emoji-self.cs new file mode 100644 index 0000000..cd4cff1 --- /dev/null +++ b/docs/faq/basics/samples/emoji-self.cs @@ -0,0 +1,17 @@ +// capture the message you're sending in a variable +var msg = await channel.SendMessageAsync("This will have reactions added."); + +// 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 msg.AddReactionAsync(emoji); +await msg.AddReactionAsync(emote); \ No newline at end of file diff --git a/docs/faq/basics/samples/missing-dep.cs b/docs/faq/basics/samples/missing-dep.cs new file mode 100644 index 0000000..4fd034e --- /dev/null +++ b/docs/faq/basics/samples/missing-dep.cs @@ -0,0 +1,32 @@ +public class MyModule : ModuleBase +{ + private readonly DatabaseService _dbService; + public MyModule(DatabaseService dbService) + => _dbService = dbService; +} +public class CommandHandler +{ + private readonly CommandService _commands; + private readonly IServiceProvider _services; + public CommandHandler(DiscordSocketClient client) + { + _services = new ServiceCollection() + .AddSingleton() + .AddSingleton(client) + // We are missing DatabaseService! + .BuildServiceProvider(); + } + public async Task RegisterCommandsAsync() + { + // ... + // The method fails here because DatabaseService is a required + // dependency and cannot be resolved by the dependency + // injection service at runtime since the service is not + // registered in this instance of _services. + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + // ... + + // The same approach applies to the interaction service. + // Make sure to resolve these issues! + } +} diff --git a/docs/faq/build_overrides/what-are-they.md b/docs/faq/build_overrides/what-are-they.md new file mode 100644 index 0000000..2986701 --- /dev/null +++ b/docs/faq/build_overrides/what-are-they.md @@ -0,0 +1,41 @@ +--- +uid: FAQ.BuildOverrides.WhatAreThey +title: Build Overrides +--- + +# Build Overrides + +Build overrides are a way for library developers to override the default behavior of the library on the fly. Adding them to your code is really simple. + +## Installing the package + +The build override package can be installed on nuget [here](https://www.nuget.org/packages/Discord.Net.BuildOverrides) or by using the package manager + +``` +PM> Install-Package Discord.Net.BuildOverrides +``` + +## Adding an override + +```cs +public async Task MainAsync() +{ + // hook into the log function + BuildOverrides.Log += (buildOverride, message) => + { + Console.WriteLine($"{buildOverride.Name}: {message}"); + return Task.CompletedTask; + }; + + // add your overrides + await BuildOverrides.AddOverrideAsync("example-override-name"); +} + +``` + +Overrides are normally built for specific problems, for example if someone is having an issue and we think we might have a fix then we can create a build override for them to test out the fix. + +## Security and Transparency + +Overrides can only be created and updated by library developers, you should only apply an override if a library developer asks you to. +Code for the overrides server and the overrides themselves can be found [here](https://github.com/discord-net/Discord.Net.BuildOverrides). diff --git a/docs/faq/int_framework/framework.md b/docs/faq/int_framework/framework.md new file mode 100644 index 0000000..1e8b24c --- /dev/null +++ b/docs/faq/int_framework/framework.md @@ -0,0 +1,71 @@ +--- +uid: FAQ.Interactions.Framework +title: Interaction Framework +--- + +# The Interaction Framework + +Common misconceptions and questions about the Interaction Framework. + +## 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`*). + +[RequireUserPermission]: xref:Discord.Commands.RequireUserPermissionAttribute + +> [!NOTE] +> There are many more preconditions to use, including being able to make some yourself. +> Examples on self-made preconditions can be found +> [here](https://github.com/discord-net/Discord.Net/blob/dev/samples/InteractionFramework/Attributes/RequireOwnerAttribute.cs) + +## Why do preconditions not hide my commands? + +In the current permission design by Discord, +it is not very straight forward to limit vision of slash/context commands to users. +If you want to hide commands, you should take a look at the commands' `DefaultPermissions` parameter. + +## Module dependencies aren't getting populated by Property Injection? + +Make sure the properties are publicly accessible and publicly settable. + +[!code-csharp[Property Injection](samples/propertyinjection.cs)] + +## `InteractionService.ExecuteAsync()` always returns a successful result, how do i access the failed command execution results? + +If you are using `RunMode.Async` you need to setup your post-execution pipeline around +`..Executed` events exposed by the Interaction Service. + +## How do I check if the executing user has * permission? + +Refer to the [documentation about preconditions] + +[documentation about preconditions]: xref:Guides.IntFw.Preconditions + +## How do I send the HTTP Response from inside the command modules. + +Set the `RestResponseCallback` property of [InteractionServiceConfig] with a delegate for handling HTTP Responses and use +`RestInteractionModuleBase` to create your command modules. `RespondWithModalAsync()`, `RespondAsync()` and `DeferAsync()` methods of this module base will use the +`RestResponseCallback` to create interaction responses. + +## Is there a cleaner way of creating parameter choices other than using `[Choice]`? + +The default `enum` [TypeConverter] of the Interaction Service will +automatically register `enum`s as multiple choice options. + +## How do I add an optional `enum` parameter but make the default value not visible to the user? + +The default `enum` [TypeConverter] of the Interaction Service comes with `[Hide]` attribute that +can be used to prevent certain enum values from getting registered. + +## How does the InteractionService determine the generic TypeConverter to use for a parameter type? + +It compares the _target base type_ key of the +[TypeConverter] and chooses the one that sits highest on the inheritance hierarchy. + +[TypeConverter]: xref:Discord.Interactions.TypeConverter +[Interactions FAQ]: xref: FAQ.Basics.Interactions +[InteractionServiceConfig]: xref:Discord.Interactions.InteractionServiceConfig diff --git a/docs/faq/int_framework/general.md b/docs/faq/int_framework/general.md new file mode 100644 index 0000000..b9e9e94 --- /dev/null +++ b/docs/faq/int_framework/general.md @@ -0,0 +1,70 @@ +--- +uid: FAQ.Interactions.General +title: Interaction Basics +--- + +# Interaction Basics + +This chapter mostly refers to interactions in general, +and will include questions that are common among users of the Interaction Framework +as well as users that register and handle commands manually. + +## What's the difference between RespondAsync, DeferAsync and FollowupAsync? + +The difference between these 3 functions is in how you handle the command response. +[RespondAsync] and +[DeferAsync] let the API know you have successfully received the command. This is also called 'acknowledging' a command. +DeferAsync will not send out a response, RespondAsync will. +[FollowupAsync] follows up on successful acknowledgement. + +> [!WARNING] +> If you have not acknowledged the command FollowupAsync will not work! the interaction has not been responded to, so you cannot follow it up! + +[RespondAsync]: xref:Discord.IDiscordInteraction +[DeferAsync]: xref:Discord.IDiscordInteraction +[FollowUpAsync]: xref:Discord.IDiscordInteraction + +## Im getting System.TimeoutException: 'Cannot respond to an interaction after 3 seconds!' + +This happens because your computer's clock is out of sync or you're trying to respond after 3 seconds. +If your clock is out of sync and you can't fix it, you can set the `UseInteractionSnowflakeDate` to false in the [DiscordSocketConfig]. + +[!code-csharp[Interaction Sync](samples/interactionsyncing.cs)] + +[DiscordClientConfig]: xref:Discord.WebSocket.DiscordSocketConfig + +## How do I use this * interaction specific method/property? + +If your interaction context holds a down-casted version of the interaction object, you need to up-cast it. +Ideally, use pattern matching to make sure its the type of interaction you are expecting it to be. + +> [!NOTE] +> Further documentation on pattern matching can be found [here](xref:Guides.Entities.Casting). + +## My interaction commands are not showing up? + +- Try to check for any errors in the console, there is a good chance something might have been thrown. +- - Make sure you have setup logging. If you use `InteractionService` hook into [`InteractionService.Log`]) event + +- Register your commands after the Ready event in the client. The client is not configured to register commands before this moment. + +- Check if no bad form exception is thrown; + +- Do you have the application commands scope checked when adding your bot to guilds? + +- Try reloading your Discord client. On desktop it's done with `Ctrl+R` key combo. + +## Do I need to create commands on startup? + +If you are registering your commands for the first time, it is required to create them once. +After this, commands will exist indefinitely until you overwrite them. +Overwriting is only required if you make changes to existing commands, or add new ones. + +## I can't see all of my user/message commands, why? + +Message and user commands have a limit of 5 per guild, and another 5 globally. +If you have more than 5 guild-only message commands being registered, no more than 5 will actually show up. +You can get up to 10 entries to show if you register 5 per guild, and another 5 globally. + + +[`InteractionService.Log`]: xref:Discord.Interactions.InteractionService.Log diff --git a/docs/faq/int_framework/images/response-scheme-component.png b/docs/faq/int_framework/images/response-scheme-component.png new file mode 100644 index 0000000..117ce45 Binary files /dev/null and b/docs/faq/int_framework/images/response-scheme-component.png differ diff --git a/docs/faq/int_framework/images/response-scheme-modal.png b/docs/faq/int_framework/images/response-scheme-modal.png new file mode 100644 index 0000000..a4b6d63 Binary files /dev/null and b/docs/faq/int_framework/images/response-scheme-modal.png differ diff --git a/docs/faq/int_framework/images/response-scheme-slash.png b/docs/faq/int_framework/images/response-scheme-slash.png new file mode 100644 index 0000000..1014b22 Binary files /dev/null and b/docs/faq/int_framework/images/response-scheme-slash.png differ diff --git a/docs/faq/int_framework/images/scope.png b/docs/faq/int_framework/images/scope.png new file mode 100644 index 0000000..04d60bc Binary files /dev/null and b/docs/faq/int_framework/images/scope.png differ diff --git a/docs/faq/int_framework/manual.md b/docs/faq/int_framework/manual.md new file mode 100644 index 0000000..d847ce7 --- /dev/null +++ b/docs/faq/int_framework/manual.md @@ -0,0 +1,45 @@ +--- +uid: FAQ.Interactions.Manual +title: Manual Handling +--- + +# Manually Handling Interactions + +This section talks about the manual building and responding to interactions. +If you are using the interaction framework (highly recommended) this section does not apply to you. + +## Bad form Exception when I try to create my commands, why do I get this? + +Bad form exceptions are thrown if the slash, user or message command builder has invalid values. +The following options could resolve your error. + +#### Is your command name lowercase? + +If your command name is not lowercase, it is not seen as a valid command entry. +`Avatar` is invalid; `avatar` is valid. + +#### Are your values below or above the required amount? (This also applies to message components) + +Discord expects all values to be below maximum allowed. +Going over this maximum amount of characters causes an exception. + +> [!NOTE] +> All maximum and minimum value requirements can be found in the [Discord Developer Docs]. +> For components, structure documentation is found [here]. + +[Discord Developer Docs]: https://discord.com/developers/docs/interactions/application-commands#application-commands +[here]: https://discord.com/developers/docs/interactions/message-components#message-components + +#### Is your subcommand branching correct? + +Branching structure is covered properly here: xref:Guides.SlashCommands.SubCommand + +![Scope check](images/scope.png) + +## There are many options for creating commands, which do I use? + +[!code-csharp[Register examples](samples/registerint.cs)] + +> [!NOTE] +> You can use bulkoverwrite even if there are no commands in guild, nor globally. +> The bulkoverwrite method disposes the old set of commands and replaces it with the new. diff --git a/docs/faq/int_framework/respondings-schemes.md b/docs/faq/int_framework/respondings-schemes.md new file mode 100644 index 0000000..0276b2d --- /dev/null +++ b/docs/faq/int_framework/respondings-schemes.md @@ -0,0 +1,32 @@ +--- +uid: FAQ.Interactions.RespondingSchemes +title: Interaction Response Schemes +--- + +# Interaction Response Schemes + +Working with interactions can appear hard and confusing - you might accidentally miss a cast or use a wrong method. These schemes should help you create efficient interaction response flows. + +## Responding to a slash command interaction + +Slash command interactions support the most commonly used response methods. + +> [!NOTE] +> Same scheme applies to context command interactions. + +![Slash command interaction](images/response-scheme-slash.png) + +## Responding to a component interaction + +Component interactions share a lot of response mwthods with [slash command interactions](#responding-to-a-slash-command-interaction), but they also provide a way to update the message components were attached to. + +> [!NOTE] +> Some followup methods change their behavior depending on what initial response you've sent. + +![Slash command interaction](images/response-scheme-component.png) + +## Responding to a modal interaction + +While being similar to [Component Interaction Scheme](#responding-to-a-modal-interaction), modal interactions lack the option of responding with a modal. + +![Slash command interaction](images/response-scheme-modal.png) \ No newline at end of file diff --git a/docs/faq/int_framework/samples/interactionsyncing.cs b/docs/faq/int_framework/samples/interactionsyncing.cs new file mode 100644 index 0000000..6406619 --- /dev/null +++ b/docs/faq/int_framework/samples/interactionsyncing.cs @@ -0,0 +1,6 @@ +DiscordSocketConfig config = new() +{ + UseInteractionSnowflakeDate = false +}; + +DiscordSocketclient client = new(config); diff --git a/docs/faq/int_framework/samples/propertyinjection.cs b/docs/faq/int_framework/samples/propertyinjection.cs new file mode 100644 index 0000000..fcacd52 --- /dev/null +++ b/docs/faq/int_framework/samples/propertyinjection.cs @@ -0,0 +1,8 @@ +public class MyModule +{ + // Intended. + public InteractionService Service { get; set; } + + // Will not work. A private setter cannot be accessed by the serviceprovider. + private InteractionService Service { get; private set; } +} diff --git a/docs/faq/int_framework/samples/registerint.cs b/docs/faq/int_framework/samples/registerint.cs new file mode 100644 index 0000000..68fd2e6 --- /dev/null +++ b/docs/faq/int_framework/samples/registerint.cs @@ -0,0 +1,21 @@ +private async Task ReadyAsync() +{ + // pull your commands from some array, everyone has a different approach for this. + var commands = _builders.ToArray(); + + // write your list of commands globally in one go. + await _client.Rest.BulkOverwriteGlobalCommands(commands); + + // write your array of commands to one guild in one go. + // You can do a foreach (... in _client.Guilds) approach to write to all guilds. + await _client.Rest.BulkOverwriteGuildCommands(commands, /* some guild ID */); + + foreach (var c in commands) + { + // Create a global command, repeating usage for multiple commands. + await _client.Rest.CreateGlobalCommand(c); + + // Create a guild command, repeating usage for multiple commands. + await _client.Rest.CreateGuildCommand(c, guildId); + } +} diff --git a/docs/faq/misc/legacy.md b/docs/faq/misc/legacy.md new file mode 100644 index 0000000..faaf1cd --- /dev/null +++ b/docs/faq/misc/legacy.md @@ -0,0 +1,46 @@ +--- +uid: FAQ.Legacy +title: Legacy Questions +--- + +# Legacy Questions + +This section refers to legacy library-related questions that do not +apply to the latest or recent version of the Discord.Net library. + +## Migrating your commands to application commands. + +The new interaction service was designed to act like the previous service for text-based commands. +Your pre-existing code will continue to work, but you will need to migrate your modules and response functions to use the new +interaction service methods. Documentation on this can be found in the [Guides](xref:Guides.IntFw.Intro). + +## Gateway event parameters changed, why? + +With 3.0, a higher focus on [Cacheable]'s was introduced. +[Cacheable]'s get an entity from cache, rather than making an API call to retrieve it's data. +The entity can be retrieved from cache by calling `GetOrDownloadAsync()` on the [Cacheable] type. + +> [!NOTE] +> GetOrDownloadAsync will download the entity if its not available directly from the cache. + +[Cacheable]: xref:Discord.Cacheable`2 + +## X, Y, Z does not work! It doesn't return a valid value anymore. + +If you are currently using an older version of the stable branch, +please upgrade to the latest 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 release. + +[release tag]: https://github.com/discord-net/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/Discord-Net-Labs/Discord.Net-Labs/blob/release/3.x/samples/InteractionFramework/Program.cs#L66). + +[DependencyInjection]: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection diff --git a/docs/faq/text_commands/general.md b/docs/faq/text_commands/general.md new file mode 100644 index 0000000..202ceb2 --- /dev/null +++ b/docs/faq/text_commands/general.md @@ -0,0 +1,142 @@ +--- +uid: FAQ.TextCommands.General +title: General Questions about Text Commands +--- + +# Chat Command-related Questions + +In the following section, you will find commonly asked questions and +answered regarding general command usage when using @Discord.Commands. + +## How can I restrict some of my commands so only specific users can execute them? + +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`*). + +> [!NOTE] +> There are many more preconditions to use, including being able to make some yourself. +> Precondition documentation is covered [here](xref:Guides.TextCommands.Preconditions) + +[RequireUserPermission]: xref:Discord.Commands.RequireUserPermissionAttribute + +## Why am I getting an error about `Assembly.GetEntryAssembly`? + +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 +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. + +[!code-csharp[Remainder](samples/Remainder.cs)] + +[RemainderAttribute]: xref:Discord.Commands.RemainderAttribute + +## 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 +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. + +> [!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 +this issue. + +There are 2 main `RunMode`s. + +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`. + +# [CommandAttribute](#tab/cmdattrib) + +[!code-csharp[Command Attribute](samples/runmode-cmdattrib.cs)] + +# [CommandServiceConfig](#tab/cmdconfig) + +[!code-csharp[Command Service Config](samples/runmode-cmdconfig.cs)] + +*** + +*** + +[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 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 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). +4. Exceptions are swallowed in the `ExecuteAsync` result. + +However, there are ways to remedy some of these. + +For #3, in Discord.Net 2.0, the library introduces a new event called +[CommandService.CommandExecuted], which is raised whenever the command is executed. +This event will be raised regardless of +the `RunMode` type and will return the appropriate execution result +and the associated @Discord.Commands.CommandInfo if applicable. + +For #4, exceptions are caught in [CommandService.Log] event under +[LogMessage.Exception] as [CommandException] and in the +[CommandService.CommandExecuted] event under the [IResult] as +[ExecuteResult.Exception]. + +[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 +[CommandService.CommandExecuted]: xref:Discord.Commands.CommandService.CommandExecuted +[CommandService.Log]: xref:Discord.Commands.CommandService.Log +[LogMessage.Exception]: xref:Discord.LogMessage.Exception* +[ExecuteResult.Exception]: xref:Discord.Commands.ExecuteResult.Exception* +[CommandException]: xref:Discord.Commands.CommandException +[IResult]: xref:Discord.Commands.IResult diff --git a/docs/faq/text_commands/samples/Remainder.cs b/docs/faq/text_commands/samples/Remainder.cs new file mode 100644 index 0000000..337fb6e --- /dev/null +++ b/docs/faq/text_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/text_commands/samples/runmode-cmdattrib.cs b/docs/faq/text_commands/samples/runmode-cmdattrib.cs new file mode 100644 index 0000000..253acc4 --- /dev/null +++ b/docs/faq/text_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/text_commands/samples/runmode-cmdconfig.cs b/docs/faq/text_commands/samples/runmode-cmdconfig.cs new file mode 100644 index 0000000..11d9cc2 --- /dev/null +++ b/docs/faq/text_commands/samples/runmode-cmdconfig.cs @@ -0,0 +1,10 @@ +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/toc.yml b/docs/faq/toc.yml new file mode 100644 index 0000000..3f8ddff --- /dev/null +++ b/docs/faq/toc.yml @@ -0,0 +1,28 @@ +- 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: Dependency Injection + topicUid: FAQ.Basics.DI +- name: Interactions + items: + - name: Starting out + topicUid: FAQ.Interactions.General + - name: Interaction Service/Framework + topicUid: FAQ.Interactions.Framework + - name: Manual handling + topicUid: FAQ.Interactions.Manual + - name: Interaction response schemes + topicUid: FAQ.Interactions.RespondingSchemes +- name: Text Commands + items: + - name: Text Command basics + topicUid: FAQ.TextCommands.General +- name: Legacy Questions + topicUid: FAQ.Legacy +- name: Build Overrides + topicUid: FAQ.BuildOverrides.WhatAreThey diff --git a/docs/favicon.png b/docs/favicon.png new file mode 100644 index 0000000..f3ab9c6 Binary files /dev/null and b/docs/favicon.png differ diff --git a/docs/filterConfig.yml b/docs/filterConfig.yml new file mode 100644 index 0000000..598fd34 --- /dev/null +++ b/docs/filterConfig.yml @@ -0,0 +1,10 @@ +apiRules: +- exclude: + uidRegex: ^Discord\.Net\..*$ + type: Namespace +- exclude: + uidRegex: ^Discord\.Analyzers$ + type: Namespace +- exclude: + uidRegex: ^Discord\.API$ + type: Namespace \ No newline at end of file diff --git a/docs/guides/bearer_token/bearer_token_guide.md b/docs/guides/bearer_token/bearer_token_guide.md new file mode 100644 index 0000000..6fc01f1 --- /dev/null +++ b/docs/guides/bearer_token/bearer_token_guide.md @@ -0,0 +1,68 @@ +--- +uid: Guides.BearerToken +title: Working with Bearer token +--- + +# Working with Bearer token + +Some endpoints in Discord API require a Bearer token, which can be obtained through [OAuth2 flow](https://discord.com/developers/docs/topics/oauth2). Discord.Net allows you to interact with these endpoints using the [DiscordRestClient]. + +## Initializing a new instance of the client +[!code-csharp[Initialize DiscordRestClient](samples/rest_client_init.cs)] + +## Getting current user + +The [DiscordRestClient] gets the current user when `LoginAsync()` is called. The user object can be found in the `CurrentUser` property. + +If you need to fetch the user again, the `GetCurrentUserAsync()` method can be used. + +[!code-csharp[Get current user](samples/current_user.cs)] + +> [!NOTE] +> Some properties might be `null` depending on which scopes users authorized your app with. +> For example: `email` scope is required to fetch current user's email address. + +## Fetching current user's guilds + +The `GetGuildSummariesAsync()` method is used to fetch current user's guilds. Since it returns an `IAsyncEnumerable` you need to call `FlattenAsync()` to get a plain `IEnumerable` containing [RestUserGuild] objects. + +[!code-csharp[Get current user's guilds](samples/current_user_guilds.cs)] + +> [!WARNING] +> This method requires `guilds` scope + +## Fetching current user's guild member object + +To fetch the current user's guild member object, the `GetCurrentUserGuildMemberAsync()` method can be used. + +[!code-csharp[Get current user's guild member](samples/current_user_guild_member.cs)] + +> [!WARNING] +> This method requires `guilds.members.read` scope + +## Get user connections + +The `GetConnectionsAsync` method can be used to fetch current user's connections to other platforms. + +[!code-csharp[Get current user's connections](samples/current_user_connections.cs)] + +> [!WARNING] +> This method requires `connections` scope + +## Application role connection + +In addition to previous features, Discord.Net supports fetching & updating user's application role connection metadata values. `GetUserApplicationRoleConnectionAsync()` returns a [RoleConnection] object of the current user for the given application id. + +The `ModifyUserApplicationRoleConnectionAsync()` method is used to update current user's role connection metadata values. A new set of values can be created with [RoleConnectionProperties] object. + +[!code-csharp[Get current user's connections](samples/app_role_connection.cs)] + +> [!WARNING] +> This method requires `role_connections.write` scope + + + +[DiscordRestClient]: xref:Discord.Rest.DiscordRestClient +[RestUserGuild]: xref:Discord.Rest.RestUserGuild +[RoleConnection]: xref:Discord.RoleConnection +[RoleConnectionProperties]: xref:Discord.RoleConnectionProperties diff --git a/docs/guides/bearer_token/samples/app_role_connection.cs b/docs/guides/bearer_token/samples/app_role_connection.cs new file mode 100644 index 0000000..cff57a8 --- /dev/null +++ b/docs/guides/bearer_token/samples/app_role_connection.cs @@ -0,0 +1,11 @@ +// fetch application role connection of the current user for the app with provided id. +var roleConnection = await client.GetUserApplicationRoleConnectionAsync(applicationid); + +// create a new role connection metadata properties object & set some values. +var properties = new RoleConnectionProperties("Discord.Net Docs", "Cool Coding Guy") + .WithNumber("eaten_cookies", 69) + .WithBool("loves_cookies", true) + .WithDate("last_eaten_cookie", DateTimeOffset.UtcNow); + +// update current user's values with the given properties. +await client.ModifyUserApplicationRoleConnectionAsync(applicationId, properties); diff --git a/docs/guides/bearer_token/samples/current_user.cs b/docs/guides/bearer_token/samples/current_user.cs new file mode 100644 index 0000000..1b7337d --- /dev/null +++ b/docs/guides/bearer_token/samples/current_user.cs @@ -0,0 +1,5 @@ +// gets the user object stored in the DiscordRestClient. +var user = client.CurrentUser; + +// fetches the current user with a REST call & updates the CurrentUser property. +var refreshedUser = await client.GetCurrentUserAsync(); \ No newline at end of file diff --git a/docs/guides/bearer_token/samples/current_user_connections.cs b/docs/guides/bearer_token/samples/current_user_connections.cs new file mode 100644 index 0000000..be33975 --- /dev/null +++ b/docs/guides/bearer_token/samples/current_user_connections.cs @@ -0,0 +1,2 @@ +// fetches the current user's connections. +var connections = await client.GetConnectionsAsync(); \ No newline at end of file diff --git a/docs/guides/bearer_token/samples/current_user_guild_member.cs b/docs/guides/bearer_token/samples/current_user_guild_member.cs new file mode 100644 index 0000000..bfbe363 --- /dev/null +++ b/docs/guides/bearer_token/samples/current_user_guild_member.cs @@ -0,0 +1,6 @@ +// fetches the current user's guild member object in a guild with provided id. +var member = await client.GetCurrentUserGuildMemberAsync(guildId); + +// fetches the current user's guild member object in a RestUserGuild. +var guild = await client.GetGuildSummariesAsync().FlattenAsync().First(); +var member = await guild.GetCurrentUserGuildMemberAsync(); \ No newline at end of file diff --git a/docs/guides/bearer_token/samples/current_user_guilds.cs b/docs/guides/bearer_token/samples/current_user_guilds.cs new file mode 100644 index 0000000..f98e360 --- /dev/null +++ b/docs/guides/bearer_token/samples/current_user_guilds.cs @@ -0,0 +1,2 @@ +// fetches the guilds the current user participate in. +var guilds = await client.GetGuildSummariesAsync().FlattenAsync(); \ No newline at end of file diff --git a/docs/guides/bearer_token/samples/rest_client_init.cs b/docs/guides/bearer_token/samples/rest_client_init.cs new file mode 100644 index 0000000..e810a4f --- /dev/null +++ b/docs/guides/bearer_token/samples/rest_client_init.cs @@ -0,0 +1,5 @@ +using Discord; +using Discord.Rest; + +await using var client = new DiscordRestClient(); +await client.LoginAsync(TokenType.Bearer, "bearer token obtained through oauth2 flow"); \ No newline at end of file diff --git a/docs/guides/concepts/connections.md b/docs/guides/concepts/connections.md new file mode 100644 index 0000000..d9951a8 --- /dev/null +++ b/docs/guides/concepts/connections.md @@ -0,0 +1,53 @@ +--- +uid: Guides.Concepts.ManageConnections +title: Managing Connections +--- + +# Managing Connections with Discord.Net + +In Discord.Net, once a client has been started, it will automatically +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, 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 made.** + +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. + +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 +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 completed guild cache. + +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient + +## Reconnection + +> [!TIP] +> Avoid running long-running code on the gateway! If you deadlock the +> 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 +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 does not manage to reconnect, you have found a bug! + +[events]: xref:Guides.Concepts.Events diff --git a/docs/guides/concepts/events.md b/docs/guides/concepts/events.md new file mode 100644 index 0000000..293b5dc --- /dev/null +++ b/docs/guides/concepts/events.md @@ -0,0 +1,84 @@ +--- +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 @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 + +To receive data from an event, hook into it using C#'s delegate +event pattern. + +You may either opt to hook an event to an anonymous function (lambda) +or a named function. + +## Safety + +All events are designed to be thread-safe; events are executed +synchronously off the gateway task in the same context as the gateway +task. + +As a side effect, this makes it possible to deadlock the gateway task +and kill a connection. As a general rule of thumb, any task that takes +longer than three seconds should **not** be awaited directly in the +context of an event, but should be wrapped in a `Task.Run` or +offloaded to another task. + +This also means that you should not await a task that requests data +from Discord's gateway in the same context of an event. Since the +gateway will wait on all invoked event handlers to finish before +processing any additional data from the gateway, this will create +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 + +As you may know, events in Discord.Net are only given a signature of +`Func`. There is no room for predefined argument names, +so you must either consult IntelliSense, or view the API documentation +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 + +An event handler with a signature of `Func` +typically means that the first object will be a clone of the entity +_before_ a change was made, and the latter object will be an attached +model of the entity _after_ the change was made. + +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 +API, so it can either be pulled from the client's cache or +downloaded from the API. + +See the documentation for [Cacheable] for more information on this +object. + +[Cacheable]: xref:Discord.Cacheable`2 + +> [!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. + +## Sample + +[!code-csharp[Event Sample](samples/events.cs)] diff --git a/docs/guides/concepts/logging.md b/docs/guides/concepts/logging.md new file mode 100644 index 0000000..b92d2bd --- /dev/null +++ b/docs/guides/concepts/logging.md @@ -0,0 +1,48 @@ +--- +uid: Guides.Concepts.Logging +title: Logging Events/Data +--- + +# 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. + +[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 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]. + +It is recommended that you use an established function instead of a +lambda for handling logs, because most addons accept a reference +to a logging function to write their own messages. + +[LogMessage]: xref:Discord.LogMessage + +## Usage in Commands + +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 +and error can be found and handled. + +[CommandService]: xref:Discord.Commands.CommandService +[CommandException]: xref:Discord.Commands.CommandException + +## Sample + +[!code-csharp[Logging Sample](samples/logging.cs)] diff --git a/docs/guides/concepts/ratelimits.md b/docs/guides/concepts/ratelimits.md new file mode 100644 index 0000000..5c62311 --- /dev/null +++ b/docs/guides/concepts/ratelimits.md @@ -0,0 +1,49 @@ +# Ratelimits + +Ratelimits are a core concept of any API - Discords API is no exception. Each verified library must follow the ratelimit guidelines. + +### Using the ratelimit callback + +There is a new property within `RequestOptions` called RatelimitCallback. This callback is called when a request is made via the rest api. The callback is called with a `IRateLimitInfo` parameter: + +| Name | Type | Description | +| ---------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| IsGlobal | bool | Whether or not this ratelimit info is global. | +| Limit | int? | The number of requests that can be made. | +| Remaining | int? | The number of remaining requests that can be made. | +| RetryAfter | int? | The total time (in seconds) of when the current rate limit bucket will reset. Can have decimals to match previous millisecond ratelimit precision. | +| Reset | DateTimeOffset? | The time at which the rate limit resets. | +| ResetAfter | TimeSpan? | The absolute time when this ratelimit resets. | +| Bucket | string | A unique string denoting the rate limit being encountered (non-inclusive of major parameters in the route path). | +| Lag | TimeSpan? | The amount of lag for the request. This is used to denote the precise time of when the ratelimit expires. | +| Endpoint | string | The endpoint that this ratelimit info came from. | + +Let's set up a ratelimit callback that will print out the ratelimit info to the console. + +```cs +public async Task MyRatelimitCallback(IRateLimitInfo info) +{ + Console.WriteLine($"{info.IsGlobal} {info.Limit} {info.Remaining} {info.RetryAfter} {info.Reset} {info.ResetAfter} {info.Bucket} {info.Lag} {info.Endpoint}"); +} +``` + +Let's use this callback in a send message function + +```cs +[Command("ping")] +public async Task ping() +{ + var options = new RequestOptions() + { + RatelimitCallback = MyRatelimitCallback + }; + + await Context.Channel.SendMessageAsync("Pong!", options: options); +} +``` + +Running this produces the following output: + +``` +False 5 4 2021-09-09 3:48:14 AM +00:00 00:00:05 a06de0de4a08126315431cc0c55ee3dc 00:00:00.9891364 channels/848511736872828929/messages +``` diff --git a/docs/guides/concepts/samples/events.cs b/docs/guides/concepts/samples/events.cs new file mode 100644 index 0000000..eb66d59 --- /dev/null +++ b/docs/guides/concepts/samples/events.cs @@ -0,0 +1,34 @@ +using Discord; +using Discord.WebSocket; + +public class Program +{ + private static DiscordSocketClient _client; + public static async Task MainAsync() + { + // When working with events that have Cacheable parameters, + // you must enable the message cache in your config settings if you plan to + // use the cached message entity. + var _config = new DiscordSocketConfig { MessageCacheSize = 100 }; + _client = new DiscordSocketClient(_config); + + await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("DiscordToken")); + await _client.StartAsync(); + + _client.MessageUpdated += MessageUpdated; + _client.Ready += () => + { + Console.WriteLine("Bot is connected!"); + return Task.CompletedTask; + }; + + await Task.Delay(-1); + } + + private static async Task MessageUpdated(Cacheable before, SocketMessage after, ISocketMessageChannel channel) + { + // If the message was not in the cache, downloading it will result in getting a copy of `after`. + var message = await before.GetOrDownloadAsync(); + Console.WriteLine($"{message} -> {after}"); + } +} diff --git a/docs/guides/concepts/samples/logging.cs b/docs/guides/concepts/samples/logging.cs new file mode 100644 index 0000000..982fb11 --- /dev/null +++ b/docs/guides/concepts/samples/logging.cs @@ -0,0 +1,24 @@ +using Discord; +using Discord.WebSocket; + +public class LoggingService +{ + public LoggingService(DiscordSocketClient client, CommandService command) + { + client.Log += LogAsync; + command.Log += LogAsync; + } + private Task LogAsync(LogMessage message) + { + if (message.Exception is CommandException cmdException) + { + Console.WriteLine($"[Command/{message.Severity}] {cmdException.Command.Aliases.First()}" + + $" failed to execute in {cmdException.Context.Channel}."); + Console.WriteLine(cmdException); + } + else + Console.WriteLine($"[General/{message.Severity}] {message}"); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/docs/guides/dependency_injection/basics.md b/docs/guides/dependency_injection/basics.md new file mode 100644 index 0000000..b9edbca --- /dev/null +++ b/docs/guides/dependency_injection/basics.md @@ -0,0 +1,69 @@ +--- +uid: Guides.DI.Intro +title: Introduction +--- + +# Dependency Injection + +Dependency injection is a feature not required in Discord.Net, but makes it a lot easier to use. +It can be combined with a large number of other libraries, and gives you better control over your application. + +> Further into the documentation, Dependency Injection will be referred to as 'DI'. + +## Installation + +DI is not native to .NET. You need to install the extension packages to your project in order to use it: + +- [Meta](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection/). +- [Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection.Abstractions/). + +> [!WARNING] +> Downloading the abstractions package alone will not give you access to required classes to use DI properly. +> Please install both packages, or choose to only install the meta package to implicitly install both. + +### Visual Package Manager: + +![Installing](images/manager.png) + +### Command Line: + +`PM> Install-Package Microsoft.Extensions.DependencyInjection`. + +> [!TIP] +> ASP.NET already comes packed with all the necessary assemblies in its framework. +> You do not require to install any additional NuGet packages to make full use of all features of DI in ASP.NET projects. + +## Getting started + +First of all, you will need to create an application based around dependency injection, +which in order will be able to access and inject them across the project. + +[!code-csharp[Building the Program](samples/program.cs)] + +In order to freely pass around your dependencies in different classes, +you will need to register them to a new `ServiceCollection` and build them into an `IServiceProvider` as seen above. +The IServiceProvider then needs to be accessible by the startup file, so you can access your provider and manage them. + +[!code-csharp[Building the Collection](samples/collection.cs)] + +As shown above, an instance of `DiscordSocketConfig` is created, and added **before** the client itself is. +Because the collection will prefer to create the highest populated constructor available with the services already present, +it will prefer the constructor with the configuration, because you already added it. + +## Using your dependencies + +After building your provider in the Program class constructor, the provider is now available inside the instance you're actively using. +Through the provider, we can ask for the DiscordSocketClient we registered earlier. + +[!code-csharp[Applying DI in RunAsync](samples/runasync.cs)] + +> [!WARNING] +> Service constructors are not activated until the service is **first requested**. +> An 'endpoint' service will have to be requested from the provider before it is activated. +> If a service is requested with dependencies, its dependencies (if not already active) will be activated before the service itself is. + +## Injecting dependencies + +You can not only directly access the provider from a field or property, but you can also pass around instances to classes registered in the provider. +There are multiple ways to do this. Please refer to the +[Injection Documentation](xref:Guides.DI.Injection) for further information. diff --git a/docs/guides/dependency_injection/images/manager.png b/docs/guides/dependency_injection/images/manager.png new file mode 100644 index 0000000..91791f7 Binary files /dev/null and b/docs/guides/dependency_injection/images/manager.png differ diff --git a/docs/guides/dependency_injection/injection.md b/docs/guides/dependency_injection/injection.md new file mode 100644 index 0000000..087ff2d --- /dev/null +++ b/docs/guides/dependency_injection/injection.md @@ -0,0 +1,44 @@ +--- +uid: Guides.DI.Injection +title: Injection +--- + +# Injecting instances within the provider + +You can inject registered services into any class that is registered to the `IServiceProvider`. +This can be done through property or constructor. + +> [!NOTE] +> As mentioned above, the dependency *and* the target class have to be registered in order for the serviceprovider to resolve it. + +## Injecting through a constructor + +Services can be injected from the constructor of the class. +This is the preferred approach, because it automatically locks the readonly field in place with the provided service and isn't accessible outside of the class. + +[!code-csharp[Constructor Injection](samples/ctor-injecting.cs)] + +## Injecting through properties + +Injecting through properties is also allowed as follows. + +[!code-csharp[Property Injection](samples/property-injecting.cs)] + +> [!WARNING] +> Dependency Injection will not resolve missing services in property injection, and it will not pick a constructor instead. +> If a publicly accessible property is attempted to be injected and its service is missing, the application will throw an error. + +## Using the provider itself + +You can also access the provider reference itself from injecting it into a class. There are multiple use cases for this: + +- Allowing libraries (Like Discord.Net) to access your provider internally. +- Injecting optional dependencies. +- Calling methods on the provider itself if necessary, this is often done for creating scopes. + +[!code-csharp[Provider Injection](samples/provider.cs)] + +> [!NOTE] +> It is important to keep in mind that the provider will pick the 'biggest' available constructor. +> If you choose to introduce multiple constructors, +> keep in mind that services missing from one constructor may have the provider pick another one that *is* available instead of throwing an exception. diff --git a/docs/guides/dependency_injection/samples/access-activator.cs b/docs/guides/dependency_injection/samples/access-activator.cs new file mode 100644 index 0000000..29e71e8 --- /dev/null +++ b/docs/guides/dependency_injection/samples/access-activator.cs @@ -0,0 +1,9 @@ +async Task RunAsync() +{ + //... + + await _serviceProvider.GetRequiredService() + .ActivateAsync(); + + //... +} diff --git a/docs/guides/dependency_injection/samples/collection.cs b/docs/guides/dependency_injection/samples/collection.cs new file mode 100644 index 0000000..4d0457d --- /dev/null +++ b/docs/guides/dependency_injection/samples/collection.cs @@ -0,0 +1,13 @@ +static IServiceProvider CreateServices() +{ + var config = new DiscordSocketConfig() + { + //... + }; + + var collection = new ServiceCollection() + .AddSingleton(config) + .AddSingleton(); + + return collection.BuildServiceProvider(); +} diff --git a/docs/guides/dependency_injection/samples/ctor-injecting.cs b/docs/guides/dependency_injection/samples/ctor-injecting.cs new file mode 100644 index 0000000..c412bd2 --- /dev/null +++ b/docs/guides/dependency_injection/samples/ctor-injecting.cs @@ -0,0 +1,14 @@ +public class ClientHandler +{ + private readonly DiscordSocketClient _client; + + public ClientHandler(DiscordSocketClient client) + { + _client = client; + } + + public async Task ConfigureAsync() + { + //... + } +} diff --git a/docs/guides/dependency_injection/samples/enumeration.cs b/docs/guides/dependency_injection/samples/enumeration.cs new file mode 100644 index 0000000..cc8c617 --- /dev/null +++ b/docs/guides/dependency_injection/samples/enumeration.cs @@ -0,0 +1,18 @@ +public class ServiceActivator +{ + // This contains *all* registered services of serviceType IService + private readonly IEnumerable _services; + + public ServiceActivator(IEnumerable services) + { + _services = services; + } + + public async Task ActivateAsync() + { + foreach(var service in _services) + { + await service.StartAsync(); + } + } +} diff --git a/docs/guides/dependency_injection/samples/implicit-registration.cs b/docs/guides/dependency_injection/samples/implicit-registration.cs new file mode 100644 index 0000000..52f8422 --- /dev/null +++ b/docs/guides/dependency_injection/samples/implicit-registration.cs @@ -0,0 +1,12 @@ +public static ServiceCollection RegisterImplicitServices(this ServiceCollection collection, Type interfaceType, Type activatorType) +{ + // Get all types in the executing assembly. There are many ways to do this, but this is fastest. + foreach (var type in typeof(Program).Assembly.GetTypes()) + { + if (interfaceType.IsAssignableFrom(type) && !type.IsAbstract) + collection.AddSingleton(interfaceType, type); + } + + // Register the activator so you can activate the instances. + collection.AddSingleton(activatorType); +} diff --git a/docs/guides/dependency_injection/samples/modules.cs b/docs/guides/dependency_injection/samples/modules.cs new file mode 100644 index 0000000..2fadc13 --- /dev/null +++ b/docs/guides/dependency_injection/samples/modules.cs @@ -0,0 +1,16 @@ +public class MyModule : InteractionModuleBase +{ + private readonly MyService _service; + + public MyModule(MyService service) + { + _service = service; + } + + [SlashCommand("things", "Shows things")] + public async Task ThingsAsync() + { + var str = string.Join("\n", _service.Things) + await RespondAsync(str); + } +} diff --git a/docs/guides/dependency_injection/samples/program.cs b/docs/guides/dependency_injection/samples/program.cs new file mode 100644 index 0000000..14651f0 --- /dev/null +++ b/docs/guides/dependency_injection/samples/program.cs @@ -0,0 +1,16 @@ +public class Program +{ + private static IServiceProvider _serviceProvider; + + static IServiceProvider CreateProvider() + { + var collection = new ServiceCollection(); + //... + return collection.BuildServiceProvider(); + } + + static async Task Main(string[] args) + { + _serviceProvider = CreateProvider(); + } +} diff --git a/docs/guides/dependency_injection/samples/property-injecting.cs b/docs/guides/dependency_injection/samples/property-injecting.cs new file mode 100644 index 0000000..c0c50e1 --- /dev/null +++ b/docs/guides/dependency_injection/samples/property-injecting.cs @@ -0,0 +1,9 @@ +public class ClientHandler +{ + public DiscordSocketClient Client { get; set; } + + public async Task ConfigureAsync() + { + //... + } +} diff --git a/docs/guides/dependency_injection/samples/provider.cs b/docs/guides/dependency_injection/samples/provider.cs new file mode 100644 index 0000000..26b600b --- /dev/null +++ b/docs/guides/dependency_injection/samples/provider.cs @@ -0,0 +1,26 @@ +public class UtilizingProvider +{ + private readonly IServiceProvider _provider; + private readonly AnyService _service; + + // This service is allowed to be null because it is only populated if the service is actually available in the provider. + private readonly AnyOtherService? _otherService; + + // This constructor injects only the service provider, + // and uses it to populate the other dependencies. + public UtilizingProvider(IServiceProvider provider) + { + _provider = provider; + _service = provider.GetRequiredService(); + _otherService = provider.GetService(); + } + + // This constructor injects the service provider, and AnyService, + // making sure that AnyService is not null without having to call GetRequiredService + public UtilizingProvider(IServiceProvider provider, AnyService service) + { + _provider = provider; + _service = service; + _otherService = provider.GetService(); + } +} diff --git a/docs/guides/dependency_injection/samples/runasync.cs b/docs/guides/dependency_injection/samples/runasync.cs new file mode 100644 index 0000000..d24efc8 --- /dev/null +++ b/docs/guides/dependency_injection/samples/runasync.cs @@ -0,0 +1,17 @@ +async Task RunAsync(string[] args) +{ + // Request the instance from the client. + // Because we're requesting it here first, its targetted constructor will be called and we will receive an active instance. + var client = _services.GetRequiredService(); + + client.Log += async (msg) => + { + await Task.CompletedTask; + Console.WriteLine(msg); + } + + await client.LoginAsync(TokenType.Bot, ""); + await client.StartAsync(); + + await Task.Delay(Timeout.Infinite); +} diff --git a/docs/guides/dependency_injection/samples/scoped.cs b/docs/guides/dependency_injection/samples/scoped.cs new file mode 100644 index 0000000..9942f8d --- /dev/null +++ b/docs/guides/dependency_injection/samples/scoped.cs @@ -0,0 +1,6 @@ + +// With serviceType: +collection.AddScoped(); + +// Without serviceType: +collection.AddScoped(); diff --git a/docs/guides/dependency_injection/samples/service-registration.cs b/docs/guides/dependency_injection/samples/service-registration.cs new file mode 100644 index 0000000..f6e4d22 --- /dev/null +++ b/docs/guides/dependency_injection/samples/service-registration.cs @@ -0,0 +1,21 @@ +static IServiceProvider CreateServices() +{ + var config = new DiscordSocketConfig() + { + //... + }; + + // X represents either Interaction or Command, as it functions the exact same for both types. + var servConfig = new XServiceConfig() + { + //... + } + + var collection = new ServiceCollection() + .AddSingleton(config) + .AddSingleton() + .AddSingleton(servConfig) + .AddSingleton(); + + return collection.BuildServiceProvider(); +} diff --git a/docs/guides/dependency_injection/samples/services.cs b/docs/guides/dependency_injection/samples/services.cs new file mode 100644 index 0000000..2e5235b --- /dev/null +++ b/docs/guides/dependency_injection/samples/services.cs @@ -0,0 +1,9 @@ +public class MyService +{ + public List Things { get; } + + public MyService() + { + Things = new(); + } +} diff --git a/docs/guides/dependency_injection/samples/singleton.cs b/docs/guides/dependency_injection/samples/singleton.cs new file mode 100644 index 0000000..f395d74 --- /dev/null +++ b/docs/guides/dependency_injection/samples/singleton.cs @@ -0,0 +1,6 @@ + +// With serviceType: +collection.AddSingleton(); + +// Without serviceType: +collection.AddSingleton(); diff --git a/docs/guides/dependency_injection/samples/transient.cs b/docs/guides/dependency_injection/samples/transient.cs new file mode 100644 index 0000000..ae1e1a5 --- /dev/null +++ b/docs/guides/dependency_injection/samples/transient.cs @@ -0,0 +1,6 @@ + +// With serviceType: +collection.AddTransient(); + +// Without serviceType: +collection.AddTransient(); diff --git a/docs/guides/dependency_injection/scaling.md b/docs/guides/dependency_injection/scaling.md new file mode 100644 index 0000000..a360eab --- /dev/null +++ b/docs/guides/dependency_injection/scaling.md @@ -0,0 +1,39 @@ +--- +uid: Guides.DI.Scaling +title: Scaling your DI +--- + +# Scaling your DI + +Dependency injection has a lot of use cases, and is very suitable for scaled applications. +There are a few ways to make registering & using services easier in large amounts. + +## Using a range of services. + +If you have a lot of services that all have the same use such as handling an event or serving a module, +you can register and inject them all at once by some requirements: + +- All classes need to inherit a single interface or abstract type. +- While not required, it is preferred if the interface and types share a method to call on request. +- You need to register a class that all the types can be injected into. + +### Registering implicitly + +Registering all the types is done through getting all types in the assembly and checking if they inherit the target interface. + +[!code-csharp[Registering](samples/implicit-registration.cs)] + +> [!NOTE] +> As seen above, the interfaceType and activatorType are undefined. For our usecase below, these are `IService` and `ServiceActivator` in order. + +### Using implicit dependencies + +In order to use the implicit dependencies, you have to get access to the activator you registered earlier. + +[!code-csharp[Accessing the activator](samples/access-activator.cs)] + +When the activator is accessed and the `ActivateAsync()` method is called, the following code will be executed: + +[!code-csharp[Executing the activator](samples/enumeration.cs)] + +As a result of this, all the services that were registered with `IService` as its implementation type will execute their starting code, and start up. diff --git a/docs/guides/dependency_injection/services.md b/docs/guides/dependency_injection/services.md new file mode 100644 index 0000000..e021a88 --- /dev/null +++ b/docs/guides/dependency_injection/services.md @@ -0,0 +1,48 @@ +--- +uid: Guides.DI.Services +title: Using DI in Interaction & Command Frameworks +--- + +# DI in the Interaction- & Command Service + +For both the Interaction- and Command Service modules, DI is quite straight-forward to use. + +You can inject any service into modules without the modules having to be registered to the provider. +Discord.Net resolves your dependencies internally. + +> [!WARNING] +> The way DI is used in the Interaction- & Command Service are nearly identical, except for one detail: +> [Resolving Module Dependencies](xref:Guides.IntFw.Intro#resolving-module-dependencies) + +## Registering the Service + +Thanks to earlier described behavior of allowing already registered members as parameters of the available ctors, +The socket client & configuration will automatically be acknowledged and the XService(client, config) overload will be used. + +[!code-csharp[Service Registration](samples/service-registration.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. + +[!code-csharp[Module Injection](samples/modules.cs)] + +If you accept `Command/InteractionService` or `IServiceProvider` as a parameter in your constructor or as an injectable property, +these entries will be filled by the `Command/InteractionService` that the module is loaded from and the `IServiceProvider` that is passed into it respectively. + +> [!NOTE] +> Annotating a property with a [DontInjectAttribute] attribute will +> prevent the property from being injected. + +## Services + +Because modules are transient of nature and will reinstantiate on every request, +it is suggested to create a singleton service behind it to hold values across multiple command executions. + +[!code-csharp[Services](samples/services.cs)] + + diff --git a/docs/guides/dependency_injection/types.md b/docs/guides/dependency_injection/types.md new file mode 100644 index 0000000..e539d06 --- /dev/null +++ b/docs/guides/dependency_injection/types.md @@ -0,0 +1,52 @@ +--- +uid: Guides.DI.Dependencies +title: Types of Dependencies +--- + +# Dependency Types + +There are 3 types of dependencies to learn to use. Several different usecases apply for each. + +> [!WARNING] +> When registering types with a serviceType & implementationType, +> only the serviceType will be available for injection, and the implementationType will be used for the underlying instance. + +## Singleton + +A singleton service creates a single instance when first requested, and maintains that instance across the lifetime of the application. +Any values that are changed within a singleton will be changed across all instances that depend on it, as they all have the same reference to it. + +### Registration: + +[!code-csharp[Singleton Example](samples/singleton.cs)] + +> [!NOTE] +> Types like the Discord client and Interaction/Command services are intended to be singleton, +> as they should last across the entire app and share their state with all references to the object. + +## Scoped + +A scoped service creates a new instance every time a new service is requested, but is kept across the 'scope'. +As long as the service is in view for the created scope, the same instance is used for all references to the type. +This means that you can reuse the same instance during execution, and keep the services' state for as long as the request is active. + +### Registration: + +[!code-csharp[Scoped Example](samples/scoped.cs)] + +> [!NOTE] +> Without using HTTP or libraries like EFCORE, scopes are often unused in Discord bots. +> They are most commonly used for handling HTTP and database requests. + +## Transient + +A transient service is created every time it is requested, and does not share its state between references within the target service. +It is intended for lightweight types that require little state, to be disposed quickly after execution. + +### Registration: + +[!code-csharp[Transient Example](samples/transient.cs)] + +> [!NOTE] +> Discord.Net modules behave exactly as transient types, and are intended to only last as long as the command execution takes. +> This is why it is suggested for apps to use singleton services to keep track of cross-execution data. diff --git a/docs/guides/deployment/deployment.md b/docs/guides/deployment/deployment.md new file mode 100644 index 0000000..4313e85 --- /dev/null +++ b/docs/guides/deployment/deployment.md @@ -0,0 +1,109 @@ +--- +uid: Guides.Deployment +title: Deploying the Bot +--- + +# Deploying a Discord.Net 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 +* [ServerStarter.Host](https://serverstarter.host/clients/store/discord-bots) + * Description: Bot hosting with a panel for quick deployment and + no Linux knowledge required. + * Location(s): + * America: United States + * Based in: United States + +## .NET Core Deployment + +> [!NOTE] +> This section only covers the very basics of .NET Core deployment. +> To learn more about .NET Core deployment, +> visit [.NET Core application deployment] by Microsoft. + +When redistributing the application - whether for deployment on a +remote machine or for sharing with another user - you may want to +publish the application; in other words, to create a +self-contained package without installing the dependencies +and the runtime on the target platform. + +### Framework-dependent Deployment + +To deploy a framework-dependent package (i.e. files to be used on a +remote machine with the `dotnet` command), simply publish +the package with: + +* `dotnet publish -c Release` + +This will create a package with the **least dependencies** +included with the application; however, the remote machine +must have `dotnet` runtime installed before the remote could run the +program. + +> [!TIP] +> Do not know how to run a .NET Core application with +> the `dotnet` runtime? Navigate to the folder of the program +> (typically under `$projFolder/bin/Release`) and +> enter `dotnet program.dll` where `program.dll` is your compiled +> binaries. + +### Self-contained Deployment + +To deploy a self-contained package (i.e. files to be used on a remote +machine without the `dotnet` runtime), publish with a specific +[Runtime ID] with the `-r` switch. + +This will create a package with dependencies compiled for the target +platform, meaning that all the required dependencies will be included +with the program. This will result in **larger package size**; +however, that means the copy of the runtime that can be run +natively on the target platform. + +For example, the following command will create a Windows +executable (`.exe`) that is ready to be executed on any +Windows 10 x64 based machine: + +* `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 diff --git a/docs/guides/emoji/emoji.md b/docs/guides/emoji/emoji.md new file mode 100644 index 0000000..dbf654b --- /dev/null +++ b/docs/guides/emoji/emoji.md @@ -0,0 +1,102 @@ +--- +uid: Guides.Emoji +title: Emoji +--- + +# Emoji in Discord.Net + +Before we delve into the difference between an @Discord.Emoji and an +@Discord.Emote in Discord.Net, it is **crucial** to understand what +they both look like behind the scene. When the end-users are sending +or receiving an emoji or emote, they are typically in the form of +`:ok_hand:` or `:reeee:`; however, what goes under the hood is that, +depending on the type of emoji, they are sent in an entirely +different format. + +What does this all mean? It means that you should know that by +reacting with a string like `“:ok_hand:”` will **NOT** automatically +translate to `👌`; rather, it will be treated as-is, +like `:ok_hand:`, thus the server will return a `400 Bad Request`. + +## Emoji + +An emoji is a standard emoji that can be found anywhere else outside +of Discord, which means strings like `👌`, `♥`, `👀` are all +considered an emoji in Discord. However, from the +introduction paragraph we have learned that we cannot +simply send `:ok_hand:` and have Discord take +care of it, but what do we need to send exactly? + +To send an emoji correctly, one must send the emoji in its Unicode +form; this can be obtained in several different ways. + +1. (Easiest) Escape the emoji by using the escape character, `\`, in + your Discord chat client; this will reveal the emoji’s pure Unicode + form, which will allow you to copy-paste into your code. +2. Look it up on Emojipedia, from which you can copy the emoji + easily into your code. + ![Emojipedia](images/emojipedia.png) +3. (Recommended) Look it up in the Emoji list from [FileFormat.Info]; + this will give you the .NET-compatible code that + represents the emoji. + * This is the most recommended method because some systems or + IDE sometimes do not render the Unicode emoji correctly. + ![Fileformat Emoji Source Code](images/fileformat-emoji-src.png) + +### Emoji Declaration + +After obtaining the Unicode representation of the emoji, you may +create the @Discord.Emoji object by passing the string with unicode into its +constructor (e.g. `new Emoji("👌");` or `new Emoji("\uD83D\uDC4C");`). + +Your method of declaring an @Discord.Emoji should look similar to +this: +[!code-csharp[Emoji Sample](samples/emoji-sample.cs)] + +Also you can use `Emoji.Parse()` or `Emoji.TryParse()` methods +for parsing emojis from strings like `:heart:`, `<3` or `❤`. + +[FileFormat.Info]: https://www.fileformat.info/info/emoji/list.htm + +## Emote + +The meat of the debate is here; what is an emote and how does it +differ from an emoji? An emote refers to a **custom emoji** +created on Discord. + +The underlying structure of an emote also differs drastically; an +emote looks sort-of like a mention on Discord. It consists of two +main elements as illustrated below: + +![Emote illustration](images/emote-format.png) + +As you can see, emote uses a completely different format. To obtain +the raw string as shown above for your emote, you would need to +escape the emote using the escape character `\` in chat somewhere. + +### Emote Declaration + +After obtaining the raw emote string, you would need to use +@Discord.Emote.Parse* or @Discord.Emote.TryParse* to create a valid +emote object. + +Your method of declaring an @Discord.Emote should look similar to +this: + +[!code[Emote Sample](samples/emote-sample.cs)] + +> [!TIP] +> For WebSocket users, you may also consider fetching the Emote +> via the @Discord.WebSocket.SocketGuild.Emotes collection. +> [!code-csharp[Socket emote sample](samples/socket-emote-sample.cs)] + +> [!TIP] +> On Discord, any user with Discord Nitro subscription may use +> custom emotes from any guilds they are currently in. This is also +> true for _any_ standard bot accounts; this does not require +> the bot owner to have a Nitro subscription. + +## Additional Information + +To learn more about emote and emojis and how they could be used, +see the documentation of @Discord.IEmote. diff --git a/docs/guides/emoji/images/emojipedia.png b/docs/guides/emoji/images/emojipedia.png new file mode 100644 index 0000000..acad16f Binary files /dev/null and b/docs/guides/emoji/images/emojipedia.png differ diff --git a/docs/guides/emoji/images/emote-format.png b/docs/guides/emoji/images/emote-format.png new file mode 100644 index 0000000..981e182 Binary files /dev/null and b/docs/guides/emoji/images/emote-format.png differ diff --git a/docs/guides/emoji/images/fileformat-emoji-src.png b/docs/guides/emoji/images/fileformat-emoji-src.png new file mode 100644 index 0000000..a43eebb Binary files /dev/null and b/docs/guides/emoji/images/fileformat-emoji-src.png differ diff --git a/docs/guides/emoji/samples/emoji-sample.cs b/docs/guides/emoji/samples/emoji-sample.cs new file mode 100644 index 0000000..a36e6f7 --- /dev/null +++ b/docs/guides/emoji/samples/emoji-sample.cs @@ -0,0 +1,6 @@ +public async Task ReactAsync(SocketUserMessage userMsg) +{ + // equivalent to "👌" + var emoji = new Emoji("\uD83D\uDC4C"); + await userMsg.AddReactionAsync(emoji); +} \ No newline at end of file diff --git a/docs/guides/emoji/samples/emote-sample.cs b/docs/guides/emoji/samples/emote-sample.cs new file mode 100644 index 0000000..b05ecc2 --- /dev/null +++ b/docs/guides/emoji/samples/emote-sample.cs @@ -0,0 +1,7 @@ +public async Task ReactWithEmoteAsync(SocketUserMessage userMsg, string escapedEmote) +{ + if (Emote.TryParse(escapedEmote, out var emote)) + { + await userMsg.AddReactionAsync(emote); + } +} \ No newline at end of file diff --git a/docs/guides/emoji/samples/socket-emote-sample.cs b/docs/guides/emoji/samples/socket-emote-sample.cs new file mode 100644 index 0000000..3971115 --- /dev/null +++ b/docs/guides/emoji/samples/socket-emote-sample.cs @@ -0,0 +1,11 @@ +private readonly DiscordSocketClient _client; + +public async Task ReactAsync(SocketUserMessage userMsg, string emoteName) +{ + var emote = _client.Guilds + .SelectMany(x => x.Emotes) + .FirstOrDefault(x => x.Name.IndexOf( + emoteName, StringComparison.OrdinalIgnoreCase) != -1); + if (emote == null) return; + await userMsg.AddReactionAsync(emote); +} \ No newline at end of file diff --git a/docs/guides/entities/casting.md b/docs/guides/entities/casting.md new file mode 100644 index 0000000..37d084c --- /dev/null +++ b/docs/guides/entities/casting.md @@ -0,0 +1,68 @@ +--- +uid: Guides.Entities.Casting +title: Casting & Unboxing +--- + +# Casting + +Casting can be done in many ways, and is the only method to box and unbox types to/from their base definition. +Casting only works for types that inherit the base type that you want to unbox from. +`IUser` cannot be cast to `IMessage`. + +> [!NOTE] +> Interfaces **can** be cast to other interfaces, as long as they inherit each other. +> The same goes for reverse casting. As long as some entity can be simplified into what it inherits, your cast will pass. + +## Boxing + +A boxed object is the definition of an object that was simplified (or trimmed) by incoming traffic, +but still owns the data of the originally constructed type. Boxing is an implicit operation. + +Through casting, we can **unbox** this type, and access the properties that were inaccessible before. + +## Unboxing + +Unboxing is the most direct way to access the real definition of an object. +If we want to return a type from its interface, we can unbox it directly. + +[!code-csharp[Unboxing](samples/unboxing.cs)] + +## Regular casting + +In 'regular' casting, we use the `as` keyword to assign the given type to the object. +If the boxed type can indeed be cast into given type, +it will become said type, and its properties can be accessed. +[!code-csharp[Casting](samples/casting.cs)] + +> [!WARNING] +> If the type you're casting to is null, a `NullReferenceException` will be thrown when it's called. +> This makes safety casting much more interesting to use, as it prevents this exception from being thrown. + +## Safety casting + +Safety casting makes sure that the type you're trying to cast to can never be null, as it passes checks upon calling them. + +There are 3 different ways to safety cast an object: + +### Basic safety casting: + +To safety cast an object, all we need to do is check if it is of the member type in a statement. +If this check fails, it will continue below, making sure we don't try to access null. +[!code-csharp[Base](samples/safety-cast.cs)] + +### Object declaration: + +Here we declare the object we are casting to, +making it so that you can immediately work with its properties without reassigning through regular casting. +[!code-csharp[Declare](samples/safety-cast-var.cs)] + +### Reverse passage: + +In previous examples, we want to let code continue running after the check, or if the check fails. +In this example, the cast will return the entire method (ignoring the latter) upon failure, +and declare the variable for further use into the method: +[!code-csharp[Pass](samples/safety-cast-pass.cs)] + +> [!NOTE] +> Usage of `is`, `not` and `as` is required in cast assignment and/or type checks. `==`, `!=` and `=` are invalid assignment, +> as these operators only apply to initialized objects and not their types. diff --git a/docs/guides/entities/glossary.md b/docs/guides/entities/glossary.md new file mode 100644 index 0000000..709bc96 --- /dev/null +++ b/docs/guides/entities/glossary.md @@ -0,0 +1,140 @@ +--- +uid: Guides.Entities.Glossary +title: Glossary & Flowcharts +--- + +# Entity Types + +A list of all Discord.Net entities, what they can be cast to and what their properties are. + +> [!NOTE] +> All interfaces have the same inheritance tree for both `Socket` and `Rest` entities. +> Entities with that have been marked red are exclusive to the project they source from. + +## Channels + +![IChannelChart](images/IChannel.png) + +### Message Channels +* A **Text Channel** ([ITextChannel]) is a message channel from a Guild. +* A **Thread Channel** ([IThreadChannel]) is a thread channel from a Guild. +* A **News Channel** ([INewsChannel]) (also goes as announcement channel) is a news 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 **Stage Channel** ([IStageChannel]) is a stage channel in a guild. +* A **Category Channel** ([ICategoryChannel]) is a category that +holds one or more sub-channels. +* A **Nested Channel** ([INestedChannel]) is a channel that can +exist under a category. + +[INestedChannel]: xref:Discord.INestedChannel +[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 +[IChannel]: xref:Discord.IChannel +[IThreadChannel]: xref:Discord.IThreadChannel +[IStageChannel]: xref:Discord.IStageChannel +[INewsChannel]: xref:Discord.INewsChannel + +## Messages + +![IMessageChart](images/IMessage.png) + +* A **Rest Followup Message** ([RestFollowupMessage]) is a message returned by followup on on an interaction. +* A **Rest Interaction Message** ([RestInteractionMessage]) is a message returned by the interaction's original response. +* A **Rest User Message** ([RestUserMessage]) is a message sent over rest; it can be any of the above. +* An **User Message** ([IUserMessage]) is a message sent by a user. +* A **System Message** ([ISystemMessage]) is a message sent by Discord itself. +* A **Message** ([IMessage]) can be any of the above. + +[RestFollowupMessage]: xref:Discord.Rest.RestFollowupMessage +[RestInteractionMessage]: xref:Discord.Rest.RestInteractionMessage +[RestUserMEssage]: xref:Discord.Rest.RestUserMessage +[IUserMessage]: xref:Discord.IUserMessage +[ISystemMessage]: xref:Discord.ISystemMessage +[IMessage]: xref:Discord.IMessage + +## Users + +![IUserChart](images/IUser.png) + +* A **Guild User** ([IGuildUser]) is a user available inside a guild. +* A **Group User** ([IGroupUser]) is a user available inside a group. + - This is rarely used due to the bot's inability to join groups. +* A **Self User** ([ISelfUser]) is the bot user the client is currently logged in as. +* An **User** ([IUser]) can be any of the above. + +[IGuildUser]: xref:Discord.IGuildUser +[IGroupUser]: xref:Discord.IGroupUser +[ISelfUser]: xref:Discord.ISelfUser +[IUser]: xref:Discord.IUser + +## Interactions + +![IInteractionChart](images/IInteraction.png) + +* A **Slash command** ([ISlashCommandInteraction]) is an application command executed in the text box, with provided parameters. +* A **Message Command** ([IMessageCommandInteraction]) is an application command targeting a message. +* An **User Command** ([IUserCommandInteraction]) is an application command targeting a user. +* An **Application Command** ([IApplicationCommandInteraction]) is any of the above. +* A **Message component** ([IMessageComponent]) is the interaction of a button being clicked/dropdown option(s) entered. +* An **Autocomplete Interaction** ([IAutocompleteinteraction]) is an interaction that has been automatically completed. +* An **Interaction** ([IDiscordInteraction]) is any of the above. + +[ISlashCommandInteraction]: xref:Discord.ISlashCommandInteraction +[IMessageCommandInteraction]: xref:Discord.IMessageCommandInteraction +[IUserCommandInteraction]: xref:Discord.IUserCommandInteraction +[IApplicationCommandInteraction]: xref:Discord.IApplicationCommandInteraction +[IMessageComponent]: xref:Discord.IMessageComponent +[IAutocompleteinteraction]: xref:Discord.IAutocompleteInteraction +[IDiscordInteraction]: xref:Discord.IDiscordInteraction + +## Other types: + +### Emoji + +* 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 + +### Stickers + +* A **Sticker** ([ISticker]) is a standard Discord sticker. +* A **Custom Sticker** ([ICustomSticker]) is a Guild-unique sticker. + +[ISticker]: xref:Discord.ISticker +[ICustomSticker]: xref:Discord.ICustomSticker + +### Activity + +* 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://discord.com/developers/docs/rich-presence/best-practices diff --git a/docs/guides/entities/images/IChannel.png b/docs/guides/entities/images/IChannel.png new file mode 100644 index 0000000..e2cda78 Binary files /dev/null and b/docs/guides/entities/images/IChannel.png differ diff --git a/docs/guides/entities/images/IInteraction.png b/docs/guides/entities/images/IInteraction.png new file mode 100644 index 0000000..93ca789 Binary files /dev/null and b/docs/guides/entities/images/IInteraction.png differ diff --git a/docs/guides/entities/images/IMessage.png b/docs/guides/entities/images/IMessage.png new file mode 100644 index 0000000..eff4b1d Binary files /dev/null and b/docs/guides/entities/images/IMessage.png differ diff --git a/docs/guides/entities/images/IUser.png b/docs/guides/entities/images/IUser.png new file mode 100644 index 0000000..ae4a969 Binary files /dev/null and b/docs/guides/entities/images/IUser.png differ diff --git a/docs/guides/entities/introduction.md b/docs/guides/entities/introduction.md new file mode 100644 index 0000000..46e220b --- /dev/null +++ b/docs/guides/entities/introduction.md @@ -0,0 +1,87 @@ +--- +uid: Guides.Entities.Intro +title: Introduction +--- + +# Entities in Discord.Net + +Discord.Net provides a versatile entity system for navigating the +Discord API. + +> [!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. + +## Inheritance + +Due to the nature of the Discord API, some entities are designed with +multiple variants; for example, `IUser` and `IGuildUser`. + +All models will contain the most detailed version of an entity +possible, even if the type is less detailed. + +## Socket & REST + +REST entities are retrieved over REST, and will be disposed after use. +It is suggested to limit the amount of REST calls as much as possible, +as calls over REST interact with the API, and are thus prone to rate-limits. + +- [Learn more about REST](https://restfulapi.net/) + +Socket entities are created through the gateway, +most commonly through `DiscordSocketClient` events. +These entities will enter the clients' global cache for later use. + +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 +every single case. + +But that doesn't mean a message _can't_ come from a +`SocketTextChannel`, which is a message channel in a guild. To +retrieve information about a guild from a message entity, you will +need to cast its channel object to a `SocketTextChannel`. + +> [!NOTE] +> You can find out the inheritance tree & definitions of various entities +> [here](xref:Guides.Entities.Glossary) + +## 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 Socket 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. + +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 +`SocketGuildChannel`. Again, you may need to cast these objects to get +a variant of the type that you need. + +### Sample + +[!code-csharp[Socket Sample](samples/socketentities.cs)] + +## Accessing REST Entities + +REST entities work almost the same as Socket entities, but are much less frequently used. +To access REST entities, the `DiscordSocketClient`'s `Rest` property is required. +Another option here is to create your own [DiscordRestClient], independent of the Socket gateway. + +[DiscordRestClient]: xref:Discord.Rest.DiscordRestClient + +### Sample + +[!code-csharp[Rest Sample](samples/restentities.cs)] diff --git a/docs/guides/entities/samples/casting.cs b/docs/guides/entities/samples/casting.cs new file mode 100644 index 0000000..0a7b9d1 --- /dev/null +++ b/docs/guides/entities/samples/casting.cs @@ -0,0 +1,7 @@ +// Say we have an entity; for the simplicity of this example, it will appear from thin air. +IChannel channel; + +// If we want this to be an ITextChannel so we can access the properties of a text channel inside of a guild, an approach would be: +ITextChannel textChannel = channel as ITextChannel; + +await textChannel.DoSomethingICantWithIChannelAsync(); diff --git a/docs/guides/entities/samples/restentities.cs b/docs/guides/entities/samples/restentities.cs new file mode 100644 index 0000000..36a8177 --- /dev/null +++ b/docs/guides/entities/samples/restentities.cs @@ -0,0 +1,8 @@ +// RestUser entities expose the accent color and banner of a user. +// This being one of the few use-cases for requesting a RestUser instead of depending on the Socket counterpart. +public static EmbedBuilder WithUserColor(this EmbedBuilder builder, IUser user) +{ + var restUser = await _client.Rest.GetUserAsync(user.Id); + return builder.WithColor(restUser.AccentColor ?? Color.Blue); + // The accent color can still be null, so a check for this needs to be done to prevent an exception to be thrown. +} diff --git a/docs/guides/entities/samples/safety-cast-pass.cs b/docs/guides/entities/samples/safety-cast-pass.cs new file mode 100644 index 0000000..0340774 --- /dev/null +++ b/docs/guides/entities/samples/safety-cast-pass.cs @@ -0,0 +1,10 @@ +private void MyFunction(IMessage message) +{ + // Here we do the reverse as in the previous examples, and let it continue the code below if it IS an IUserMessage + if (message is not IUserMessage userMessage) + return; + + // Because we do the above check inline (don't give the statement a body), + // the code will still declare `userMessage` as available outside of the above statement. + Console.WriteLine(userMessage.Author); +} diff --git a/docs/guides/entities/samples/safety-cast-var.cs b/docs/guides/entities/samples/safety-cast-var.cs new file mode 100644 index 0000000..bf62a20 --- /dev/null +++ b/docs/guides/entities/samples/safety-cast-var.cs @@ -0,0 +1,9 @@ +IUser user; + +// Here we can pre-define the actual declaration of said IGuildUser object, +// so we don't need to cast additionally inside of the statement. +if (user is IGuildUser guildUser) +{ + Console.WriteLine(guildUser.JoinedAt); +} +// Check failed. diff --git a/docs/guides/entities/samples/safety-cast.cs b/docs/guides/entities/samples/safety-cast.cs new file mode 100644 index 0000000..684cff8 --- /dev/null +++ b/docs/guides/entities/samples/safety-cast.cs @@ -0,0 +1,8 @@ +IUser user; + +// Here we check if the user is an IGuildUser, if not, let it pass. This ensures its not null. +if (user is IGuildUser) +{ + Console.WriteLine("This user is in a guild!"); +} +// Check failed. diff --git a/docs/guides/entities/samples/socketentities.cs b/docs/guides/entities/samples/socketentities.cs new file mode 100644 index 0000000..6438385 --- /dev/null +++ b/docs/guides/entities/samples/socketentities.cs @@ -0,0 +1,11 @@ +public string GetChannelTopic(ulong id) +{ + var channel = client.GetChannel(81384956881809408) as SocketTextChannel; + return channel?.Topic; +} + +public SocketGuildUser GetGuildOwner(SocketChannel channel) +{ + var guild = (channel as SocketGuildChannel)?.Guild; + return guild?.Owner; +} \ No newline at end of file diff --git a/docs/guides/entities/samples/unboxing.cs b/docs/guides/entities/samples/unboxing.cs new file mode 100644 index 0000000..78dd7ea --- /dev/null +++ b/docs/guides/entities/samples/unboxing.cs @@ -0,0 +1,9 @@ +IUser user; + +// Here we use inline unboxing to make a call to its member (if available) only once. + +// Note that if the entity we're trying to cast to is null, this will throw a NullReferenceException. +Console.WriteLine(((IGuildUser)user).Nickname); + +// In case you are certain the entity IS said member, you can also use unboxing to declare variables. +IGuildUser guildUser = (IGuildUser)user; diff --git a/docs/guides/getting_started/first-bot.md b/docs/guides/getting_started/first-bot.md new file mode 100644 index 0000000..a5b0dbb --- /dev/null +++ b/docs/guides/getting_started/first-bot.md @@ -0,0 +1,220 @@ +--- +uid: Guides.GettingStarted.FirstBot +title: Start making a bot +--- + +# Making Your First Bot with Discord.Net + +One of the 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. On the left-hand side, under `Settings`, click `Bot`. + + ![Step 4](images/intro-bot-settings.png) + +5. Click on `Add Bot`. + + ![Step 5](images/intro-add-bot.png) + +6. Confirm the popup. +7. (Optional) If this bot will be public, tick `Public Bot`. + + ![Step 7](images/intro-public-bot.png) + +[Discord Applications Portal]: https://discord.com/developers/applications/ + +## 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. On the left-hand side, under `Settings`, click `OAuth2`. + + ![Step 2](images/intro-oauth-settings.png) + +3. Scroll down to `OAuth2 URL Generator` and under `Scopes` tick `bot`. + + ![Step 3](images/intro-scopes-bot.png) + +4. Scroll down further to `Bot Permissions` and select the + permissions that you wish to assign your bot with. + + > [!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! + +5. Open the generated authorization URL in your browser. +6. Select a server. +7. Click on Authorize. + + > [!NOTE] + > Only servers where you have the `MANAGE_SERVER` permission will be + > present in this list. + + ![Step 6](images/intro-authorize.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 for these operations to be 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. + +[!code-csharp[Async Context](samples/first-bot/async-context.cs)] + +As a result of this, your program will now start into an async context. + +> [!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 "log in to Discord" with the [LoginAsync] +method with the application's "token." + +![Token](images/intro-token.png) + +> [!NOTE] +> Pay attention to what you are copying from the developer portal! +> A token is not the same as the application's "client secret." + + +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. + +> [!IMPORTANT] +> Your bot's token can be used to gain total access to your bot, so +> **do not** share this token with anyone else! You should store this +> token in an external source if you plan on distributing +> the source code for your bot. +> +> In the following example, we retrieve the token from a pre-defined +> variable, which is **NOT** secure, especially if you plan on +> distributing the application in any shape or form. +> +> We recommend alternative storage such as +> [Environment Variables], an external configuration file, or a +> secrets manager for safe-handling of secrets. +> +> [Environment Variables]: https://en.wikipedia.org/wiki/Environment_variable + +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. + +> [!WARNING] +> Getting a warning about `A supplied token was invalid.` and/or +> having trouble logging in? Double-check whether you have put in +> the correct credentials and make sure that it is _not_ a client +> secret, which is different from a token. + +> [!WARNING] +> 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. + +> [!NOTE] +> For your reference, you may view the [completed program]. + +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient +[LoginAsync]: xref:Discord.Rest.BaseDiscordClient.LoginAsync* +[StartAsync]: xref:Discord.WebSocket.DiscordSocketClient.StartAsync* +[installation guide]: xref:Guides.GettingStarted.Installation +[completed program]: samples/first-bot/complete.cs + +# Building a bot with commands + +To create commands for your bot, you may choose from a variety of +command processors available. Throughout the guides, we will be using +the one that Discord.Net ships with. @Guides.TextCommands.Intro will +guide you through how to setup a program that is ready for +[CommandService]. + +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 diff --git a/docs/guides/getting_started/images/appveyor-artifacts.png b/docs/guides/getting_started/images/appveyor-artifacts.png new file mode 100644 index 0000000..2f31b77 Binary files /dev/null and b/docs/guides/getting_started/images/appveyor-artifacts.png differ diff --git a/docs/guides/getting_started/images/appveyor-nupkg.png b/docs/guides/getting_started/images/appveyor-nupkg.png new file mode 100644 index 0000000..0cf3cf6 Binary files /dev/null and b/docs/guides/getting_started/images/appveyor-nupkg.png differ diff --git a/docs/guides/getting_started/images/install-rider-add.png b/docs/guides/getting_started/images/install-rider-add.png new file mode 100644 index 0000000..3f0dc72 Binary files /dev/null and b/docs/guides/getting_started/images/install-rider-add.png differ diff --git a/docs/guides/getting_started/images/install-rider-nuget-manager.png b/docs/guides/getting_started/images/install-rider-nuget-manager.png new file mode 100644 index 0000000..884b32d Binary files /dev/null and b/docs/guides/getting_started/images/install-rider-nuget-manager.png differ diff --git a/docs/guides/getting_started/images/install-rider-search.png b/docs/guides/getting_started/images/install-rider-search.png new file mode 100644 index 0000000..32f2f97 Binary files /dev/null and b/docs/guides/getting_started/images/install-rider-search.png differ diff --git a/docs/guides/getting_started/images/install-vs-deps.png b/docs/guides/getting_started/images/install-vs-deps.png new file mode 100644 index 0000000..ab10bd1 Binary files /dev/null and b/docs/guides/getting_started/images/install-vs-deps.png differ diff --git a/docs/guides/getting_started/images/install-vs-nuget.png b/docs/guides/getting_started/images/install-vs-nuget.png new file mode 100644 index 0000000..ecf627d Binary files /dev/null and b/docs/guides/getting_started/images/install-vs-nuget.png differ diff --git a/docs/guides/getting_started/images/intro-add-bot.png b/docs/guides/getting_started/images/intro-add-bot.png new file mode 100644 index 0000000..3b5343a Binary files /dev/null and b/docs/guides/getting_started/images/intro-add-bot.png differ diff --git a/docs/guides/getting_started/images/intro-authorize.png b/docs/guides/getting_started/images/intro-authorize.png new file mode 100644 index 0000000..66ca4cb Binary files /dev/null and b/docs/guides/getting_started/images/intro-authorize.png differ diff --git a/docs/guides/getting_started/images/intro-bot-settings.png b/docs/guides/getting_started/images/intro-bot-settings.png new file mode 100644 index 0000000..6ac40bf Binary files /dev/null and b/docs/guides/getting_started/images/intro-bot-settings.png differ diff --git a/docs/guides/getting_started/images/intro-oauth-settings.png b/docs/guides/getting_started/images/intro-oauth-settings.png new file mode 100644 index 0000000..7d8c2a6 Binary files /dev/null and b/docs/guides/getting_started/images/intro-oauth-settings.png differ diff --git a/docs/guides/getting_started/images/intro-public-bot.png b/docs/guides/getting_started/images/intro-public-bot.png new file mode 100644 index 0000000..da91366 Binary files /dev/null and b/docs/guides/getting_started/images/intro-public-bot.png differ diff --git a/docs/guides/getting_started/images/intro-scopes-bot.png b/docs/guides/getting_started/images/intro-scopes-bot.png new file mode 100644 index 0000000..fa17deb Binary files /dev/null and b/docs/guides/getting_started/images/intro-scopes-bot.png differ diff --git a/docs/guides/getting_started/images/intro-token.png b/docs/guides/getting_started/images/intro-token.png new file mode 100644 index 0000000..0fcdac0 Binary files /dev/null and b/docs/guides/getting_started/images/intro-token.png differ diff --git a/docs/guides/getting_started/installing.md b/docs/guides/getting_started/installing.md new file mode 100644 index 0000000..d07787b --- /dev/null +++ b/docs/guides/getting_started/installing.md @@ -0,0 +1,154 @@ +--- +uid: Guides.GettingStarted.Installation +title: Installing Discord.Net +--- + +# Discord.Net Installation + +Discord.Net is distributed through the NuGet package manager; the most +recommended way for you to install this library. Alternatively, you +may also compile this library yourself should you so desire. + +## Supported Platforms + +Discord.Net targets [.NET 6.0] and [.NET 5.0], but is also available on older versions, like [.NET Standard] and [.NET Core]; this still means +that creating applications using the latest version of .NET (6.0) +is most recommended. If you are bound by Windows-specific APIs or +other limitations, you may also consider targeting [.NET Framework] +4.6.1 or higher. + +> [!WARNING] +> Using this library with [Mono] is not supported 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 6.0]: https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-6 +[.net 5.0]: https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-5 +[.net standard]: https://docs.microsoft.com/en-us/dotnet/articles/standard/library +[.net core]: https://docs.microsoft.com/en-us/dotnet/articles/core/ +[.net framework]: https://docs.microsoft.com/en-us/dotnet/framework/get-started/ +[additional steps]: #installing-on-net-standard-11 + +## Installing + +Release builds of Discord.Net will be published to the +[official NuGet feed]. + +### Experimental/Development + +Development builds of Discord.Net will be +published to our [MyGet feed]. The MyGet feed can be used to run the latest dev branch builds. +It is not advised to use MyGet packages in a production environment, as changes may be made that negatively affect certain library functions. + +### Labs + +This exterior branch of Discord.Net has been deprecated and is no longer supported. +If you have used Discord.Net-Labs in the past, you are advised to update to the latest version of Discord.Net. +All features in Labs are implemented in the main repository. + +[official NuGet feed]: https://nuget.org +[MyGet feed]: https://www.myget.org/feed/Packages/discord-net + +### [Using Visual Studio](#tab/vs-install) + +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) + +4. In the "Browse" tab, search for `Discord.Net` +5. Install the `Discord.Net` package + + ![Step 5](images/install-vs-nuget.png) + +### [Using JetBrains Rider](#tab/rider-install) + +1. Create a new solution for your bot +2. Open the NuGet window (Tools > NuGet > Manage NuGet packages for 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](#tab/vs-code) + +1. Create a new project for your bot +2. Add `Discord.Net` to your `*.csproj` + +[!code[Sample .csproj](samples/project.xml)] + +### [Using dotnet CLI](#tab/dotnet-cli) + +1. Launch a terminal of your choice +2. Navigate to where your `*.csproj` is located +3. Enter `dotnet add package Discord.Net` + +--- + +## Compiling from Source + +In order to compile Discord.Net, you will need the following: + +### Using Visual Studio + +- [Visual Studio 2019](https://visualstudio.microsoft.com/) or later. +- [.NET 5 SDK] + +The .NET 5 workload is required during Visual Studio +installation. + +### Using Command Line + +* [.NET 5 SDK] + +## Additional Information + +### Installing on Unsupported WebSocket Platform + +When running any Discord.Net-powered bot on an older operating system +(e.g. Windows 7) that does not natively support WebSocket, +you may encounter a @System.PlatformNotSupportedException upon +connecting. + +You may resolve this by either targeting .NET Core 2.1 or higher, or +by installing one or more custom packages as listed below. + +#### [Targeting .NET Core 2.1](#tab/core2-1) + +1. Download the latest [.NET Core SDK]. +2. Create or move your existing project to use .NET Core. +3. Modify your `` tag to at least `netcoreapp2.1`, or + by adding the `--framework netcoreapp2.1` switch when building. + +#### [Custom Packages](#tab/custom-pkg) + +1. Install or compile the following packages: + + - `Discord.Net.Providers.WS4Net` + - `Discord.Net.Providers.UDPClient` (Optional) + - This is _only_ required if your bot will be utilizing voice chat. + +2. Configure your [DiscordSocketClient] to use these custom providers + over the default ones. + + * To do this, set the `WebSocketProvider` and the optional + `UdpSocketProvider` properties on the [DiscordSocketConfig] that you + are passing into your client. + +[!code-csharp[Example](samples/netstd11.cs)] + +[discordsocketclient]: xref:Discord.WebSocket.DiscordSocketClient +[discordsocketconfig]: xref:Discord.WebSocket.DiscordSocketConfig + +--- + +[.NET 5 SDK]: https://dotnet.microsoft.com/download 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 0000000..266a732 --- /dev/null +++ b/docs/guides/getting_started/samples/first-bot/async-context.cs @@ -0,0 +1,6 @@ +public class Program +{ + public static async Task Main() + { + } +} \ No newline at end of file diff --git a/docs/guides/getting_started/samples/first-bot/client.cs b/docs/guides/getting_started/samples/first-bot/client.cs new file mode 100644 index 0000000..3528fa0 --- /dev/null +++ b/docs/guides/getting_started/samples/first-bot/client.cs @@ -0,0 +1,23 @@ +private static DiscordSocketClient _client; + +public static async Task Main() +{ + _client = new DiscordSocketClient(); + + _client.Log += Log; + + // You can assign your bot token to a string, and pass that in to connect. + // This is, however, insecure, particularly if you plan to have your code hosted in a public repository. + var token = "token"; + + // Some alternative options would be to keep your token in an Environment Variable or a standalone file. + // var token = Environment.GetEnvironmentVariable("NameOfYourEnvironmentVariable"); + // var token = File.ReadAllText("token.txt"); + // var token = JsonConvert.DeserializeObject(File.ReadAllText("config.json")).Token; + + 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 0000000..50d1411 --- /dev/null +++ b/docs/guides/getting_started/samples/first-bot/complete.cs @@ -0,0 +1,21 @@ +public class Program +{ + private static DiscordSocketClient _client; + + public async Task Main() + { + _client = new DiscordSocketClient(); + _client.Log += Log; + await _client.LoginAsync(TokenType.Bot, + Environment.GetEnvironmentVariable("DiscordToken")); + await _client.StartAsync(); + + // Block this task until the program is closed. + await Task.Delay(-1); + } + 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 0000000..0df57c8 --- /dev/null +++ b/docs/guides/getting_started/samples/first-bot/logging.cs @@ -0,0 +1,5 @@ +private static 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/message.cs b/docs/guides/getting_started/samples/first-bot/message.cs new file mode 100644 index 0000000..9e2c31a --- /dev/null +++ b/docs/guides/getting_started/samples/first-bot/message.cs @@ -0,0 +1,14 @@ +public static async Task Main() +{ + // ... + _client.MessageReceived += MessageReceived; + // ... +} + +private static async Task MessageReceived(SocketMessage message) +{ + if (message.Content == "!ping") + { + await message.Channel.SendMessageAsync("Pong!"); + } +} diff --git a/docs/guides/getting_started/samples/first-bot/structure.cs b/docs/guides/getting_started/samples/first-bot/structure.cs new file mode 100644 index 0000000..1059698 --- /dev/null +++ b/docs/guides/getting_started/samples/first-bot/structure.cs @@ -0,0 +1,167 @@ +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Discord; +using Discord.Commands; +using Discord.WebSocket; + +class Program +{ + // Program entry point + static async Task Main(string[] args) + { + _client = new DiscordSocketClient(new DiscordSocketConfig + { + // How much logging do you want to see? + LogLevel = LogSeverity.Info, + + // If you or another service needs to do anything with messages + // (eg. checking Reactions, checking the content of edited/deleted messages), + // you must set the MessageCacheSize. You may adjust the number as needed. + //MessageCacheSize = 50, + + // 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 + }); + + _commands = new CommandService(new CommandServiceConfig + { + // Again, log level: + LogLevel = LogSeverity.Info, + + // There's a few more properties you can set, + // for example, case-insensitive commands. + CaseSensitiveCommands = false, + }); + + // Subscribe the logging handler to both the client and the CommandService. + _client.Log += Log; + _commands.Log += Log; + + // Setup your DI container. + _services = ConfigureServices(); + } + + private static DiscordSocketClient _client; + + // Keep the CommandService and DI container around for use with commands. + // These two types require you install the Discord.Net.Commands package. + private static CommandService _commands; + private static IServiceProvider _services; + + // If any services require the client, or the CommandService, or something else you keep on hand, + // pass them as parameters into this method as needed. + // If this method is getting pretty long, you can seperate it out into another file using partials. + private static IServiceProvider ConfigureServices() + { + var map = new ServiceCollection() + // Repeat this for all the service classes + // and other dependencies that your commands might need. + .AddSingleton(new SomeServiceClass()); + + // When all your required services are in the collection, build the container. + // Tip: There's an overload taking in a 'validateScopes' bool to make sure + // you haven't made any mistakes in your dependency graph. + return map.BuildServiceProvider(); + } + + // Example of a logging handler. This can be re-used by addons + // that ask for a Func. + private static Task Log(LogMessage message) + { + switch (message.Severity) + { + case LogSeverity.Critical: + case LogSeverity.Error: + Console.ForegroundColor = ConsoleColor.Red; + break; + case LogSeverity.Warning: + Console.ForegroundColor = ConsoleColor.Yellow; + break; + case LogSeverity.Info: + Console.ForegroundColor = ConsoleColor.White; + break; + case LogSeverity.Verbose: + case LogSeverity.Debug: + Console.ForegroundColor = ConsoleColor.DarkGray; + break; + } + Console.WriteLine($"{DateTime.Now,-19} [{message.Severity,8}] {message.Source}: {message.Message} {message.Exception}"); + Console.ResetColor(); + + // If you get an error saying 'CompletedTask' doesn't exist, + // your project is targeting .NET 4.5.2 or lower. You'll need + // to adjust your project's target framework to 4.6 or higher + // (instructions for this are easily Googled). + // If you *need* to run on .NET 4.5 for compat/other reasons, + // the alternative is to 'return Task.Delay(0);' instead. + return Task.CompletedTask; + } + + private static async Task MainAsync() + { + // Centralize the logic for commands into a separate method. + await InitCommands(); + + // Login and connect. + await _client.LoginAsync(TokenType.Bot, + // < DO NOT HARDCODE YOUR TOKEN > + Environment.GetEnvironmentVariable("DiscordToken")); + await _client.StartAsync(); + + // Wait infinitely so your bot actually stays connected. + await Task.Delay(Timeout.Infinite); + } + + private static async Task InitCommands() + { + // Either search the program and add all Module classes that can be found. + // Module classes MUST be marked 'public' or they will be ignored. + // You also need to pass your 'IServiceProvider' instance now, + // so make sure that's done before you get here. + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + // Or add Modules manually if you prefer to be a little more explicit: + await _commands.AddModuleAsync(_services); + // Note that the first one is 'Modules' (plural) and the second is 'Module' (singular). + + // Subscribe a handler to see if a message invokes a command. + _client.MessageReceived += HandleCommandAsync; + } + + private static async Task HandleCommandAsync(SocketMessage arg) + { + // Bail out if it's a System Message. + var msg = arg as SocketUserMessage; + if (msg == null) return; + + // We don't want the bot to respond to itself or other bots. + if (msg.Author.Id == _client.CurrentUser.Id || msg.Author.IsBot) return; + + // Create a number to track where the prefix ends and the command begins + int pos = 0; + // Replace the '!' with whatever character + // you want to prefix your commands with. + // Uncomment the second half if you also want + // commands to be invoked by mentioning the bot instead. + if (msg.HasCharPrefix('!', ref pos) /* || msg.HasMentionPrefix(_client.CurrentUser, ref pos) */) + { + // Create a Command Context. + var context = new SocketCommandContext(_client, msg); + + // Execute the command. (result does not indicate a return value, + // rather an object stating if the command executed successfully). + var result = await _commands.ExecuteAsync(context, pos, _services); + + // Uncomment the following lines if you want the bot + // to send a message if it failed. + // This does not catch errors from commands with 'RunMode.Async', + // subscribe a handler for '_commands.CommandExecuted' to see those. + //if (!result.IsSuccess && result.Error != CommandError.UnknownCommand) + // await msg.Channel.SendMessageAsync(result.ErrorReason); + } + } +} diff --git a/docs/guides/getting_started/samples/netstd11.cs b/docs/guides/getting_started/samples/netstd11.cs new file mode 100644 index 0000000..a857369 --- /dev/null +++ b/docs/guides/getting_started/samples/netstd11.cs @@ -0,0 +1,9 @@ +using Discord.Providers.WS4Net; +using Discord.Providers.UDPClient; +using Discord.WebSocket; +// ... +var client = new DiscordSocketClient(new DiscordSocketConfig +{ + WebSocketProvider = WS4NetProvider.Instance, + UdpSocketProvider = UDPClientProvider.Instance, +}); \ No newline at end of file diff --git a/docs/guides/getting_started/samples/nuget.config b/docs/guides/getting_started/samples/nuget.config new file mode 100644 index 0000000..bf706a0 --- /dev/null +++ b/docs/guides/getting_started/samples/nuget.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docs/guides/getting_started/samples/project.xml b/docs/guides/getting_started/samples/project.xml new file mode 100644 index 0000000..b386c0a --- /dev/null +++ b/docs/guides/getting_started/samples/project.xml @@ -0,0 +1,17 @@ + + + + + + Exe + net6.0 + + + + + + + diff --git a/docs/guides/getting_started/terminology.md b/docs/guides/getting_started/terminology.md new file mode 100644 index 0000000..facf1c7 --- /dev/null +++ b/docs/guides/getting_started/terminology.md @@ -0,0 +1,40 @@ +--- +uid: Guides.GettingStarted.Terminology +title: Terminology +--- + +# Terminology + +## Preface + +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 is split into a core library and two different +implementations - `Discord.Net.Core`, `Discord.Net.Rest`, and +`Discord.Net.WebSocket`. + +You will typically only need to use `Discord.Net.WebSocket`, +but you should be aware of the differences between them. + +> [!TIP] +> If you are looking to implement Rest based interactions, or handle calls over REST in any other way, +> `Discord.Net.Rest` is the resource most applicable to you. + +`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 +implementation are prefixed with `Rest` (e.g., `RestChannel`). + +`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`). diff --git a/docs/guides/guild_events/creating-guild-events.md b/docs/guides/guild_events/creating-guild-events.md new file mode 100644 index 0000000..2b5d898 --- /dev/null +++ b/docs/guides/guild_events/creating-guild-events.md @@ -0,0 +1,31 @@ +--- +uid: Guides.GuildEvents.Creating +title: Creating Guild Events +--- + +# Creating guild events + +You can create new guild events by using the `CreateEventAsync` function on a guild. + +### Parameters + +| Name | Type | Summary | +| ------------- | --------------------------------- | ---------------------------------------------------------------------------- | +| name | `string` | Sets the name of the event. | +| startTime | `DateTimeOffset` | Sets the start time of the event. | +| type | `GuildScheduledEventType` | Sets the type of the event. | +| privacyLevel? | `GuildScheduledEventPrivacyLevel` | Sets the privacy level of the event | +| description? | `string` | Sets the description of the event. | +| endTime? | `DateTimeOffset?` | Sets the end time of the event. | +| channelId? | `ulong?` | Sets the channel id of the event, only valid on stage or voice channel types | +| location? | `string` | Sets the location of the event, only valid on external types | + +Lets create a basic test event. + +```cs +var guild = client.GetGuild(guildId); + +var guildEvent = await guild.CreateEventAsync("test event", DateTimeOffset.UtcNow.AddDays(1), GuildScheduledEventType.External, endTime: DateTimeOffset.UtcNow.AddDays(2), location: "Space"); +``` + +This code will create an event that lasts a day and starts tomorrow. It will be an external event that's in space. diff --git a/docs/guides/guild_events/getting-event-users.md b/docs/guides/guild_events/getting-event-users.md new file mode 100644 index 0000000..f4b5388 --- /dev/null +++ b/docs/guides/guild_events/getting-event-users.md @@ -0,0 +1,16 @@ +--- +uid: Guides.GuildEvents.GettingUsers +title: Getting Guild Event Users +--- + +# Getting Event Users + +You can get a collection of users who are currently interested in the event by calling `GetUsersAsync`. This method works like any other get users method as in it returns an async enumerable. This method also supports pagination by user id. + +```cs +// get all users and flatten the result into one collection. +var users = await event.GetUsersAsync().FlattenAsync(); + +// get users around the 613425648685547541 id. +var aroundUsers = await event.GetUsersAsync(613425648685547541, Direction.Around).FlattenAsync(); +``` diff --git a/docs/guides/guild_events/intro.md b/docs/guides/guild_events/intro.md new file mode 100644 index 0000000..45df517 --- /dev/null +++ b/docs/guides/guild_events/intro.md @@ -0,0 +1,47 @@ +--- +uid: Guides.GuildEvents.Intro +title: Introduction to Guild Events +--- + +# Guild Events + +Guild events are a way to host events within a guild. They offer a lot of features and flexibility. + +## Getting started with guild events + +You can access any events within a guild by calling `GetEventsAsync` on a guild. + +```cs +var guildEvents = await guild.GetEventsAsync(); +``` + +If your working with socket guilds you can just use the `Events` property: + +```cs +var guildEvents = guild.Events; +``` + +There are also new gateway events that you can hook to receive guild scheduled events on. + +```cs +// Fired when a guild event is cancelled. +client.GuildScheduledEventCancelled += ... + +// Fired when a guild event is completed. +client.GuildScheduledEventCompleted += ... + +// Fired when a guild event is started. +client.GuildScheduledEventStarted += ... + +// Fired when a guild event is created. +client.GuildScheduledEventCreated += ... + +// Fired when a guild event is updated. +client.GuildScheduledEventUpdated += ... + +// Fired when a guild event has someone mark themselves as interested. +client.GuildScheduledEventUserAdd += ... + +// Fired when a guild event has someone remove their interested status. +client.GuildScheduledEventUserRemove += ... +``` diff --git a/docs/guides/guild_events/modifying-events.md b/docs/guides/guild_events/modifying-events.md new file mode 100644 index 0000000..b26c29e --- /dev/null +++ b/docs/guides/guild_events/modifying-events.md @@ -0,0 +1,23 @@ +--- +uid: Guides.GuildEvents.Modifying +title: Modifying Guild Events +--- + +# Modifying Events + +You can modify events using the `ModifyAsync` method to modify the event. Here are the properties you can modify: + +| Name | Type | Description | +| ------------ | --------------------------------- | -------------------------------------------- | +| ChannelId | `ulong?` | Gets or sets the channel id of the event. | +| string | `string` | Gets or sets the location of this event. | +| Name | `string` | Gets or sets the name of the event. | +| PrivacyLevel | `GuildScheduledEventPrivacyLevel` | Gets or sets the privacy level of the event. | +| StartTime | `DateTimeOffset` | Gets or sets the start time of the event. | +| EndTime | `DateTimeOffset` | Gets or sets the end time of the event. | +| Description | `string` | Gets or sets the description of the event. | +| Type | `GuildScheduledEventType` | Gets or sets the type of the event. | +| Status | `GuildScheduledEventStatus` | Gets or sets the status of the event. | + +> [!NOTE] +> All of these properties are optional. diff --git a/docs/guides/int_basics/application-commands/context-menu-commands/creating-context-menu-commands.md b/docs/guides/int_basics/application-commands/context-menu-commands/creating-context-menu-commands.md new file mode 100644 index 0000000..254506f --- /dev/null +++ b/docs/guides/int_basics/application-commands/context-menu-commands/creating-context-menu-commands.md @@ -0,0 +1,110 @@ +--- +uid: Guides.ContextCommands.Creating +title: Creating Context Commands +--- + +# Creating context menu commands. + +There are two kinds of Context Menu Commands: User Commands and Message Commands. +Each of these have a Global and Guild variant. +Global menu commands are available for every guild that adds your app. An individual app's global commands are also available in DMs if that app has a bot that shares a mutual guild with the user. + +Guild commands are specific to the guild you specify when making them. Guild commands are not available in DMs. Command names are unique per application within each scope (global and guild). That means: + +- Your app cannot have two global commands with the same name +- Your app cannot have two guild commands within the same name on the same guild +- Your app can have a global and guild command with the same name +- Multiple apps can have commands with the same names + +[!IMPORTANT] +> Apps can have a maximum of 5 global context menu commands, +> and an additional 5 guild-specific context menu commands per guild. + +## UserCommandBuilder + +The context menu user command builder will help you create user commands. The builder has these available fields and methods: + +| Name | Type | Description | +| -------- | -------- | ------------------------------------------------------------------------------------------------ | +| Name | string | The name of this context menu command. | +| WithName | Function | Sets the field name. | +| Build | Function | Builds the builder into the appropriate `UserCommandProperties` class used to make Menu commands | + +## MessageCommandBuilder + +The context menu message command builder will help you create message commands. The builder has these available fields and methods: + +| Name | Type | Description | +| -------- | -------- | --------------------------------------------------------------------------------------------------- | +| Name | string | The name of this context menu command. | +| WithName | Function | Sets the field name. | +| Build | Function | Builds the builder into the appropriate `MessageCommandProperties` class used to make Menu commands | + +> [!NOTE] +> Context Menu command names can be upper and lowercase, and use spaces. +> They cannot be registered pre-ready. + +Let's use the user command builder to make a global and guild command. + +```cs +// Let's hook the ready event for creating our commands in. +client.Ready += Client_Ready; + +... + +public async Task Client_Ready() +{ + // Let's build a guild command! We're going to need a guild so lets just put that in a variable. + var guild = client.GetGuild(guildId); + + // Next, lets create our user and message command builder. This is like the embed builder but for context menu commands. + var guildUserCommand = new UserCommandBuilder(); + var guildMessageCommand = new MessageCommandBuilder(); + + // Note: Names have to be all lowercase and match the regular expression ^[\w -]{3,32}$ + guildUserCommand.WithName("Guild User Command"); + guildMessageCommand.WithName("Guild Message Command"); + + // Descriptions are not used with User and Message commands + //guildCommand.WithDescription(""); + + // Let's do our global commands + var globalUserCommand = new UserCommandBuilder(); + globalUserCommand.WithName("Global User Command"); + var globalMessageCommand = new MessageCommandBuilder(); + globalMessageCommand.WithName("Global Message Command"); + + + try + { + // Now that we have our builder, we can call the BulkOverwriteApplicationCommandAsync to make our context commands. Note: this will overwrite all your previous commands with this array. + await guild.BulkOverwriteApplicationCommandAsync(new ApplicationCommandProperties[] + { + guildUserCommand.Build(), + guildMessageCommand.Build() + }); + + // With global commands we dont need the guild. + await client.BulkOverwriteGlobalApplicationCommandsAsync(new ApplicationCommandProperties[] + { + globalUserCommand.Build(), + globalMessageCommand.Build() + }); + } + catch(ApplicationCommandException exception) + { + // If our command was invalid, we should catch an ApplicationCommandException. This exception contains the path of the error as well as the error message. You can serialize the Error field in the exception to get a visual of where your error is. + var json = JsonConvert.SerializeObject(exception.Error, Formatting.Indented); + + // You can send this error somewhere or just print it to the console, for this example we're just going to print it. + Console.WriteLine(json); + } +} + +``` + +> [!NOTE] +> Application commands only need to be created once. They do _not_ have to be +> 'created' on every startup or connection. +> The example simple shows creating them in the ready event +> as it's simpler than creating normal bot commands to register application commands. diff --git a/docs/guides/int_basics/application-commands/context-menu-commands/receiving-context-menu-command-events.md b/docs/guides/int_basics/application-commands/context-menu-commands/receiving-context-menu-command-events.md new file mode 100644 index 0000000..c38feff --- /dev/null +++ b/docs/guides/int_basics/application-commands/context-menu-commands/receiving-context-menu-command-events.md @@ -0,0 +1,33 @@ +--- +uid: Guides.ContextCommands.Reveiving +title: Receiving Context Commands +--- + +# Receiving Context Menu events + +User commands and Message commands have their own unique event just like the other interaction types. For user commands the event is `UserCommandExecuted` and for message commands the event is `MessageCommandExecuted`. + +```cs +// For message commands +client.MessageCommandExecuted += MessageCommandHandler; + +// For user commands +client.UserCommandExecuted += UserCommandHandler; + +... + +public async Task MessageCommandHandler(SocketMessageCommand arg) +{ + Console.WriteLine("Message command received!"); +} + +public async Task UserCommandHandler(SocketUserCommand arg) +{ + Console.WriteLine("User command received!"); +} +``` + +User commands contain a SocketUser object called `Member` in their data class, showing the user that was clicked to run the command. +Message commands contain a SocketMessage object called `Message` in their data class, showing the message that was clicked to run the command. + +Both return the user who ran the command, the guild (if any), channel, etc. diff --git a/docs/guides/int_basics/application-commands/intro.md b/docs/guides/int_basics/application-commands/intro.md new file mode 100644 index 0000000..a2fd2ee --- /dev/null +++ b/docs/guides/int_basics/application-commands/intro.md @@ -0,0 +1,54 @@ +--- +uid: Guides.SlashCommands.Intro +title: Introduction to slash commands +--- + + +# Getting started with application commands. + +This guide will show you how to use application commands. +If you have extra questions that aren't covered here you can come to our +[Discord](https://discord.gg/dnet) server and ask around there. + +> [!NOTE] +> This guide shows the manual way of creating and handling application commands. We recommend using the Interaction Framework because it allows you to work with application commands +> and handle interactions in a much simpler and structurized way. You can find more info in the [Interaction Framework Intro] docs. + +## What is an application command? + +Application commands consist of three different types. Slash commands, context menu User commands and context menu Message commands. +Slash commands are made up of a name, description, and a block of options, which you can think of like arguments to a function. +The name and description help users find your command among many others, and the options validate user input as they fill out your command. +Message and User commands are only a name, to the user. So try to make the name descriptive. +They're accessed by right clicking (or long press, on mobile) a user or a message, respectively. + +All three varieties of application commands have both Global and Guild variants. +Your global commands are available in every guild that adds your application. +You can also make commands for a specific guild; they're only available in that guild. +The User and Message commands are more limited in quantity than the slash commands. +For specifics, check out their respective guide pages. + +An Interaction is the message that your application receives when a user uses a command. +It includes the values that the user submitted, as well as some metadata about this particular instance of the command being used: +the guild_id, +channel_id, +member and other fields. +You can find all the values in our data models. + +## Authorizing your bot for application commands + +There is a new special OAuth2 scope for applications called `applications.commands`. +In order to make Application Commands work within a guild, the guild must authorize your application +with the `applications.commands` scope. The bot scope is not enough. + +Head over to your discord applications OAuth2 screen and make sure to select the `application.commands` scope. + +![OAuth2 scoping](slash-commands/images/oauth.png) + +From there you can then use the link to add your bot to a server. + +> [!NOTE] +> In order for users in your guild to use your slash commands, they need to have +> the "Use Application Commands" permission on the guild. + +[Interaction Framework Intro]: xref:Guides.IntFw.Intro diff --git a/docs/guides/int_basics/application-commands/slash-commands/bulk-overwrite-of-global-slash-commands.md b/docs/guides/int_basics/application-commands/slash-commands/bulk-overwrite-of-global-slash-commands.md new file mode 100644 index 0000000..6a64b9c --- /dev/null +++ b/docs/guides/int_basics/application-commands/slash-commands/bulk-overwrite-of-global-slash-commands.md @@ -0,0 +1,41 @@ +--- +uid: Guides.SlashCommands.BulkOverwrite +title: Slash Command Bulk Overwrites +--- + +If you have too many global commands then you might want to consider using the bulk overwrite function. + +```cs +public async Task Client_Ready() +{ + List applicationCommandProperties = new(); + try + { + // Simple help slash command. + SlashCommandBuilder globalCommandHelp = new SlashCommandBuilder(); + globalCommandHelp.WithName("help"); + globalCommandHelp.WithDescription("Shows information about the bot."); + applicationCommandProperties.Add(globalCommandHelp.Build()); + + // Slash command with name as its parameter. + SlashCommandOptionBuilder slashCommandOptionBuilder = new(); + slashCommandOptionBuilder.WithName("name"); + slashCommandOptionBuilder.WithType(ApplicationCommandOptionType.String); + slashCommandOptionBuilder.WithDescription("Add a family"); + slashCommandOptionBuilder.WithRequired(true); // Only add this if you want it to be required + + SlashCommandBuilder globalCommandAddFamily = new SlashCommandBuilder(); + globalCommandAddFamily.WithName("add-family"); + globalCommandAddFamily.WithDescription("Add a family"); + globalCommandAddFamily.AddOptions(slashCommandOptionBuilder); + applicationCommandProperties.Add(globalCommandAddFamily.Build()); + + await _client.BulkOverwriteGlobalApplicationCommandsAsync(applicationCommandProperties.ToArray()); + } + catch (ApplicationCommandException exception) + { + var json = JsonConvert.SerializeObject(exception.Error, Formatting.Indented); + Console.WriteLine(json); + } +} +``` diff --git a/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md b/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md new file mode 100644 index 0000000..46805eb --- /dev/null +++ b/docs/guides/int_basics/application-commands/slash-commands/choice-slash-command.md @@ -0,0 +1,85 @@ +--- +uid: Guides.SlashCommands.Choices +title: Slash Command Choices +--- + +# Slash Command Choices. + +With slash command options you can add choices, making the user select between some set values. Lets create a command that asks how much they like our bot! + +Let's set up our slash command: + +```cs +private async Task Client_Ready() +{ + ulong guildId = 848176216011046962; + + var guildCommand = new SlashCommandBuilder() + .WithName("feedback") + .WithDescription("Tell us how much you are enjoying this bot!") + .AddOption(new SlashCommandOptionBuilder() + .WithName("rating") + .WithDescription("The rating your willing to give our bot") + .WithRequired(true) + .AddChoice("Terrible", 1) + .AddChoice("Meh", 2) + .AddChoice("Good", 3) + .AddChoice("Lovely", 4) + .AddChoice("Excellent!", 5) + .WithType(ApplicationCommandOptionType.Integer) + ); + + try + { + await client.Rest.CreateGuildCommand(guildCommand.Build(), guildId); + } + catch(ApplicationCommandException exception) + { + var json = JsonConvert.SerializeObject(exception.Error, Formatting.Indented); + Console.WriteLine(json); + } +} +``` +> [!NOTE] +> Your `ApplicationCommandOptionType` specifies which type your choices are, you need to use `ApplicationCommandOptionType.Integer` for choices whos values are whole numbers, `ApplicationCommandOptionType.Number` for choices whos values are doubles, and `ApplicationCommandOptionType.String` for string values. + +We have defined 5 choices for the user to pick from, each choice has a value assigned to it. The value can either be a string or an int. In our case we're going to use an int. This is what the command looks like: + +![feedback style](images/feedback1.png) + +Lets add our code for handling the interaction. + +```cs +private async Task SlashCommandHandler(SocketSlashCommand command) +{ + // Let's add a switch statement for the command name so we can handle multiple commands in one event. + switch(command.Data.Name) + { + case "list-roles": + await HandleListRoleCommand(command); + break; + case "settings": + await HandleSettingsCommand(command); + break; + case "feedback": + await HandleFeedbackCommand(command); + break; + } +} + +private async Task HandleFeedbackCommand(SocketSlashCommand command) +{ + var embedBuilder = new EmbedBuilder() + .WithAuthor(command.User) + .WithTitle("Feedback") + .WithDescription($"Thanks for your feedback! You rated us {command.Data.Options.First().Value}/5") + .WithColor(Color.Green) + .WithCurrentTimestamp(); + + await command.RespondAsync(embed: embedBuilder.Build()); +} +``` + +And this is the result: + +![feedback working](images/feedback2.png) diff --git a/docs/guides/int_basics/application-commands/slash-commands/creating-slash-commands.md b/docs/guides/int_basics/application-commands/slash-commands/creating-slash-commands.md new file mode 100644 index 0000000..24dab0e --- /dev/null +++ b/docs/guides/int_basics/application-commands/slash-commands/creating-slash-commands.md @@ -0,0 +1,96 @@ +--- +uid: Guides.SlashCommands.Creating +title: Creating Slash Commands +--- + +# Creating your first slash commands. + +There are two kinds of Slash Commands: global commands and guild commands. +Global commands are available for every guild that adds your app. An individual app's global commands are also available in DMs if that app has a bot that shares a mutual guild with the user. + +Guild commands are specific to the guild you specify when making them. Guild commands are not available in DMs. Command names are unique per application within each scope (global and guild). That means: + +- Your app cannot have two global commands with the same name +- Your app cannot have two guild commands within the same name on the same guild +- Your app can have a global and guild command with the same name +- Multiple apps can have commands with the same names + +**Note**: Apps can have a maximum of 100 global commands, and an additional 100 guild-specific commands per guild. + +If you don't have the code for a bot ready yet please follow [this guide](https://discordnet.dev/guides/getting_started/first-bot.html). + +## SlashCommandBuilder + +The slash command builder will help you create slash commands. The builder has these available fields and methods: + +| Name | Type | Description | +| --------------------- | -------------------------------- | -------------------------------------------------------------------------------------------- | +| MaxNameLength | const int | The maximum length of a name for a slash command allowed by Discord. | +| MaxDescriptionLength | const int | The maximum length of a commands description allowed by Discord. | +| MaxOptionsCount | const int | The maximum count of command options allowed by Discord | +| Name | string | The name of this slash command. | +| Description | string | A 1-100 length description of this slash command | +| Options | List\ | The options for this command. | +| DefaultPermission | bool | Whether the command is enabled by default when the app is added to a guild. | +| WithName | Function | Sets the field name. | +| WithDescription | Function | Sets the description of the current command. | +| WithDefaultPermission | Function | Sets the default permission of the current command. | +| AddOption | Function | Adds an option to the current slash command. | +| Build | Function | Builds the builder into a `SlashCommandCreationProperties` class used to make slash commands | + +> [!NOTE] +> Slash command names must be all lowercase! + +## Creating a Slash Command + +Let's use the slash command builder to make a global and guild command. + +```cs +// Let's hook the ready event for creating our commands in. +client.Ready += Client_Ready; + +... + +public async Task Client_Ready() +{ + // Let's build a guild command! We're going to need a guild so lets just put that in a variable. + var guild = client.GetGuild(guildId); + + // Next, lets create our slash command builder. This is like the embed builder but for slash commands. + var guildCommand = new SlashCommandBuilder(); + + // Note: Names have to be all lowercase and match the regular expression ^[\w-]{3,32}$ + guildCommand.WithName("first-command"); + + // Descriptions can have a max length of 100. + guildCommand.WithDescription("This is my first guild slash command!"); + + // Let's do our global command + var globalCommand = new SlashCommandBuilder(); + globalCommand.WithName("first-global-command"); + globalCommand.WithDescription("This is my first global slash command"); + + try + { + // Now that we have our builder, we can call the CreateApplicationCommandAsync method to make our slash command. + await guild.CreateApplicationCommandAsync(guildCommand.Build()); + + // With global commands we don't need the guild. + await client.CreateGlobalApplicationCommandAsync(globalCommand.Build()); + // Using the ready event is a simple implementation for the sake of the example. Suitable for testing and development. + // For a production bot, it is recommended to only run the CreateGlobalApplicationCommandAsync() once for each command. + } + catch(ApplicationCommandException exception) + { + // If our command was invalid, we should catch an ApplicationCommandException. This exception contains the path of the error as well as the error message. You can serialize the Error field in the exception to get a visual of where your error is. + var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented); + + // You can send this error somewhere or just print it to the console, for this example we're just going to print it. + Console.WriteLine(json); + } +} + +``` + +> [!NOTE] +> Slash commands only need to be created once. They do _not_ have to be 'created' on every startup or connection. The example simple shows creating them in the ready event as it's simpler than creating normal bot commands to register slash commands. The global commands take up to an hour to register every time the CreateGlobalApplicationCommandAsync() is called for a given command. diff --git a/docs/guides/int_basics/application-commands/slash-commands/images/ephemeral1.png b/docs/guides/int_basics/application-commands/slash-commands/images/ephemeral1.png new file mode 100644 index 0000000..61eab94 Binary files /dev/null and b/docs/guides/int_basics/application-commands/slash-commands/images/ephemeral1.png differ diff --git a/docs/guides/int_basics/application-commands/slash-commands/images/feedback1.png b/docs/guides/int_basics/application-commands/slash-commands/images/feedback1.png new file mode 100644 index 0000000..08e5b8c Binary files /dev/null and b/docs/guides/int_basics/application-commands/slash-commands/images/feedback1.png differ diff --git a/docs/guides/int_basics/application-commands/slash-commands/images/feedback2.png b/docs/guides/int_basics/application-commands/slash-commands/images/feedback2.png new file mode 100644 index 0000000..3e75c87 Binary files /dev/null and b/docs/guides/int_basics/application-commands/slash-commands/images/feedback2.png differ diff --git a/docs/guides/int_basics/application-commands/slash-commands/images/listroles1.png b/docs/guides/int_basics/application-commands/slash-commands/images/listroles1.png new file mode 100644 index 0000000..43015e2 Binary files /dev/null and b/docs/guides/int_basics/application-commands/slash-commands/images/listroles1.png differ diff --git a/docs/guides/int_basics/application-commands/slash-commands/images/listroles2.png b/docs/guides/int_basics/application-commands/slash-commands/images/listroles2.png new file mode 100644 index 0000000..d0b9543 Binary files /dev/null and b/docs/guides/int_basics/application-commands/slash-commands/images/listroles2.png differ diff --git a/docs/guides/int_basics/application-commands/slash-commands/images/oauth.png b/docs/guides/int_basics/application-commands/slash-commands/images/oauth.png new file mode 100644 index 0000000..e0f8224 Binary files /dev/null and b/docs/guides/int_basics/application-commands/slash-commands/images/oauth.png differ diff --git a/docs/guides/int_basics/application-commands/slash-commands/images/settings1.png b/docs/guides/int_basics/application-commands/slash-commands/images/settings1.png new file mode 100644 index 0000000..0eb4d71 Binary files /dev/null and b/docs/guides/int_basics/application-commands/slash-commands/images/settings1.png differ diff --git a/docs/guides/int_basics/application-commands/slash-commands/images/settings2.png b/docs/guides/int_basics/application-commands/slash-commands/images/settings2.png new file mode 100644 index 0000000..5ced631 Binary files /dev/null and b/docs/guides/int_basics/application-commands/slash-commands/images/settings2.png differ diff --git a/docs/guides/int_basics/application-commands/slash-commands/images/settings3.png b/docs/guides/int_basics/application-commands/slash-commands/images/settings3.png new file mode 100644 index 0000000..4851108 Binary files /dev/null and b/docs/guides/int_basics/application-commands/slash-commands/images/settings3.png differ diff --git a/docs/guides/int_basics/application-commands/slash-commands/images/slashcommand1.png b/docs/guides/int_basics/application-commands/slash-commands/images/slashcommand1.png new file mode 100644 index 0000000..0c4e0ae Binary files /dev/null and b/docs/guides/int_basics/application-commands/slash-commands/images/slashcommand1.png differ diff --git a/docs/guides/int_basics/application-commands/slash-commands/images/slashcommand2.png b/docs/guides/int_basics/application-commands/slash-commands/images/slashcommand2.png new file mode 100644 index 0000000..828d8a2 Binary files /dev/null and b/docs/guides/int_basics/application-commands/slash-commands/images/slashcommand2.png differ diff --git a/docs/guides/int_basics/application-commands/slash-commands/parameters.md b/docs/guides/int_basics/application-commands/slash-commands/parameters.md new file mode 100644 index 0000000..a9371ab --- /dev/null +++ b/docs/guides/int_basics/application-commands/slash-commands/parameters.md @@ -0,0 +1,104 @@ +--- +uid: Guides.SlashCommands.Parameters +title: Slash Command Parameters +--- + +# Slash command parameters + +Slash commands can have a bunch of parameters, each their own type. Let's first go over the types of parameters we can have. + +| Name | Description | +| --------------- | -------------------------------------------------- | +| SubCommand | A subcommand inside of a subcommand group. | +| SubCommandGroup | The parent command group of subcommands. | +| String | A string of text. | +| Integer | A number. | +| Boolean | True or False. | +| User | A user | +| Role | A role. | +| Channel | A channel, this includes voice text and categories | +| Mentionable | A role or a user. | +| File | A file | + +Each one of the parameter types has its own DNET type in the `SocketSlashCommandDataOption`'s Value field: +| Name | C# Type | +| --------------- | ------------------------------------------------ | +| SubCommand | NA | +| SubCommandGroup | NA | +| String | `string` | +| Integer | `int` | +| Boolean | `bool` | +| User | `SocketGuildUser` or `SocketUser` | +| Role | `SocketRole` | +| Channel | `SocketChannel` | +| Mentionable | `SocketUser`, `SocketGuildUser`, or `SocketRole` | +| File | `IAttachment` | + +Let's start by making a command that takes in a user and lists their roles. + +```cs +client.Ready += Client_Ready; + +... + +public async Task Client_Ready() +{ + ulong guildId = 848176216011046962; + + var guildCommand = new Discord.SlashCommandBuilder() + .WithName("list-roles") + .WithDescription("Lists all roles of a user.") + .AddOption("user", ApplicationCommandOptionType.User, "The users whos roles you want to be listed", isRequired: true); + + try + { + await client.Rest.CreateGuildCommand(guildCommand.Build(), guildId); + } + catch(ApplicationCommandException exception) + { + var json = JsonConvert.SerializeObject(exception.Error, Formatting.Indented); + Console.WriteLine(json); + } +} + +``` + +![list roles command](images/listroles1.png) + +That seems to be working, now Let's handle the interaction. + +```cs +private async Task SlashCommandHandler(SocketSlashCommand command) +{ + // Let's add a switch statement for the command name so we can handle multiple commands in one event. + switch(command.Data.Name) + { + case "list-roles": + await HandleListRoleCommand(command); + break; + } +} + +private async Task HandleListRoleCommand(SocketSlashCommand command) +{ + // We need to extract the user parameter from the command. since we only have one option and it's required, we can just use the first option. + var guildUser = (SocketGuildUser)command.Data.Options.First().Value; + + // We remove the everyone role and select the mention of each role. + var roleList = string.Join(",\n", guildUser.Roles.Where(x => !x.IsEveryone).Select(x => x.Mention)); + + var embedBuiler = new EmbedBuilder() + .WithAuthor(guildUser.ToString(), guildUser.GetAvatarUrl() ?? guildUser.GetDefaultAvatarUrl()) + .WithTitle("Roles") + .WithDescription(roleList) + .WithColor(Color.Green) + .WithCurrentTimestamp(); + + // Now, Let's respond with the embed. + await command.RespondAsync(embed: embedBuiler.Build()); +} +``` + +![working list roles](images/listroles2.png) + +That has worked! Next, we will go over responding ephemerally. diff --git a/docs/guides/int_basics/application-commands/slash-commands/responding-ephemerally.md b/docs/guides/int_basics/application-commands/slash-commands/responding-ephemerally.md new file mode 100644 index 0000000..89ece71 --- /dev/null +++ b/docs/guides/int_basics/application-commands/slash-commands/responding-ephemerally.md @@ -0,0 +1,23 @@ +--- +uid: Guides.SlashCommands.Ephemeral +title: Ephemeral Responses +--- + +# Responding ephemerally + +What is an ephemeral response? Basically, only the user who executed the command can see the result of it, this is pretty simple to implement. + +> [!NOTE] +> You don't have to run arg.DeferAsync() to capture the interaction, you can use arg.RespondAsync() with a message to capture it, this also follows the ephemeral rule. + +When responding with either `FollowupAsync` or `RespondAsync` you can pass in an `ephemeral` property. When setting it to true it will respond ephemerally, false and it will respond non-ephemerally. + +Let's use this in our list role command. + +```cs +await command.RespondAsync(embed: embedBuilder.Build(), ephemeral: true); +``` + +Running the command now only shows the message to us! + +![ephemeral command](images/ephemeral1.png) diff --git a/docs/guides/int_basics/application-commands/slash-commands/responding-to-slash-commands.md b/docs/guides/int_basics/application-commands/slash-commands/responding-to-slash-commands.md new file mode 100644 index 0000000..3dbc579 --- /dev/null +++ b/docs/guides/int_basics/application-commands/slash-commands/responding-to-slash-commands.md @@ -0,0 +1,40 @@ +--- +uid: Guides.SlashCommands.Receiving +title: Receiving and Responding to Slash Commands +--- + +# Responding to interactions. + +Interactions are the base thing sent over by Discord. Slash commands are one of the interaction types. We can listen to the `SlashCommandExecuted` event to respond to them. Lets add this to our code: + +```cs +client.SlashCommandExecuted += SlashCommandHandler; + +... + +private async Task SlashCommandHandler(SocketSlashCommand command) +{ + +} +``` + +With every type of interaction there is a `Data` field. This is where the relevant information lives about our command that was executed. In our case, `Data` is a `SocketSlashCommandData` instance. In the data class, we can access the name of the command triggered as well as the options if there were any. For this example, we're just going to respond with the name of the command executed. + +```cs +private async Task SlashCommandHandler(SocketSlashCommand command) +{ + await command.RespondAsync($"You executed {command.Data.Name}"); +} +``` + +Let's try this out! + +![slash command picker](images/slashcommand1.png) + +![slash command result](images/slashcommand2.png) + +> [!NOTE] +> After receiving an interaction, you must respond to acknowledge it. You can choose to respond with a message immediately using `RespondAsync()` or you can choose to send a deferred response with `DeferAsync()`. +> If choosing a deferred response, the user will see a loading state for the interaction, and you'll have up to 15 minutes to edit the original deferred response using `ModifyOriginalResponseAsync()`. You can read more about response types [here](https://discord.com/developers/docs/interactions/slash-commands#interaction-response) + +This seems to be working! Next, we will look at parameters for slash commands. diff --git a/docs/guides/int_basics/application-commands/slash-commands/subcommands.md b/docs/guides/int_basics/application-commands/slash-commands/subcommands.md new file mode 100644 index 0000000..83d7b28 --- /dev/null +++ b/docs/guides/int_basics/application-commands/slash-commands/subcommands.md @@ -0,0 +1,219 @@ +--- +uid: Guides.SlashCommands.SubCommand +title: Sub Commands +--- + +# Subcommands + +Subcommands allow you to have multiple commands available in a single command. They can be useful for representing sub options for a command. For example: A settings command. Let's first look at some limitations with subcommands set by discord. + +- An app can have up to 25 subcommand groups on a top-level command +- An app can have up to 25 subcommands within a subcommand group +- commands can have up to 25 `options` +- options can have up to 25 `choices` + +``` +VALID + +command +| +|__ subcommand +| +|__ subcommand + +---- + +command +| +|__ subcommand-group + | + |__ subcommand +| +|__ subcommand-group + | + |__ subcommand + + +------- + +INVALID + + +command +| +|__ subcommand-group + | + |__ subcommand-group +| +|__ subcommand-group + | + |__ subcommand-group + +---- + +INVALID + +command +| +|__ subcommand + | + |__ subcommand-group +| +|__ subcommand + | + |__ subcommand-group +``` + +Let's write a settings command that can change 3 fields in our bot. + +```cs +public string FieldA { get; set; } = "test"; +public int FieldB { get; set; } = 10; +public bool FieldC { get; set; } = true; + +public async Task Client_Ready() +{ + ulong guildId = 848176216011046962; + + var guildCommand = new SlashCommandBuilder() + .WithName("settings") + .WithDescription("Changes some settings within the bot.") + .AddOption(new SlashCommandOptionBuilder() + .WithName("field-a") + .WithDescription("Gets or sets the field A") + .WithType(ApplicationCommandOptionType.SubCommandGroup) + .AddOption(new SlashCommandOptionBuilder() + .WithName("set") + .WithDescription("Sets the field A") + .WithType(ApplicationCommandOptionType.SubCommand) + .AddOption("value", ApplicationCommandOptionType.String, "the value to set the field", isRequired: true) + ).AddOption(new SlashCommandOptionBuilder() + .WithName("get") + .WithDescription("Gets the value of field A.") + .WithType(ApplicationCommandOptionType.SubCommand) + ) + ).AddOption(new SlashCommandOptionBuilder() + .WithName("field-b") + .WithDescription("Gets or sets the field B") + .WithType(ApplicationCommandOptionType.SubCommandGroup) + .AddOption(new SlashCommandOptionBuilder() + .WithName("set") + .WithDescription("Sets the field B") + .WithType(ApplicationCommandOptionType.SubCommand) + .AddOption("value", ApplicationCommandOptionType.Integer, "the value to set the fie to.", isRequired: true) + ).AddOption(new SlashCommandOptionBuilder() + .WithName("get") + .WithDescription("Gets the value of field B.") + .WithType(ApplicationCommandOptionType.SubCommand) + ) + ).AddOption(new SlashCommandOptionBuilder() + .WithName("field-c") + .WithDescription("Gets or sets the field C") + .WithType(ApplicationCommandOptionType.SubCommandGroup) + .AddOption(new SlashCommandOptionBuilder() + .WithName("set") + .WithDescription("Sets the field C") + .WithType(ApplicationCommandOptionType.SubCommand) + .AddOption("value", ApplicationCommandOptionType.Boolean, "the value to set the fie to.", isRequired: true) + ).AddOption(new SlashCommandOptionBuilder() + .WithName("get") + .WithDescription("Gets the value of field C.") + .WithType(ApplicationCommandOptionType.SubCommand) + ) + ); + + try + { + await client.Rest.CreateGuildCommand(guildCommand.Build(), guildId); + } + catch(ApplicationCommandException exception) + { + var json = JsonConvert.SerializeObject(exception.Error, Formatting.Indented); + Console.WriteLine(json); + } +} +``` + +All that code generates a command that looks like this: +![settings](images/settings1.png) + +Now that we have our command made, we need to handle the multiple options with this command. So lets add this into our handler: + +```cs +private async Task SlashCommandHandler(SocketSlashCommand command) +{ + // Let's add a switch statement for the command name so we can handle multiple commands in one event. + switch(command.Data.Name) + { + case "list-roles": + await HandleListRoleCommand(command); + break; + case "settings": + await HandleSettingsCommand(command); + break; + } +} + +private async Task HandleSettingsCommand(SocketSlashCommand command) +{ + // First lets extract our variables + var fieldName = command.Data.Options.First().Name; + var getOrSet = command.Data.Options.First().Options.First().Name; + // Since there is no value on a get command, we use the ? operator because "Options" can be null. + var value = command.Data.Options.First().Options.First().Options?.FirstOrDefault().Value; + + switch (fieldName) + { + case "field-a": + { + if(getOrSet == "get") + { + await command.RespondAsync($"The value of `field-a` is `{FieldA}`"); + } + else if (getOrSet == "set") + { + this.FieldA = (string)value; + await command.RespondAsync($"`field-a` has been set to `{FieldA}`"); + } + } + break; + case "field-b": + { + if (getOrSet == "get") + { + await command.RespondAsync($"The value of `field-b` is `{FieldB}`"); + } + else if (getOrSet == "set") + { + this.FieldB = (int)value; + await command.RespondAsync($"`field-b` has been set to `{FieldB}`"); + } + } + break; + case "field-c": + { + if (getOrSet == "get") + { + await command.RespondAsync($"The value of `field-c` is `{FieldC}`"); + } + else if (getOrSet == "set") + { + this.FieldC = (bool)value; + await command.RespondAsync($"`field-c` has been set to `{FieldC}`"); + } + } + break; + } +} + +``` + +Now, let's try this out! Running the 3 get commands seems to get the default values we set. + +![settings get](images/settings2.png) + +Now let's try changing each to a different value. + +![settings set](images/settings3.png) + +That has worked! Next, let't look at choices in commands. diff --git a/docs/guides/int_basics/message-components/advanced.md b/docs/guides/int_basics/message-components/advanced.md new file mode 100644 index 0000000..14dc94e --- /dev/null +++ b/docs/guides/int_basics/message-components/advanced.md @@ -0,0 +1,87 @@ +--- +uid: Guides.MessageComponents.Advanced +title: Advanced Concepts +--- + +# Advanced + +Lets say you have some components on an ephemeral slash command, and you want to modify the message that the button is on. The issue with this is that ephemeral messages are not stored and can not be get via rest or other means. + +Luckily, Discord thought of this and introduced a way to modify them with interactions. + +### Using the UpdateAsync method + +Components come with an `UpdateAsync` method that can update the message that the component was on. You can use it like a `ModifyAsync` method. + +Lets use it with a command, we first create our command, in this example im just going to use a message command: + +```cs +var command = new MessageCommandBuilder() + .WithName("testing").Build(); + +await client.GetGuild(guildId).BulkOverwriteApplicationCommandAsync(new [] { command, buttonCommand }); +``` + +Next, we listen for this command, and respond with some components when its used: + +```cs +var menu = new SelectMenuBuilder() +{ + CustomId = "select-1", + Placeholder = "Select Somthing!", + MaxValues = 1, + MinValues = 1, +}; + +menu.AddOption("Meh", "1", "Its not gaming.") + .AddOption("Ish", "2", "Some would say that this is gaming.") + .AddOption("Moderate", "3", "It could pass as gaming") + .AddOption("Confirmed", "4", "We are gaming") + .AddOption("Excellent", "5", "It is renowned as gaming nation wide", new Emoji("🔥")); + +var components = new ComponentBuilder() + .WithSelectMenu(menu); + + +await arg.RespondAsync("On a scale of one to five, how gaming is this?", component: components.Build(), ephemeral: true); +break; +``` + +Now, let's listen to the select menu executed event and add a case for `select-1` + +```cs +client.SelectMenuExecuted += SelectMenuHandler; + +... + +public async Task SelectMenuHandler(SocketMessageComponent arg) +{ + switch (arg.Data.CustomId) + { + case "select-1": + var value = arg.Data.Values.First(); + var menu = new SelectMenuBuilder() + { + CustomId = "select-1", + Placeholder = $"{(arg.Message.Components.First().Components.First() as SelectMenu).Options.FirstOrDefault(x => x.Value == value).Label}", + MaxValues = 1, + MinValues = 1, + Disabled = true + }; + + menu.AddOption("Meh", "1", "Its not gaming.") + .AddOption("Ish", "2", "Some would say that this is gaming.") + .AddOption("Moderate", "3", "It could pass as gaming") + .AddOption("Confirmed", "4", "We are gaming") + .AddOption("Excellent", "5", "It is renowned as gaming nation wide", new Emoji("🔥")); + + // We use UpdateAsync to update the message and its original content and components. + await arg.UpdateAsync(x => + { + x.Content = $"Thank you {arg.User.Mention} for rating us {value}/5 on the gaming scale"; + x.Components = new ComponentBuilder().WithSelectMenu(menu).Build(); + }); + break; + } +} +``` diff --git a/docs/guides/int_basics/message-components/buttons-in-depth.md b/docs/guides/int_basics/message-components/buttons-in-depth.md new file mode 100644 index 0000000..ace1e05 --- /dev/null +++ b/docs/guides/int_basics/message-components/buttons-in-depth.md @@ -0,0 +1,46 @@ +--- +uid: Guides.MessageComponents.Buttons +title: Buttons in Depth +--- + +# Buttons in depth + +There are many changes you can make to buttons, lets take a look at the parameters in the `WithButton` function. + +| Name | Type | Description | +|----------|---------------|----------------------------------------------------------------| +| label | `string` | The label text for the button. | +| customId | `string` | The custom id of the button. | +| style | `ButtonStyle` | The style of the button. | +| emote | `IEmote` | A IEmote to be used with this button. | +| url | `string` | A URL to be used only if the `ButtonStyle` is a Link. | +| disabled | `bool` | Whether or not the button is disabled. | +| row | `int` | The row to place the button if it has enough room, otherwise 0 | + +### Label + +This is the front facing text that the user sees. The maximum length is 80 characters. + +### CustomId + +This is the property sent to you by discord when a button is clicked. It is not required for link buttons as they do not emit an event. The maximum length is 100 characters. + +### Style + +Styling your buttons are important for indicating different actions: + +![](images/image3.png) + +You can do this by using the `ButtonStyle` which has all the styles defined. + +### Emote + +You can specify an `IEmote` when creating buttons to add them to your button. They have the same restrictions as putting guild based emotes in messages. + +### Url + +If you use the link style with your button you can specify a url. When this button is clicked the user is taken to that url. + +### Disabled + +You can specify if your button is disabled, meaning users won't be able to click on it. diff --git a/docs/guides/int_basics/message-components/images/image1.png b/docs/guides/int_basics/message-components/images/image1.png new file mode 100644 index 0000000..a161d8a Binary files /dev/null and b/docs/guides/int_basics/message-components/images/image1.png differ diff --git a/docs/guides/int_basics/message-components/images/image2.png b/docs/guides/int_basics/message-components/images/image2.png new file mode 100644 index 0000000..9303de9 Binary files /dev/null and b/docs/guides/int_basics/message-components/images/image2.png differ diff --git a/docs/guides/int_basics/message-components/images/image3.png b/docs/guides/int_basics/message-components/images/image3.png new file mode 100644 index 0000000..7480e1d Binary files /dev/null and b/docs/guides/int_basics/message-components/images/image3.png differ diff --git a/docs/guides/int_basics/message-components/images/image4.png b/docs/guides/int_basics/message-components/images/image4.png new file mode 100644 index 0000000..c54ab79 Binary files /dev/null and b/docs/guides/int_basics/message-components/images/image4.png differ diff --git a/docs/guides/int_basics/message-components/images/image5.png b/docs/guides/int_basics/message-components/images/image5.png new file mode 100644 index 0000000..096b758 Binary files /dev/null and b/docs/guides/int_basics/message-components/images/image5.png differ diff --git a/docs/guides/int_basics/message-components/images/image6.png b/docs/guides/int_basics/message-components/images/image6.png new file mode 100644 index 0000000..1536096 Binary files /dev/null and b/docs/guides/int_basics/message-components/images/image6.png differ diff --git a/docs/guides/int_basics/message-components/images/image7.png b/docs/guides/int_basics/message-components/images/image7.png new file mode 100644 index 0000000..5ff55a5 Binary files /dev/null and b/docs/guides/int_basics/message-components/images/image7.png differ diff --git a/docs/guides/int_basics/message-components/images/image8.png b/docs/guides/int_basics/message-components/images/image8.png new file mode 100644 index 0000000..0268313 Binary files /dev/null and b/docs/guides/int_basics/message-components/images/image8.png differ diff --git a/docs/guides/int_basics/message-components/images/image9.png b/docs/guides/int_basics/message-components/images/image9.png new file mode 100644 index 0000000..6a9850f Binary files /dev/null and b/docs/guides/int_basics/message-components/images/image9.png differ diff --git a/docs/guides/int_basics/message-components/intro.md b/docs/guides/int_basics/message-components/intro.md new file mode 100644 index 0000000..ed44c32 --- /dev/null +++ b/docs/guides/int_basics/message-components/intro.md @@ -0,0 +1,66 @@ +--- +uid: Guides.MessageComponents.Intro +title: Getting Started with Components +--- + +# Message Components + +Message components are a framework for adding interactive elements to a message your app or bot sends. They're accessible, customizable, and easy to use. + +## What is a Component + +Components are a new parameter you can use when sending messages with your bot. There are currently 2 different types of components you can use: Buttons and Select Menus. + +## Creating components + +Lets create a simple component that has a button. First thing we need is a way to trigger the message, this can be done via commands or simply a ready event. Lets make a command that triggers our button message. + +```cs +[Command("spawner")] +public async Task Spawn() +{ + // Reply with some components +} +``` + +We now have our command, but we need to actually send the buttons with the command. To do that, lets look at the `ComponentBuilder` class: + +| Name | Description | +| ---------------- | --------------------------------------------------------------------------- | +| `FromMessage` | Creates a new builder from a message. | +| `FromComponents` | Creates a new builder from the provided list of components. | +| `WithSelectMenu` | Adds a `SelectMenuBuilder` to the `ComponentBuilder` at the specific row. | +| `WithButton` | Adds a `ButtonBuilder` to the `ComponentBuilder` at the specific row. | +| `Build` | Builds this builder into a `MessageComponent` used to send your components. | + +We see that we can use the `WithButton` function so lets do that. looking at its parameters it takes: + +- `label` - The display text of the button. +- `customId` - The custom id of the button, this is whats sent by discord when your button is clicked. +- `style` - The discord defined style of the button. +- `emote` - An emote to be displayed with the button. +- `url` - The url of the button if its a link button. +- `disabled` - Whether or not the button is disabled. +- `row` - The row the button will occupy. + +Since were just making a busic button, we dont have to specify anything else besides the label and custom id. + +```cs +var builder = new ComponentBuilder() + .WithButton("label", "custom-id"); +``` + +Lets add this to our command: + +```cs +[Command("spawner")] +public async Task Spawn() +{ + var builder = new ComponentBuilder() + .WithButton("label", "custom-id"); + + await ReplyAsync("Here is a button!", components: builder.Build()); +} +``` + +![](images/image1.png) diff --git a/docs/guides/int_basics/message-components/responding-to-buttons.md b/docs/guides/int_basics/message-components/responding-to-buttons.md new file mode 100644 index 0000000..7546c6f --- /dev/null +++ b/docs/guides/int_basics/message-components/responding-to-buttons.md @@ -0,0 +1,37 @@ +--- +uid: Guides.MessageComponents.Responding +title: Responding to Components +--- + +# Responding to button clicks + +Responding to buttons is pretty simple, there are a couple ways of doing it and we can cover both. + +### Method 1: Hooking the InteractionCreated Event + +We can hook the `ButtonExecuted` event for button type interactions: + +```cs +client.ButtonExecuted += MyButtonHandler; +``` + +Now, lets write our handler. + +```cs +public async Task MyButtonHandler(SocketMessageComponent component) +{ + // We can now check for our custom id + switch(component.Data.CustomId) + { + // Since we set our buttons custom id as 'custom-id', we can check for it like this: + case "custom-id": + // Lets respond by sending a message saying they clicked the button + await component.RespondAsync($"{component.User.Mention} has clicked the button!"); + break; + } +} +``` + +Running it and clicking the button: + +![](images/image2.png) \ No newline at end of file diff --git a/docs/guides/int_basics/message-components/select-menus.md b/docs/guides/int_basics/message-components/select-menus.md new file mode 100644 index 0000000..6dfe151 --- /dev/null +++ b/docs/guides/int_basics/message-components/select-menus.md @@ -0,0 +1,76 @@ +--- +uid: Guides.MessageComponents.SelectMenus +title: Select Menus +--- + +# Select menus + +Select menus allow users to select from a range of options, this can be quite useful with configuration commands etc. + +## Creating a select menu + +We can use a `SelectMenuBuilder` to create our menu. + +```cs +var menuBuilder = new SelectMenuBuilder() + .WithPlaceholder("Select an option") + .WithCustomId("menu-1") + .WithMinValues(1) + .WithMaxValues(1) + .AddOption("Option A", "opt-a", "Option B is lying!") + .AddOption("Option B", "opt-b", "Option A is telling the truth!"); + +var builder = new ComponentBuilder() + .WithSelectMenu(menuBuilder); +``` + +Lets add this to a command: + +```cs +[Command("spawner")] +public async Task Spawn() +{ + var menuBuilder = new SelectMenuBuilder() + .WithPlaceholder("Select an option") + .WithCustomId("menu-1") + .WithMinValues(1) + .WithMaxValues(1) + .AddOption("Option A", "opt-a", "Option B is lying!") + .AddOption("Option B", "opt-b", "Option A is telling the truth!"); + + var builder = new ComponentBuilder() + .WithSelectMenu(menuBuilder); + + await ReplyAsync("Whos really lying?", components: builder.Build()); +} +``` + +Running this produces this result: + +![](images/image4.png) + +And opening the menu we see: + +![](images/image5.png) + +Lets handle the selection of an option, We can hook the `SelectMenuExecuted` event to handle our select menu: + +```cs +client.SelectMenuExecuted += MyMenuHandler; +``` + +The `SelectMenuExecuted` also supplies a `SocketMessageComponent` argument, we can confirm that its a select menu by checking the `ComponentType` inside of the data field if we need, but the library will do that for us and only execute our handler if its a select menu. + +The values that the user has selected will be inside of the `Values` collection in the Data field. we can list all of them back to the user for this example. + +```cs +public async Task MyMenuHandler(SocketMessageComponent arg) +{ + var text = string.Join(", ", arg.Data.Values); + await arg.RespondAsync($"You have selected {text}"); +} +``` + +Running this produces this result: + +![](images/image6.png) diff --git a/docs/guides/int_basics/message-components/text-input.md b/docs/guides/int_basics/message-components/text-input.md new file mode 100644 index 0000000..92679ae --- /dev/null +++ b/docs/guides/int_basics/message-components/text-input.md @@ -0,0 +1,46 @@ +--- +uid: Guides.MessageComponents.TextInputs +title: Text Input Components +--- + +# Text Input Components + +> [!WARNING] +> Text input components can only be used in +> [modals](../modals/intro.md). + +Text input components are a type of MessageComponents that can only be +used in modals. Texts inputs can be longer (the `Paragraph`) style or +shorter (the `Short` style). Text inputs have a variable min and max +length. + +![A modal with short and paragraph text inputs](images/image7.png) + +## Creating text inputs +Text input components can be built using the `TextInputBuilder`. +The simplest text input can built with: +```cs +var tb = new TextInputBuilder() + .WithLabel("My Text") + .WithCustomId("text_input"); +``` + +and would produce a component that looks like: + +![basic text input component](images/image8.png) + +Additional options can be specified to control the placeholder, style, +and min/max length of the input: +```cs +var tb = new TextInputBuilder() + .WithLabel("Labeled") + .WithCustomId("text_input") + .WithStyle(TextInputStyle.Paragraph) + .WithMinLength(6) + .WithMaxLength(42) + .WithRequired(true) + .WithPlaceholder("Consider this place held."); +``` + +![more advanced text input](images/image9.png) + diff --git a/docs/guides/int_basics/modals/images/image1.png b/docs/guides/int_basics/modals/images/image1.png new file mode 100644 index 0000000..779bf78 Binary files /dev/null and b/docs/guides/int_basics/modals/images/image1.png differ diff --git a/docs/guides/int_basics/modals/images/image2.png b/docs/guides/int_basics/modals/images/image2.png new file mode 100644 index 0000000..7c1c325 Binary files /dev/null and b/docs/guides/int_basics/modals/images/image2.png differ diff --git a/docs/guides/int_basics/modals/images/image3.png b/docs/guides/int_basics/modals/images/image3.png new file mode 100644 index 0000000..49ca61c Binary files /dev/null and b/docs/guides/int_basics/modals/images/image3.png differ diff --git a/docs/guides/int_basics/modals/images/image4.png b/docs/guides/int_basics/modals/images/image4.png new file mode 100644 index 0000000..453b2ee Binary files /dev/null and b/docs/guides/int_basics/modals/images/image4.png differ diff --git a/docs/guides/int_basics/modals/intro.md b/docs/guides/int_basics/modals/intro.md new file mode 100644 index 0000000..3e738c6 --- /dev/null +++ b/docs/guides/int_basics/modals/intro.md @@ -0,0 +1,135 @@ +--- +uid: Guides.Modals.Intro +title: Getting Started with Modals +--- +# Modals + +## Getting started with modals +This guide will show you how to use modals and give a few examples of +valid use cases. If your question is not covered by this guide ask in the +[Discord.Net Discord Server](https://discord.gg/dnet). + +### What is a modal? +Modals are forms bots can send when responding to interactions. Modals +are sent to Discord as an array of message components and converted +into the form layout by user's clients. Modals are required to have a +custom id, title, and at least one component. + +![Screenshot of a modal](images/image2.png) + +When users submit modals, your client fires the ModalSubmitted event. +You can get the components of the modal from the `Data.Components` property +on the SocketModal: + +![Screenshot of modal data](images/image1.png) + +### Using modals + +Lets create a simple modal with an entry field for users to +tell us their favorite food. We can start by creating a slash +command that will respond with the modal. +```cs +[SlashCommand("food", "Tell us about your favorite food!")] +public async Task FoodPreference() +{ + // send a modal +} +``` + +Now that we have our command set up, we need to build a modal. +We can use the aptly named `ModalBuilder` for that: + +| Method | Description | +| --------------- | ----------------------------------------- | +| `WithTitle` | Sets the modal's title. | +| `WithCustomId` | Sets the modal's custom id. | +| `AddTextInput` | Adds a `TextInputBuilder` to the modal. | +| `AddComponents` | Adds multiple components to the modal. | +| `Build` | Builds the `ModalBuilder` into a `Modal`. | + +We know we need to add a text input to the modal, so let's look at that +method's parameters. + +| Parameter | Description | +| ------------- | ------------------------------------------ | +| `label` | Sets the input's label. | +| `customId` | Sets the input's custom id. | +| `style` | Sets the input's style. | +| `placeholder` | Sets the input's placeholder. | +| `minLength` | Sets the minimum input length. | +| `maxLength` | Sets the maximum input length. | +| `required` | Sets whether or not the modal is required. | +| `value` | Sets the input's default value. | + +To make a basic text input we would only need to set the `label` and +`customId`, but in this example we will also use the `placeholder` +parameter. Next we can build our modal: + +```cs +var mb = new ModalBuilder() + .WithTitle("Fav Food") + .WithCustomId("food_menu") + .AddTextInput("What??", "food_name", placeholder:"Pizza") + .AddTextInput("Why??", "food_reason", TextInputStyle.Paragraph, + "Kus it's so tasty"); +``` + +Now that we have a ModalBuilder we can update our command to respond +with the modal. + +```cs +[SlashCommand("food", "Tell us about your favorite food!")] +public async Task FoodPreference() +{ + var mb = new ModalBuilder() + .WithTitle("Fav Food") + .WithCustomId("food_menu") + .AddTextInput("What??", "food_name", placeholder:"Pizza") + .AddTextInput("Why??", "food_reason", TextInputStyle.Paragraph, + "Kus it's so tasty"); + + await Context.Interaction.RespondWithModalAsync(mb.Build()); +} +``` + +When we run the command, our modal should pop up: + +![screenshot of the above modal](images/image3.png) + +### Respond to modals + +> [!WARNING] +> Modals can not be sent when responding to a modal. + +Once a user has submitted the modal, we need to let everyone know what +their favorite food is. We can start by hooking a task to the client's +`ModalSubmitted` event. +```cs +_client.ModalSubmitted += async modal => +{ + // Get the values of components. + List components = + modal.Data.Components.ToList(); + string food = components + .First(x => x.CustomId == "food_name").Value; + string reason = components + .First(x => x.CustomId == "food_reason").Value; + + // Build the message to send. + string message = "hey @everyone; I just learned " + + $"{modal.User.Mention}'s favorite food is " + + $"{food} because {reason}."; + + // Specify the AllowedMentions so we don't actually ping everyone. + AllowedMentions mentions = new AllowedMentions(); + mentions.AllowedTypes = AllowedMentionTypes.Users; + + // Respond to the modal. + await modal.RespondAsync(message, allowedMentions:mentions); +} +``` + +Now responding to the modal should inform everyone of our tasty +choices. + +![Response of the modal submitted event](images/image4.png) diff --git a/docs/guides/int_framework/autocompletion.md b/docs/guides/int_framework/autocompletion.md new file mode 100644 index 0000000..35b4858 --- /dev/null +++ b/docs/guides/int_framework/autocompletion.md @@ -0,0 +1,48 @@ +--- +uid: Guides.IntFw.AutoCompletion +title: Command Autocompletion +--- + +# AutocompleteHandlers + +[AutocompleteHandler]s provide a similar pattern to TypeConverters. +[AutocompleteHandler]s are cached, singleton services and they are used by the +Interaction Service to handle Autocomplete Interactions targeted to a specific Slash Command parameter. + +To start using AutocompleteHandlers, use the `[AutocompleteAttribute(Type type)]` overload of the [AutocompleteAttribute]. +This will dynamically link the parameter to the [AutocompleteHandler] type. + +AutocompleteHandlers raise the `AutocompleteHandlerExecuted` event on execution. This event can be also used to create a post-execution logic, just like the `*CommandExecuted` events. + +## Creating AutocompleteHandlers + +A valid AutocompleteHandlers must inherit [AutocompleteHandler] base type and implement all of its abstract methods. + +[!code-csharp[Autocomplete Command Example](samples/autocompletion/autocomplete-example.cs)] + +### GenerateSuggestionsAsync() + +The Interactions Service uses this method to generate a response of an Autocomplete Interaction. +This method should return `AutocompletionResult.FromSuccess(IEnumerable)` to +display parameter suggestions to the user. If there are no suggestions to be presented to the user, you have two results: + +1. Returning the parameterless `AutocompletionResult.FromSuccess()` will display a "No options match your search." message to the user. +2. Returning `AutocompleteResult.FromError()` will make the Interaction Service **not** respond to the interaction, +consequently displaying the user a "Loading options failed." message. `AutocompletionResult.FromError()` is solely used for error handling purposes. Discord currently doesn't allow +you to display custom error messages. This result type will be directly returned to the `AutocompleteHandlerExecuted` method. + +## Resolving AutocompleteHandler Dependencies + +AutocompleteHandler dependencies are resolved using the same dependency injection +pattern as the Interaction Modules. +Property injection and constructor injection are both valid ways to get service dependencies. + +Because [AutocompleteHandler]s are constructed at service startup, +class dependencies are resolved only once. + +> [!NOTE] +> If you need to access per-request dependencies you can use the +> IServiceProvider parameter of the `GenerateSuggestionsAsync()` method. + +[AutoCompleteHandler]: xref:Discord.Interactions.AutocompleteHandler +[AutoCompleteAttribute]: xref:Discord.Interactions.AutocompleteAttribute diff --git a/docs/guides/int_framework/intro.md b/docs/guides/int_framework/intro.md new file mode 100644 index 0000000..7718fbf --- /dev/null +++ b/docs/guides/int_framework/intro.md @@ -0,0 +1,469 @@ +--- +uid: Guides.IntFw.Intro +title: Introduction to the Interaction Service +--- + +# Getting Started + +The Interaction Service provides an attribute based framework for creating Discord Interaction handlers. + +To start using the Interaction Service, you need to create a service instance. +Optionally you can provide the [InteractionService] constructor with a +[InteractionServiceConfig] to change the services behaviour to suit your needs. + +```csharp +... +// _client here is DiscordSocketClient. +// A different approach to passing in a restclient is also possible. +var _interactionService = new InteractionService(_client.Rest); + +... +``` + +## Modules + +Attribute based Interaction handlers must be defined within a command module class. +Command modules are responsible for executing the Interaction handlers and providing them with the necessary execution info and helper functions. + +Command modules are transient objects. +A new module instance is created before a command execution starts then it will be disposed right after the method returns. + +Every module class must: + +- Be public +- Inherit from [InteractionModuleBase] + +Optionally you can override the included : + +- OnModuleBuilding (executed after the module is built) +- BeforeExecute (executed before a command execution starts) +- AfterExecute (executed after a command execution concludes) + +methods to configure the modules behaviour. + +Every command module exposes a set of helper methods, namely: + +- `RespondAsync()` => Respond to the interaction +- `FollowupAsync()` => Create a followup message for an interaction +- `ReplyAsync()` => Send a message to the origin channel of the interaction +- `DeleteOriginalResponseAsync()` => Delete the original interaction response + +## Commands + +Valid **Interaction Commands** must comply with the following requirements: + +| | return type | max parameter count | allowed parameter types | attribute | +|-------------------------------|------------------------------|---------------------|-------------------------------|--------------------------| +|[Slash Command](#slash-commands)| `Task`/`Task` | 25 | any* | `[SlashCommand]` | +|[User Command](#user-commands) | `Task`/`Task` | 1 | Implementations of `IUser` | `[UserCommand]` | +|[Message Command](#message-commands)| `Task`/`Task` | 1 | Implementations of `IMessage` | `[MessageCommand]` | +|[Component Interaction Command](#component-interaction-commands)| `Task`/`Task` | inf | `string` or `string[]` | `[ComponentInteraction]` | +|[Autocomplete Command](#autocomplete-commands)| `Task`/`Task` | - | - | `[AutocompleteCommand]`| + +> [!NOTE] +> A `TypeConverter` that is capable of parsing type in question must be registered to the [InteractionService] instance. +> You should avoid using long running code in your command module. +> Depending on your setup, long running code may block the Gateway thread of your bot, interrupting its connection to Discord. + +## Slash Commands + +Slash Commands are created using the [SlashCommandAttribute]. +Every Slash Command must declare a name and a description. +You can check Discords **Application Command Naming Guidelines** +[here](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming). + +[!code-csharp[Slash Command](samples/intro/slashcommand.cs)] + +### Parameters + +Slash Commands can have up to 25 method parameters. You must name your parameters in accordance with +[Discords Naming Guidelines](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming). +[InteractionService] also features a pascal casing seperator for formatting parameter names with +pascal casing into Discord compliant parameter names('parameterName' => 'parameter-name'). +By default, your methods can feature the following parameter types: + +- Implementations of [IUser] +- Implementations of [IChannel] +- Implementations of [IRole] +- Implementations of [IMentionable] +- Implementations of [IAttachment] +- `string` +- `float`, `double`, `decimal` +- `bool` +- `char` +- `sbyte`, `byte` +- `int16`, `int32`, `int64` +- `uint16`, `uint32`, `uint64` +- `enum` +- `DateTime` +- `TimeSpan` + +> [!NOTE] +> Enum values are registered as multiple choice options and are enforced by Discord. Use the `[Hide]` attribute on enum values to prevent them from getting registered. + +--- + +**You can use more specialized implementations of [IChannel] to restrict the allowed channel types for a channel type option.** + +| interface | Channel Type | +|---------------------|-------------------------------| +| `IStageChannel` | Stage Channels | +| `IVoiceChannel` | Voice Channels | +| `IDMChannel` | DM Channels | +| `IGroupChannel` | Group Channels | +| `ICategoryChannel` | Category Channels | +| `INewsChannel` | News Channels | +| `IThreadChannel` | Public, Private, News Threads | +| `ITextChannel` | Text Channels | + +--- + +#### Optional Parameters + +Parameters with default values (ie. `int count = 0`) will be displayed as optional parameters on Discord Client. + +#### Parameter Summary + +By using the [SummaryAttribute] you can customize the displayed name and description of a parameter + +[!code-csharp[Summary Attribute](samples/intro/summaryattribute.cs)] + +#### Parameter Choices + +[ChoiceAttribute] can be used to add choices to a parameter. + +[!code-csharp[Choice Attribute](samples/intro/groupattribute.cs)] + +This Slash Command will be displayed exactly the same as the previous example. + +#### Channel Types + +Channel types for an [IChannel] parameter can also be restricted using the [ChannelTypesAttribute]. + +[!code-csharp[Channel Attribute](samples/intro/channelattribute.cs)] + +In this case, user can only input Stage Channels and Text Channels to this parameter. + +#### Min/Max Value + +You can specify the permitted max/min value for a number type parameter using the [MaxValueAttribute] and [MinValueAttribute]. + +#### Complex Parameters + +This allows users to create slash command options using an object's constructor allowing complex objects to be created which cannot be infered from only one input value. +Constructor methods support every attribute type that can be used with the regular slash commands ([Autocomplete], [Summary] etc. ). +Preferred constructor of a Type can be specified either by passing a `Type[]` to the `[ComplexParameterAttribute]` or tagging a type constructor with the `[ComplexParameterCtorAttribute]`. If nothing is specified, the InteractionService defaults to the only public constructor of the type. +TypeConverter pattern is used to parse the constructor methods objects. + +[!code-csharp[Complex Parameter](samples/intro/complexparams.cs)] + +Interaction service complex parameter constructors are prioritized in the following order: + +1. Constructor matching the signature provided in the `[ComplexParameter(Type[])]` overload. +2. Constuctor tagged with `[ComplexParameterCtor]`. +3. Type's only public constuctor. + +#### DM Permissions +> [!WARNING] +> [EnabledInDmAttribute] is being deprecated in favor of [CommandContextType] attribute. + +You can use the [EnabledInDmAttribute] to configure whether a globally-scoped top level command should be enabled in Dms or not. Only works on top level commands. + +#### Default Member Permissions + +[DefaultMemberPermissionsAttribute] can be used when creating a command to set the permissions a user must have to use the command. Permission overwrites can be configured from the Integrations page of Guild Settings. [DefaultMemberPermissionsAttribute] cumulatively propagates down the class hierarchy until it reaches a top level command. This attribute can be only used on top level commands and will not work on commands that are nested in command groups. + +## User Commands + +A valid User Command must have the following structure: + +[!code-csharp[User Command](samples/intro/usercommand.cs)] + +> [!WARNING] +> User commands can only have one parameter and its type must be an implementation of [IUser]. + +## Message Commands + +A valid Message Command must have the following structure: + +[!code-csharp[Message Command](samples/intro/messagecommand.cs)] + +> [!WARNING] +> Message commands can only have one parameter and its type must be an implementation of [IMessage]. + +## Component Interaction Commands + +Component Interaction Commands are used to handle interactions that originate from **Discord Message Component**s. +This pattern is particularly useful if you will be reusing a set a **Custom ID**s. + +Component Interaction Commands support wild card matching, +by default `*` character can be used to create a wild card pattern. +Interaction Service will use lazy matching to capture the words corresponding to the wild card character. +And the captured words will be passed on to the command method in the same order they were captured. + +[!code-csharp[Button](samples/intro/button.cs)] + +You may use as many wild card characters as you want. + +> [!NOTE] +> If Interaction Service receives a component interaction with **player:play,rickroll** custom id, +> `op` will be *play* and `name` will be *rickroll* + +## Select Menus + +Unlike button interactions, select menu interactions also contain the values of the selected menu items. +In this case, you should structure your method to accept a string array. + +> [!NOTE] +> Use arrays of `IUser`, `IChannel`, `IRole`, `IMentionable` or their implementations to get data from a select menu with respective type. + +[!code-csharp[Dropdown](samples/intro/dropdown.cs)] + +> [!NOTE] +> Wildcards may also be used to match a select menu ID, +> though keep in mind that the array containing the select menu values should be the last parameter. + +## Autocomplete Commands + +Autocomplete commands must be parameterless methods. A valid Autocomplete command must have the following structure: + +[!code-csharp[Autocomplete Command](samples/intro/autocomplete.cs)] + +Alternatively, you can use the [AutocompleteHandlers] to simplify this workflow. + +## Modals + +Modal commands last parameter must be an implementation of `IModal`. +A Modal implementation would look like this: + +[!code-csharp[Modal Command](samples/intro/modal.cs)] + +> [!NOTE] +> If you are using Modals in the interaction service it is **highly +> recommended** that you enable `PreCompiledLambdas` in your config +> to prevent performance issues. + +## Interaction Context + +Every command module provides its commands with an execution context. +This context property includes general information about the underlying interaction that triggered the command execution. +The base command context. + +You can design your modules to work with different implementation types of [IInteractionContext]. +To achieve this, make sure your module classes inherit from the generic variant of the [InteractionModuleBase]. + +> [!NOTE] +> Context type must be consistent throughout the project, or you will run into issues during runtime. + +The [InteractionService] ships with 4 different kinds of [InteractionContext]: + +1. [InteractionContext]: A bare-bones execution context consisting of only implementation neutral interfaces +2. [SocketInteractionContext]: An execution context for use with [DiscordSocketClient]. Socket entities are exposed in this context without the need of casting them. +3. [ShardedInteractionContext]: [DiscordShardedClient] variant of the [SocketInteractionContext] +4. [RestInteractionContext]: An execution context designed to be used with a [DiscordRestClient] and webhook based interactions pattern + +You can create custom Interaction Contexts by implementing the [IInteractionContext] interface. + +One problem with using the concrete type InteractionContexts is that you cannot access the information that is specific to different interaction types without casting. Concrete type interaction contexts are great for creating shared interaction modules but you can also use the generic variants of the built-in interaction contexts to create interaction specific interaction modules. + +> [!NOTE] +> Message component interactions have access to a special method called `UpdateAsync()` to update the body of the method the interaction originated from. +> Normally this wouldn't be accessible without casting the `Context.Interaction`. + +[!code-csharp[Context Example](samples/intro/context.cs)] + +## Loading Modules + +[InteractionService] can automatically discover and load modules that inherit [InteractionModuleBase] from an `Assembly`. +Call `InteractionService.AddModulesAsync()` to use this functionality. + +> [!NOTE] +> You can also manually add Interaction modules using the `InteractionService.AddModuleAsync()` +> method by providing the module type you want to load. + +## Resolving Module Dependencies + +Module dependencies are resolved using the Constructor Injection and Property Injection patterns. +Meaning, the constructor parameters and public settable properties of a module will be assigned using the `IServiceProvider`. +For more information on dependency injection, read the [DependencyInjection] guides. + +> [!NOTE] +> On every command execution, if the 'AutoServiceScopes' option is enabled in the config , module dependencies are resolved using a new service scope which allows you to utilize scoped service instances, just like in Asp.Net. +> Including the precondition checks, every module method is executed using the same service scope and service scopes are disposed right after the `AfterExecute` method returns. This doesn't apply to methods other than `ExecuteAsync()`. + +## Module Groups + +Module groups allow you to create sub-commands and sub-commands groups. +By nesting commands inside a module that is tagged with [GroupAttribute] you can create prefixed commands. + +> [!WARNING] +> Although creating nested module structures are allowed, +> you are not permitted to use more than 2 [GroupAttribute]'s in module hierarchy. + +> [!NOTE] +> To not use the command group's name as a prefix for component or modal interaction's custom id set `ignoreGroupNames` parameter to `true` in classes with [GroupAttribute] +> +> However, you have to be careful to prevent overlapping ids of buttons and modals. + +[!code-csharp[Command Group Example](samples/intro/groupmodule.cs)] + +## Executing Commands + +Any of the following socket events can be used to execute commands: + +- [InteractionCreated] +- [ButtonExecuted] +- [SelectMenuExecuted] +- [AutocompleteExecuted] +- [UserCommandExecuted] +- [MessageCommandExecuted] +- [ModalExecuted] + +These events will trigger for the specific type of interaction they inherit their name from. The [InteractionCreated] event will trigger for all. +An example of executing a command from an event can be seen here: + +[!code-csharp[Command Event Example](samples/intro/event.cs)] + +Commands can be either executed on the gateway thread or on a separate thread from the thread pool. +This behaviour can be configured by changing the `RunMode` property of `InteractionServiceConfig` or by setting the *runMode* parameter of a command attribute. + +> [!WARNING] +> In the example above, no form of post-execution is presented. +> Please carefully read the [Post-Execution Documentation] for the best approach in resolving the result based on your `RunMode`. + +You can also configure the way [InteractionService] executes the commands. +By default, commands are executed using `ConstructorInfo.Invoke()` to create module instances and +`MethodInfo.Invoke()` method for executing the method bodies. +By setting, `InteractionServiceConfig.UseCompiledLambda` to `true`, you can make [InteractionService] create module instances and execute commands using +*Compiled Lambda* expressions. This cuts down on command execution time but it might add some memory overhead. + +Time it takes to create a module instance and execute a `Task.Delay(0)` method using the Reflection methods compared to Compiled Lambda expressions: + +| Method | Mean | Error | StdDev | +|----------------- |----------:|---------:|---------:| +| ReflectionInvoke | 225.93 ns | 4.522 ns | 7.040 ns | +| CompiledLambda | 48.79 ns | 0.981 ns | 1.276 ns | + +## Registering Commands to Discord + +Application commands loaded to the Interaction Service can be registered to Discord using a number of different methods. +In most cases `RegisterCommandsGloballyAsync()` and `RegisterCommandsToGuildAsync()` are the methods to use. +Command registration methods can only be used after the gateway client is ready or the rest client is logged in. + +[!code-csharp[Registering Commands Example](samples/intro/registering.cs)] + +Methods like `AddModulesToGuildAsync()`, `AddCommandsToGuildAsync()`, `AddModulesGloballyAsync()` and `AddCommandsGloballyAsync()` +can be used to register cherry picked modules or commands to global/guild scopes. + +> [!NOTE] +> [DontAutoRegisterAttribute] can be used on module classes to prevent `RegisterCommandsGloballyAsync()` and `RegisterCommandsToGuildAsync()` from registering them to the Discord. + +## Interaction Utility + +Interaction Service ships with a static `InteractionUtility` +class which contains some helper methods to asynchronously waiting for Discord Interactions. +For instance, `WaitForInteractionAsync()` method allows you to wait for an Interaction for a given amount of time. +This method returns the first encountered Interaction that satisfies the provided predicate. + +> [!WARNING] +> If you are running the Interaction Service on `RunMode.Sync` you should avoid using this method in your commands, +> as it will block the gateway thread and interrupt your bots connection. + +## Webhook Based Interactions + +Instead of using the gateway to receive Discord Interactions, Discord allows you to receive Interaction events over Webhooks. +Interaction Service also supports this Interaction type but to be able to +respond to the Interactions within your command modules you need to perform the following: + +- Make your modules inherit `RestInteractionModuleBase` +- Set the `ResponseCallback` property of `InteractionServiceConfig` so that the `ResponseCallback` +delegate can be used to create HTTP responses from a deserialized json object string. +- Use the interaction endpoints of the module base instead of the interaction object (ie. `RespondAsync()`, `FollowupAsync()`...). + +## Localization + +Discord Slash Commands support name/description localization. Localization is available for names and descriptions of Slash Command Groups ([GroupAttribute]), Slash Commands ([SlashCommandAttribute]), Slash Command parameters and Slash Command Parameter Choices. Interaction Service can be initialized with an `ILocalizationManager` instance in its config which is used to create the necessary localization dictionaries on command registration. Interaction Service has two built-in `ILocalizationManager` implementations: `ResxLocalizationManager` and `JsonLocalizationManager`. + +### ResXLocalizationManager + +`ResxLocalizationManager` uses `.` delimited key names to traverse the resource files and get the localized strings (`group1.group2.command.parameter.name`). A `ResxLocalizationManager` instance must be initialized with a base resource name, a target assembly and a collection of `CultureInfo`s. Every key path must end with either `.name` or `.description`, including parameter choice strings. [Discord.Tools.LocalizationTemplate.Resx](https://www.nuget.org/packages/Discord.Tools.LocalizationTemplate.Resx) dotnet tool can be used to create localization file templates. + +### JsonLocalizationManager + +`JsonLocalizationManager` uses a nested data structure similar to Discord's Application Commands schema. You can get the Json schema [here](https://gist.github.com/Cenngo/d46a881de24823302f66c3c7e2f7b254). `JsonLocalizationManager` accepts a base path and a base file name and automatically discovers every resource file ( \basePath\fileName.locale.json ). A Json resource file should have a structure similar to: + +```json +{ + "command_1":{ + "name": "localized_name", + "description": "localized_description", + "parameter_1":{ + "name": "localized_name", + "description": "localized_description" + } + }, + "group_1":{ + "name": "localized_name", + "description": "localized_description", + "command_1":{ + "name": "localized_name", + "description": "localized_description", + "parameter_1":{ + "name": "localized_name", + "description": "localized_description" + }, + "parameter_2":{ + "name": "localized_name", + "description": "localized_description" + } + } + } +} +``` + +## User Apps + +User apps are the kind of Discord applications that are installed onto a user instead of a guild, thus making commands usable anywhere on Discord. Note that only users who have installed the application will see the commands. This sample shows you how to create a simple user install command. + +[!code-csharp[Registering Commands Example](samples/intro/userapps.cs)] + +[AutocompleteHandlers]: xref:Guides.IntFw.AutoCompletion +[DependencyInjection]: xref:Guides.DI.Intro +[Post-Execution Documentation]: xref:Guides.IntFw.PostExecution + +[GroupAttribute]: xref:Discord.Interactions.GroupAttribute +[DontAutoRegisterAttribute]: xref:Discord.Interactions.DontAutoRegisterAttribute +[InteractionService]: xref:Discord.Interactions.InteractionService +[InteractionServiceConfig]: xref:Discord.Interactions.InteractionServiceConfig +[InteractionModuleBase]: xref:Discord.Interactions.InteractionModuleBase +[SlashCommandAttribute]: xref:Discord.Interactions.SlashCommandAttribute +[InteractionCreated]: xref:Discord.WebSocket.BaseSocketClient +[ButtonExecuted]: xref:Discord.WebSocket.BaseSocketClient +[SelectMenuExecuted]: xref:Discord.WebSocket.BaseSocketClient +[AutocompleteExecuted]: xref:Discord.WebSocket.BaseSocketClient +[UserCommandExecuted]: xref:Discord.WebSocket.BaseSocketClient +[MessageCommandExecuted]: xref:Discord.WebSocket.BaseSocketClient +[ModalExecuted]: xref:Discord.WebSocket.BaseSocketClient +[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient +[DiscordRestClient]: xref:Discord.Rest.DiscordRestClient +[DiscordShardedClient]: xref:Discord.WebSocket.DiscordShardedClient +[SocketInteractionContext]: xref:Discord.Interactions.SocketInteractionContext +[ShardedInteractionContext]: xref:Discord.Interactions.ShardedInteractionContext +[InteractionContext]: xref:Discord.Interactions.InteractionContext +[IInteractionContext]: xref:Discord.IInteractionContext +[RestInteractionContext]: xref:Discord.Rest.RestInteractionContext +[SummaryAttribute]: xref:Discord.Interactions.SummaryAttribute +[ChoiceAttribute]: xref:Discord.Interactions.ChoiceAttribute +[ChannelTypesAttribute]: xref:Discord.Interactions.ChannelTypesAttribute +[MaxValueAttribute]: xref:Discord.Interactions.MaxValueAttribute +[MinValueAttribute]: xref:Discord.Interactions.MinValueAttribute +[EnabledInDmAttribute]: xref:Discord.Interactions.EnabledInDmAttribute +[CommandContextType]: xref:Discord.Interactions.CommandContextTypeAttribute +[DefaultMemberPermissionsAttribute]: xref:Discord.Interactions.DefaultMemberPermissionsAttribute + +[IChannel]: xref:Discord.IChannel +[IAttachment]: xref:Discord.IAttachment +[IRole]: xref:Discord.IRole +[IUser]: xref:Discord.IUser +[IMessage]: xref:Discord.IMessage +[IMentionable]: xref:Discord.IMentionable diff --git a/docs/guides/int_framework/permissions.md b/docs/guides/int_framework/permissions.md new file mode 100644 index 0000000..f02c50e --- /dev/null +++ b/docs/guides/int_framework/permissions.md @@ -0,0 +1,64 @@ +--- +uid: Guides.IntFw.Perms +title: How to handle permissions. +--- + +# Permissions + +This page covers everything to know about setting up permissions for Slash & context commands. + +Application command (Slash, User & Message) permissions are set up at creation. +When you add your commands to a guild or globally, the permissions will be set up from the attributes you defined. + +Commands that are added will only show up for members that meet the required permissions. +There is no further internal handling, as Discord deals with this on its own. + +> [!WARNING] +> Permissions can only be configured at top level commands. Not in subcommands. + +## Disallowing commands in DM + +Commands can be blocked from being executed in DM if a guild is required to execute them in as followed: + +[!code-csharp[no-DM permission](samples/permissions/guild-only.cs)] + +> [!TIP] +> This attribute only works on global-level commands. Commands that are registered in guilds alone do not have a need for it. + +## Server permissions + +As previously shown, a command like ban can be blocked from being executed inside DMs, +as there are no members to ban inside of a DM. However, for a command like this, +we'll also want to make block it from being used by members that do not have the [permissions]. +To do this, we can use the `DefaultMemberPermissions` attribute: + +[!code-csharp[Server permissions](samples/permissions/guild-perms.cs)] + +### Stacking permissions + +If you want a user to have multiple [permissions] in order to execute a command, you can use the `|` operator, just like with setting up intents: + +[!code-csharp[Permission stacking](samples/permissions/perm-stacking.cs)] + +### Nesting permissions + +Alternatively, permissions can also be nested. +It will look for all uses of `DefaultMemberPermissions` up until the highest level class. +The `EnabledInDm` attribute can be defined at top level as well, +and will be set up for all of the commands & nested modules inside this class. + +[!code-csharp[Permission stacking](samples/permissions/perm-nesting.cs)] + +The amount of nesting you can do is realistically endless. + +> [!NOTE] +> If the nested class is marked with `Group`, as required for setting up subcommands, this example will not work. +> As mentioned before, subcommands cannot have seperate permissions from the top level command. + +### NSFW Commands +Commands can be limited to only age restricted channels and DMs: + +[!code-csharp[Nsfw-Permissions](samples/permissions/nsfw-permissions.cs)] + +[permissions]: xref:Discord.GuildPermission + diff --git a/docs/guides/int_framework/post-execution.md b/docs/guides/int_framework/post-execution.md new file mode 100644 index 0000000..4cc3966 --- /dev/null +++ b/docs/guides/int_framework/post-execution.md @@ -0,0 +1,69 @@ +--- +uid: Guides.IntFw.PostExecution +title: Post-Command execution +--- + +# Post-Execution Logic + +Interaction Service uses [IResult] to provide information about the state of command execution. +These can be used to log internal exceptions or provide some insight to the command user. + +If you are running your commands using `RunMode.Sync` these command results can be retrieved from +the return value of [InteractionService.ExecuteCommandAsync] method or by +registering delegates to Interaction Service events. + +If you are using the `RunMode.Async` to run your commands, +you must use the Interaction Service events to get the execution results. When using `RunMode.Async`, +[InteractionService.ExecuteCommandAsync] will always return a successful result. + +[InteractionService.ExecuteCommandAsync]: xref:Discord.Interactions.InteractionService.ExecuteCommandAsync* + +## Results + +Interaction Result come in a handful of different flavours: + +1. [AutocompletionResult]: returned by Autocompleters +2. [ExecuteResult]: contains the result of method body execution process +3. [PreconditionGroupResult]: returned by Precondition groups +4. [PreconditionResult]: returned by preconditions +5. [RuntimeResult]: a user implementable result for returning user defined results +6. [SearchResult]: returned by command lookup map +7. [TypeConverterResult]: returned by TypeConverters + +> [!NOTE] +> You can either use the [IResult.Error] property of an Interaction result or create type check for the +> aforementioned result types to branch out your post-execution logic to handle different situations. + + +[AutocompletionResult]: xref:Discord.AutocompleteResult +[ExecuteResult]: xref:Discord.Interactions.ExecuteResult +[PreconditionGroupResult]: xref:Discord.Interactions.PreconditionGroupResult +[PreconditionResult]: xref:Discord.Interactions.PreconditionResult +[SearchResult]: xref:Discord.Interactions.SearchResult`1 +[TypeConverterResult]: xref:Discord.Interactions.TypeConverterResult +[IResult.Error]: xref:Discord.Interactions.IResult.Error* + +## CommandExecuted Events + +Every time a command gets executed, Interaction Service raises a `CommandExecuted` event. +These events can be used to create a post-execution pipeline. + +[!code-csharp[Error Review](samples/postexecution/error_review.cs)] + +## Log Event + +InteractionService regularly outputs information about the occuring events to keep the developer informed. + +## Runtime Result + +Interaction commands allow you to return `Task` to pass on additional information about the command execution +process back to your post-execution logic. + +Custom [RuntimeResult] classes can be created by inheriting the base [RuntimeResult] class. + +If command execution process reaches the method body of the command and no exceptions are thrown during +the execution of the method body, [RuntimeResult] returned by your command will be accessible by casting/type-checking the +[IResult] parameter of the `CommandExecuted` event delegate. + +[RuntimeResult]: xref:Discord.Interactions.RuntimeResult +[IResult]: xref:Discord.Interactions.IResult diff --git a/docs/guides/int_framework/preconditions.md b/docs/guides/int_framework/preconditions.md new file mode 100644 index 0000000..75e5727 --- /dev/null +++ b/docs/guides/int_framework/preconditions.md @@ -0,0 +1,77 @@ +--- +uid: Guides.IntFw.Preconditions +title: Preconditions +--- + +# Preconditions + +Precondition logic is the same as it is for Text-based commands. +A list of attributes and usage is still given for people who are new to both. + +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.Interactions.PreconditionAttribute +[ParameterPreconditionAttribute]: xref:Discord.Interactions.ParameterPreconditionAttribute + +## Bundled Preconditions + +@Discord.Interactions ships with several bundled Preconditions for you +to use. + +* @Discord.Interactions.RequireContextAttribute +* @Discord.Interactions.RequireOwnerAttribute +* @Discord.Interactions.RequireBotPermissionAttribute +* @Discord.Interactions.RequireUserPermissionAttribute +* @Discord.Interactions.RequireNsfwAttribute +* @Discord.Interactions.RequireRoleAttribute + +## Using Preconditions + +To use a precondition, simply apply any valid precondition candidate to +a command method signature as an attribute. + +[!code-csharp[Precondition usage](samples/preconditions/precondition_usage.cs)] + +## ORing Preconditions + +When writing commands, you may want to allow some of them to be +executed when only some of the precondition checks are passed. + +This is where the [Group] property of a precondition attribute comes in +handy. By assigning two or more preconditions to a group, the command +system will allow the command to be executed when one of the +precondition passes. + +### Example - ORing Preconditions + +[!code-csharp[OR Precondition](samples/preconditions/group_precondition.cs)] + +[Group]: xref:Discord.Commands.PreconditionAttribute.Group + +## 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. + +[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/int_framework/samples/autocompletion/autocomplete-example.cs b/docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs new file mode 100644 index 0000000..30c0697 --- /dev/null +++ b/docs/guides/int_framework/samples/autocompletion/autocomplete-example.cs @@ -0,0 +1,20 @@ +// you need to add `Autocomplete` attribute before parameter to add autocompletion to it +[SlashCommand("command_name", "command_description")] +public async Task ExampleCommand([Summary("parameter_name"), Autocomplete(typeof(ExampleAutocompleteHandler))] string parameterWithAutocompletion) + => await RespondAsync($"Your choice: {parameterWithAutocompletion}"); + +public class ExampleAutocompleteHandler : AutocompleteHandler +{ + public override async Task GenerateSuggestionsAsync(IInteractionContext context, IAutocompleteInteraction autocompleteInteraction, IParameterInfo parameter, IServiceProvider services) + { + // Create a collection with suggestions for autocomplete + IEnumerable results = new[] + { + new AutocompleteResult("Name1", "value111"), + new AutocompleteResult("Name2", "value2") + }; + + // max - 25 suggestions at a time (API limit) + return AutocompletionResult.FromSuccess(results.Take(25)); + } +} \ No newline at end of file diff --git a/docs/guides/int_framework/samples/intro/autocomplete.cs b/docs/guides/int_framework/samples/intro/autocomplete.cs new file mode 100644 index 0000000..11de489 --- /dev/null +++ b/docs/guides/int_framework/samples/intro/autocomplete.cs @@ -0,0 +1,21 @@ +[AutocompleteCommand("parameter_name", "command_name")] +public async Task Autocomplete() +{ + string userInput = (Context.Interaction as SocketAutocompleteInteraction).Data.Current.Value.ToString(); + + IEnumerable results = new[] + { + new AutocompleteResult("foo", "foo_value"), + new AutocompleteResult("bar", "bar_value"), + new AutocompleteResult("baz", "baz_value"), + }.Where(x => x.Name.StartsWith(userInput, StringComparison.InvariantCultureIgnoreCase)); // only send suggestions that starts with user's input; use case insensitive matching + + + // max - 25 suggestions at a time + await (Context.Interaction as SocketAutocompleteInteraction).RespondAsync(results.Take(25)); +} + +// you need to add `Autocomplete` attribute before parameter to add autocompletion to it +[SlashCommand("command_name", "command_description")] +public async Task ExampleCommand([Summary("parameter_name"), Autocomplete] string parameterWithAutocompletion) + => await RespondAsync($"Your choice: {parameterWithAutocompletion}"); \ No newline at end of file diff --git a/docs/guides/int_framework/samples/intro/button.cs b/docs/guides/int_framework/samples/intro/button.cs new file mode 100644 index 0000000..2b04d08 --- /dev/null +++ b/docs/guides/int_framework/samples/intro/button.cs @@ -0,0 +1,5 @@ +[ComponentInteraction("player:*,*")] +public async Task Play(string op, string name) +{ + ... +} diff --git a/docs/guides/int_framework/samples/intro/channelattribute.cs b/docs/guides/int_framework/samples/intro/channelattribute.cs new file mode 100644 index 0000000..09eb027 --- /dev/null +++ b/docs/guides/int_framework/samples/intro/channelattribute.cs @@ -0,0 +1,5 @@ +[SlashCommand("name", "Description")] +public async Task Command([ChannelTypes(ChannelType.Stage, ChannelType.Text)] IChannel channel) +{ + ... +} diff --git a/docs/guides/int_framework/samples/intro/complexparams.cs b/docs/guides/int_framework/samples/intro/complexparams.cs new file mode 100644 index 0000000..72c0616 --- /dev/null +++ b/docs/guides/int_framework/samples/intro/complexparams.cs @@ -0,0 +1,37 @@ +public class Vector3 +{ + public int X {get;} + public int Y {get;} + public int Z {get;} + + public Vector3() + { + X = 0; + Y = 0; + Z = 0; + } + + [ComplexParameterCtor] + public Vector3(int x, int y, int z) + { + X = x; + Y = y; + Z = z; + } +} + +// Both of the commands below are displayed to the users identically. + +// With complex parameter +[SlashCommand("create-vector", "Create a 3D vector.")] +public async Task CreateVector([ComplexParameter]Vector3 vector3) +{ + ... +} + +// Without complex parameter +[SlashCommand("create-vector", "Create a 3D vector.")] +public async Task CreateVector(int x, int y, int z) +{ + ... +} \ No newline at end of file diff --git a/docs/guides/int_framework/samples/intro/context.cs b/docs/guides/int_framework/samples/intro/context.cs new file mode 100644 index 0000000..5547cca --- /dev/null +++ b/docs/guides/int_framework/samples/intro/context.cs @@ -0,0 +1,14 @@ +discordClient.ButtonExecuted += async (interaction) => +{ + var ctx = new SocketInteractionContext(discordClient, interaction); + await _interactionService.ExecuteCommandAsync(ctx, serviceProvider); +}; + +public class MessageComponentModule : InteractionModuleBase> +{ + [ComponentInteraction("custom_id")] + public async Task Command() + { + await Context.Interaction.UpdateAsync(...); + } +} diff --git a/docs/guides/int_framework/samples/intro/dropdown.cs b/docs/guides/int_framework/samples/intro/dropdown.cs new file mode 100644 index 0000000..2b0af47 --- /dev/null +++ b/docs/guides/int_framework/samples/intro/dropdown.cs @@ -0,0 +1,11 @@ +[ComponentInteraction("role_selection")] +public async Task RoleSelection(string[] selectedRoles) +{ + ... +} + +[ComponentInteraction("role_selection_*")] +public async Task RoleSelection(string id, string[] selectedRoles) +{ + ... +} diff --git a/docs/guides/int_framework/samples/intro/event.cs b/docs/guides/int_framework/samples/intro/event.cs new file mode 100644 index 0000000..0c9f032 --- /dev/null +++ b/docs/guides/int_framework/samples/intro/event.cs @@ -0,0 +1,14 @@ +// Theres multiple ways to subscribe to the event, depending on your application. Please use the approach fit to your type of client. +// DiscordSocketClient: +_socketClient.InteractionCreated += async (x) => +{ + var ctx = new SocketInteractionContext(_socketClient, x); + await _interactionService.ExecuteCommandAsync(ctx, _serviceProvider); +} + +// DiscordShardedClient: +_shardedClient.InteractionCreated += async (x) => +{ + var ctx = new ShardedInteractionContext(_shardedClient, x); + await _interactionService.ExecuteCommandAsync(ctx, _serviceProvider); +} diff --git a/docs/guides/int_framework/samples/intro/groupattribute.cs b/docs/guides/int_framework/samples/intro/groupattribute.cs new file mode 100644 index 0000000..99d6cd6 --- /dev/null +++ b/docs/guides/int_framework/samples/intro/groupattribute.cs @@ -0,0 +1,23 @@ +[SlashCommand("blep", "Send a random adorable animal photo")] +public async Task Blep([Choice("Dog", "dog"), Choice("Cat", "cat"), Choice("Guinea pig", "GuineaPig")] string animal) +{ + ... +} + +// In most cases, you can use an enum to replace the separate choice attributes in a command. + +public enum Animal +{ + Cat, + Dog, + // You can also use the ChoiceDisplay attribute to change how they appear in the choice menu. + [ChoiceDisplay("Guinea pig")] + GuineaPig +} + +[SlashCommand("blep", "Send a random adorable animal photo")] +public async Task Blep(Animal animal) +{ + ... +} +``` diff --git a/docs/guides/int_framework/samples/intro/groupmodule.cs b/docs/guides/int_framework/samples/intro/groupmodule.cs new file mode 100644 index 0000000..a07b2e4 --- /dev/null +++ b/docs/guides/int_framework/samples/intro/groupmodule.cs @@ -0,0 +1,26 @@ +// You can put commands in groups +[Group("group-name", "Group description")] +public class CommandGroupModule : InteractionModuleBase +{ + // This command will look like + // group-name ping + [SlashCommand("ping", "Get a pong")] + public async Task PongSubcommand() + => await RespondAsync("Pong!"); + + // And even in sub-command groups + [Group("subcommand-group-name", "Subcommand group description")] + public class SubСommandGroupModule : InteractionModuleBase + { + // This command will look like + // group-name subcommand-group-name echo + [SlashCommand("echo", "Echo an input")] + public async Task EchoSubcommand(string input) + => await RespondAsync(input, components: new ComponentBuilder().WithButton("Echo", $"echoButton_{input}").Build()); + + // Component interaction with ignoreGroupNames set to true + [ComponentInteraction("echoButton_*", true)] + public async Task EchoButton(string input) + => await RespondAsync(input); + } +} \ No newline at end of file diff --git a/docs/guides/int_framework/samples/intro/messagecommand.cs b/docs/guides/int_framework/samples/intro/messagecommand.cs new file mode 100644 index 0000000..5cb0c4b --- /dev/null +++ b/docs/guides/int_framework/samples/intro/messagecommand.cs @@ -0,0 +1,5 @@ +[MessageCommand("Bookmark")] +public async Task Bookmark(IMessage msg) +{ + ... +} diff --git a/docs/guides/int_framework/samples/intro/modal.cs b/docs/guides/int_framework/samples/intro/modal.cs new file mode 100644 index 0000000..8a6ba9d --- /dev/null +++ b/docs/guides/int_framework/samples/intro/modal.cs @@ -0,0 +1,43 @@ +// Registers a command that will respond with a modal. +[SlashCommand("food", "Tell us about your favorite food.")] +public async Task Command() + => await Context.Interaction.RespondWithModalAsync("food_menu"); + +// Defines the modal that will be sent. +public class FoodModal : IModal +{ + public string Title => "Fav Food"; + // Strings with the ModalTextInput attribute will automatically become components. + [InputLabel("What??")] + [ModalTextInput("food_name", placeholder: "Pizza", maxLength: 20)] + public string Food { get; set; } + + // Additional paremeters can be specified to further customize the input. + // Parameters can be optional + [RequiredInput(false)] + [InputLabel("Why??")] + [ModalTextInput("food_reason", TextInputStyle.Paragraph, "Kuz it's tasty", maxLength: 500)] + public string Reason { get; set; } +} + +// Responds to the modal. +[ModalInteraction("food_menu")] +public async Task ModalResponse(FoodModal modal) +{ + // Check if "Why??" field is populated + string reason = string.IsNullOrWhiteSpace(modal.Reason) + ? "." + : $" because {modal.Reason}"; + + // Build the message to send. + string message = "hey @everyone, I just learned " + + $"{Context.User.Mention}'s favorite food is " + + $"{modal.Food}{reason}"; + + // Specify the AllowedMentions so we don't actually ping everyone. + AllowedMentions mentions = new(); + mentions.AllowedTypes = AllowedMentionTypes.Users; + + // Respond to the modal. + await RespondAsync(message, allowedMentions: mentions, ephemeral: true); +} \ No newline at end of file diff --git a/docs/guides/int_framework/samples/intro/registering.cs b/docs/guides/int_framework/samples/intro/registering.cs new file mode 100644 index 0000000..f603df7 --- /dev/null +++ b/docs/guides/int_framework/samples/intro/registering.cs @@ -0,0 +1,5 @@ +#if DEBUG + await interactionService.RegisterCommandsToGuildAsync(); +#else + await interactionService.RegisterCommandsGloballyAsync(); +#endif diff --git a/docs/guides/int_framework/samples/intro/slashcommand.cs b/docs/guides/int_framework/samples/intro/slashcommand.cs new file mode 100644 index 0000000..5f4f7fb --- /dev/null +++ b/docs/guides/int_framework/samples/intro/slashcommand.cs @@ -0,0 +1,5 @@ +[SlashCommand("echo", "Echo an input")] +public async Task Echo(string input) +{ + await RespondAsync(input); +} diff --git a/docs/guides/int_framework/samples/intro/summaryattribute.cs b/docs/guides/int_framework/samples/intro/summaryattribute.cs new file mode 100644 index 0000000..8a9b7d3 --- /dev/null +++ b/docs/guides/int_framework/samples/intro/summaryattribute.cs @@ -0,0 +1 @@ +[Summary(description: "this is a parameter description")] string input diff --git a/docs/guides/int_framework/samples/intro/userapps.cs b/docs/guides/int_framework/samples/intro/userapps.cs new file mode 100644 index 0000000..9e25840 --- /dev/null +++ b/docs/guides/int_framework/samples/intro/userapps.cs @@ -0,0 +1,19 @@ + +// This parameteres can be configured on the module level +// Set supported command context types to Bot DMs and Private Channels (regular DM & GDM) +[CommandContextType(InteractionContextType.BotDm, InteractionContextType.PrivateChannel)] +// Set supported integration installation type to User Install +[IntegrationType(ApplicationIntegrationType.UserInstall)] +public class CommandModule() : InteractionModuleBase +{ + [SlashCommand("test", "Just a test command")] + public async Task TestCommand() + => await RespondAsync("Hello There"); + + // But can also be overridden on the command level + [CommandContextType(InteractionContextType.BotDm, InteractionContextType.PrivateChannel, InteractionContextType.Guild)] + [IntegrationType(ApplicationIntegrationType.GuildInstall)] + [SlashCommand("echo", "Echo the input")] + public async Task EchoCommand(string input) + => await RespondAsync($"You said: {input}"); +} diff --git a/docs/guides/int_framework/samples/intro/usercommand.cs b/docs/guides/int_framework/samples/intro/usercommand.cs new file mode 100644 index 0000000..02c4a1e --- /dev/null +++ b/docs/guides/int_framework/samples/intro/usercommand.cs @@ -0,0 +1,5 @@ +[UserCommand("Say Hello")] +public async Task SayHello(IUser user) +{ + ... +} diff --git a/docs/guides/int_framework/samples/permissions/guild-only.cs b/docs/guides/int_framework/samples/permissions/guild-only.cs new file mode 100644 index 0000000..2e907e2 --- /dev/null +++ b/docs/guides/int_framework/samples/permissions/guild-only.cs @@ -0,0 +1,6 @@ +[EnabledInDm(false)] +[SlashCommand("ban", "Bans a user in this guild")] +public async Task BanAsync(...) +{ + ... +} diff --git a/docs/guides/int_framework/samples/permissions/guild-perms.cs b/docs/guides/int_framework/samples/permissions/guild-perms.cs new file mode 100644 index 0000000..2853f23 --- /dev/null +++ b/docs/guides/int_framework/samples/permissions/guild-perms.cs @@ -0,0 +1,7 @@ +[EnabledInDm(false)] +[DefaultMemberPermissions(GuildPermission.BanMembers)] +[SlashCommand("ban", "Bans a user in this guild")] +public async Task BanAsync(...) +{ + ... +} diff --git a/docs/guides/int_framework/samples/permissions/nsfw-permissions.cs b/docs/guides/int_framework/samples/permissions/nsfw-permissions.cs new file mode 100644 index 0000000..21f93b5 --- /dev/null +++ b/docs/guides/int_framework/samples/permissions/nsfw-permissions.cs @@ -0,0 +1,6 @@ +[NsfwCommand(true)] +[SlashCommand("beautiful-code", "Get an image of perfect code")] +public async Task BeautifulCodeAsync(...) +{ + ... +} \ No newline at end of file diff --git a/docs/guides/int_framework/samples/permissions/perm-nesting.cs b/docs/guides/int_framework/samples/permissions/perm-nesting.cs new file mode 100644 index 0000000..8913b1a --- /dev/null +++ b/docs/guides/int_framework/samples/permissions/perm-nesting.cs @@ -0,0 +1,16 @@ +[EnabledInDm(true)] +[DefaultMemberPermissions(GuildPermission.ViewChannels)] +public class Module : InteractionModuleBase +{ + [DefaultMemberPermissions(GuildPermission.SendMessages)] + public class NestedModule : InteractionModuleBase + { + // While looking for more permissions, it has found 'ViewChannels' and 'SendMessages'. The result of this lookup will be: + // ViewChannels + SendMessages + ManageMessages. + // If these together are not found for target user, the command will not show up for them. + [DefaultMemberPermissions(GuildPermission.ManageMessages)] + [SlashCommand("ping", "Pong!")] + public async Task Ping() + => await RespondAsync("pong"); + } +} diff --git a/docs/guides/int_framework/samples/permissions/perm-stacking.cs b/docs/guides/int_framework/samples/permissions/perm-stacking.cs new file mode 100644 index 0000000..92cc514 --- /dev/null +++ b/docs/guides/int_framework/samples/permissions/perm-stacking.cs @@ -0,0 +1,4 @@ +[DefaultMemberPermissions(GuildPermission.SendMessages | GuildPermission.ViewChannels)] +[SlashCommand("ping", "Pong!")] +public async Task Ping() + => await RespondAsync("pong"); diff --git a/docs/guides/int_framework/samples/postexecution/error_review.cs b/docs/guides/int_framework/samples/postexecution/error_review.cs new file mode 100644 index 0000000..d5f8a9c --- /dev/null +++ b/docs/guides/int_framework/samples/postexecution/error_review.cs @@ -0,0 +1,28 @@ +interactionService.SlashCommandExecuted += SlashCommandExecuted; + +async Task SlashCommandExecuted(SlashCommandInfo arg1, Discord.IInteractionContext arg2, IResult arg3) +{ + if (!arg3.IsSuccess) + { + switch (arg3.Error) + { + case InteractionCommandError.UnmetPrecondition: + await arg2.Interaction.RespondAsync($"Unmet Precondition: {arg3.ErrorReason}"); + break; + case InteractionCommandError.UnknownCommand: + await arg2.Interaction.RespondAsync("Unknown command"); + break; + case InteractionCommandError.BadArgs: + await arg2.Interaction.RespondAsync("Invalid number or arguments"); + break; + case InteractionCommandError.Exception: + await arg2.Interaction.RespondAsync($"Command exception: {arg3.ErrorReason}"); + break; + case InteractionCommandError.Unsuccessful: + await arg2.Interaction.RespondAsync("Command could not be executed"); + break; + default: + break; + } + } +} diff --git a/docs/guides/int_framework/samples/preconditions/group_precondition.cs b/docs/guides/int_framework/samples/preconditions/group_precondition.cs new file mode 100644 index 0000000..bae102b --- /dev/null +++ b/docs/guides/int_framework/samples/preconditions/group_precondition.cs @@ -0,0 +1,9 @@ +// The following example only requires the user to either have the +// Administrator permission in this guild or own the bot application. +[RequireUserPermission(GuildPermission.Administrator, Group = "Permission")] +[RequireOwner(Group = "Permission")] +public class AdminModule : ModuleBase +{ + [Command("ban")] + public Task BanAsync(IUser user) => Context.Guild.AddBanAsync(user); +} \ No newline at end of file diff --git a/docs/guides/int_framework/samples/preconditions/precondition_usage.cs b/docs/guides/int_framework/samples/preconditions/precondition_usage.cs new file mode 100644 index 0000000..bea2918 --- /dev/null +++ b/docs/guides/int_framework/samples/preconditions/precondition_usage.cs @@ -0,0 +1,3 @@ +[RequireOwner] +[SlashCommand("hi")] +public Task SayHiAsync() => RespondAsync("hello owner!"); diff --git a/docs/guides/int_framework/samples/typeconverters/enum_converter.cs b/docs/guides/int_framework/samples/typeconverters/enum_converter.cs new file mode 100644 index 0000000..6e1b9de --- /dev/null +++ b/docs/guides/int_framework/samples/typeconverters/enum_converter.cs @@ -0,0 +1,30 @@ +internal sealed class EnumConverter : TypeConverter where T : struct, Enum +{ + public override ApplicationCommandOptionType GetDiscordType() => ApplicationCommandOptionType.String; + + public override Task ReadAsync(IInteractionCommandContext context, SocketSlashCommandDataOption option, IServiceProvider services) + { + if (Enum.TryParse((string)option.Value, out var result)) + return Task.FromResult(TypeConverterResult.FromSuccess(result)); + else + return Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, $"Value {option.Value} cannot be converted to {nameof(T)}")); + } + + public override void Write(ApplicationCommandOptionProperties properties, IParameterInfo parameterInfo) + { + var names = Enum.GetNames(typeof(T)); + if (names.Length <= 25) + { + var choices = new List(); + + foreach (var name in names) + choices.Add(new ApplicationCommandOptionChoiceProperties + { + Name = name, + Value = name + }); + + properties.Choices = choices; + } + } +} diff --git a/docs/guides/int_framework/typeconverters.md b/docs/guides/int_framework/typeconverters.md new file mode 100644 index 0000000..a15fcea --- /dev/null +++ b/docs/guides/int_framework/typeconverters.md @@ -0,0 +1,118 @@ +--- +uid: Guides.IntFw.TypeConverters +title: Parameter Type Converters +--- + +# TypeConverters + +[TypeConverter]s are responsible for registering command parameters to Discord and parsing the user inputs into method parameters. + +By default, TypeConverters for the following types are provided with @Discord.Interactions library. + +- Implementations of [IUser] +- Implementations of [IChannel] +- Implementations of [IRole] +- Implementations of [IMentionable] +- `string` +- `float`, `double`, `decimal` +- `bool` +- `char` +- `sbyte`, `byte` +- `int16`, `int32`, `int64` +- `uint16`, `uint32`, `uint64` +- `enum` +- `DateTime` +- `TimeSpan` + +## Creating TypeConverters + +Depending on your needs, there are two types of TypeConverters you can create: + +- Concrete type +- Generic type + +A valid converter must inherit [TypeConverter] base type. And override the abstract base methods. + +### CanConvertTo() Method + +This method is used by Interaction Service to search for alternative Type Converters. + +Interaction Services determines the most suitable [TypeConverter] for a parameter type in the following order: + +1. It searches for a [TypeConverter] that is registered to specifically target that parameter type +2. It searches for a [TypeConverter] that returns `true` when its `CanConvertTo()` method is invoked for thaty parameter type. +3. It searches for a generic `TypeConverter` with a matching type constraint. If there are more multiple matches, +the one whose type constraint is the most specialized will be chosen. + +> [!NOTE] +> Alternatively, you can use the generic variant (`TypeConverter`) of the +> [TypeConverter] base class which implements the following method body for `CanConvertTo()` method + +```csharp +public sealed override bool CanConvertTo (Type type) => + typeof(T).IsAssignableFrom(type); +``` + +### GetDiscordType() Method + +This method is used by [InteractionService] to determine the +[Discord Application Command Option type](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-type) +of a parameter type. + +### ReadAsync() Method + +This method is used by [InteractionService] to parse the user input. +This method should return @Discord.Interactions.TypeConverterResult.FromSuccess* if the parsing operation is successful, +otherwise it should return @Discord.Interactions.TypeConverterResult.FromError* . +The inner logic of this method is totally up to you, +however you should avoid using long running code. + +### Write() Method + +This method is used to configure the **Discord Application Command Option** before it gets registered to Discord. +Command Option is configured by modifying the `ApplicationCommandOptionProperties` instance. + +> [!WARNING] +> The default parameter building pipeline is isolated and will not be disturbed by the [TypeConverter] workflow. +> But changes made in this method will override the values generated by the +> [InteractionService] for a **Discord Application Command Option**. + +## Example Enum TypeConverter + +[!code-csharp[Enum Converter](samples/typeconverters/enum_converter.cs)] + +> [!IMPORTANT] +> TypeConverters must be registered prior to module discovery. +> If Interaction Service encounters a parameter type that doesn't belong to any of the +> registered [TypeConverters] during this phase, it will throw an exception. + +## Concrete TypeConverters + +Registering Concrete TypeConverters are as simple as creating an instance of your custom converter and invoking `AddTypeConverter()` method. + +```csharp +interactionService.AddTypeConverter(new StringArrayConverter()); +``` + +## Generic TypeConverters + +To register a generic `TypeConverter`, you need to invoke the `AddGenericTypeConverter()` method of the Interaction Service class. +You need to pass the type of your `TypeConverter` and a target base type to this method. + +For instance, to register the previously mentioned enum converter the following can be used: + +```csharp +interactionService.AddGenericTypeConverter(typeof(EnumConverter<>)); +``` + +Interaction service checks if the target base type satisfies the type constraints of the Generic `TypeConverter` class. + +> [!NOTE] +> Dependencies of Generic TypeConverters are also resolved using the Dependency Injection pattern. + +[TypeConverter]: xref:Discord.Interactions.TypeConverter +[InteractionService]: xref:Discord.Interactions.InteractionService +[IChannel]: xref:Discord.IChannel +[IRole]: xref:Discord.IRole +[IUser]: xref:Discord.IUser +[IMentionable]: xref:Discord.IMentionable diff --git a/docs/guides/introduction/intro.md b/docs/guides/introduction/intro.md new file mode 100644 index 0000000..9b9e5ec --- /dev/null +++ b/docs/guides/introduction/intro.md @@ -0,0 +1,48 @@ +--- +uid: Guides.Introduction +title: Introduction to Discord.Net +--- + +# Introduction + +## Looking to get started? + +Welcome! Before you dive into this 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. With all +that being said, feel free to visit us on Discord at the link below +if you have any questions! + +An official collection of samples can be found +in [our GitHub repository]. + +> [!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. + +[our GitHub repository]: https://github.com/discord-net/Discord.Net/ +[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? + +All examples or snippets featured in this guide and all API +documentation will be written in C#. + +If you are new to the language, using this wrapper 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](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/) (Microsoft, Free) +- [Learn .NET](https://dotnet.microsoft.com/en-us/learn) (Microsoft, Free) +- [C# Path](https://www.pluralsight.com/paths/csharp) (Pluralsight, Paid) + +## Still have questions? + +Please visit us at our [Discord](https://discord.gg/dnet) server. +Describe the problem in details to us, what you've tried and what you need help with. diff --git a/docs/guides/other_libs/efcore.md b/docs/guides/other_libs/efcore.md new file mode 100644 index 0000000..ffdea42 --- /dev/null +++ b/docs/guides/other_libs/efcore.md @@ -0,0 +1,61 @@ +--- +uid: Guides.OtherLibs.EFCore +title: EFCore +--- + +# Entity Framework Core + +In this guide we will set up EFCore with a PostgreSQL database. Information on other databases will be at the bottom of this page. + +## Prerequisites + +- A simple bot with dependency injection configured +- A running PostgreSQL instance +- [EFCore CLI tools](https://docs.microsoft.com/en-us/ef/core/cli/dotnet#installing-the-tools) + +## Downloading the required packages + +You can install the following packages through your IDE or go to the nuget link to grab the dotnet cli command. + +|Name|Link| +|--|--| +| `Microsoft.EntityFrameworkCore` | [link](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore) | +| `Npgsql.EntityFrameworkCore.PostgreSQL` | [link](https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL)| + +## Configuring the DbContext + +To use EFCore, you need a DbContext to access everything in your database. The DbContext will look like this. Here is an example entity to show you how you can add more entities yourself later on. + +[!code-csharp[DBContext Sample](samples/DbContextSample.cs)] + +> [!NOTE] +> To learn more about creating the EFCore model, visit the following [link](https://docs.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli#create-the-model) + +## Adding the DbContext to your Dependency Injection container + +To add your newly created DbContext to your Dependency Injection container, simply use the extension method provided by EFCore to add the context to your container. It should look something like this + +[!code-csharp[DBContext Dependency Injection](samples/DbContextDepInjection.cs)] + +> [!NOTE] +> You can find out how to get your connection string [here](https://www.connectionstrings.com/npgsql/standard/) + +## Migrations + +Before you can start using your DbContext, you have to migrate the changes you've made in your code to your actual database. +To learn more about migrations, visit the official Microsoft documentation [here](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli) + +## Using the DbContext + +You can now use the DbContext wherever you can inject it. Here's an example on injecting it into an interaction command module. + +[!code-csharp[DBContext injected into interaction module](samples/InteractionModuleDISample.cs)] + +## Using a different database provider + +Here's a couple of popular database providers for EFCore and links to tutorials on how to set them up. The only thing that usually changes is the provider inside of your `DbContextOptions` + +| Provider | Link | +|--|--| +| MySQL | [link](https://dev.mysql.com/doc/connector-net/en/connector-net-entityframework-core-example.html) | +| SQLite | [link](https://docs.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli) | diff --git a/docs/guides/other_libs/images/mediatr_output.png b/docs/guides/other_libs/images/mediatr_output.png new file mode 100644 index 0000000..8018093 Binary files /dev/null and b/docs/guides/other_libs/images/mediatr_output.png differ diff --git a/docs/guides/other_libs/images/serilog_output.png b/docs/guides/other_libs/images/serilog_output.png new file mode 100644 index 0000000..67de0fa Binary files /dev/null and b/docs/guides/other_libs/images/serilog_output.png differ diff --git a/docs/guides/other_libs/mediatr.md b/docs/guides/other_libs/mediatr.md new file mode 100644 index 0000000..8326287 --- /dev/null +++ b/docs/guides/other_libs/mediatr.md @@ -0,0 +1,70 @@ +--- +uid: Guides.OtherLibs.MediatR +title: MediatR +--- + +# Configuring MediatR + +## Prerequisites + +- A simple bot with dependency injection configured + +## Downloading the required packages + +You can install the following packages through your IDE or go to the NuGet link to grab the dotnet cli command. + +|Name|Link| +|--|--| +| `MediatR` | [link](https://www.nuget.org/packages/MediatR) | +| `MediatR.Extensions.Microsoft.DependencyInjection` | [link](https://www.nuget.org/packages/MediatR.Extensions.Microsoft.DependencyInjection)| + +## Adding MediatR to your dependency injection container + +Adding MediatR to your dependency injection is made easy by the `MediatR.Extensions.Microsoft.DependencyInjection` package. You can use the following piece of code to configure it. The parameter of `.AddMediatR()` can be any type that is inside of the assembly you will have your event handlers in. + +[!code-csharp[Configuring MediatR](samples/MediatrConfiguringDI.cs)] + +## Creating notifications + +The way MediatR publishes events throughout your applications is through notifications and notification handlers. For this guide we will create a notification to handle the `MessageReceived` event on the `DiscordSocketClient`. + +[!code-csharp[Creating a notification](samples/MediatrCreatingMessageNotification.cs)] + +## Creating the notification publisher / event listener + +For MediatR to actually publish the events we need a way to listen for them. We will create a class to listen for discord events like so: + +[!code-csharp[Creating an event listener](samples/MediatrDiscordEventListener.cs)] + +The code above does a couple of things. First it receives the DiscordSocketClient from the dependency injection container. It can then use this client to register events. In this guide we will be focusing on the MessageReceived event. You register the event like any ordinary event, but inside of the handler method we will use MediatR to publish our event to all of our notification handlers. + +## Adding the event listener to your dependency injection container + +To start the listener we have to call the `StartAsync()` method on our `DiscordEventListener` class from inside of our main function. To do this, first register the `DiscordEventListener` class in your dependency injection container and get a reference to it in your main method. + +[!code-csharp[Starting the event listener](samples/MediatrStartListener.cs)] + +## Creating your notification handler + +MediatR publishes notifications to all of your notification handlers that are listening for a specific notification. We will create a handler for our newly created `MessageReceivedNotification` like this: + +[!code-csharp[Creating an event listener](samples/MediatrMessageReceivedHandler.cs)] + +The code above implements the `INotificationHandler<>` interface provided by MediatR, this tells MediatR to dispatch `MessageReceivedNotification` notifications to this handler class. + +> [!NOTE] +> You can create as many notification handlers for the same notification as you desire. That's the beauty of MediatR! + +## Testing + +To test if we have successfully implemented MediatR, we can start up the bot and send a message to a server the bot is in. It should print out the message we defined earlier in our `MessageReceivedHandler`. + +![MediatR output](images/mediatr_output.png) + +## Adding more event types + +To add more event types you can follow these steps: + +1. Create a new notification class for the event. it should contain all of the parameters that the event would send. (Ex: the `MessageReceived` event takes one `SocketMessage` as an argument. The notification class should also map this argument) +2. Register the event in your `DiscordEventListener` class. +3. Create a notification handler for your new notification. diff --git a/docs/guides/other_libs/samples/ConfiguringSerilog.cs b/docs/guides/other_libs/samples/ConfiguringSerilog.cs new file mode 100644 index 0000000..0d47064 --- /dev/null +++ b/docs/guides/other_libs/samples/ConfiguringSerilog.cs @@ -0,0 +1,36 @@ +using Discord; +using Serilog; +using Serilog.Events; + +public class Program +{ + static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); + + public async Task MainAsync() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + _client = new DiscordSocketClient(); + + _client.Log += LogAsync; + + // You can assign your bot token to a string, and pass that in to connect. + // This is, however, insecure, particularly if you plan to have your code hosted in a public repository. + var token = "token"; + + // Some alternative options would be to keep your token in an Environment Variable or a standalone file. + // var token = Environment.GetEnvironmentVariable("NameOfYourEnvironmentVariable"); + // var token = File.ReadAllText("token.txt"); + // var token = JsonConvert.DeserializeObject(File.ReadAllText("config.json")).Token; + + await _client.LoginAsync(TokenType.Bot, token); + await _client.StartAsync(); + + // Block this task until the program is closed. + await Task.Delay(Timeout.Infinite); + } +} diff --git a/docs/guides/other_libs/samples/DbContextDepInjection.cs b/docs/guides/other_libs/samples/DbContextDepInjection.cs new file mode 100644 index 0000000..5d98999 --- /dev/null +++ b/docs/guides/other_libs/samples/DbContextDepInjection.cs @@ -0,0 +1,9 @@ +private static ServiceProvider ConfigureServices() +{ + return new ServiceCollection() + .AddDbContext( + options => options.UseNpgsql("Your connection string") + ) + [...] + .BuildServiceProvider(); +} diff --git a/docs/guides/other_libs/samples/DbContextSample.cs b/docs/guides/other_libs/samples/DbContextSample.cs new file mode 100644 index 0000000..96104ae --- /dev/null +++ b/docs/guides/other_libs/samples/DbContextSample.cs @@ -0,0 +1,19 @@ +// ApplicationDbContext.cs +using Microsoft.EntityFrameworkCore; + +public class ApplicationDbContext : DbContext +{ + public ApplicationDbContext(DbContextOptions options) : base(options) + { + + } + + public DbSet Users { get; set; } = null!; +} + +// UserEntity.cs +public class UserEntity +{ + public ulong Id { get; set; } + public string Name { get; set; } +} diff --git a/docs/guides/other_libs/samples/InteractionModuleDISample.cs b/docs/guides/other_libs/samples/InteractionModuleDISample.cs new file mode 100644 index 0000000..777d6ae --- /dev/null +++ b/docs/guides/other_libs/samples/InteractionModuleDISample.cs @@ -0,0 +1,20 @@ +using Discord; + +public class SampleModule : InteractionModuleBase +{ + private readonly ApplicationDbContext _db; + + public SampleModule(ApplicationDbContext db) + { + _db = db; + } + + [SlashCommand("sample", "sample")] + public async Task Sample() + { + // Do stuff with your injected DbContext + var user = _db.Users.FirstOrDefault(x => x.Id == Context.User.Id); + + ... + } +} diff --git a/docs/guides/other_libs/samples/LogDebugSample.cs b/docs/guides/other_libs/samples/LogDebugSample.cs new file mode 100644 index 0000000..e796e20 --- /dev/null +++ b/docs/guides/other_libs/samples/LogDebugSample.cs @@ -0,0 +1 @@ +Log.Debug("Your log message, with {Variables}!", 10); // This will output "[21:51:00 DBG] Your log message, with 10!" diff --git a/docs/guides/other_libs/samples/MediatrConfiguringDI.cs b/docs/guides/other_libs/samples/MediatrConfiguringDI.cs new file mode 100644 index 0000000..3bef7bd --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrConfiguringDI.cs @@ -0,0 +1 @@ +.AddMediatR(typeof(Bot)) diff --git a/docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs b/docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs new file mode 100644 index 0000000..449c96e --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrCreatingMessageNotification.cs @@ -0,0 +1,16 @@ +// MessageReceivedNotification.cs + +using Discord.WebSocket; +using MediatR; + +namespace MediatRSample.Notifications; + +public class MessageReceivedNotification : INotification +{ + public MessageReceivedNotification(SocketMessage message) + { + Message = message ?? throw new ArgumentNullException(nameof(message)); + } + + public SocketMessage Message { get; } +} diff --git a/docs/guides/other_libs/samples/MediatrDiscordEventListener.cs b/docs/guides/other_libs/samples/MediatrDiscordEventListener.cs new file mode 100644 index 0000000..09583c3 --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrDiscordEventListener.cs @@ -0,0 +1,46 @@ +// DiscordEventListener.cs + +using Discord.WebSocket; +using MediatR; +using MediatRSample.Notifications; +using Microsoft.Extensions.DependencyInjection; +using System.Threading; +using System.Threading.Tasks; + +namespace MediatRSample; + +public class DiscordEventListener +{ + private readonly CancellationToken _cancellationToken; + + private readonly DiscordSocketClient _client; + private readonly IServiceScopeFactory _serviceScope; + + public DiscordEventListener(DiscordSocketClient client, IServiceScopeFactory serviceScope) + { + _client = client; + _serviceScope = serviceScope; + _cancellationToken = new CancellationTokenSource().Token; + } + + private IMediator Mediator + { + get + { + var scope = _serviceScope.CreateScope(); + return scope.ServiceProvider.GetRequiredService(); + } + } + + public async Task StartAsync() + { + _client.MessageReceived += OnMessageReceivedAsync; + + await Task.CompletedTask; + } + + private Task OnMessageReceivedAsync(SocketMessage arg) + { + return Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken); + } +} diff --git a/docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs b/docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs new file mode 100644 index 0000000..1ab2491 --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrMessageReceivedHandler.cs @@ -0,0 +1,17 @@ +// MessageReceivedHandler.cs + +using System; +using MediatR; +using MediatRSample.Notifications; + +namespace MediatRSample; + +public class MessageReceivedHandler : INotificationHandler +{ + public async Task Handle(MessageReceivedNotification notification, CancellationToken cancellationToken) + { + Console.WriteLine($"MediatR works! (Received a message by {notification.Message.Author.Username})"); + + // Your implementation + } +} diff --git a/docs/guides/other_libs/samples/MediatrStartListener.cs b/docs/guides/other_libs/samples/MediatrStartListener.cs new file mode 100644 index 0000000..72a54bf --- /dev/null +++ b/docs/guides/other_libs/samples/MediatrStartListener.cs @@ -0,0 +1,4 @@ +// Program.cs + +var listener = services.GetRequiredService(); +await listener.StartAsync(); diff --git a/docs/guides/other_libs/samples/ModifyLogMethod.cs b/docs/guides/other_libs/samples/ModifyLogMethod.cs new file mode 100644 index 0000000..0f7c11d --- /dev/null +++ b/docs/guides/other_libs/samples/ModifyLogMethod.cs @@ -0,0 +1,15 @@ +private static async Task LogAsync(LogMessage message) +{ + var severity = message.Severity switch + { + LogSeverity.Critical => LogEventLevel.Fatal, + LogSeverity.Error => LogEventLevel.Error, + LogSeverity.Warning => LogEventLevel.Warning, + LogSeverity.Info => LogEventLevel.Information, + LogSeverity.Verbose => LogEventLevel.Verbose, + LogSeverity.Debug => LogEventLevel.Debug, + _ => LogEventLevel.Information + }; + Log.Write(severity, message.Exception, "[{Source}] {Message}", message.Source, message.Message); + await Task.CompletedTask; +} diff --git a/docs/guides/other_libs/serilog.md b/docs/guides/other_libs/serilog.md new file mode 100644 index 0000000..5086b4b --- /dev/null +++ b/docs/guides/other_libs/serilog.md @@ -0,0 +1,45 @@ +--- +uid: Guides.OtherLibs.Serilog +title: Serilog +--- + +# Configuring serilog + +## Prerequisites + +- A basic working bot with a logging method as described in [Creating your first bot](xref:Guides.GettingStarted.FirstBot) + +## Installing the Serilog package + +You can install the following packages through your IDE or go to the nuget link to grab the dotnet cli command. + +|Name|Link| +|--|--| +|`Serilog.Extensions.Logging`| [link](https://www.nuget.org/packages/Serilog.Extensions.Logging)| +|`Serilog.Sinks.Console`| [link](https://www.nuget.org/packages/Serilog.Sinks.Console)| + +## Configuring Serilog + +Serilog will be configured at the top of your async Main method, it looks like this + +[!code-csharp[Configuring serilog](samples/ConfiguringSerilog.cs)] + +## Modifying your logging method + +For Serilog to log Discord events correctly, we have to map the Discord `LogSeverity` to the Serilog `LogEventLevel`. You can modify your log method to look like this. + +[!code-csharp[Modifying your log method](samples/ModifyLogMethod.cs)] + +## Testing + +If you run your application now, you should see something similar to this +![Serilog output](images/serilog_output.png) + +## Using your new logger in other places + +Now that you have set up Serilog, you can use it everywhere in your application by simply calling + +[!code-csharp[Log debug sample](samples/LogDebugSample.cs)] + +> [!NOTE] +> Depending on your configured log level, the log messages may or may not show up in your console. Refer to [Serilog's github page](https://github.com/serilog/serilog/wiki/Configuration-Basics#minimum-level) for more information about log levels. diff --git a/docs/guides/text_commands/intro.md b/docs/guides/text_commands/intro.md new file mode 100644 index 0000000..08d8730 --- /dev/null +++ b/docs/guides/text_commands/intro.md @@ -0,0 +1,228 @@ +--- +uid: Guides.TextCommands.Intro +title: Introduction to the Chat Command Service +--- + +# The Text Command Service + +[Discord.Commands](xref:Discord.Commands) provides an attribute-based +command parser. + +> [!IMPORTANT] +> The 'Message Content' intent, required for text commands, is now a +> privileged intent. Please use [Slash commands](xref:Guides.SlashCommands.Intro) +> instead for making commands. For more information about this change +> please check [this announcement made by discord](https://support-dev.discord.com/hc/en-us/articles/4404772028055-Message-Content-Privileged-Intent-FAQ) + + +## 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/intro/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 by the +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/intro/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. +> Long-running code, by default, within a command module +> can cause gateway thread to be blocked; therefore, interrupting +> the bot's connection to Discord. +> +> You may read more about it in @FAQ.Commands.General . + +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 space-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 descending 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/intro/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 [Dependency Injection](xref:Guides.DI.Intro). 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/intro/groups.cs)] diff --git a/docs/guides/text_commands/namedarguments.md b/docs/guides/text_commands/namedarguments.md new file mode 100644 index 0000000..18281d6 --- /dev/null +++ b/docs/guides/text_commands/namedarguments.md @@ -0,0 +1,79 @@ +--- +uid: Guides.TextCommands.NamedArguments +title: Named Arguments +--- + +# Named Arguments + +By default, arguments for commands are parsed positionally, meaning +that the order matters. But sometimes you may want to define a command +with many optional parameters, and it'd be easier for end-users +to only specify what they want to set, instead of needing them +to specify everything by hand. + +## Setting up Named Arguments + +In order to be able to specify different arguments by name, you have +to create a new class that contains all of the optional values that +the command will use, and apply an instance of +[NamedArgumentTypeAttribute] on it. + +### Example - Creating a Named Arguments Type + +```cs +[NamedArgumentType] +public class NamableArguments +{ + public string First { get; set; } + public string Second { get; set; } + public string Third { get; set; } + public string Fourth { get; set; } +} +``` + +## Usage in a Command + +The command where you want to use these values can be declared like so: +```cs +[Command("act")] +public async Task Act(int requiredArg, NamableArguments namedArgs) +``` + +The command can now be invoked as +`.act 42 first: Hello fourth: "A string with spaces must be wrapped in quotes" second: World`. + +A TypeReader for the named arguments container type is +automatically registered. +It's important that any other arguments that would be required +are placed before the container type. + +> [!IMPORTANT] +> A single command can have only __one__ parameter of a +> type annotated with [NamedArgumentTypeAttribute], and it +> **MUST** be the last parameter in the list. +> A command parameter of such an annotated type +> is automatically treated as if that parameter +> has [RemainderAttribute](xref:Discord.Commands.RemainderAttribute) +> applied. + +## Complex Types + +The TypeReader for Named Argument Types will look for a TypeReader +of every property type, meaning any other command parameter type +will work just the same. + +You can also read multiple values into a single property +by making that property an `IEnumerable`. So for example, if your +Named Argument Type has the following field, +```cs +public IEnumerable Numbers { get; set; } +``` +then the command can be invoked as +`.cmd numbers: "1, 2, 4, 8, 16, 32"` + +## Additional Notes + +The use of [`[OverrideTypeReader]`](xref:Discord.Commands.OverrideTypeReaderAttribute) +is also supported on the properties of a Named Argument Type. + +[NamedArgumentTypeAttribute]: xref:Discord.Commands.NamedArgumentTypeAttribute diff --git a/docs/guides/text_commands/post-execution.md b/docs/guides/text_commands/post-execution.md new file mode 100644 index 0000000..49fe2f5 --- /dev/null +++ b/docs/guides/text_commands/post-execution.md @@ -0,0 +1,120 @@ +--- +uid: Guides.TextCommands.PostExecution +title: Post-command Execution Handling +--- + +# Post-execution Handling for Text Commands + +When developing commands, you may want to consider building a +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 have shown the following +example for executing and handling commands, + +[!code[Command Handler](samples/intro/command_handler.cs)] + +You may notice that after we perform [ExecuteAsync], we store the +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, + +[!code[Basic Command Handler](samples/post-execution/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 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 +[ExecuteResult] instead of the actual result. You can learn more +about the impact in @FAQ.Commands.General. + +## CommandExecuted Event + +Enter [CommandExecuted], an event that was introduced in +Discord.Net 2.0. This event is raised whenever a command is +executed regardless of its execution status. This means this +event can be used to streamline your post-execution design, +is not prone to `RunMode.Async`'s [ExecuteAsync] drawbacks. + +Thus, we can begin working on code such as: + +[!code[CommandExecuted demo](samples/post-execution/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 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. + +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 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/post-execution/customresult_base.cs)] + +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 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/post-execution/customresult_extended.cs)] + +After you're done creating your [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/post-execution/customresult_usage.cs)] + +And now we can check for it in our [CommandExecuted] handler: + +[!code[Usage](samples/post-execution/command_executed_adv_demo.cs)] + +## CommandService.Log Event + +We have so far covered the handling of various result types, but we +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 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. + +[!code[Logger Sample](samples/post-execution/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.TextCommands.Intro diff --git a/docs/guides/text_commands/preconditions.md b/docs/guides/text_commands/preconditions.md new file mode 100644 index 0000000..4be7ca2 --- /dev/null +++ b/docs/guides/text_commands/preconditions.md @@ -0,0 +1,83 @@ +--- +uid: Guides.TextCommands.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 + +## Using Preconditions + +To use a precondition, simply apply any valid precondition candidate to +a command method signature as an attribute. + +### Example - Using a Precondition + +[!code-csharp[Precondition usage](samples/preconditions/precondition_usage.cs)] + +## ORing Preconditions + +When writing commands, you may want to allow some of them to be +executed when only some of the precondition checks are passed. + +This is where the [Group] property of a precondition attribute comes in +handy. By assigning two or more preconditions to a group, the command +system will allow the command to be executed when one of the +precondition passes. + +### Example - ORing Preconditions + +[!code-csharp[OR Precondition](samples/preconditions/group_precondition.cs)] + +[Group]: xref:Discord.Commands.PreconditionAttribute.Group + +## 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/preconditions/require_role.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/text_commands/samples/intro/command_handler.cs b/docs/guides/text_commands/samples/intro/command_handler.cs new file mode 100644 index 0000000..480e43c --- /dev/null +++ b/docs/guides/text_commands/samples/intro/command_handler.cs @@ -0,0 +1,55 @@ +public class CommandHandler +{ + private readonly DiscordSocketClient _client; + private readonly CommandService _commands; + + // Retrieve client and CommandService instance via ctor + public CommandHandler(DiscordSocketClient client, CommandService commands) + { + _commands = commands; + _client = client; + } + + public async Task InstallCommandsAsync() + { + // Hook the MessageReceived event into our command handler + _client.MessageReceived += HandleCommandAsync; + + // Here we discover all of the command modules in the entry + // assembly and load them. Starting from Discord.NET 2.0, a + // service provider is required to be passed into the + // module registration method to inject the + // required dependencies. + // + // If you do not use Dependency Injection, pass null. + // See Dependency Injection guide for more information. + await _commands.AddModulesAsync(assembly: Assembly.GetEntryAssembly(), + services: null); + } + + private async Task HandleCommandAsync(SocketMessage messageParam) + { + // Don't process the command if it was a system message + var message = messageParam as SocketUserMessage; + if (message == null) return; + + // Create a number to track where the prefix ends and the command begins + int argPos = 0; + + // Determine if the message is a command based on the prefix and make sure no bots trigger commands + if (!(message.HasCharPrefix('!', ref argPos) || + message.HasMentionPrefix(_client.CurrentUser, ref argPos)) || + message.Author.IsBot) + return; + + // Create a WebSocket-based command context based on the message + var context = new SocketCommandContext(_client, message); + + // Execute the command with the command context we just + // created, along with the service provider for precondition checks. + await _commands.ExecuteAsync( + context: context, + argPos: argPos, + services: null); + } +} diff --git a/docs/guides/text_commands/samples/intro/empty-module.cs b/docs/guides/text_commands/samples/intro/empty-module.cs new file mode 100644 index 0000000..db62032 --- /dev/null +++ b/docs/guides/text_commands/samples/intro/empty-module.cs @@ -0,0 +1,8 @@ +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 +{ + +} \ No newline at end of file diff --git a/docs/guides/text_commands/samples/intro/groups.cs b/docs/guides/text_commands/samples/intro/groups.cs new file mode 100644 index 0000000..e117a52 --- /dev/null +++ b/docs/guides/text_commands/samples/intro/groups.cs @@ -0,0 +1,25 @@ +[Group("admin")] +public class AdminModule : ModuleBase +{ + [Group("clean")] + public class CleanModule : ModuleBase + { + // ~admin clean + [Command] + public async Task DefaultCleanAsync() + { + // ... + } + + // ~admin clean messages 15 + [Command("messages")] + public async Task CleanAsync(int count) + { + // ... + } + } + // ~admin ban foxbot#0282 + [Command("ban")] + public Task BanAsync(IGuildUser user) => + Context.Guild.AddBanAsync(user); +} \ No newline at end of file diff --git a/docs/guides/text_commands/samples/intro/module.cs b/docs/guides/text_commands/samples/intro/module.cs new file mode 100644 index 0000000..e5fe705 --- /dev/null +++ b/docs/guides/text_commands/samples/intro/module.cs @@ -0,0 +1,45 @@ +// Create a module with no prefix +public class InfoModule : ModuleBase +{ + // ~say hello world -> hello world + [Command("say")] + [Summary("Echoes a message.")] + 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 SampleModule : ModuleBase +{ + // ~sample square 20 -> 400 + [Command("square")] + [Summary("Squares a number.")] + 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)}"); + } + + // ~sample userinfo --> foxbot#0282 + // ~sample userinfo @Khionu --> Khionu#8708 + // ~sample userinfo Khionu#8708 --> Khionu#8708 + // ~sample userinfo Khionu --> Khionu#8708 + // ~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.")] + [Alias("user", "whois")] + 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/text_commands/samples/post-execution/command_exception_log.cs b/docs/guides/text_commands/samples/post-execution/command_exception_log.cs new file mode 100644 index 0000000..fa3673e --- /dev/null +++ b/docs/guides/text_commands/samples/post-execution/command_exception_log.cs @@ -0,0 +1,12 @@ +public async Task LogAsync(LogMessage logMessage) +{ + 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/text_commands/samples/post-execution/command_executed_adv_demo.cs b/docs/guides/text_commands/samples/post-execution/command_executed_adv_demo.cs new file mode 100644 index 0000000..dbd75f0 --- /dev/null +++ b/docs/guides/text_commands/samples/post-execution/command_executed_adv_demo.cs @@ -0,0 +1,13 @@ +public async Task OnCommandExecutedAsync(Optional 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/text_commands/samples/post-execution/command_executed_demo.cs b/docs/guides/text_commands/samples/post-execution/command_executed_demo.cs new file mode 100644 index 0000000..a0b2618 --- /dev/null +++ b/docs/guides/text_commands/samples/post-execution/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(Optional 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) + var commandName = command.IsSpecified ? command.Value.Name : "A command"; + await _log.LogAsync(new LogMessage(LogSeverity.Info, + "CommandExecution", + $"{commandName} 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)) || + message.Author.IsBot) return; + var context = new SocketCommandContext(_client, message); + await _commands.ExecuteAsync(context, argPos, _services); +} diff --git a/docs/guides/text_commands/samples/post-execution/customresult_base.cs b/docs/guides/text_commands/samples/post-execution/customresult_base.cs new file mode 100644 index 0000000..895a370 --- /dev/null +++ b/docs/guides/text_commands/samples/post-execution/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/text_commands/samples/post-execution/customresult_extended.cs b/docs/guides/text_commands/samples/post-execution/customresult_extended.cs new file mode 100644 index 0000000..6754c96 --- /dev/null +++ b/docs/guides/text_commands/samples/post-execution/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/text_commands/samples/post-execution/customresult_usage.cs b/docs/guides/text_commands/samples/post-execution/customresult_usage.cs new file mode 100644 index 0000000..44266cf --- /dev/null +++ b/docs/guides/text_commands/samples/post-execution/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, I don't want that!"); + return MyCustomResult.FromSuccess($"Give me the {food}!"). + } +} \ No newline at end of file diff --git a/docs/guides/text_commands/samples/post-execution/post-execution_basic.cs b/docs/guides/text_commands/samples/post-execution/post-execution_basic.cs new file mode 100644 index 0000000..d1361a1 --- /dev/null +++ b/docs/guides/text_commands/samples/post-execution/post-execution_basic.cs @@ -0,0 +1,14 @@ +// Bad code!!! +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/text_commands/samples/preconditions/group_precondition.cs b/docs/guides/text_commands/samples/preconditions/group_precondition.cs new file mode 100644 index 0000000..bae102b --- /dev/null +++ b/docs/guides/text_commands/samples/preconditions/group_precondition.cs @@ -0,0 +1,9 @@ +// The following example only requires the user to either have the +// Administrator permission in this guild or own the bot application. +[RequireUserPermission(GuildPermission.Administrator, Group = "Permission")] +[RequireOwner(Group = "Permission")] +public class AdminModule : ModuleBase +{ + [Command("ban")] + public Task BanAsync(IUser user) => Context.Guild.AddBanAsync(user); +} \ No newline at end of file diff --git a/docs/guides/text_commands/samples/preconditions/precondition_usage.cs b/docs/guides/text_commands/samples/preconditions/precondition_usage.cs new file mode 100644 index 0000000..eacee93 --- /dev/null +++ b/docs/guides/text_commands/samples/preconditions/precondition_usage.cs @@ -0,0 +1,3 @@ +[RequireOwner] +[Command("echo")] +public Task EchoAsync(string input) => ReplyAsync(input); \ No newline at end of file diff --git a/docs/guides/text_commands/samples/preconditions/require_role.cs b/docs/guides/text_commands/samples/preconditions/require_role.cs new file mode 100644 index 0000000..d9a393a --- /dev/null +++ b/docs/guides/text_commands/samples/preconditions/require_role.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Discord.Commands; +using Discord.WebSocket; + +// Inherit from PreconditionAttribute +public class RequireRoleAttribute : PreconditionAttribute +{ + // Create a field to store the specified name + private readonly string _name; + + // Create a constructor so the name can be specified + public RequireRoleAttribute(string name) => _name = name; + + // Override the CheckPermissions method + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + // Check if this user is a Guild User, which is the only context where roles exist + if (context.User is SocketGuildUser gUser) + { + // If this command was executed by a user with the appropriate role, return a success + if (gUser.Roles.Any(r => r.Name == _name)) + // Since no async work is done, the result has to be wrapped with `Task.FromResult` to avoid compiler errors + return Task.FromResult(PreconditionResult.FromSuccess()); + // Since it wasn't, fail + else + return Task.FromResult(PreconditionResult.FromError($"You must have a role named {_name} to run this command.")); + } + else + return Task.FromResult(PreconditionResult.FromError("You must be in a guild to run this command.")); + } +} diff --git a/docs/guides/text_commands/samples/typereaders/typereader-register.cs b/docs/guides/text_commands/samples/typereaders/typereader-register.cs new file mode 100644 index 0000000..292caea --- /dev/null +++ b/docs/guides/text_commands/samples/typereaders/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/text_commands/samples/typereaders/typereader.cs b/docs/guides/text_commands/samples/typereaders/typereader.cs new file mode 100644 index 0000000..2861187 --- /dev/null +++ b/docs/guides/text_commands/samples/typereaders/typereader.cs @@ -0,0 +1,17 @@ +// Please note that the library already supports type reading +// primitive types such as bool. This example is merely used +// to demonstrate how one could write a simple TypeReader. +using Discord; +using Discord.Commands; + +public class BooleanTypeReader : TypeReader +{ + public override Task ReadAsync(ICommandContext context, string input, IServiceProvider services) + { + bool result; + if (bool.TryParse(input, out result)) + return Task.FromResult(TypeReaderResult.FromSuccess(result)); + + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input could not be parsed as a boolean.")); + } +} diff --git a/docs/guides/text_commands/typereaders.md b/docs/guides/text_commands/typereaders.md new file mode 100644 index 0000000..bf911da --- /dev/null +++ b/docs/guides/text_commands/typereaders.md @@ -0,0 +1,70 @@ +--- +uid: Guides.TextCommands.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` +* `enum` +* `DateTime`/`DateTimeOffset`/`TimeSpan` +* Any nullable value-type (e.g. `int?`, `bool?`) +* 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/typereaders/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/typereaders/typereader-register.cs)] diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml new file mode 100644 index 0000000..f2cefda --- /dev/null +++ b/docs/guides/toc.yml @@ -0,0 +1,134 @@ +- name: Introduction + topicUid: Guides.Introduction +- name: V2 to V3 Guide + topicUid: Guides.V2V3Guide +- name: Getting Started + items: + - name: Installation + topicUid: Guides.GettingStarted.Installation + - name: Your First Bot + topicUid: Guides.GettingStarted.FirstBot + - name: Terminology + topicUid: Guides.GettingStarted.Terminology +- name: Basic Concepts + items: + - name: Logging Data + topicUid: Guides.Concepts.Logging + - name: Working with Events + topicUid: Guides.Concepts.Events + - name: Managing Connections + topicUid: Guides.Concepts.ManageConnections +- name: Entities + items: + - name: Introduction + topicUid: Guides.Entities.Intro + - name: Casting + topicUid: Guides.Entities.Casting + - name: Glossary & Flowcharts + topicUid: Guides.Entities.Glossary +- name: Dependency Injection + items: + - name: Introduction + topicUid: Guides.DI.Intro + - name: Injection + topicUid: Guides.DI.Injection + - name: Command- & Interaction Services + topicUid: Guides.DI.Services + - name: Service Types + topicUid: Guides.DI.Dependencies + - name: Scaling your Application + topicUid: Guides.DI.Scaling +- name: Working with Text-based Commands + items: + - name: Introduction + topicUid: Guides.TextCommands.Intro + - name: TypeReaders + topicUid: Guides.TextCommands.TypeReaders + - name: Named Arguments + topicUid: Guides.TextCommands.NamedArguments + - name: Preconditions + topicUid: Guides.TextCommands.Preconditions + - name: Post-execution Handling + topicUid: Guides.TextCommands.PostExecution +- name: Working with the Interaction Framework + items: + - name: Introduction + topicUid: Guides.IntFw.Intro + - name: Auto-Completion + topicUid: Guides.IntFw.AutoCompletion + - name: TypeConverters + topicUid: Guides.IntFw.TypeConverters + - name: Preconditions + topicUid: Guides.IntFw.Preconditions + - name: Post-execution Handling + topicUid: Guides.IntFw.PostExecution + - name: Permissions + topicUid: Guides.IntFw.Perms +- name: Slash Command Basics + items: + - name: Introduction + topicUid: Guides.SlashCommands.Intro + - name: Creating slash commands + topicUid: Guides.SlashCommands.Creating + - name: Receiving and responding to slash commands + topicUid: Guides.SlashCommands.Receiving + - name: Slash command parameters + topicUid: Guides.SlashCommands.Parameters + - name: Ephemeral responses + topicUid: Guides.SlashCommands.Ephemeral + - name: Sub commands + topicUid: Guides.SlashCommands.SubCommand + - name: Slash command choices + topicUid: Guides.SlashCommands.Choices + - name: Slash commands Bulk Overwrites + topicUid: Guides.SlashCommands.BulkOverwrite +- name: Context Command Basics + items: + - name: Creating Context Commands + topicUid: Guides.ContextCommands.Creating + - name: Receiving Context Commands + topicUid: Guides.ContextCommands.Reveiving +- name: Message Component Basics + items: + - name: Introduction + topicUid: Guides.MessageComponents.Intro + - name: Responding to Components + topicUid: Guides.MessageComponents.Responding + - name: Buttons in depth + topicUid: Guides.MessageComponents.Buttons + - name: Select menus + topicUid: Guides.MessageComponents.SelectMenus + - name: Text Input + topicUid: Guides.MessageComponents.TextInputs + - name: Advanced Concepts + topicUid: Guides.MessageComponents.Advanced +- name: Modal Basics + items: + - name: Introduction + topicUid: Guides.Modals.Intro +- name: Guild Events + items: + - name: Introduction + topicUid: Guides.GuildEvents.Intro + - name: Creating Events + topicUid: Guides.GuildEvents.Creating + - name: Getting Event Users + topicUid: Guides.GuildEvents.GettingUsers + - name: Modifying Events + topicUid: Guides.GuildEvents.Modifying +- name: Working with other libraries + items: + - name: Serilog + topicUid: Guides.OtherLibs.Serilog + - name: EFCore + topicUid: Guides.OtherLibs.EFCore + - name: MediatR + topicUid: Guides.OtherLibs.MediatR +- name: Emoji + topicUid: Guides.Emoji +- name: Bearer Tokens + topicUid: Guides.BearerToken +- name: Voice + topicUid: Guides.Voice.SendingVoice +- name: Deployment + topicUid: Guides.Deployment diff --git a/docs/guides/v2_v3_guide/v2_to_v3_guide.md b/docs/guides/v2_v3_guide/v2_to_v3_guide.md new file mode 100644 index 0000000..91fc1b4 --- /dev/null +++ b/docs/guides/v2_v3_guide/v2_to_v3_guide.md @@ -0,0 +1,91 @@ +--- +uid: Guides.V2V3Guide +title: V2 -> V3 Guide +--- + +# V2 to V3 Guide + +V3 is designed to be a more feature complete, more reliable, +and more flexible library than any previous version. + +Below are the most notable breaking changes that you would need to update your code to work with V3. + +### GatewayIntents + +As Discord.NET has upgraded from Discord API v6 to API v9, +`GatewayIntents` must now be specified in the socket config, as well as on the [developer portal]. + +```cs + +// Where ever you declared your websocket client. +DiscordSocketClient _client; + +... + +var config = new DiscordSocketConfig() +{ + .. // Other config options can be presented here. + GatewayIntents = GatewayIntents.All +} + +_client = new DiscordSocketClient(config); + +``` + +#### Common intents: + +- AllUnprivileged: This is a group of most common intents, that do NOT require any [developer portal] intents to be enabled. + This includes intents that receive messages such as: `GatewayIntents.GuildMessages, GatewayIntents.DirectMessages` +- GuildMembers: An intent disabled by default, as you need to enable it in the [developer portal]. +- MessageContent: An intent also disabled by default as you also need to enable it in the [developer portal]. +- GuildPresences: Also disabled by default, this intent together with `GuildMembers` are the only intents not included in `AllUnprivileged`. +- All: All intents, it is ill advised to use this without care, as it _can_ cause a memory leak from presence. + The library will give responsive warnings if you specify unnecessary intents. + +> [!NOTE] +> All gateway intents, their Discord API counterpart and their enum value are listed +> [HERE](xref:Discord.GatewayIntents) + +#### Stacking intents: + +It is common that you require several intents together. +The example below shows how this can be done. + +```cs + +GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.GuildMembers | .. + +``` + +> [!NOTE] +> Further documentation on the ` | ` operator can be found +> [HERE](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/bitwise-and-shift-operators) + +[developer portal]: https://discord.com/developers/ + +### UserLeft event + +UserLeft has been changed to have the `SocketUser` and `SocketGuild` parameters instead of a `SocketGuildUser` parameter. +Because of this, guild-only user data cannot be retrieved from this user anymore, as this user is not part of the guild. + +### ReactionAdded event + +The reaction added event has been changed to have both parameters cacheable. +This allows you to download the channel and message if they aren't cached instead of them being null. + +### UserIsTyping Event + +The user is typing event has been changed to have both parameters cacheable. +This allows you to download the user and channel if they aren't cached instead of them being null. + +### Presence + +There is a new event called `PresenceUpdated` that is called when a user's presence changes, +instead of `GuildMemberUpdated` or `UserUpdated`. +If your code relied on these events to get presence data then you need to update it to work with the new event. + +## Migrating your commands to application commands + +The new interaction service was designed to act like the previous service for text-based commands. +Your pre-existing code will continue to work, but you will need to migrate your modules and response functions to use the new +interaction service methods. Documentation on this can be found in the [Guides](xref:Guides.IntFw.Intro). diff --git a/docs/guides/voice/samples/audio_create_ffmpeg.cs b/docs/guides/voice/samples/audio_create_ffmpeg.cs new file mode 100644 index 0000000..dda560e --- /dev/null +++ b/docs/guides/voice/samples/audio_create_ffmpeg.cs @@ -0,0 +1,10 @@ +private Process CreateStream(string path) +{ + return Process.Start(new ProcessStartInfo + { + FileName = "ffmpeg", + Arguments = $"-hide_banner -loglevel panic -i \"{path}\" -ac 2 -f s16le -ar 48000 pipe:1", + UseShellExecute = false, + RedirectStandardOutput = true, + }); +} diff --git a/docs/guides/voice/samples/audio_ffmpeg.cs b/docs/guides/voice/samples/audio_ffmpeg.cs new file mode 100644 index 0000000..d36fbbc --- /dev/null +++ b/docs/guides/voice/samples/audio_ffmpeg.cs @@ -0,0 +1,11 @@ +private async Task SendAsync(IAudioClient client, string path) +{ + // Create FFmpeg using the previous example + using (var ffmpeg = CreateStream(path)) + using (var output = ffmpeg.StandardOutput.BaseStream) + using (var discord = client.CreatePCMStream(AudioApplication.Mixed)) + { + try { await output.CopyToAsync(discord); } + finally { await discord.FlushAsync(); } + } +} diff --git a/docs/guides/voice/samples/joining_audio.cs b/docs/guides/voice/samples/joining_audio.cs new file mode 100644 index 0000000..8803d35 --- /dev/null +++ b/docs/guides/voice/samples/joining_audio.cs @@ -0,0 +1,11 @@ +// The command's Run Mode MUST be set to RunMode.Async, otherwise, being connected to a voice channel will block the gateway thread. +[Command("join", RunMode = RunMode.Async)] +public async Task JoinChannel(IVoiceChannel channel = null) +{ + // Get the audio channel + channel = channel ?? (Context.User as IGuildUser)?.VoiceChannel; + if (channel == null) { await Context.Channel.SendMessageAsync("User must be in a voice channel, or a voice channel must be passed as an argument."); return; } + + // For the next step with transmitting audio, you would want to pass this Audio Client in to a service. + var audioClient = await channel.ConnectAsync(); +} diff --git a/docs/guides/voice/sending-voice.md b/docs/guides/voice/sending-voice.md new file mode 100644 index 0000000..48306f9 --- /dev/null +++ b/docs/guides/voice/sending-voice.md @@ -0,0 +1,112 @@ +--- +uid: Guides.Voice.SendingVoice +title: Sending Voice +--- + +**Information on this page is subject to change!** + +>[!WARNING] +>This article is out of date, and has not been rewritten yet. +Information is not guaranteed to be accurate. + +## Installing + +Audio requires two native libraries, `libsodium` and `opus`. +Both of these libraries must be placed in the runtime directory of your +bot. (When developing on .NET Framework, this would be `bin/debug`, +when developing on .NET Core, this is where you execute `dotnet run` +from; typically the same directory as your csproj). + +**For Windows users, precompiled binaries are available for your convenience [here](https://github.com/discord-net/Discord.Net/tree/dev/voice-natives).** + +**For Linux users, you will need to compile [Sodium] and [Opus] from source, or install them from your package manager.** + +[Sodium]: https://download.libsodium.org/libsodium/releases/ +[Opus]: http://downloads.xiph.org/releases/opus/ + +## Joining a Channel + +>[!NOTE] +>`GatewayIntents.GuildVoiceStates` and `GatewayIntents.Guilds` intents are required to connect to a voice channel + +Joining a channel is the first step to sending audio, and will return +an [IAudioClient] to send data with. + +To join a channel, simply await [ConnectAsync] on any instance of an +@Discord.IAudioChannel. + +[!code-csharp[Joining a Channel](samples/joining_audio.cs)] + +>[!WARNING] +>Commands which mutate voice states, such as those where you join/leave +>an audio channel, or send audio, should use [RunMode.Async]. RunMode.Async +>is necessary to prevent a feedback loop which will deadlock clients +>in their default configuration. If you know that you're running your +>commands in a different task than the gateway task, RunMode.Async is +>not required. + +The client will sustain a connection to this channel until it is +kicked, disconnected from Discord, or told to disconnect. + +It should be noted that voice connections are created on a per-guild +basis; only one audio connection may be open by the bot in a single +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.ConnectAsync* +[RunMode.Async]: xref:Discord.Commands.RunMode + +## Transmitting Audio + +### With FFmpeg + +[FFmpeg] is an open source, highly versatile AV-muxing tool. This is +the recommended method of transmitting audio. + +Before you begin, you will need to have a version of FFmpeg downloaded +and placed somewhere in your PATH (or alongside the bot, in the same +location as libsodium and opus). Windows binaries are available on +[FFmpeg's download page]. + +[FFmpeg]: https://ffmpeg.org/ +[FFmpeg's download page]: https://ffmpeg.org/download.html + +First, you will need to create a Process that starts FFmpeg. An +example of how to do this is included below, though it is important +that you return PCM at 48000hz. + +>[!NOTE] +>As of the time of this writing, Discord.Audio struggles significantly +>with processing audio that is already opus-encoded; you will need to +>use the PCM write streams. + +[!code-csharp[Creating FFmpeg](samples/audio_create_ffmpeg.cs)] + +Next, to transmit audio from FFmpeg to Discord, you will need to +pull an [AudioOutStream] from your [IAudioClient]. Since we're using +PCM audio, use [IAudioClient.CreatePCMStream]. + +The sample rate argument doesn't particularly matter, so long as it is +a valid rate (120, 240, 480, 960, 1920, or 2880). For the sake of +simplicity, I recommend using 1920. + +Channels should be left at `2`, unless you specified a different value +for `-ac 2` when creating FFmpeg. + +[AudioOutStream]: xref:Discord.Audio.AudioOutStream +[IAudioClient.CreatePCMStream]: xref:Discord.Audio.IAudioClient#Discord_Audio_IAudioClient_CreateDirectPCMStream_Discord_Audio_AudioApplication_System_Nullable_System_Int32__System_Int32_ + +Finally, audio will need to be piped from FFmpeg's stdout into your +AudioOutStream. This step can be as complex as you'd like it to be, but +for the majority of cases, you can just use [Stream.CopyToAsync], as +shown below. + +[Stream.CopyToAsync]: https://msdn.microsoft.com/en-us/library/hh159084(v=vs.110).aspx + +If you are implementing a queue for sending songs, it's likely that +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)] diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..ed0da64 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,59 @@ +--- +uid: Home.Landing +title: Home +_disableToc: true +_disableBreadcrumb: true +--- + +![logo](marketing/logo/SVG/Combinationmark%20White%20Border.svg) + +[![GitHub](https://img.shields.io/github/last-commit/discord-net/Discord.Net)](https://github.com/discord-net/Discord.Net) +[![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000)](https://www.nuget.org/packages/Discord.Net) +[![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) +[![Dotnet Build](https://github.com/discord-net/Discord.Net/actions/workflows/dotnet.yml/badge.svg)](https://github.com/discord-net/Discord.Net/actions/workflows/dotnet.yml) +[![Deploy Docs](https://github.com/discord-net/Discord.Net/actions/workflows/docs.yml/badge.svg)](https://github.com/discord-net/Discord.Net/actions/workflows/docs.yml) +[![Discord](https://img.shields.io/discord/848176216011046962?logo=discord&logoColor=white&label=discord&color=%235865F2)](https://discord.gg/dnet) + + +## What is Discord.NET? + +Discord.Net is an unofficial asynchronous, multi-platform .NET library used to +interface with the [Discord API](https://discord.com/). + +## Where to begin? + +If this is your first time using Discord.Net, you should refer to the +[Intro](xref:Guides.Introduction) for tutorials. + +If you're coming from Discord.Net V2, you should refer to the [V2 -> V3](xref:Guides.V2V3Guide) guides. + +More experienced users might want to refer to the +[API Documentation](xref:API.Docs) for a breakdown of the individual +objects in the library. + +## Nightlies + +Nightlies are builds of Discord.NET that are still in an experimental phase, and have not been released. +They are available through 2 different sources: +- [BaGet](https://baget.discordnet.dev/) +- [GitHub Packages](https://github.com/orgs/discord-net/packages?repo_name=Discord.Net) + +> [!NOTE] +> GitHub Packages requires authentication. You can find more information [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-nuget-registry#authenticating-to-github-packages). + +## Questions? + +Frequently asked questions are covered in the +FAQ. Read it thoroughly because most common questions are already answered there. + +If you still have unanswered questions after reading the [FAQ](xref:FAQ.Basics.GetStarted), further support is available on +[Discord](https://discord.gg/dnet). + +## Supporting Discord.Net + +Discord.Net is an MIT-licensed open source project with its development made possible entirely by volunteers. +If you'd like to support our efforts financially, please consider contributing on: + +- [Open Collective](https://opencollective.com/discordnet) +- [GitHub Sponsors](https://github.com/sponsors/quinchs) +- [PayPal](https://paypal.me/quinchs) diff --git a/docs/langwordMapping.yml b/docs/langwordMapping.yml new file mode 100644 index 0000000..9ddd00c --- /dev/null +++ b/docs/langwordMapping.yml @@ -0,0 +1,61 @@ +references: +- uid: langword_csharp_null + name.csharp: "null" + name.vb: "Nothing" +- uid: langword_vb_Nothing + name.csharp: "null" + name.vb: "Nothing" +- uid: langword_csharp_static + name.csharp: static + name.vb: Shared +- uid: langword_vb_Shared + name.csharp: static + name.vb: Shared +- uid: langword_csharp_virtual + name.csharp: virtual + name.vb: Overridable +- uid: langword_vb_Overridable + name.csharp: virtual + name.vb: Overridable +- uid: langword_csharp_true + name.csharp: "true" + name.vb: "True" +- uid: langword_vb_True + name.csharp: "true" + name.vb: "True" +- uid: langword_csharp_false + name.csharp: "false" + name.vb: "False" +- uid: langword_vb_False + name.csharp: "false" + name.vb: "False" +- uid: langword_csharp_abstract + name.csharp: abstract + name.vb: MustInherit +- uid: langword_vb_MustInherit + name.csharp: abstract + name.vb: MustInherit +- uid: langword_csharp_sealed + name.csharp: sealed + name.vb: NotInheritable +- uid: langword_vb_NotInheritable + name.csharp: sealed + name.vb: NotInheritable +- uid: langword_csharp_async + name.csharp: async + name.vb: Async +- uid: langword_vb_Async + name.csharp: async + name.vb: Async +- uid: langword_csharp_await + name.csharp: await + name.vb: Await +- uid: langword_vb_Await + name.csharp: await + name.vb: Await +- uid: langword_csharp_async/await + name.csharp: async/await + name.vb: Async/Await +- uid: langword_vb_Async/Await + name.csharp: async/await + name.vb: Async/Await \ No newline at end of file diff --git a/docs/marketing/logo/Logo.ai b/docs/marketing/logo/Logo.ai new file mode 100644 index 0000000..f145111 --- /dev/null +++ b/docs/marketing/logo/Logo.ai @@ -0,0 +1,1445 @@ +%PDF-1.5 % +1 0 obj <>/OCGs[6 0 R 7 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + Logo + + + Adobe Illustrator CC 23.0 (Windows) + 2018-12-23T04:14+02:00 + 2018-12-23T04:14+01:00 + 2018-12-23T04:14+01:00 + + + + 256 + 128 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAgAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXmH50/l55m85Np C6MbdYrAXDTi4laMM03phKBUevEIczNJnjju+rmaTPHHd9WM6j+S2q6jfWsV1Pf2zrpdnplzd2k6 /V2dI1WaTi37x/T5nj9np4nKsnDImVtOSpEytOLj/nHTS5rBrP8AxHqagxSwrKvoK9JrZLepKRpX h6QCUoFjLRj4WyhpZD+W/wCVFt5Hur24h1e81E3kcMJS4b4QsCLGjGpYs/FB3AG9FFcVR/nLyJf+ YJ4Liz8z6rokkNxaz+jZT8Ld1t2YyRvGnBmEyPRqvSoU02IZVK778nxd3NlOfOnmmNbOOKI26akP Rn9Eg8rhDF+8MlPj3FcVVLb8rL1dKt7S685+YXu0gkhubuC89P1WliijMgWVbgoyGAOlG2Zm68ji q1PyjPqXM03m7zCbi4hlthPHeBHWJ1RUoSjnnH6ZYHpydzx+KgVTHS/y5h068iuYvMGuSpDIsiWk 18Xgovp/A0fH4lb0t67/ABNv8WKsuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV 2KuxV8of8rQ/MD/q+XP3r/TN/wDlcfc9R+TxfzQ7/laH5gf9Xy5+9f6Y/lcfcv5PF/NDv+VofmB/ 1fLn71/pj+Vx9y/k8X80O/5Wh+YH/V8ufvX+mP5XH3L+TxfzQ7/laH5gf9Xy5+9f6Y/lcfcv5PF/ NDv+VofmB/1fLn71/pj+Vx9y/k8X80O/5Wh+YH/V8ufvX+mP5XH3L+TxfzQ7/laH5gf9Xy5+9f6Y /lcfcv5PF/NDv+VofmB/1fLn71/pj+Vx9y/k8X80O/5Wh+YH/V8ufvX+mP5XH3L+TxfzQ7/laH5g f9Xy5+9f6Y/lcfcv5PF/NDv+VofmB/1fLn71/pj+Vx9y/k8X80O/5Wh+YH/V8ufvX+mP5XH3L+Tx fzQ7/laH5gf9Xy5+9f6Y/lcfcv5PF/NDv+VofmB/1fLn71/pj+Vx9y/k8X80O/5Wh+YH/V8ufvX+ mP5XH3L+TxfzQ+jvy+v7zUPJekXt7K091PAHmmf7TNU7nNNqIgTIDz2qiI5JAcmQ5S0OxV2KuxV2 KuxV2KuxV2KuxV2KuxV2KuxV2KvifOnexdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir6w/K/ /wAl/of/ADDD/iRzn9V/eF5fWf3sveyjKHGdirsVdirsVdirsVdirsVdirsVdirsVdirsVfE+dO9 i7FXoPkLyroGqaOlzfrbNctetE/1u6Fsot1SIlgBIjk1ZgCEYda9sw8+WUZUO7ucDU5pxlQuq6C9 3mOveXvzEje41HTYFTQXflaTymIH0iQoIJqaVNNxX2oMjKeTiqJDCeTMZ1EhJ7pfPfpH0Ht/U3p9 kj+87VA240pX/KrvTJS8etqZzGoravx+Pv8AJNNG/TRSY6pxBL1hUBQQvh8JYU6U3rl2Lj34nIwe Jvxpjlze7FUDZ33lkeZb628zahJYaZHpZktZIpZInF4ZxTiEV1kPoh+KMKFqbgVOa7V5ZCdA1s6n XZpRnQNCv1rfL8s82jWks8jTSOnL1WHFmUk8SR/q0zMwEmAJc/TknGCTaYZa3uxV7ZoX5d6ZNpOi 3Mukiezmso7jUJZaKCZIjIzJIhVxQNX4m6jNXPUEEi97dPk1UhKQve9niebR3DsVdir6w/K//wAl /of/ADDD/iRzn9V/eF5fWf3sveyjKHGdiqX+YdGg1vQNT0W4PGDU7SezlahNEuI2jY0BXs3iMVeU WH/OMXl6zupbga9qMrSEkRyLbSR7/VzwaOSJ1aMfVFASlAKD9kYq67/5xj0Ka8e5XXb4iWokinWK VCvrrcAFQEVv3kSO3IEM/J2DFsVev6dZJY6fbWSSSTJaxJCsszc5XEahQ0jn7TGlSe5xV5bP+XP5 vvqsk9v53a2snvpLmGFzLcGGBmcpHSkXq05KeLEKPs/EooyqBvPyw/O9rqwntPPhSG2kt5LmzLzA SLFGVkj9VllJ5MPtMhryqwPBQVXpPkXR/MGj+WLTT/MGpHVtUh5+reMS5Ks5KJ6jBGk4KQvNgCcV T/FXYq7FXYq7FXYq+J86d7F2KuxVeZpSnAuxT+Uk029sFIpZhS7FXYq7FXUAJIG564Fd02GFXYq7 FVYXl4oIE8gDKEYB23ULxAO/TiKfLBQY8IUcLJ2KuxV9Yflf/wCS/wBD/wCYYf8AEjnP6r+8Ly+s /vZe9lGUOM7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq+J86d7F2KuxV2KuxV2KuxV2KuxV2Kux V2KuxV2KuxV2KvrD8r//ACX+h/8AMMP+JHOf1X94Xl9Z/ey97KMocZ2KuxV2KuxV2KuxV2KuxV2K uxV2KuxV2KuxVR+o2X/LPF/wC/0w8RTxHvd9Rsv+WeL/AIBf6Y8RXiPe76jZf8s8X/AL/THiK8R7 3fUbL/lni/4Bf6Y8RXiPe76jZf8ALPF/wC/0x4ivEe931Gy/5Z4v+AX+mPEV4j3u+o2X/LPF/wAA v9MeIrxHvd9Rsv8Alni/4Bf6Y8RXiPe76jZf8s8X/AL/AEx4ivEe931Gy/5Z4v8AgF/pjxFeI97v qNl/yzxf8Av9MeIrxHvd9Rsv+WeL/gF/pjxFeI97vqNl/wAs8X/AL/THiK8R73fUbL/lni/4Bf6Y 8RXiPe76jZf8s8X/AAC/0x4ivEe931Gy/wCWeL/gF/pjxFeI96qiIihUUKo6KBQD7sCG8VdirsVd irsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdi qld3drZ2s15eTR21pbRtNcXEzBI440BZ3d2IVVVRUk9MVSj/AB55Hox/xFplEXm5+uW+ykRkMfj6 UnjP+yXxGKtz+evJMBkE/mHTYjEC0oe8gXgFlFuxar7UmYRmv7Xw9cVTpWVlDKQysKqw3BB7jFW8 VdirsVdirsVdirsVdiryvW/+cg/Lmka9f6Tc6dduNPna2lnj9M1ZDxchSw2G9N8zYaGUoggjdzYa KUo8QIeqZhOE7FXjf5uap+a0PmyKPyjFqB0+O0iVmtYfViNw8j8ixKsv2Sla9OubDTRxcPrq3P0w xcJ46tLtbl/ObUfPV5YeWfM0cOn/AFjglvPGBwCkMx9X6nIvBkicBQTv+3vTMM4yBbhnGQLRuueV P+ck5bemlearKKYVBaT0mBP1ktyCiyTgGiIoCW4gFfi5cxWwZ9+X+n+ebOzvv8W3qXc01y0lkisk jRQn9hnjit1Ir9kcTTx3oFWVYq7FXzl/zkdd+f283Wa+V5NRjtrO1hFy1n6oiDyPKzc2j2FQI6/t bbeOZ+niODpfF1rk52nA4Ol29o/LY6qfImivqsk8uoPbh55LolpzzJZfULfFXiR13zGz1xmuTjZ6 4zXJkuUtTsVfLXm+/wDzEl/MHVW06XWIIZtTlt7OS3+spHSOUwDgV+Ar8Pyzc4hj8MXXJ3GLw/DF 1dPqXNM6d2KuxV2KuxV2KoTV9MtdW0m90u7Ba0v4JbW4UUqY5kMbgVBH2W7jFWA2P/OPX5aWNzJd Wttdw3Ej+r6iXlwjCUmJjICrr8TNApJNdycVSvXf+cePLMmpW15olslv8RN489xcl6KUMPokFuJj EaqDUEBVoRvWceHrbKPD1eqaXYxWGmWljCixQ2kMcEcUfLgqxoFCryJagA2qa5AsXls//OP6Tas+ oDzPf2we/kvxBbVVYzIztwh9R5QhBevIgnl8S8TSiqDvf+ccPXu9Pu4vNuoQyWEttMtuAwt2NshT j6ccsbKh22D1FXofiqFWZ+VPy+1Py5pVhpdv5huJrOzjdGieMUZnlaQFCXZ0VQ3BV5EcR3O+TjIA cmYkAOTNMgwdir5sf87fzvt7OR00i11P/RJJbS7t9M1EJNOr2iuiRVMjek08qBjwVyvUUOKslvPz b/NWFL1l8t8JobwQR2Umn3zOqCFmgRpY2aFnvpFRUZGKwF/3nKm6rHl/Of8AO2W91S3TTLOCaC/k h0exlsL157yIPcRoitG4jKmSBUMvJeJJJHHfFUZq/wCVuiah5tmuGudSM19qD3Fw3owpC3rXyo3p cjyKfvuu+wJ9s2kdTIQrbYfodlHUyEK25fofQeat1rsVdirsVdirsVdirsVdirsVdirsVdirsVdi rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVY/wD8rD8gf9TNpX/Sdbf814q7/lYfkD/q ZtK/6Trb/mvFXf8AKw/IH/UzaV/0nW3/ADXirv8AlYfkD/qZtK/6Trb/AJrxV3/Kw/IH/UzaV/0n W3/NeKu/5WH5A/6mbSv+k62/5rxV3/Kw/IH/AFM2lf8ASdbf814q7/lYfkD/AKmbSv8ApOtv+a8V d/ysPyB/1M2lf9J1t/zXirv+Vh+QP+pm0r/pOtv+a8Vd/wArD8gf9TNpX/Sdbf8ANeKu/wCVh+QP +pm0r/pOtv8AmvFXf8rD8gf9TNpX/Sdbf814q7/lYfkD/qZtK/6Trb/mvFXf8rD8gf8AUzaV/wBJ 1t/zXirv+Vh+QP8AqZtK/wCk62/5rxVOrO8s722ju7OeO5tZhyinhdZI3XxVlJBHyxVWxV2KuxV2 KuxV2KuxV2KuxV2KuxV2KuxV2KuxV+VeKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV+iv5A /wDkmvKf/MCv/EmxVn+KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvyrxV2KuxV2KuxV2KuxV2 KuxV2KuxV2KuxV2KuxV2Kv0V/IH/AMk15T/5gV/4k2Ks/wAVdiqyaaGCGSeeRYoYlLyyuQqqqirM zHYADqcQFAQo1zRTUjULY0FTSaPYUU+PhIp/2Q8clwHuZcJ7nS65okRYS6hbRlAS4aaNaASekSan b958H+tt1x4D3LwnuRuRYuxV2KuxV2KuxV2KuxV2KvNNY/PzynpOuX+lXVlfudPnNvPcwxxMgda8 zQyK/FeJOynbMuOjlIAgjdyo6WRAO2789sxHFdirKPLWl+X7nTZZNSZRPzb0v3hQ8QB2qB1rmw0u HHKNy5+9z9LixyiTPmzzy5Z/84/L5Us7jzHBfR639X5XLQNO8Bfi9Dx/dlpOZXkqlUpxowPM5hHH IC62cM4zV9F2gt/zjAZv9y66qIqDhxEpP+8wp6nBh8Xq8ufHbnx4/u65BgwX8wR+X63lgvk31DEt so1As0rxmcGlUadIZCSN2+BV/lGKsTxV2KvTvyg0TyLfw6kfNoUQmGT6pJyZZBKrRU4cVfkeDPRS KE0qR1zP0+K4XQJv7HO0+MGF0Cbef61FbxardRW4CwpIVQDpt/bmLnAEyBycbOAJkDkgcqanYq9A 0vSNAOgRSXNvG8/oiRmFQ/xLyqzZucWDH4YJA5O4xYcfhixvTz/NM6d2KuxV+iv5A/8AkmvKf/MC v/EmxVn+KuxVDapp8GpaZd6dcV+r3sMlvNxpXhKhRqcgy9D3BwxlRtMTRthNr+SPkq1mkmh+tiR2 58jN0esZLU40NTENmqBU0A7ZB1cy3nUyLpPyQ8ktP68YuYXqDtIjigZWUUlSQfDwQA9dutSSX83N fzMmdWdpBZ2kFpbrwt7eNYoUqTREUKoqak0A75jk2baCbNsAm/KjU5NQa6XzPd26NdvdiGASKEVy xEcZeZwPtVqQRXcKMyRqBVcLkDOK5IW5/J7WZbiznj83XcRtHhk9ALO0LeivEpxNzzCnwD7Atuag gjUjf0/j5JGoH838fJm/lTQptC0K20ya9e/kg5VuXDCvJiwVQ7ysFWtAC5+eY+SfFK6poyS4jab5 Bg7FXYq7FXYq+dtY/LTRr/zbcSnVb4y6jqMlxOkVlF6DeverGwSRp0LJWYbgNsCabcc2kc5EeQ2H f5OxjmIjyGw/Q+NM1brnYq7FWyzEUJJHhja21irsVdirsVbqfu6Yq1irsVdirdTirWKuxV2Kv0V/ IH/yTXlP/mBX/iTYqz/FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX57f9C3fnb/1K0/8AyOtv +quKu/6Fu/O3/qVp/wDkdbf9VcVd/wBC3fnb/wBStP8A8jrb/qrirv8AoW787f8AqVp/+R1t/wBV cVd/0Ld+dv8A1K0//I62/wCquKu/6Fu/O3/qVp/+R1t/1VxV3/Qt352/9StP/wAjrb/qrirv+hbv zt/6laf/AJHW3/VXFXf9C3fnb/1K0/8AyOtv+quKu/6Fu/O3/qVp/wDkdbf9VcVd/wBC3fnb/wBS tP8A8jrb/qrirv8AoW787f8AqVp/+R1t/wBVcVd/0Ld+dv8A1K0//I62/wCquKu/6Fu/O3/qVp/+ R1t/1VxV3/Qt352/9StP/wAjrb/qrirv+hbvzt/6laf/AJHW3/VXFX2v+TuiapoX5YeXdI1a3Nrq NnaLHc27FWKOGJoSpZe/Y4qzLFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX//2Q== + + + + uuid:9E3E5C9A8C81DB118734DB58FDDE4BA7 + xmp.did:7b6ad08a-6c34-094a-99ed-6fd2184825ab + uuid:00f17b8b-7d1e-47af-92bc-2084415a76cb + proof:pdf + + xmp.iid:53573ae3-c43e-0d4a-8781-3ffb12417c5c + xmp.did:53573ae3-c43e-0d4a-8781-3ffb12417c5c + uuid:9E3E5C9A8C81DB118734DB58FDDE4BA7 + proof:pdf + + + + + saved + xmp.iid:7cc70b61-b475-d44e-a574-e657edcb3f5b + 2018-10-23T21:23:47+02:00 + Adobe Illustrator CC 22.1 (Windows) + / + + + saved + xmp.iid:7b6ad08a-6c34-094a-99ed-6fd2184825ab + 2018-12-23T04:13:58+01:00 + Adobe Illustrator CC 23.0 (Windows) + / + + + + Basic RGB + Document + 1 + False + False + + 5000.000000 + 5000.000000 + Pixels + + + + + MyriadPro-Regular + Myriad Pro + Regular + Open Type + Version 2.106;PS 2.000;hotconv 1.0.70;makeotf.lib2.5.58329 + False + MyriadPro-Regular.otf + + + + + + Cyan + Magenta + Yellow + Black + + + + + + Default Swatch Group + 0 + + + + White + RGB + PROCESS + 255 + 255 + 255 + + + Black + RGB + PROCESS + 0 + 0 + 0 + + + RGB Red + RGB + PROCESS + 255 + 0 + 0 + + + RGB Yellow + RGB + PROCESS + 255 + 255 + 0 + + + RGB Green + RGB + PROCESS + 0 + 255 + 0 + + + RGB Cyan + RGB + PROCESS + 0 + 255 + 255 + + + RGB Blue + RGB + PROCESS + 0 + 0 + 255 + + + RGB Magenta + RGB + PROCESS + 255 + 0 + 255 + + + R=193 G=39 B=45 + RGB + PROCESS + 193 + 39 + 45 + + + R=237 G=28 B=36 + RGB + PROCESS + 237 + 28 + 36 + + + R=241 G=90 B=36 + RGB + PROCESS + 241 + 90 + 36 + + + R=247 G=147 B=30 + RGB + PROCESS + 247 + 147 + 30 + + + R=251 G=176 B=59 + RGB + PROCESS + 251 + 176 + 59 + + + R=252 G=238 B=33 + RGB + PROCESS + 252 + 238 + 33 + + + R=217 G=224 B=33 + RGB + PROCESS + 217 + 224 + 33 + + + R=140 G=198 B=63 + RGB + PROCESS + 140 + 198 + 63 + + + R=57 G=181 B=74 + RGB + PROCESS + 57 + 181 + 74 + + + R=0 G=146 B=69 + RGB + PROCESS + 0 + 146 + 69 + + + R=0 G=104 B=55 + RGB + PROCESS + 0 + 104 + 55 + + + R=34 G=181 B=115 + RGB + PROCESS + 34 + 181 + 115 + + + R=0 G=169 B=157 + RGB + PROCESS + 0 + 169 + 157 + + + R=41 G=171 B=226 + RGB + PROCESS + 41 + 171 + 226 + + + R=0 G=113 B=188 + RGB + PROCESS + 0 + 113 + 188 + + + R=46 G=49 B=146 + RGB + PROCESS + 46 + 49 + 146 + + + R=27 G=20 B=100 + RGB + PROCESS + 27 + 20 + 100 + + + R=102 G=45 B=145 + RGB + PROCESS + 102 + 45 + 145 + + + R=147 G=39 B=143 + RGB + PROCESS + 147 + 39 + 143 + + + R=158 G=0 B=93 + RGB + PROCESS + 158 + 0 + 93 + + + R=212 G=20 B=90 + RGB + PROCESS + 212 + 20 + 90 + + + R=237 G=30 B=121 + RGB + PROCESS + 237 + 30 + 121 + + + R=199 G=178 B=153 + RGB + PROCESS + 199 + 178 + 153 + + + R=153 G=134 B=117 + RGB + PROCESS + 153 + 134 + 117 + + + R=115 G=99 B=87 + RGB + PROCESS + 115 + 99 + 87 + + + R=83 G=71 B=65 + RGB + PROCESS + 83 + 71 + 65 + + + R=198 G=156 B=109 + RGB + PROCESS + 198 + 156 + 109 + + + R=166 G=124 B=82 + RGB + PROCESS + 166 + 124 + 82 + + + R=140 G=98 B=57 + RGB + PROCESS + 140 + 98 + 57 + + + R=117 G=76 B=36 + RGB + PROCESS + 117 + 76 + 36 + + + R=96 G=56 B=19 + RGB + PROCESS + 96 + 56 + 19 + + + R=66 G=33 B=11 + RGB + PROCESS + 66 + 33 + 11 + + + + + + Cold + 1 + + + + C=56 M=0 Y=20 K=0 + RGB + PROCESS + 101 + 200 + 208 + + + C=51 M=43 Y=0 K=0 + RGB + PROCESS + 131 + 139 + 197 + + + C=26 M=41 Y=0 K=0 + RGB + PROCESS + 186 + 155 + 201 + + + + + + Grays + 1 + + + + R=0 G=0 B=0 + RGB + PROCESS + 0 + 0 + 0 + + + R=26 G=26 B=26 + RGB + PROCESS + 26 + 26 + 26 + + + R=51 G=51 B=51 + RGB + PROCESS + 51 + 51 + 51 + + + R=77 G=77 B=77 + RGB + PROCESS + 77 + 77 + 77 + + + R=102 G=102 B=102 + RGB + PROCESS + 102 + 102 + 102 + + + R=128 G=128 B=128 + RGB + PROCESS + 128 + 128 + 128 + + + R=153 G=153 B=153 + RGB + PROCESS + 153 + 153 + 153 + + + R=179 G=179 B=179 + RGB + PROCESS + 179 + 179 + 179 + + + R=204 G=204 B=204 + RGB + PROCESS + 204 + 204 + 204 + + + R=230 G=230 B=230 + RGB + PROCESS + 230 + 230 + 230 + + + R=242 G=242 B=242 + RGB + PROCESS + 242 + 242 + 242 + + + + + + + Adobe PDF library 15.00 + + + + + + + + + + + + + + + + + + + + + + + + + +endstream endobj 3 0 obj <> endobj 9 0 obj <>/Resources<>/ExtGState<>/Font<>/ProcSet[/PDF/Text]/Properties<>>>/Thumb 14 0 R/TrimBox[0.0 0.0 5000.0 5000.0]/Type/Page>> endobj 10 0 obj <>stream +HI& u@fya?o/>-l_p4Oݾӟ__–f/Gc!-99/xc#}Som +vgG b-}P+a1jKe),t+==so/{9zk;BB1j; ra6:َR;%#̤-bv2a}__tt6ȚU]M=rKOӜr_e7u53OG#Oçh|s[-p۰3-vԷڏ2 +dNeKkBu~شQ23,W[=܎"pc;j1}ȉ~o +:ӏQ1K |dR8B0ǏN%~߉.o7{|E7;7͘4ffݗX#ߏjX["//-y9{JQU'uR9bOY^$ƐŰ( ~xqr"u$fh9hzڷ朄Esu@xJ~ߪdQ9#d.V$q,/='JZo&dbTX-lEBh1@~TU1Fg-r[X"{3R2EJ٘ M- +#ye^O'*ALvGgtu*ؙ0خ5MQ.=uD+1za Bb,4[8C"'hNoȃ'f򑽋Hm^!Q!4?;P,EGprPcJxh?j1bA4Pp<uB3WAh +XhY 4J݋^fû=XG^RԠ"/c!9VZ^'̔/G;DϳIF %1Ї+R"r6z/Crxg>{}?Ab%oZ/fETVߖV/BʋsCZX 1D.Kl]yv_K Yʐ0f\y.Լ8 6/۫5,Eozlh"xu$#U> (fbq +]$@JjN34ϭN0P/RPI(V bc=$OKS 4%;|(mDGs l7C)6H!3%1C5iTq4'S >2V̚0vj<;< $`"b2)+yg>pWoƝdՌ^`JdyFlL!Wt*2f͛Q9TNՀ7M]#?#?7Q~?tTp;wǯoGN/?ZGQ[s|ԇ?bv *ZPܓMrZA*.l}сtts5*@6\TcF500Càa T;GSnTBǵ|0#h)NE`,MB ^9f:Z kn1ЛMS'OT4T@a*߇bS}Uj4[@Sm9[e45WlD ik;Zkm,3ÌQ z^7d&|Xd@*X#H|ةC7ޭʥams?}W໬6"S\ ~84FuA {KKؽ7 cˤGsݟDfƩ/$<}yQ06WKOU5Uؚ7QR27`f03oO۱k}ݾܜ,Xz + bȏ=˓baLen>ZQ-*Z1X;%`\^ddE;o)> E8`5LG9)Ыnd4鱥2Nz%o6d`p]U +5rƀ0u1{{H@@/*{? 𧃕JMI^sɸǶH*&Sm>NB_0-2g5vV ?wZ88%lo jI =5~ 5X=hx7b)S=BtּdqLjvqu96&{4ҤQJ.x` !du(C IK%S_׎jQ.fAJ7rWzWJI(2aMm*x5Sȧ^4knumjl3s o-֤)!pOН-UmhA%`]WIg{hY~O/kJa`iJ^L>Mp!o)SPE@văΨ|=6(6< Ү_q"T= #, +6 탸CMx, +Sr4Y.ѴЧ૨mFG9scJNG0t_.8+9PA3sA5 %[3L<-vw8`qG ^3rMyM %^WSO+PEhNRU-׎oqAsg? +?"\l_Kp”Gʍ(mnDW7(dsCDf/݌[=bP̎֞D-:s7j;?uN6j'_$xէr+/^Q@$wu\9f_#, + m]lkFJ#ߢiIrZ}qf?U\5emw|& "j,lOQAu>ː6)f9$tl&jSlOp9(|So$}-5GlsS#Xj3Ԟ-܆ b +|tIz/lX3ˏ3ͮ]`=NWo i5&=6|?5a<,)4bCӦӔ,4',5b*Ai,hmA:39B׊+SX3'TUZ,TC4#, +)X.G'z.-az@ֿƞݲ"Bh@b1==7awxT6 x[݋Ġ{N:^HV f[І|HK%gJ\r$J)dTcΥOqiAS'9fJɊ?=c6;M)xyiw/WMUuJTklgB[k{ɞ.4Z:'LC*ȁZ=h$16;PEc$Nr||4{6F>%Tr HhαX -Ln9bo33u=spJ+ 8g@Yy*צ֤ t6nRSmU@WmMK@[BR@@'iTA`PKql;?ejhIZ)bWÊȐ- +N#%Jle"0MSL(x,*% XO\T/>#H+#|+.lAzLR~uz93`5b9 7(>[z@RG[%HF3`\y<^5{M6fk=m,?%(em+-z"4YG.Č +U ׎mɸq"cCV S a z`;}Z'Cl?ѿ"ָ ޼Y˟}u{)bO$lwܨ'Dk?vnF!;ǟ&dK02;FmQÝ5;QQ|E['[]reQ)˱v;0L~fƩ#/JJd-< A7"#Κ|}bҦ5=|c3) 5+rz5\~#WI9 S"5GlsC8 +hTR6gJXCnM̮ŞP듽r/3X3KBE^ + t1sIن[£5 Iv +$O˥Ǎ\3Z&]Ad˰wnu79/IņbTWW +Ĩ. ?Ҽȁ;O@XoE؜n^Qy%dQيyG1+ NL;ooꛨ[FfC[8W9hR ag '˳§bMRhl, R/L![%1V䕦ӄ\[E] [ߚMX:D1gT-X>.=wL[a7 z3f3S^:qH]GnJ2UAVP%z@mn;N%J߭C/vq{3ʯ&OcSu@^ mђF'w%GV# =D΂$VrNlcv}wut ƌ9}F3;ާE4g~\°O#5i c`e2DLp{A_^6r9`p(51&M e&\\f&\ 2>U4kMMj2Ql٤]U Vf-ghBZx/#՛m/q| Y1+{ +fyZh)(.(x6QS:*'$:(ꔥb Q,:G2AX1^ʏR)&~HYq WQߏO aKZS_Kg!!&ުu"Gu`aF|Lb6!TsԽ93f)?R%鐜⸱--9d DQWfdI!ZF{'uB@"'ą>>;8:kA*%)ŶKQώFyF#FS9cm{ F9(+j 4(p48-RԶ(G< 8HѱI]I1|G-K5ۗ|}6Ut2y8%M +(Te x:H]a\8T&( T"L;'JE$#Ux0#օmCzTq*3j0ё\>4˨"{C(:MBMs&m؁4#:bCUVU,Q(@ixNx OmU5"U+01FRMꎣW -m""*}doV/5#QI,$2%?=%=%JzJPJ]M`>zBY e$t4"ȡHƮP'GbN)u-*_Gau݊.<\r|}iÉc%"rjP@9YQzsʛbr<_{ usNZmf:kVI2O#jw'N"m <.le>]Xݓ9>sL„ 'w5,Ѿ2TZd\.t6Lv$&2ۖ:G_K_rLuw |gi}C.{1ӹW3F{8k:y9OleNz&8~{w8 ꣰q 7SlD>D#y,=a*1M$m׷|]综:O?r?|xͻ߼|5#/l +endstream endobj 14 0 obj <>stream +8;Z]"@?Q6c%#$]KIZ_P/XB]C0W3]EN8@I6tmd0fj["XSXe1maVEYDno;Kf]?8@ssL +TuX^&?fc:aqL=IKKsj4XYnu$Y/(.<@>dWI%)_7-<98H3"G/[%AWa[%^qU69)KTI(J +>]_Y`[*Qj?WWblGn<":9I&rR$D^&fb1I'`//'67j]%D7DeDZC0'C@Z/;j/2Lb6 +_cu:;1MZ;]D'=S)*RcnijF?dS,)HjtaEpI,cGcdjYSAtHJ"ZMZ;0Q#561OU8.X8[a +k(NHi&J.hN`dkh(TsPT`S<-.t!rQajS,/ZtmF3\E,@ohUd7TH38X7[uc%aJbIR5I) +2!,`7#U/7mp*kP(63t+sm*O-Fm)h*o]F[`s/?_aIiq^:M>72l0Die8n2W2o^(/Qe^ +,$GAnWbo0g'Ytq:M`OK,;<#A!&e>3&+\h7oBA%(/B]np0C_Cf]XB0=8]Q"Yp6sjOp +KTP*8f)RAD/AM~> +endstream endobj 15 0 obj [/Indexed/DeviceRGB 255 16 0 R] endobj 16 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> +endstream endobj 6 0 obj <> endobj 7 0 obj <> endobj 19 0 obj [/View/Design] endobj 20 0 obj <>>> endobj 17 0 obj [/View/Design] endobj 18 0 obj <>>> endobj 5 0 obj <> endobj 21 0 obj <> endobj 22 0 obj <>stream +H|TmTg!dMjgf\ZnQԈUkEȧ+!$J"zȗaYDE* +buEni=9;a}s{܏ǜ0on?(teČdy9Qh*~B oÏ﴿K cN8[iBlb[!WK7w^>Iƫzz LUiuiZҠN>II)NW+4zN1$uRp&jx:3 J]2ux?8M*'pqQ4F=LMX.h糨FY@[%b1 >-6c6,ێbaX8]0S=?S#+@{"hH# @Sa/?ۋ ^Q3o {c"]FR 3P? g^HG94@R/ycxȽq}O)sUGRkbk`T+5Ơ .b̥X[9rﮎneLmrb,nW*L نWuheמּ9%)9:=&/ǩ+1[Ԗ|#kzMc;ͻCtB3]a˦vZSpt/ +s%lezP;m*A `Q' +$d0ҼBM E* ۆ iJt׃w6*Bjga7A=]4 P2}~0]Ŝ 4uuK| +- F ddg&Zb9"9E1盎0T*G]J7`xSPHǃ*:e)=QV%y!'KadEY#3">O>W3[4B 'U'XROoan"/ӽ &+,-,61r8nwZYKUu6&NtUzFYg7^sD2h-,͌/jr0Yњ({?,8ݚޡcSr(7G&E5F>+ݯNj>M|9' 5暺'Fc>FV3q]dE>fMf<;9)ء(2=]mW AXtXyyG{b o UEdүpa2OEl7Z= +,{XHlGb|uW?tό:n:;9kC4TX 4fs`}DRX"+x=a.ŧKrvrcv(cl\jеü?;26i(.X1kg5zK~(!-GTO` x#Q(:B&GmOR-ԓOdlvVQZuYp9溸"-,Too=Ž2ؘt6gfY +lvUJ"NŝKgs/NVcbSA>4B#w`%Rr_ +aGa-GPx xUCW -DejE-/'!|bob{ٺ&">܁||Vk@{V S붠u-W%7 +endstream endobj 13 0 obj <> endobj 12 0 obj [/ICCBased 23 0 R] endobj 23 0 obj <>stream +HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  + 2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 +V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= +x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- +ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 +N')].uJr + wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 +n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! +zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km +endstream endobj 11 0 obj <> endobj 24 0 obj <> endobj 25 0 obj <>stream +%!PS-Adobe-3.0 +%%Creator: Adobe Illustrator(R) 17.0 +%%AI8_CreatorVersion: 23.0.0 +%%For: (Brian Ebeling) () +%%Title: (Discord.Ne Logo.ai) +%%CreationDate: 12/23/2018 4:14 AM +%%Canvassize: 16383 +%%BoundingBox: -2336 -3284 7564 1593 +%%HiResBoundingBox: -2335.22935779817 -3283.60582212756 7563.8555757689 1592.16169635493 +%%DocumentProcessColors: Cyan Magenta Yellow Black +%AI5_FileFormat 13.0 +%AI12_BuildNumber: 530 +%AI3_ColorUsage: Color +%AI7_ImageSettings: 0 +%%RGBProcessColor: 0 0 0 ([Registration]) +%AI3_Cropmarks: -2335.22935779817 -3256.62385321101 2664.77064220183 1743.37614678899 +%AI3_TemplateBox: 250.5 -250.5 250.5 -250.5 +%AI3_TileBox: -120.867144663403 -1165.35896063288 450.332867543628 -347.678967957101 +%AI3_DocumentPreview: None +%AI5_ArtSize: 14400 14400 +%AI5_RulerUnits: 6 +%AI9_ColorModel: 1 +%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 +%AI5_TargetResolution: 800 +%AI5_NumLayers: 2 +%AI9_OpenToView: -2342.1743119266 1517.80733944954 0.378472222222222 3136 1318 18 0 0 46 117 0 0 0 1 1 0 1 1 0 1 +%AI5_OpenViewLayers: 77 +%%PageOrigin:-150 -550 +%AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 +%AI9_Flatten: 1 +%AI12_CMSettings: 00.MS +%%EndComments + +endstream endobj 26 0 obj <>stream +%%BoundingBox: -2336 -3284 7564 1593 +%%HiResBoundingBox: -2335.22935779817 -3283.60582212756 7563.8555757689 1592.16169635493 +%AI7_Thumbnail: 128 64 8 +%%BeginData: 4082 Hex Bytes +%0000330000660000990000CC0033000033330033660033990033CC0033FF +%0066000066330066660066990066CC0066FF009900009933009966009999 +%0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 +%00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 +%3333663333993333CC3333FF3366003366333366663366993366CC3366FF +%3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 +%33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 +%6600666600996600CC6600FF6633006633336633666633996633CC6633FF +%6666006666336666666666996666CC6666FF669900669933669966669999 +%6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 +%66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF +%9933009933339933669933999933CC9933FF996600996633996666996699 +%9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 +%99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF +%CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 +%CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 +%CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF +%CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC +%FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 +%FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 +%FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 +%000011111111220000002200000022222222440000004400000044444444 +%550000005500000055555555770000007700000077777777880000008800 +%000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB +%DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF +%00FF0000FFFFFF0000FF00FFFFFF00FFFFFF +%524C45FD0AFF7746A2525227525252275227FFFFFFA87D52A87D5252A87D +%A87DA852FD057DA8FD5EFF4646777D5252527D52522752A8FFFFA852277D +%7D525252A87D7D52FD067DA8FD5DFFA8A277A8A2A2A8FD7EFFA8FD71FFCB +%A8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CB +%A8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CBA8CB +%A8CBA8FD40FFFD407177FD3FFFFD3F716B9CFD3FFFFD407177FD3FFFFD40 +%71A2FD3FFFFD407177FD3FFFFD40719CFD3FFFFD0A7146FD277146FD0D71 +%77FD11FFA8FFA8FFA8FD29FFFD0A719C71717196FD13714C46FD0D71779C +%FD0D719CFD0EFF7D2727525252275227A8FFFFFFFD07A87DA8FD1BFFFD08 +%716B9CA2A27121F821212721212127FD0B7121527DF8FD0B7177CB9CCBFD +%0C7177FD0EFFA85252527D7D522752A8FFFFFF52FD0527F82727A8FD1AFF +%FD0971A2CBA2772127214C2127F82746FD0A71962177A24CFD0A716BA2A8 +%A2A29CFD0B719CFD1BFFFD07A87DA8FD1BFFFD087146A2A2A271A29CFD05 +%7146FD0C7127212721FD0B7177FFA8CBFD0C7177FD3FFFFD0C719C779CFD +%057196FD0E714CFD0C719C779CFD0D71A2FD3FFFFD30716BFD0F7177FD3F +%FFFD40719CFD3FFFFD407177FD3FFFFD40719CFD3FFFFD407177FD3FFFFD +%3F716B9CFD3FFFFD407177FD40FFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFF +%CBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFF +%CBFFCBFFCBFFCBFFCBFFCBFFCBFFCBFFA8FDFCFFFDFCFFFDFCFFFD48FFCB +%779C77FD4FFFA8527D7DA8FD047DA8FFFFFFA8FD1FFF7D464646FD0AFFCB +%A9FFA8FFA8FFA8FFA8FD0DFFA87777A9FD0CFF77A2A2FD1BFF7D27F82752 +%52F82727A8FFFFFF7D5252527D52275252A8FFCBFD14FFA246714CFD09FF +%A246717D27F8272752F8F82752FD0BFFA200214CFD0BFF7E47464DA8FD1A +%FFFD07A8527DA8FFFFFF7DFD0552275252A277A8FD08FF52FD087D52527D +%7D784D77FD09FF77474DA852527DA87D7D527D7DFD0BFF77222177FD0BFF +%A246714DFD22FFA9A8FD18FF52F827F82752522727F85227A8A2A8A8FD09 +%FFA87DA8A87EA2FD12FFA277777EFD0BFF7E774DA2FD3CFF527D52FD057D +%5252527DA8A27DA9FDFCFFFDFCFFFDFCFFFD8CFFFD417DFD3FFFFD40F827 +%FD3FFFFD40F827FD3FFFFD40F852FD3FFFFD40F827FD3FFFFD40F852FD3F +%FFFD40F827FD3FFFFD40F852FD3CFF77A2A2FD40F827FD0EFFCBA2A8A8CB +%A8A8A2A8CBFD23FF7E46464DFD0AF821FD17F8272121FD0BF8002127FD0D +%F852FD0EFFA24D4C7777774C774DA9FFFFFFA877A27EA27E787EA2FD17FF +%A246774DFD09F821464C27A87D7D527D7DA87D52FD0BF82177A222FD0CF8 +%71714DFD0CF827FD0EFFCBA2A8A8CBA8A8A2A8FD04FF7D774D7777774C77 +%77FD17FF7D774C77FD09F8217246277DA87D7D52A8A8A852FD0BF821A2A8 +%4CFD0BF827717146FD0CF852FD22FFCBFD19FFA8A8A8FD09F8212121F828 +%21FD12F8214C7121FD0BF8004C4622FD0CF827FD3BFFA2A277A8FD0CF821 +%FD17F827FD0BF827FD0FF852FD3FFFFD40F827FD3FFFFD40F852FD3FFFFD +%40F827FD3FFFFD40F852FD3FFFFD40F827FD3FFFFD40F852FD3FFFFD41A8 +%FD3EFFFF +%%EndData + +endstream endobj 27 0 obj <>stream +%AI12_CompressedDataxk%u> HQu=B`zhXsQ.=jvqb ϯ?yWd%ʒ1 MqgdFFu]޵޿~]zÏrwˇ/޼;.?{Ѫ;ջo~xW?<7Vxw._=^ܿv⋘^%Cqȇ˟r;jꉋW߾\=~S󡕚 __٭"ƑJkE]J1D{*E/j/B uT|7o?ˇ<{w?J͛ܿ3wo?ؽ?:U?|{ës˻opICqE_^}|?|6,%8k'/Kx^^~]ZpܿOo5^R a Xkhm92fYNՐk}Y/M \,E??֛/|C\.zm!ZS^] \G].{0Vr[#sFiYq~|Ƿ/}Oʜ=~û_}U]?y|ƞ8Vq^pz`Zv}{ %?/QM6ooC6m94r%Z-nR-@?ڒ ڼplo}kKZc̦O߽(R9W|W/~_v<ËL-^7> W]_xFvz`?y?գ^?/~x5xݻ/ů=B/_|YWyo_X~y_o_fezKej[;ɏ_|i߽ˏ^|VBzk^^yso5v=a ˏoԢ)_3o~l|.'y{}%l‹[=V[-'n}w}/{n?|BOq{;/ng]ݗ|oFs#͏_I Ons=.&oSPҚ3Z^?{ +ox? ƒJMZ5[{|[ūxc?^~yO^k'O? 3~fdS9gso +yxI~goڨS?Q=Vɍ{iܷ-e?(_mGvyl9_׾Ov/W>7[ϔoo:-SO~7~|7kNo~ioTm޽[7<J?o۞zMV-&3Ͻc_g^?- a'_,T7߾vǵͷ_|}Vz/UNsG?'8\=o!GΟ WZCKהօԲ>UWc1klc*q:Eջ_'tmٛg/Z`:]0^~ſ/;n{vusĴ77`_/l5urk[ƻx)no+oS_ݷZ(ktm.K9gȗ*_|W\JQ.˵ݔ?>6X:e7޵ضJk.Ui}$ko}~կmXҰv2r\;O_?M,ewm7wVr?k^>X7wwkϵ]ߥ ^>޾}M?N?5nJ+aM;k;Ϯ\]ݭ]]]_]]]^~ծUWfrZ..oחW_zY.e.q7w죮mP6Ն9ۀl +X3lBmjT"&J vkqm&SIUM`VYhKv/->!0On]eY;6ZXCѽ.W&l$%x_鶖CNWiؚZY{\gыYlp Rj6hw6Ն۰__4lj$"e'K)~S29urvzVvq{H홑b#UFS|nNF*|#jbUxXlOFd櫴Fz[sϿiϩ2&͹ݬoO s_`',;Z*}7Ms/gN1}jeNYUX# M|ѿ6=n+]LHjCQf&.ek2}o}4]~2lVaʠ5v얱jmuFKYȚPV0d76ʆf,/jvF\:V_hvWvRZ,@-Na>''ZPv0':JE(=jJN"n %/i@ϫVOUʳRGJ7psjB0U#Se:D%|jܙ (j Ԕa}l{k~ŸC!ߝemܭ}mϙKn&@Z I1h65, X?mlO]5ZQ6hpTvx[ܬYn)ez.4Wx?i˶ MzmJ] wgS3)L6olNnNw'm+h;咝ƚR)@bSX^WP;owߖ\5UMriSGkV1VA89O>il O]yٕ7iz#MH{3)8~ogkݻӲ]kXOG똨E.֝)qT?)2~+r{5To_=TW8 {%*̿*\T .mB̕μnS wsd,;g]:ێr3I{W6?!yJukOI6't\g_]Q\ +ih_ma?>^V!7a|\T FYJ]pٸb6iȹ.g\O2|O￳vwO?ëϹamva_+7_w݇Wo?bvڠ(ٗ?W֑7zşT>b>U;Vlى{$=\"h;'ŇO<=tWr@vaJ Q8VJfmK9P`[35{l@*P/ZA,K/U_*mP\X]ތB7s+Q)і56b_L_;\.l_bfpal"ֳI[󆵢&k^ ĚVơ 7J(5qta>* {L}{NaZZ5ė[)h{((yAO$+ cʶ^=$Vm!LE51aÈ6}vYg)utO"&2SixaLj\vóiܕmzr|ܐ4\ca\pjl~MG?.+YikU4Rlr] ћ-ٚZMx k6Mig$6&lTǗa=ksgY ǒ֋f WAK|l_[ ngY]8kucmS EHqޙ| +U~ae.? +iv .oKWP*b3]+26qGVwsm;*%fY7l*UQ!Hdp,L !*W *"gtNUm!5¤oKYԔ}}Ul*7k¶TPoȋ-:c)kMoH4;:`-G 4^=B<*z +@TpF3 +kǶ_-ֳn#2-bQE/wߚ.:{pц"F mZ&M` ޮspݚ޽yt;YrG% 5ZF)`~z# f`glus4Xm9wR>#_,a} kŨ\J^ni[}oFR[NlR65=EJ6= &~Q'<ޙ۠f|CF6le! !YtK@mMRs3*jz +L=%0x!Φű:۶"JSXV`m[ g:21٧-(6*SL[LhIʔeC˩܎ 6fNR9f]ssU+.J蒃Xk}&z o6"6EWg:hlw<<ΙFmEYB]G=7 Kh}p˶bGN:K'6ߨW6ћ T ?JYf_aw:{M- yZκXGPw[+9Ukީd^t{ZOڅ6ʔBp^I6A)P(]8mcSeVW- r)ʹl^V9)&5OD e}SSJ*bQ߭ݟ;bT04z3SSJmjL1mooymFUC6{-VMt܏Yk# CRl;UĎޞH\mhmʼ;s(귉H^зW' N,rЊ1fh` w]_7yia6rk"{픋6֯FXDg=Z nIDzj=-̀ǔن*_/!;û!CYf +dbSch@{>̰q2̛F"G{2) yH TPH Uf5f̹Tu,v->OΪ\nWMAaVn$Ǿdrq|xlv.R}:-sMbӾ[m]ۆ`f-Ⲉ0Qh_2Tts(%dhUm"m+dU +YIēȦO +Xil碫D\B UVѰ)\^J)J]N%<ZZ0"߷%\-Ȭ.p"}f}L݈PLx)>/,WXFvdlwIOiq}> 9ա[uEek˂cɮsiuLVCEұyiLޥQXV`U*}MNG,*4b(' |IփE_9]0FzLf)U +~U#km)en-hLf."&GؠRhoR# #d,T6?E.nXPo % Z(-5~oht + G6qywtqǝlЯcOiG{MNb7m&|'RW ܫ&+)7D,3& uLQ*0 Y`)+wr!<{.Kd/R6F8rtH#v>B{`(ըzLK&Pݠ;Y 9ҹ ,O$t5{mq#F@*59lSڌ=L̴Ew]ADV7.CsA?&ۆ,CU?d_ +=7EY=0"Qr'oӶ1)G;JoQPN:ٻ{;l@uYu.p7*ΠW!*UdHd;^˜d#ͻR^#)p֓O4iJ MDei{pk&kSNjKJ>_QC^CMNT^[=2Er ؙ8d10D?a_E]D0l2%\(Y%'Bԑ[2ZpǶ b\j Q{exowXTa؊dWMx hb-fߖ671R!rv2i֩R`h0 $l?=Az=VD5ڽR!jUlx=]C񲶚⺍aKCd0):>HlhS|~aBPd] #bG0ynHrAq:S0]|Ub$ӀC Ҥ,ӓ{jcK4#rڞf#)bQA9rXR?)mce=TW-cV\B_mv +νt"o*U]UtYM)/flyd]3ukNjg +^?ц;w,8:ꊕg cHht/pngm'9p:Q xG]M]'g~axkIcTmqtUd7$pBH_%m;?zM$B nxS 7.U[lB@%;ޓsUyew&a#3۱Mi3%-̜x| 9L\Yܦh_8 N'Y!q:I,\GW(PBBT5-kt^c4Wښ&\pYa* e:5 HxZeŕ';LJ,nEZcpc񭬶T;]V&L45$j1X1a)GyDxn m 3QF +RauS2Sz`XAAߪxK2OWuރ"sG61V>jEwZ461B ĺaՙuv,5+m8%3FZג+b)n̴Ju&Jkt dYAծp[ufB|{Ln =FTt}T ' ;:rqD]][}x{$CZwpH4[,LE>EjՃ +xSډ<*Yc{U:ϧ ]4.1&=JQ +a \vݖJ`knfJwl:ܦ*CQCY8RUw`IA/(:!= \1)d#R@/(Hqp>q1pFYjXc5 ڶyg[ I( 6m!\6JmyjjZvXi@D̡̟C;"-vx6w\7t9%PZPݷՏr G˛؛mfEvbACl_ Os;[/XOxLqhfZ'&6Rm\$s4Ӣ4rF2CbS/K;`?NrʄFh 8=iL;̢0LYcm@#p4EjU2$8ЕZvo 8PfT&yU70 +̱;^R";&}ef+ =+q`Jt}e~E=Xd_("W@^o>y +3`T$239. g:<[ V;i2}V ok!ر)A0 t6~`IbQ^ (U0ȱ5)d>䊎 'F 7f߭kl0)999汈爱trst؃ Lr +>-(qlRR{@P㈇PP !ٌ)(cS9$6ęE)1]l}Jݍ05;[hQ"(89IOLO"Ɍ^`2M!fK_ 48H cMA>Ȼ8ͺJIR )G 0\$qďP¬ICG(b' +-1nUUn3rt^.mVcF?}&i6Mxftz +-əHJnD6@DS0 kNsܧٚs*?A(PoD~0uH,N0ȟ +RTTisBNx R  !}/:"{וB835y>f Zc&DҲi ]5ň̒MH*rzꙔ+m H@؀F/iBS̐Y!t=!eOo +8,pQ +<I5e4y+ ,M W,ɓט('ՍM#OχkI,/iY$r }λ +rDeNVR z\ty2/:s[pxǾlE^ᨧ]uU#@GS x0lի!6Lz'BwCUD˵n[ĦenP +K:M7qy&7u<;'tv7Bcp{(@D)8 +o4\j72_+t\n} ]蓱`'J.B;5>> +\ngJcׇ. W=(PwɈX)J K +_mP*р=r5+( &N0L9, 4d\e铅1l~thLu-7NPE"*VP>Lmבv囄uA|Y,uY}yGdU}7CXOnԩ,ClgqX-ָE+iپn(OFD_Avb1AĠ$Gqi0U +3Z) k~6q.!F,H38"W[Y^!v쩔t0Z[ٮ@ JCΎb +OAn`+z +?ZI ~]2Y\>[m (t> \a*Y4k=ণ+n NQ_7l]O~ IOp`,zT(͔] J@%{ݼ9QJȱ5JJW%V$X'Bئ؁`&x@d*c<4-0⻔ao +`ڣ^"AnwLyQG% +f͢J[^]XTMfqUE~;R ^#.uYȒ<yӄK# C6&DYT Svk_3$zd%OZ#/ZjFГ'!Y"hBscS*p}uQa:OaRWA`kT޷բ>;jL-8W]$R؍i= i9wfC^%>R_v׫!즒S2jNlrI)af%aA}#W-D/P&R;$Sd.-nph 5cpP68*D-2> +g_93JXaﲥ EǺ$(JPf)R :V `/2 DȢIaĶ9DU)% oUB6Cy7sR` d֚%I[q֖\$,"&?2i3fl q&Nʄr84EKyx! &sW/p%1 -ꔋM@ Jrfp +W +[Z73JΪOWQГ6ܝN"R&'dXTrAE3x|8,M:E Zb楒 6,$HQ$bi;w=?"4Ë_|xW\]]|?~t7LB^7)XHS\]i۔o#.@|Ks;!S(E#Hy d(Qn{ ;L0]dWa +7Lclfm%mgO=8^Q((S$GaseTO(U 1%A.ALtbUma20MsO@d\wR`(P"Oa +Yۤ(N iBek8T}cRx2z;AȮ٧x =V.osU=1kG-uwzttO@'k3?>NΙM'BϘ*V2Pdr%z6֕{H +bT4#UGd| ie|p+RɨǨdr! EGIw旵>bdpQ?Y"H_ 487Ąǂiҩ0vV,J|E`'%dTRM_IPlU`i^JT-}PZ{P#>*eoz64=6D6cFK ɽWuOq*N 锺Ht +8%-UF (1)!Ze/cŒ0{N21HL_>@Jw= +Lȑ%/DKF䫱*+@fO7ټA_e=B6$%P0$+9V5 #R):DO'A,!$LE4NS@.'NeB(ߦw124OV_O-x`ͷ1HFTyfrZMRnvŧ8# +[B3 +^t-yyÁ/%F-sPˊI%\h*O0tʌ@ƘlLBBɶer Ɔ\I{vS!EEu2/2A ,f$bEOL&I=Jnp@IΌӶH빛qo^ESxB%ǥ?"HRxɺAuGl:hbDĶĬvW)ԱJI&`!rMՕ#4 B{QA1ϒ A##{q[tq7&ٙ4U){J=Yw!ݦ̬;ixl@)WyKMm`{zWCK؜"@AoYH1E$eM̷Jdh 7K%BaGTLDLM qʀ&fApD{(BjI\KHT&+z>(Ψ;fKJo'aaK:W +s2 +RD +ɔ+9VB8%ON,sReyːL@6Us# .y6J´t*JK n\p(g+n#UkA.zodI9N!+ krڣH;<ðMdHĐ{r [4!z8i@O'`N6IW Xd"l2wxTBf{ΔXGҊ/ǖzmTN< ;W?zk6=t7&~S˟ʣ0n) ꛑ', PZB @{D` 4eHGnT![Җ<(At2VXhϹ4P +DA(h[2⩚:$>Ϭ) xݣ,cRq + +)XxTqĢ}J޺ h [#B$li@a(y +RY\j%J;Wa;6K +br&Dp"6C !97Υ4o`P2PigLi6.y-DY&]OI 1K> h7׎04J-m!Q׻7`f +f$OZ<{SȮv:TT 7P0o70* p@ j< .SNE~%*{BCzҚ,J,PR^#Vg>F+Pj;KMj!%^ f++_WP׷+8fW'T#TgVܦa礊{^L7c@z6G.ޥbéC,EW?- +<<ɺ83L(k9fT*c'Ei +^fF[f|D8[o C_\6M+(ƳnUԴ\&aW|'շph.J+J$&T]$[Jj +.)8mp+<,z)184O*4 G@mI``tOM/`~nœ>ymh) >͆=k,+㿂+׺zKQqU* >v _bM yLDNg 1,S@rq+)V@8yD +lݚ7Y2]<9D۠W6%dIU!-XVH!8:aє4O,.RsL +@@9>5F֮ه]r4CS-up>L^@+q%IA(\OGIy 3LC.E9DD[=>={_3 n'CtGlAMT@lSROF6 +~Z'`E`(% |B$7$ص`j`ݡP;+CG +Я^HDY%u,xlx>6ƹ&΃<8L̈́묇W`ӤJu!q3}Bpo-{iT_)YYP+x4z!{F`͕ޟJNɃ59<#yP-iЅ$6 zf[]'ᶯ6>뇨VP☙74+*!ISVP4Gv$zBIl09\ gN L]3{ZaZ=Y]:'\y-kUn!QP ׅ 5(aa!=OaW|@} ASH*4vgf>s R@drHs]LH1Wo +eZ:~V b%y-$iddVſR)rBXY_&kGSE1l|!a/ <}umJ}N$gI01߂{͜VǼ + EjOx_ʅzL*LЄF"Hls$Bw8&@tB!TԜ +]dVJ׊M$i3sq!/|2(PF@nᳱئ9H[L0Ӝ,(ׇr.%T3I>rV*%ɼ*:4/( 6qaKxo#NDQ9LG(U1%l6-RhO/P>դ [bLj"2]4#=эsx5g{Rg&ٖ3D ç E`A$&9Y s2=o].=O?>YY8z||s?/x}K{bV?o7_޾zo=~?<?O mZQ?䳱J e4A*kFm )Aϙkg +$ەpR\ @a`DU9rJh?m<%̚'A'XW-KMp5wQ/4ҧ"nHȟ;UBNߴIb-bTC⾢ք1I(G> l,ٱ0B^2qq<@#Gn;) ;>N@iA8MC&@!mN#AVk9_`{mƆ I}QK$*\>s X<3r񓰛$Ƕŭt8l&Ξ+SYfs³1sp3WJ*OhkQIϐcHFMOKԬ㜇\gӶ{޳hՕwlj})tm%g{e][\O[F-{r|2➅8~o;x22\i=+=RX K85YmLt@;t1De8J^&Lٕ4l ++-:avlE37$ﰣw=NJ`ZΑ2*ؼ:l3nGg5,愲,3cVJvYYOT+Uk3HLNZGo J zAP$;47c?h_ZI,OeCie] ğGGڌF +|GNBDz)$3l;np6JpPNCˏ) \yySy ysgVADe9HBRy_M~W-'ukqӖN~ɧ^6*n~]feo,^Tvi+]a+Q&B+@!cs6uc6:fQ6b]$?rkǐmŦرaϼr`AaZ ~b3^sqڧ<\<*yOjJW.kbX+53vh$& ꙕZĩ{>vv+5ڞfqP[cӐjq2MrܘE!Z&cG,BGViIeI z#StJ+;h/ѽ{/_M] c;2yLHO/Z99 +}|`'3):#3)}1{fZ;5E'͋13gV>fђ'PYЎd΍|wG{e@(K(Ƀ7^,NY79!{OȓQg2Hr%wŕyMfC +k3[PZ‘x}w^\iIBr;R$y2Nn(afG]Wb`JYR/ +xQz,w%)Yċg%& b"V2b0~ObJSG8lF{F4,Fz/l + 0 /BA8#LJpYӫC$.28=]3ۄ#K2Vv`fIdO1|R9p])Aa2\_D Η]l|' (C{_9 +pLԏG^KW{`'!cڎwRQP^aPLʻKx_NywӴlti\YvA }'H'.YA;:)uwʤ|َ񰈭S2JONjJ0访w ɠ[8:8{%Luo?G+"N(g +Os +N^{BD UR582\6ܤq{:WwcnQ|BtN8=RaT8CH_r()cWB?A(8:&ϘW)D,猹1; 5g\w;\J,zRlPʬ^'rgpX3WVM1Yo?eF=˙KN41|)CuRc}JZ952V]Lve:ge4\\ CDhsG jƄZaoOj2Ȏ=OxƎU%wDmQcA;Zֳ'!;s }V@]J<厉.F2s;O)jU*xQsZeR<&H{Z>3}EP.)H+W/9$ V;q +MW-dkR+ŎV?[)FV +[hϣ} 6V;cHI-bSnZcpD_iQa#p ---c$g,gEpN5K&NfkvJ,[{=)lmo]omN׆Wl'D&V)EvmRv GfXK9v̰xv33<)zirZ;9_9ez3ڗ 3g7" esG=X\chX[ی\9[0je>2w1+~ߙEJ|¾sŽt-$4IWq'y٪19Vkt++*ӄ܍ZEGU#mDOj;MյW +gȩŒPju ؁`$+%*8\ X +R!g$>9 Tn< Qeқ[r59e3UcȒ2IL`e. SRaLJ><JNxI!R8HD0,Ҟ$++•pJtopoa0g7 7|dG#D}D%[C;ОSOx +L"t[uc0nOy@kbZs}VO,/GoFt‰6=‘R׽ǍSOODN픜G4ȁOF)D$&x}:I30ƞs|JJLF%.ub#f%Tۥ'm#b' ztyǙ%\ZOГhD鈈qFUQF,jOԠWBO%EnFIz#Ƀ-{BOSzS*UwJwʅ=Q;FOY]c¥?J|+~"2c1yϞG,B+-iečTGyʏEUD݈+L.<ÞLZpZ;sҤRv +1VE>{G۩ Vr.m bx2LN侧 +5Q 卶Z`[y;w$Jz-ĄheDSd7;2JP09'!){Ir# [i& 6nIX(%ʑJr)mc|ӌ#,1" 3$ؓCOq3\7*H 1@ďD\ \y" +zw%ՑHH"&#.6 Jrq8WF4aH(BvDcj+?2Ɯ2Qmec$ JHS5n\͏ԕi6E9ižI1-^ԕ`/>x#UE+9Fzš%ƤjIJfM&hܢpr7Ey ,Y}ϧHX^6BE71m*PT*m3nˁqɬH`=z#VTkR=G۲1+ +;__Ƭ9BS+Jj\{ި +JtJI)h3,iz[q8QT^S"E?ş8&wƖH}'ITv7 25C1-G]&g (ϑ\34gZ9F.Mx{{svjD}xrɎQ*TDG{*EWGD;FDĊJ7\Ou9ڨ#dQk?`ӝWjD 4 +I?ϮD6jDOO\R6nDAJ@ +hǍ(_'\ktsFz 2oԈRb3"J\VD}X#"=.53gD7#5Oa/i#BT<f P{- "V"Dh6z y#BԹqDRuews? =U +eLJleeDlWBCb'BmIBQj"{F>6؈ #BT:U@⇶!:H˕<;RQ"*.1dD\w͕Qzڎс" 5OtChaV4P8#Ro$=o4AI `Q@@AT$lGD=>8WD5@xwNgA ߄R,L:D8*l5lg;ɔH+JH VN*(]QcyҲJ1%]?%J|Z$ YVuMW MFDA6|.tqjGQpQNBPr'V#*@`GnDE ,r8F'ٕ$W,i9ܞ$>xaOII|9IWv$cH8M$=I-8Yw, +R# yE;D北۔#cL`H9p#* GNe,yʍxVxƍ(MA KB:q#j;_6mӔ{d%T@S^BGM^Cw !J,8]8PVz>Et9.FC gG +.4]v_9M%&|B $Dz%6%er|ƘkO:-gg\2##e\x;A} e񛋠)ݥy s@P]<hU j)C y_N5H(q\^5H׻$pͯ2{q(ޯ_7zy-mό G7}j:Q#oMRRZ20j~sCcg|JZ6;u߭2ғKG=#چETAZe>?y7i>on}|ލ#6nonIͭHjǼ iX1l,U:#Buha=i/9Zw6@: hoHcakm~V랲*m +1Rc + ?-ǣIإQ|]ڕ\ 3}Xl6mZBb ИMBl{~W3i>f4{z,͎ߌ }vuӶFe1ɨne<}1)_Mt$݈%2uVi;&o6<.c-\Lz`s):d!Qwxca8|%MXN=MA_"y,c0LC0`w4ެ Sz4/h1EY1B5` 5Q$$iſ5.P,Aw.i)L/5+chb(>[Z#4B7a-ȦkjEl77-x$c DnYf +iuF﫝X l<\,J N_߿a=|\v$=>V;Rs=T/VGEj8oUgURZKQ櫣L9CH7 ~α?Cy)itK~ܓ77%"O7C{NBTG s?#!YVi$7L6Tsz|(%ONGvʮ?H=GTa7`7K#Rfj4u]ep?X~0."ak"֮W4cͦ' ?݉8Ô/"{"+BjX)\DU%yC3Cx._ghP― + +q`.מ%Kf'$q_<]s*ҀRc;𗨳0iL]fXObT48 RJP: u/ +=%vz:0x/J2:,txx?ֵ Zn[P)Z@۶$Ytu`G-u8* fd6H7J.m`R %S$Bv(iJc "~ Iv7?hKLjƥqO_Y#D}4۪)Wzi`biu9DRZ擸R`/ tm^O75+b{c}*=VuW9(s-J2>RtF)l#mE7}t`VW.t D|D ]2PKj,i;.A=:=dJ]mneNb}1ndlʉmq4 ,D?as\Co룒QD6\%EId 4֋Vt#OiŠ4u"9 !-xEu c^3/nK-`?91v ~MSy 胟`qa,=pqZLјr1Y=LHAVK@:D1߶֫ +hw -eX^iSϡ#f/+O3liBǓJ&8C؂O؄*{C0i,g;:qɺWǯu_VЃIF$CcSMhՓt+ȩi3%dk4z7>V&C'b?dd҅Ҷj{pqF0Aњqb1DWyGR nI%n6ИG{ o 2OZ"tWKQi ;cjrFN9o>ӐxU}a䪍 H/t"A*ǜQSIk4 +8Fm$a 0[3x@p5xO 閍~#]ԁ[DH=eЌZ9h ՛߼<\~D![G5rAk \]<т.BӋd=c=&9|BVXbH}r-Y{SZeW(ƈitX*#Hx&{TLk*y$r0[NJt#x:]VeF!H)ͥkU$zXѥDt]/>ACDIﱬ|XmW`|z+^'MZ (mpcs5Z jTTOƞdPV=u9h\N5An?Mim_ZI/3>'=7Hd:K}A 65ԇgIvC!/p{R|xH '̻??F T}xs cЂB% ZIQRTb"iVW>amD4b"SydP#Bj-#D#|:#QqxaNq^%h%5viN:6pLxZdDWM_J; ,-|G+Iו"c?'V}vV +3BKr9 Oe)Qoo4 +Rg0,l\@Z1=ƃ6Ϋ>ư5!g٫QL2+Iugf>ߕI}Ri<ͱd>8S(7dm0qRU'Z?ZRCQ|P_$^V<3:YH1OS,ZêHjFȁ<=FqybR#~Ŕ1g8=緜{~|)o T*r3&ޒ1?_ihT7F40"Ig,zA!,60-uuǘS6>HRH@"rl]c? +&Sue?μX`AQ-a$e4b.pێdx^|}pgS5ڑg\$ܦ6Fן>d*e 8>[PFY,xÆ ;%z+h2fq@ŭ3m\Ņ0;xQe 0ԅD7Hm}]l\Ą1ke)-Yސ^h2#Yot;/V7:Owuzdk n,XV~?Gdafwl+8=|f$T Fzk*/Q\*sp}U%eI-%{sw$Abi Jq*{"n"ECsI{Œj\JxGT킱苊r=2;0T+2ԙ9#fx8-Qc.|8TnpKߴd,3\'_rYD>qVk +Vv"f}NtiƑM%ޙT CXؐ hxY+:7ܨƄ*tbP Ml=k%1_*IץD:q:ҕ3b#*jC>p9hG2;ňBrtOX6&46X&rm!٩k^FtB0& v}s(@źG[hLB;?R c^ay34 ]( e{Ut &FP՘{^pY@҉E?~9Kbfw\A*YPFCls&Yr Ya-g3Jf&HODY/4o$Th9YF,mڞ F! ĒI2 o F^AaǬG$*?(mD4XkҲWabXY#cY/F-)bo{rz#*X,|4*e#>+Ty%rRC*-p>WX%{@@N۴ǓM)ˊ )!$Iɸ `OƿkvH#n$W-NFȩڛL|ރJ$xk}@ݎWG +:;Ffub)⇪Ey<ͬYؿs}6?0]$)/gnv:QlK eu2^t(K nȂWX;"Cv 8oSyâG'{]rz"IZ݋ZEH긫AJrUPheJˆG]cRH/y -K"- 8҃=G4lT@BhED'kǒ:T# Bxؒ {V1S.6 e^ ;cX_vY.L8R:1Gxش{dhٶ,S;Ǵ +f(NO#L [;MI_e],žL=A=ATF O땘RaēGd|F42|oHEBD5R/F2:X+8miwҿpD,йuiCX [eMisc;Uׇ+JzJcL=zZ0V5ci] AW 7aÀ5v:~cjI*ȽY *2pܶ i9p (o47t1dncRnvHY[͵u9ӵPLdZRSVtSc}0<*h;機VN|hC^UN6U Ąt?J`b5BJ%4!6Oz&%FB+,ʭv}bQX;z)S>=Xղy{yZ#+]8W &Uvbu@EsV׬,1 VW-2 N!xmp@ !Q/p3h~iճIJw媂.oJdzs吐+yAw gKj[ i\YI$(8Z촟aiԈFI~bFJڄ9P ^>>_NV#PqeIhׁꃕ:u8GA2++޳e*-}~F;ExZ:jyMA>bX1O E!2W{t9N=s0ȁʼnz ID=Yl ڮ%Q0H +Z> <C #n*K$yuA`jm(QwI@TO㵵ǂ[BC[7,<&1 y&#hhqkyi(3ղIRE&&!f["/_~G!C# ߤ6J3?nlF `Vm-+xLlW e*LdAm6* +j!5e0nPtQ]v2|*%;;*̥I#tgbSB/cA\b^w鋕qᬐ?^Ag61lyq2+_;Aatߢ}%q/'Fb^Fc 0xN1Rr nj@K%9aδ8osHN +2P.}LL@2hS ] K~ՄXž}I +0l5 )oEYc`D1|Rx 2D0ψElPzCAHkR +` xZB:V9y ݗK$qqQ4F$ oD̗Ϻ^T#tk_CԳp +}#X7= +8@M7Xr.q٩j&=_Pʰmܖ4Vٚxܫ\BR/=r/BakT)}*羦HbwqӀvSP-G>0NwlU.F-!ԲDJrMٲWfmS&&9CNN5;U-bw Hqbj9rB/1fCL78]FVqU.FyU>o9$dz/1,~G©pEg:MEJ\lZ;teЇ-\C-;-ou-ь0(Ya!t[.f|\Lu;_ :<&!z4SH0}E:NĞ _#Y=Q7% ѭZa!>fJ1F,`c)j09rǰ1~m>W3S{~ڠs8P^ %JḨ)Etïo +XR(e\%X +M b&qyL\%C0Jx<} Pe2cU4b:@;G:Fǵo&kЫ` +py9CC׏EБWv}ϒHOUyUKbG;%M %.C!aF.<>S;fsƫ\beu"턑r> x&.'9Lt( )Fh%\Pmz@ܪc\I+E5eZHPh+4T~$? 2F|Eē%rI_$<:`դ!IZt̬im_42چ] #sVT&-tĴ":as>VThSAdSe5]}ucZِж}gXabR:ZO"h߹rD0Փ򈑂߻&y4G0Ū Ww{+EQ՜S̈́%N|gOysv8֑3$1n& aQ[SܬA vH>h4ϕJU{ԥ |qK ɝzDK2<^*xvlf &ÿ|QPEbҢCuSAVF ?r-&͗|)`5=i5>jlv >. }2G6o.A}羰 YZ] +Ʉ)a6{h'ըK2GT\pSw$pw(FmG@rJ3l%X~"H ŕ1 lU9 BHf96GMb$Uȱ1 MĥFшU}Ed + c=,oa{ |U#Et't71ƪg^GĀDN|<rZ4v ;[Tw5@[JTM\2>K/e^kT OI1(7DEFYW\y2(Ir"s+)#-B}ďxOנL k~D?(FxdL{1z?6ot'_'ഴhaAftc[Kr{pDl6"w]FYNE'i7 ]=)R,h@T8?oLE#̨%GNH^sY\hK@" vE1S&޻joqbDT)5!sRodBRżl}M媪M,UqU<-O^sصzL{LYJ`iޭ=9Pj(_ x[m #?.m C!'ϠD-xTc>)P$UnW`8rRj+omQ}Q?%.rY|Aq1bl r6b5,V7C AbeA*udn j! Cqk%z!!(%X2!-1c`?#5eMjFQNĬ@TiumYj S,f[ %.Nxdʐs*nZC$QoNb25 n/gȕ>xF Dݤz oozT#_C1Dω OYP + /PHW~Nt]k&B'O#-eSyOaI +5=E9eo "^uBF\ -1mXۥ5+6 F=+FW)"*m_Ƹ:hTYO$2z:vo?Ue[+gn0hO1f"ֿ1E9 "b|(HۄSl_xŃk7MPt60:_$(9IG\6ťiUr) f}${$#"9tpP*ti[K5WUiěߞ [/Vt.ٸSYMZ-|/Dw>9d>H\xiK: +q-XKidJcXp*w"V( 󃚚g<@a썝f=2w&+Ds!IGh^%z¿PtuR}+hl;Hou?iPc T\bk ,/_nU(v= ZT_#.3oF)NZoJ]HeY鏓Ĉ:-|! amo#f/:<y$)>.-d5r23vlMR d`T-"׺4ӾOf~gUDGgJ}$H *vT<&Y1@BnL]eo b>P9_)WJZ[D5S¯+D ֭aW" yTz2Fnf)^u+lоT{LΜ;ijgP=*Cze7h| 7Ϡ٠+^w,  +OߙX8Sbnso$WѰ:.ŻY-cT S.M-DZMz2$X$ij>:ji0Lau{O޼T)apڦ!3!!~+vvm\Wŵ1KeRAÈ +[4fV@DjDf/! 4?HDB&Y\\b^)%DwZE{#5-5[#T?_!aJYdž+=x/Aj + ՖO|Hk7aH N!i`HIl(#0,@jk`Ee':7E3vLofjS`Xf 䡣;Bjm+-%߅jz(ECHr-'ba6ՅvxWbՑ1/_2 )}Xee^ $Nx悧?4rhM/zv*,0 !m)uiUU_ky{j3QMSt#(F[UO&9Şr꾾U>Tf2Vm0QʭͩX@S e`] =xYUa!&6n"PJDLN/MҩX־n% =xDx]Ha6w6ˌŻfL%+(hZÎ5 +yg +uE5KI6!E%. ClwdϣPt'N%OY?!}'3[g,Nй-@Waj\;x”Jf;/@k/| _P-lVF*VvZ2,u- +A|0ߏV- wCTh=ks+]XɌne|PpUHd{K.HF]1{0qLkpթ_8~Ǟr䮪MTaG-jqS Z AM#(qI+@,%WD l#Xg4q$#FkUq#@Wh7b0c1p})@4Si@*PZ!+9INGdBd4KEި)}魪ȳ,f8fHy|<76\W$TtMl|-q4LRiD , Qq$vO}Uyv)_%̀kMp|]rxe%0\Ttǚ@tU[[(D>B&{|wC4GxXwӃ̤%gQa$8ܣ#`IZ*EKaE}V b*HΎl0eÓ ާPI8FDh?!BD>CkhmqZʌ(I9$|xlϹXה҉#  9$ oU!(aKKEڛxѬ3m{-K j`%s89eaJɷ0;m8ާns@5WpsZ0$̦@koqme˷LN3 )&kơtY"UD5Y籮=쒌M0a]Ijb*cQW9Ote -ښatCD ]R{vKaU{<ê!&K_[S%ܻMph@X:V/":U`ˡxlaq 7H>s8=R>Y +:=W6>U0G>ԇYʥzI-zcq)uX.`3KOX MJ@Dd{%yѓs`PhMmfNZ܏އ_t6%2g#tSr[Q̀c,(If;cEکҒ&=bkyۜWi<>Lj8KUt92&cFH#+k@(d{~0#@eŐe֏pEtdXU4@#T의_ϗ˒ŝ.cm(˼[L?V uŻkq^cc'DO"ecɔsJ5Ӯ^n-8Rl[7V̫/_;)# 0U ,_*lX ,}}l@ *g >zu~ _FR/CM7?:}[(HK?tJ8no\2qp?N)ѳ-nG|WLnl={[}|SGI,RafLsnkD,e[CrF)ArBŠtD@CΒ&rrSxzD lD ^)IPF AelU\-.{}*4:DdDynODB^G411uOIJQ|Cq.DrFuf[bCqʤf%\јx(C2 zV[՜,  +Ÿ9́^b}088, e1ԫ3wDq75*]o-F4*+Ef.2Rt>>_KK.%bvS,A,H*yCp΁Ō6X h"sxHe ԣ9Tw(QI#V%_AY.("-Cض +6`՚΂i|mIJ@"N É7twS+M{v +}T–T(\&҂eRdDG Eni7us7[`:Lh?P4`N 1[ܚ1(d2DQI I4U Tg\ҏuM *sZZ$Qe-"b׷Kbb+mkvo':$יdGQVU!ӧ3] +2qaRY4+ AmlU'bEX4I?ԎtYhǘax˃?-1$ކY,*-uWY% K`S} +1vռ]K7Y^oa=1vLJ[/f#vC>7?~6%NhUA +8FA5=ZE:+L> "O^;"x(iG5r.3GHޕK`LnRU@d$ƿ_G]}?F8TdYJZEdPRĸn=H3RHp͂Ԝ8ŞYynʲtG'K$iBUH|c#9DeK9 %s jϔn$fU g٥􂔅A/_D)&B@ 1(<7^?n1w*>uFf"w3)ߩ)^-껏I2u˄%V׬-ic?Z@J +*`E^]􌀳|q>*r9vTMqT+$P/^6Uc.ĕ!;.EUjCa׀]1XV=xvDQJ А-*e H.S:-\ۨV;YF*IQeE5\vUź2.V'rrf`Cv@xAai_b.IG`kt#(a" IH( T\I#O+ Ĉ4pV i(A~J"sI 5_q@ww |"nF,QpڥԒ^)?n!>Ͳ{(50|IQ+O5\{cЮ]BE2T/CaU0.H"Zk%'vIde $bt)NcIǐ&I:aJ\ R0׿RsbѰ&mm-T%+A7,Of ˈ.qs m7fò4#@&[qd,RX'{k~,%.Bv»EQ)R ]GITx#3%3DE .c7&P#3]V򯕓:#v i2Jn=7bW61 .ɩ7uk7uҧfpJk43uԟ:T`&imOȡp_lnZa4P/kX1PpLwQ>/}5 +m:z섶B#KfbKw~}BbN}H[gݴhjW.!7as(5d5Yg3DZu$l4ZUG"˺'-ޯ;Wc:)֌Yr~pљQzLBi +Іk+ӃTZӲdT`eb *kCn&IK0;_ ՅXke7ح32aƲڏi,%i"15W|l[/e.X*#Hl[[iBC#!Ƕ6!(! +#/~)(tODfa=kK"'=ʟJ{Msc8ԟ[L;vAj~^o6FܺT_9,U*YsTnmb|~~CU}iRt=^>N+SaX5]D2:54f/+l^2H%ѡ1!n1{sEgPS8Ċ5Sdw 0QFrc$!iX{jbMR-IעrGϖ_ !veg\^ M^g + +k$4YIR 5*Y膧R:ѴRGFDS* )`LѠ*qsTJq5YnZa W0qNF0wAa:yEF꒕(J2TYA'zŀN"FO$F<ȯ {_u* ȃ/Tڕ7J[Ri梟cT~ԕƓ`HMʰ`lD"EtPeR}472E#+ +tU wi#$wiٶ>e3FXߓ,tO-(O\=~y +._l%c7$kgFl?2Rn-;-i;i;Xmȁ6 5퟾ xhK롢)[.UrwʷXeGV, KΉ:!K2ڶDB}2NވQ;rmA%O@1!(vk}Bu㴝oRa0E%ԫWbI6WL_J(qBTDJlRJc5ed+~@6p%`\|N +,-'zԖ!1aKgؐՒxXvUMVߩM޿#V|oyV̊=3VR=eFa$EzZtUy1zP6Wfuk[k?&^*y1Mvj0:v{U̖˿'M=>uSwAm8iDW"CcéAޒvԻvv~#؟<–JGUWёloPDWoİ![?(*Ky" BNGI:~fdf1a)8cSj5AqhJJ?* i&4O(4c}jqQANOT &Gd˴dTkY?Gd *v['Rxl&ۼ/f]<^az:ݵ8YeiuN ba c1۰MB)mF `foyj2W8[VƗoR( +,5㣫amA*뜏nm֪b#ui2mj#MKoEwx7VItWa^_0ŏQF"tY>32 p=IǵzG5t%Ay2lΙ]5*?#e +|+Ql z/l{e1A <%Mz ~-J|f:ߐXQ#.UI\*ouT) 3a lxef: +F1Ps<C3_M&՛qqfɓ7]&wC44=/D+ OeyqLy"mTHb$TT? &x$oFaؘsmB'2t?>(+LФ˺<<=damʦ0Z]}#^Iߓ_L%VY"N-y5mOyiJygqb4,Ca"io 2f{A1^(rN O.V^]AV?Fcw|KD"1oH1ԙ{Kx%B +B19"7_4Dp".u4^zUɥ+8))/ms9hkq"$' L絩Nf=y3P7BP]t9Z(b1T8&w#(ࣥ>'˥a9 +69{يzH|R'q~̃՚RRoxEzQDcM#LjR}EN#@)]V)gE1_$7֐Il}JJlܕ@K3wN%+نR"į!fWo  ݒ6JF$!QyKJid ,aI/:n&_UXdI_9d#K,_ck +C ϝe8sZ`, zRC :F߶Ỹxӊ˕=6nMiCLxa9'.>w??_>?~ $c~צ`q~PM\Q8.!g L0z`{E%sKx}3?Zw@hZݕ.cVVK "%nPi>c +Gl#-4Ķp{I"Ưx[ۏK (AP]8c(5eSuUD\}Dbr?^uHRnhFJNJ@Z?Y_Y%3MǩƥIjq_i$)ޔO2 6}%>d^$9 +ph7,1L΋3㚈V/U^B1HۃMB6P)ΐ%H-aio +JxVG!ķyoiNԒAڃA R@Gm;IcRl/ +J#@h!RT*9g;( CRj.ESK+Ba;h$ᢑxC@$; /~) "&51Wt>< O_RizNJU;`nArj.C4"GM7?;vvy^KH%g׆xY/ BoyxF1iw4-gQ\+3qcHU,:EطkoWP ]Wl\k# A2.gz=U˓9R]@a8}1XtZu |M3æ͈K]<2@夜UI9B)ngMB4#YfZ-%b~)s"|X55^/Z{~S쟑 E+@&x-/ +R%E4.**K9!UR%R--MD^< K0- 2!-uU.sDn^F2l_;4Q7K)3d(ty!~o3XZ.@J}]QaSAѯHR?gJ'gW?r35P6]ƿ s^INRZPmq.^ᴥ$Á- cշǍk6%*!m#J +Snu(1jFG7x@ċJ+Jgq'" wfu: +d:\eP(\:.J6~)EX(4GKdBsR ڍ9`xAK  J8v 5HޗhRn993k2)h aȾ[$ѣ̐r8+?~]#ߏJ Eĕ:e[U=ْCС nk63ң~5[70y`]?*='UFlA+68RSw]p~is@/BTCo<\(|U[lj5aXx'}"(O=Dg*KP%LJ\:xnA[5yU(8-d ُ=&g*b(.ŌtO -TN'BÙTu@W5^9R3Y'LbƏV4mD 9$,O:a4DZZ +yOYu_vcU{azl]˕iL-ĿyfO#x޵˦6-h\[y1<Ѕe4,^KÌq- +$$ȧ. 8u ++j.::\Q K$,5ՕY3O)_^Z)lTA|%!4ӏM^tMtj4Sj5J@M"9IE䃭0̤2CiHINavv|Əj-jEm/p6Ր{9Ҫ]PV`ēYqAGLɤ*9^s^II{F*}]Lp,S-"J=&?O r)-drt)~k'QPC`U8JIfMCG]7Mb#7IYm`?+J΢a:Xu)I:ʝ]L61ɭ}^2&3C.}Hټ?X +PkMvYd%GIYgbKxf['X]VGvI]4* C@9dr|;+}K`3:Rз>4hOٚ~+aT;ǿ&M '2IE0\_5)YD>%?^&|B^%x9 2@x}$@x80n7%[ rShi/+k$Dtřv]?Q>J5 +#Hw8lRf_ QpT,lKWLPh1BD9: Tff\uF&$u};LZ@jT0%e)YmRr$gDKO%#\Y?ӄzRHhL!9 +-swԄXQOQvX*H*x]+ UJ*Pt"T~ )"" Mb. <R* +\zVijꉱlMyh=x +K'ڷnZDY&)lY{*]\x}ИDBP\l4YUX|TQ2Ō4z7)oT U|(n esA($j)'Zf:rҫ5  cB o+ԋbg0B D %CAJ{rIY?E;7hpzp4LuZSe|]kcd\g˺qctFcأ ROGgm>1kr5ņ;cJzᵄB 7leU_GLԙTl\A,:r,>K/?G6Ԉ~9 6ﮌ Ɩ;ҥDG{>>4LN*@RӒ-5(PǴ7T<6]"d1m gN3fNZXI !xFV"9܅̸g.B\{z;!bl :]BPQ4|ta_lw1*E \؞;%F,) ULŒYhɭ lgAeg3,&ϾNJ|$>S6e6:Ṭ,99$B\mfNFJH@gqs!cQ(VՁQQH~9r=~o%<}IENĪXugD$Jr6A!BED}"'c3FO14"2C ",Z ?>dҵlZ`B7sLEsBI='Ek{fHFuߎ`Wy$:A^ڪ|r.[J5󰧝tx*tP7whl8_M% eIAjXXW }2HJ%Q*ce/c{NwU!zBݧ`MxmnE&wHA༙qZ +@mbi~ +w° [DP|KpÉ7dePw\ >zn)x ۦ7cA_%ôKcdmH{WN[TR3c@#M^=a8]|Jc>Wm+(ˌЛrJ>2vϊCT p!0Um'khN*5 SWw/exh-t7}k+2?jkv騌Ql=aT ;6AsF0b4 KTd +tdc͓eTyǖq"W0޴ei?5k?1)Jv$JbE B*՛.)dJzbOjN ;8RFӧX0TTQweL1ٻ͒eS,$A>֐.5پǖXRUR5]H) FYʙ(WϒakGxkZN71HKId Ov>!LxoT^'k\َ// !lfp!&|>ꢃ,2),.x^J"G>7"xPJ: PpLp90KWWTI H!:w$ B;*׺40˭y +;{QY9rz. TAJw/ss%&F$9"/K":v46F W/{ۗdYpK&"wa! aK+迣e IrvaŎlE +N0‚IPй>>5Gt"\E2JT#ByiK~~ $ˍ ř+'7Rlo鋬Uw!`$Qg\,BU#h Wj9)퀮A|跣x/ ^'瑫-|EJGt%ʎ-:8 GZ`LƆ9MDS=_W'EP)F(1l" +s0/B>kSttoG[y~ 4j{`&x ?Gzv |liʸgN5Lu#i;_+Vg5mKȟbiaD /\#A,o#n!(s@&Bpi.uYkRu'/9|Eb+! +gZr$x',":x.R1V"_+-# **7pQD,P+o +Rɸؑ +X dr~cWuobHkb/oNˁaA}&QlױPe淠/su%u-[!vP/k8Fi˭fn`b>Wղi9(Z$rO$ +]zsN*x˗tGRΚiD)V]dJf3Pa兝H/MkAwa'u7}@.3kzZnGl?IG0Wf(D?#&e +}z6o?9URE+(_WKBnLkWI$E^ƀy +v|Q7H+Hj&C +0$rhWsTֿqTn:|QyG'AQ A#pMߖO,zE[@" _PEE`{esA O}jI3XYgnVxmOt+&yç9R+AmS[7a$hW,u&Y CX]?ۉ7NR@ŭtb;e=K$svD[b7l7&U#ѻMj΄˒hLƋ4v"^+Fw_I&!,&j&Y+AFd'uZBro= z%^F4%$=cE#>mjP8 7&Y-K\4_@H2*֔6qz +uK +ƅ$ˈCo|+̺!UMA15/\XP簓$It <ť#lE_:oK)풰]TSa6' Ѵ_X*!k m5rؘdrKϼec IRzTK^U2616/*]/xh=mKsѲs ƅ+~WT`Z"ZC|תOf1M>[w+XZzմbsXչ8KWFXTG* $NDyL='}ukf La8/GNX[q^)6X/Jm/mY$Tj;[.f+lA Ǟ% &MtnH)"[=yo[YqsIv&-+ze9?K/5?-lQ)CH4i_!IWLk:0= s {bC9aǐe6n!<[N 0kziXqXM%H +; t!zj~FWG`eOLP:tǖMF1+iȔ륍sk= R^bF2zpWW!g3m=v Ѷת?LD$K4ͽQv\G_͚*m)\qit5 )WGNZK 'Zfz7c'V.QдAI@>T։/ "p7/Qd͌WC<SW Te1le}$54.7ō EH,;@!eNSJ#yܛd(ꝾUf}HܒWOlN^]D|SH +름ƮNoF]IzF*o.8d-63kVSAKUToe]ʗGﺼlNt4eGuJRKM<[⊭s4))-)qE'e(B55w" 2O?I'W@$"tT.f ?Sj +1T͇v"NxD.> +3SR!nAW_ F"6.}Gx.吕KYq& }@zsyjJDԱ`ILx^IsUQ%@F -ɭx'TH@ Xp?uɪXQp p֥ҿ}`^(y}S$] '[bX6i9w)fMwXzGqEYCA*{o aN׊eRqkTRO]8^_L'Z ̷ &X6Ǚ ++ $'S>PQ~EK:vPB:wKƋ_?ũ{DN|pId:qa,"O%JJ]k5)P6yqLd;[VJIp諛E!^ 8 M(8xV41N-_ -8u5mA9Q_UlH}) ФPu JTx ekRRv0🉀`}{r-/{x_~)DˍaDS$$˲#tF$~| tvJ^ɰeicPr٪,]yb¦!oC2Z?tFy]ݴ'NFYVgP }0{D)! PbeB)C!iJH8&WҪgFG(דBz9h'07PZJ0\q}QjUxv.=@9IYUF2iZ!6Zq"V0Յ(+A,NͅYL8 QXםčr),nPzHYbAs(SbQ08$7p"Y u2Ҽo[ p,8\顰oy0L0ȱbJ}rr0/93KRڈT +UHԄ?$ ,ɩx|-3MɑYfvOTrbKABs-:`DK9'PYW")M G< \9&Wma@?)%sBbvi gfkRyN#L|D{[}F[S!֫.9A/Z%9EKs܂krT \Y-j9@ '3GO:,j!?#g7$߁\[՟|viR*~`Yq檅"hQm8$Py|k MإieeOٴ'Qi3?Ȃ jl_]LG<`ʅ]  6Ȭ-Zv|l  vEC4lb.K61. Jص h" cGg,}a^Rhs?["")ZqhKC\FbcNQ .U?@K+I(֤kS`ZL>R:E$nz0";ٕCk +I=؁7 ?8\' Nh8s %"lQ.!^ š>jG6qI'|v oBY6FtsˣBq-N=pRUdDԀ#}Ȏ >\HkG񃁽!EܝLNa qfq@"Ijzu).ނJPGs̋;p ł9hVG%ׅ +T5pN9/~ln.8F|E9!d:ڱ|ۚ$C #!Z)] [$؈Ki`y,ņFApT4+FꀓQIf1y8W_BdMS\e.8#UP=+GD +,ҟ^ÕʠOR#sOΕb$=WCCƪe!S/?ka.q&μ\Ba4%K 9!FDe(UfWdõ;ԋ\9̋ oDbG"IgQdEV +E++F0983\fCb\ 9qd>t뇜YWB 4ט' 0&qQH䄋x*g}Mx*(_tUQ]9'jDxDN,RuPS"F K<gU~Tǭ[$>0ZgLV+ji/h*1x + KBa 1I6h?l͙cD@VcTڋ❄2>,j2$NvϿ ɅΙI-םX %EL?2>Z+e&IF1S9q 쎅<2$E2j < $P I[ -V,a`q vlЊ`EĢyMR!g&QR`)G9TPe}uᒕ#jYڌa7/䤢B韛+!8?c=!f8ou8/Ɂ300X?OL:@pX>8qGopܺJX!ըdJh@!_Uϳd<蠧p5ZUxX2jOahE9a~J +pbA= +5ܤF>m +E 9e#1+ $Գ佚hXԓ$#"W ,A*屍QVwu [?u'D@5hDJqi +6"P{gX)c< +\PB*toXWPA|^/ %/}4VRwD(omvԁq)c+ªZEqh(`Xbg=E癨,+ JNN]%>ި.g!qJE9$$Cj`~1VfFEK^շ5A!ťP=9^D"̮ o]ȑo;_\>O~Ó/~z'W_]^zqaɧ9:—' CO89?sL}Պ]9J;}mWBqr_>om~0vˋ[[pbR3?պWov7v 7<_Mfn:»}pq4yiy_~y~0 aڠa`h"p$ x)yfpi ~P:`WIl5G_\Qty> S$@Ԯ[.4=*5ƑD;*pl +AC4`1YL'x3F_6q5xH'('*Ӿ G^2JN<$V.|8F!F6q޷/lwP86[@VXdk/IN2Ev2]w;PUn$$b,gYxdʄHD6kC}^=5誝M EDhxP JQEX +DZ +UdjkC #0Aw_:{g\(%i0Z28rGlF0.!S*|ށ"Y'u¼#g3<`D9JBͽ`,CU#8,7 Fkv:~f1[L[@xt٨x0EDG[~v+> :V+uR>L%+ + !N<#ͤ+&e?2 Ya0Dp VE9ZoC09Lo0S %;̥<'yc*)#nf!0>Orj)þ2Y;0cXgx ,<+k2[8#2z%xx6A.%JF C, TM]z8/C9AC`A浱 g5UCw}YFpT GBgU]lHE+ +{\҃ `+{YcN@T`UVq9N0D Ґ%|Wmuc`+m2k;q(ph "Tr{۶f#x>סrvF8 1w@lO$L6t~g&M`FFIG!)%@~OӢs(sg\13ծQ42 +RMIN⧣ah(uqr| d +%3`> +{&qd,#o:oոC'0TԱ O!LW:<03nmT!Q]piE&K8ua\Wl̊} 6 } +X UC1|W^r!e +#PVd`Da"4?ΥAdn]+]֌h2W(v%f7 +B{LW`D&=e#a i6dKaj]ZEf;V`UX h_&~UW\UdcnG?pe`w ^pkU ~Y]iз)Q*:9Twp6S`pU(@b ^")G'@Yx G ՌsYBtqZ9>lwJU +*밹\XT_f ߐ:~z0d~IaD-= ј6fBU:kN,h'jADX)l$ BȒph,Wuܿjg'EEUqU8s !Rc⻲,'tM2pp2+Ht)Țf=P]Գ]zVʁuqc^J3|xK&#qP ~y9XѸ 'cWOL\Wj8Y*&P' =Մ]' +]´?(o6'c^E__rEOc@r@9`\YU}5.rpْT%WѺ"Ppbo]?waECW6 +.Ru͒ lF/j[/ӑ*#IHhK]=4"*g4K5AmsuzR Q +u] xq7ubAEBָ +ӻ>stream +Qwroƞ2:FyLy~ Pp̟ G[z(΀DSyZ;Isk N TDosR`ꚋɺ!f`Lu;0Q.Nd-},kuy8Yki++(X[#Ȧ $3݈xp"h01# ںQU >x;>R +pPs'#0:Y[ F [Ǡe ԔK'ss +U `.YچN6L# +Df3όS/[}U\wiDBP夾WvR-uF6w :<(FFskE5#^Zl@"ԯC:hYb鵎]BxV",((dy71Tu~z1;pQbidn;B/'ƥs *]\1CGEA`OtmR\f"A7_ _,y4- Xg0 E75RhS1Kź¢Vf]s;J+ǤIQYV{Rvad(B?&QdEQM+sxtpʼIU2ÈڀGT\C-)@TPd[dU7B a>'h8w&;.N^HDUĉ.R ^C0'a*ܽ$cdž)uP5Qj?1ՏUHӦh$-,]`@T1vҸoAgK]$XemԾBK 0>]u\QmSr5og3ܪ8 \=X:D/@澅XDSm\o^2D̓`7ZHNYHSHkw@WփW0Xz4*ꏓO^j~P&B;H{`@ 2䑉1+\o̷i`ܤ# cHcxnk"t&ZJS [CemME`쒇kJI`UF]حt~S(Jmޥ=nDc4{P lzްszYz*G[essndNCW$Q5Bo59uQ:Ԩn05u$hAn XxQcădK@qQK<9%!]D=@AMMau"0Ǖv'C-&r*8Qh Ca{yan*#eN,mկM};FKlV^r2[Ĭ:NSfP=m5:CR=\mFg ,96kpD7H8[* +4 ytW"vB!ϵm?8gz.kYt|q1WChOuF_FüQ,;<x;5Iyub)SCr8hINT ,8ݍ Yq( >ȷNqRo n_s7 +1%s-r1=> c얜ĹęR=*R=p΢(("oy%7"I!]] [3G{ HQZKj{(CqC>AT91g/fa9ɮschLDH쉸 WB?!u cRYqJҤa$.Y ;ˢ/ cx=hR#2`CycP0'M9G;ƞ"NGoym窹 03-I͝ ]܁La]Աh\;~ `\O/RPwغ +^#Tk3˳ EQXzS]xU$ƍqn+=kc '90xs#i|kE:Cڜ~JzFy4דX)NLH1=I]N%c\DI/ +j3Tp:q$\F ٨]$xGAu٨̄S*:fSLm="1z\VM ^Qrv44r|]7}l£䭒]8-=\9T;< o="6(>3\G_\eZ[v4 pq򦤰*9'h+QW`X-rl\[`َ8<^6K0z] +0NgT&@.% < +♭=`Rr 9c4JdRl.:yICodE,rѾ +3 :.S$`,2.Ԗl8v#OSN;k'u̓$ΐ d햺a"xuBÞ'[|"4tR&PG{0Ʈ=Ҧe妈<+ +$^/߇Yqy )c«~(n1y!k6[]{L4 F*TÉSD7fZs0YμdbwWݛ+[^ + Ä"b_vvK'o*H{HBsFљ2=)Q`'$9<HhḰsޡAVAuR逃%X +`\oV={SXsy}ϼpR$xi-q%"7JV0BШ;fv3W$fw,ӼS%'Z #(PÎ̤&E+~tCHap4k+y:}{"h1<L_"~s"FhE`=UWR Ca" >:; A Pgk޳e-Bi7+nLB+̅\2MSV''~=>AL{p*²(+nUǴO}䛹Op.Xjc! k5d  vSNo +ڃsPJ5q@.*w`d8Esfۛt;Z +j \M& #dI cPd7q1 WULz51)bMlNY bs𗩔l sXriK~눧R3)zM'Z$**2uRxluum]J'aL=N=/p]B:s3|;v7):J6CwcJ +ZTBC 9qIԝ:eJK'53m=I ÍC,HI>Sra-ټQP{:2!8QzH\Ѡ{!ޢ%I(13j䬍֢HRZ4{WjAd*h[qpbT; Ce]RHf\[Kc֪hyh:Uvjjg +ØgG>6sjؿ:6 D,.~k|wf&+9k5NrX1ľz`L /S øc&1Yu]sgf5*snٮA!يfSyߴwsIxicWFX Ѕ2!Ғb3z,>չt R T{!3S*C FU!TIUf:9({ +kYؙX,xjQܯD\$O' 剥{4S.L 7 lqvj8BwĚm3c?Թ;W<^ũM=zhxΡ?/l ]>j>z1Cp] UыƱP$fյJnnup ogXR xvf(=͊\#Ym{XJzEТێIbiLT2kSI`rv84V +H{ cnOK$^ ϩj ͙ݧH&3|GMi,n@Ύ~9;ҘEHTCicw&B 4K#(58q[,N/àB/C+]\1\UU<(+pSE-^5 mS=׍+[)){h k&/>VTKH :{$-=@3|E`tƇ +|0c$W`jRu;h9ָvs\NulmDF*=1u&-uaqvnjKwQ6r[3vZ<%'x 0 jg2Hc>1OsL;> wm1(VGGjIW +]9zn2ުu;0=|RRj 衙9/aQ"{3ZpDĨ$0T ȈE1:&+ִ&dZeja1M"C9 F176+LER"Ǭ 3hā뉈S]tCBD_mc0- 1y oLgkG 1i5+dtz;5g^8Xzk8#= +7m Zal 3"-'[5yh$&` ,+PU~oƖ{/*[B|wp[ǢLx+$ ip_I{l_rSFG(*t 1֊U۟lGLRbBbKCfRW%n]do(byn#e  3Z7l `M>E^夁=q5$ V Y \|,)_ٸ.K!bjI,74 HX'PB}!+hQש` +U+hƊY֙FC2jHcۘ9aư? ˆfv56Va§lO&+c7S" l#.5:?QK%Z rxr Z4)L.b2k nA +6Y͞EKJ pa;;W )gUi:34 +X +fbc[on;ߟ,.X`uwW͞LK.Vc\sD1}dus'JL :ᛴJFNyģC9+Is4LWVZ@H"*`RfkP"'@u x%qЭbP&[ݴL{še7.zK&FSC'Ŗ37aɈ ;a +#ZPm>$U=BXGYS[ۦyÒebiZdk,fi5 K% |V={ KƆ?4r7%z2엟L8{ađoX-Ϫ)3t]8-]eY +Ŭd%"ȓ|Fo&Y&s73z'۽KoZ̎=$פO^g -L>7TH8߹j +g WІ-=铻y7,:/-TC-q;[] g~Y~N\VZS)\".@ qaן<2G{h\_CM܀}Xzr +Jt q~6k +$c4Fo^[r,#Րj'RKl\$ u g{ Lzյ/pZo6G8ekO&8*\'KݙkagrrJ]}gЯ9[iog WWH ?<[g___'?ūc>}#j>t⋿7׿Ɛ_.;_?ç~ɞS&Fw5'MS[1󓟟_?7k\3oF},"?rG\kևyۣ_;w5'W'WGD_rNN7Kw5իW?=W )k{uJeu'?nّ)6aYN :9_{.^/..ߧ񍭸g'vjZO.^]Ϳ<|hm2;`g/.^\mt`XZ{}16N8i ;Om'np +leN6ɦw)>$c$p%MmFGلv]R{.:b!2}f[h_˗'o?ЄoOے-ٳۯoyی$wyŷDVr^ьI|1?-mޭi)^^~ۋ\~Φl/_^l7Th"1k%~ImQdNDWw'W_FDmfT>bٔ/[J|{#3}C SwMwmm0[4B 3J3n03qVm$țm!3TXW&h}#o@2o?ϱym|%?Mzy`h_ۂ&UۡV~mp{맙<<>yNw'ԷQr~ދhL{u|?O É[&|љD0]Hn_{-۽=l=a7ٛ-Inowbz{ ĕ-#3{}߻-p#!-OSaFmr$? o07@}w27.??Ώ6R-yW|}v;&k_|qzx4?_}sbigK?hson(!u#V'9`߬sYnA[)6y@뺶 n0w4-uS. +mwYw^)_;<:zuv߆yN޹I]Mr"w||ru&[߸Ύ׿dDү/7䅻pӣo.6qWS|Y@ijӍ"?NXH: ߥ뻴Զ i6bm#(tyIlnw`@bϿ~'z'ooy[.;Eoj-Z;C;0◗18N)zHq*>|VqwHqBS 0n\%ޒM|7noݴMЂߝ6?F?`yr~<vr~kAë/6I3yZrQ={op\wɨS2 9Z{ۘr~vq۩ b?7f??уϺ; R;7n!ט}z*oq Fۅܑ3lےb+%˓nvo9%ٷn3Y_A[TmB;S?[l{ 3Jw?[fbF!6?:(Nw$h\}wxrcGM%4y nG M~3qr[ƔkH. ܆4y,6]s0` 1_wu>ݸ߼CIQ~kssۻ:}0߶>m *x +r~R-U-]pu;/wv&LX{6wȲ֟ү'Ǜj;Z`jt[q76~)oRvʛ"=M{pʛ fS7;6lo۵S7;Ͷ+o71|GnO_.j~~;;S}ۣ_6sWSۘlK-o"ޕ']&@[7wҠm3]Woхv-F5gp7kL~c̊~w]^=2Ҷy/۝L;UK gq$:9-fl)C2Qmφmi{m1?w=a/rr^[h-GW?kCKwuxYWGo3ګ37۝ćqw(E3d% J>oG-7bߗ ON-cOOfݷy#7sfU=WcX_/ο^8k#?|r>}G}!$y.Na|uuuqnُ⟟?^a_;~'?u?͎Cnh{ι<{ ??z43ףfGEUifء-0 3G7_BkuB%ԁ+t"G=n{%0 DGq9á98SBu^yu_~DrwI姎 ?#|HP?a?9ugSGNx ckMC]\苃:0l]zKwk=>z>~F31]}0s{tTƻ1]]t_v#۪tOܪ?)==vA鱚NMO.]iQ$xۻ5xwiN?<%|ȌiVC(<~G|~y=!WlhYelSq#Å''HW'wv?<|W-||qq*^+ `Oq?>tߧLi*D g~~tqyLs(N_[)cx|_OiPOP8Èxy}-뻌fyJG4urȷ iCj V Db%A +ќ"2H=KH^ȑ0A<["HQ~呼Fށ*=Wka:zݧ֊2DHV|4HƸWaPbjh[' 'R?pJHr(f0azWt$hh) +;gH;`XFAI\( &+tA}nZ-C-DgduF؍!+ ˖UU4=)H<D9Ugѵ QXGKPѹќo4n:557OpGQ7Z/z{ C^Cg..37T]&3TpTYs a-L; DŽhv"wpf gMC.^֕%┨ m~'ѡ'䑶-šT>J ھaW#?Ct& FG0>D7Ũ SN)-4[ J5oi @vAuto-8 ^͎~m̆v͕-"uUAga; ?i:("=Z -meEgbߴY)sY"+ E*,앫iڨ47LKNz|=>ta_A=#}\2uPd I"ZI0*.c ' +GRv kiq z} ha#[~jPB8c՘D5@*HgחK:[Zեu9:kۨCt JJe赃vrѸsoxd#)L}No>f?"XL'qgC"v {B`[:tݫ *{|1’8i:7b,}е[6^H!~7B"{ĉ-F0O % 宄uhF`H4Xט4)GYo|3 nsZe&lCTg Y+@D$ȇ 'O 0T;c_i8U7op}pF΁2 oc1 d0[E9 +5skoa$&LGVD`D4 +gu,^ .AL 5ArDAAGDΠ$.KAOG4A*t!A($KJt)92HiӁYD{"V&Ήs Eb[yC?w/ЈLѾ1v{9fa,j60c"`@XE.)}~$+'uȳ ![ $#&/@`P!z5yҍkg7ACdY?M&@ W炱%2]SsB.80vb= m= K (P\ENUb\_S8tFi>"L DV9V'TG2Pn+tjc\˛գ=,+k}9ݙDē4ÙAGq^pK>pbt0k)3:@r]WMuJ#G]6qfd!fփ#f[W5qai9g9@7|* Ms+.,+ :e'TlϺ5âm00SLP<$d&e~zCK,қA4քHXBNB,BQ4Je Ħwes;WD bU3 Uڠ=A1dJxJBDv[[cWfxǎt?Jo93m3]"@ ꄈ+Ad,tm=S^O,tG-DU-b o!LJt8ld#]Hf⍚4I A#1H4`RBWqBBG ]Npzl@PJhK +ĒM7vۜYʄIan!\)^&@N[In'V #ۃE%"LN h8yDX0 XPDC2q3_B]=F7h[ UծYs:Kj`_\~W&&n ւٯf[_#xVN +>hE3 Ѽk[p>FO}Mgxx_cřu^yruP7|EONNK}:ݢ^ܫw2q4X9vp(dQ%]9p]ȧO]xʛ(zjbωFgy#?T,͌/쿄>cوKGD'D9Û N̤ 5_U@YՈ8Y wj`=:p? #>0?$ $_?;dtkm~|5}\7!6˛Ag(_xut%8_gjQN~pLEcEu6:yyB;]ɓãn×'Gd\]^s>u]|+f..!VV^ +|Vcg +*WN/O8i8yw)ҙ.8{%wGO&@ca䁬L`Z˫;q~js)nZr/oeq6 8^ {C_XLxz>>WnEiⵓwm~ + +=x%f;FXƎI[+MAŽCS;4CSo62T/./^̞wd^PM$@LJ\l_ Y~:=?^\W/=9xl+Yx}ۋCޏ.._]@OOzC;yB>fΖ.ptO7t ߳ 5ǽY+X,]yos:b9UBd al]'m"Ye}|uz,^\>`6{V'l_Z:\D.ǯs?֩c7dz}<Iա YDX4BbS 9#ėlTuNr>?ZF*@"bb_cŖZtz$- Ď]yeHDn=ВC1 ̄$ `Ms0ßW_y nCEloDxJ Ć$~$7.|ItG.Y)vBFْq=\s 8FA8ӻu`zBrG_e:Lwq`E΅$CkhA Q iH QWhtWh^dO>OO8|e<|6wj.#a *`/ӜdFF:D[ZEBx GV :ѦL1/Sy +Hb,P3LxЈG%\nPH~UƣW^)0)F2e4uX@}85.6>DU*]pOόt. }B@ԉHtQo ~ gmŭ%(4Kl~M)Ggce 9]f`ƽð #A>FI-ς{lP}bR8Z&Xtm#Ӥ+A"44@ hѰ-GL]H9TrKD2#}nHDYIHs5E42 F(Z"*,w4ؐu0W7 B|0]&M0"%dM HPdL Swk P;#Ô)*`hD?DRZr²`1?m"aA|8m/,z5sd~ +E`]eSe4ɮ + wG#!rO# Mp00H(< ȋzh7eOq8x4L W!DPBECp;3{-$1"7HcH#M"Kf1tq5L +3.G Yv79M9LM0} ̝9sZ7ե*TR&Q & u-tqR؁P&/G %KsZb>1E9>x a2="yJhan@y R7HAjv>r܈,Dg +43ZM  20/@Ca%=)HSo8(Bp j`pxa? IO0"Cw<>E' +_qhH9f$`Q4>px(ܜ4 j3 91^1_(H2 2^" 7xeHSLNv A$1w1[nt*06x,'x͝υn&=xDjo^AQBFMSre{!T,xH9a;{\0m9]@Kkdì0`$tJxx(G*@, H!׶⟙HCڨ7rQ~ oTD 2a ~!{ j؛|)2 ( 2`&5 l@V{({w?o@ँr П?>39R5E&Zt]~`j؞]VPUܔ+ȍ.($X/q-Vnm;0 =>HX/n~/"}gw@B'*ؼ.x?  oEƬ"aB@`7i>@&_a6?Ky  ]x-."o@ D=Z4e$X%.b0 +N`{wlx,1R+ 3T`ʿ܆Bj rƂ̮vrJ` +#=߃ $yz唂ʧHkWWt3+  ~Edy)^x/IMw~f,1wN@7;5Pjx]y$7N=~8W1tAL pET?0r `Lڢ‘ +#a3!{BMy?@NэLJG=ŜgvW * +/=ZGgC_ 1eٵ80EP BM0Kf|D +XEKKFPou$(ReDkG&S[S' P3I@3FD͟IGloH*&UfB!zg<XЇo%y c +P=Lf,V>/@H39Z F P![.-=?XwnV޿ֳJӄxTj^TTD.+膇ByANX̏]eNk`zS0?C8vx' &hgWEͱZv>5Pz>nt&H~{">{iχ +Qǘ, -K4NuN/y/΂C9Ԯ`|~y//zdgQwod%({xv ^saATa Z15#KnЩk\$q:erfRggZ ՓMIqO=nxn `sg9߶MrP(N=&np& t Ob +}bxM5Χq#҂߾ Z5ˍ^րƗܮG@ 02-=xlp9/zGN}6ts<h*!I/h<< H7vi k6#mJx>ۍ7Bvq<,x0/Y|I^/ZNDxR업'ŎOBED ޿dgxޮ/7 +*nDK-N/=_:o O5x7J3/t'4=NMz^.F[`[%R6}2ÕvO|yX@mHP(6cԗVg˂޽sm%Ϟ8T_S1sftҊӚݢ ]#MY[&Y]iPyੈ9_/+ ݒx+LbRC5jvNC~ +ҋTkJ R}r<_n<0TœL 2grzϳ$mV|V?)1 =?+w#ÅR.0] ]/SW,l!*ږ%@$q1&~sXA\N!K 3etz/@Q[7y(2 e­ga"<2-ٴ-d%`Tψ]+q~Eri } dA뎡[7F-+9a4d'wn+?\r|vt%8y ˙rXv za m1PfpbO9BLi[dߏ!/̊ŸT_ !H F; ^,?L[@_q N'"' +q<=~ś)υq!B}B+.B" W% \H".gPo"(PB +A!GE-~{|ӜW0T`>^,{@*_G8 ($fBHB&V?{0vćz= '%TP<j$P#!\5Q?!TpB B!!l%Y +.BB0-%427@K#G'kAG z5)# Bx?G=!?4v{jT2SWc7߹|{G42,[)(B:86}!`;p>9*v"o|ZU+J~VO6rN/Qt>L0/ Dnsx}EOQ6so\ ;+%\/KV^`GoU3HU:_ŅEdv%X4q;+fX)p58%8}65c&gÔ$"T!)lr6?0#t@;'wԎ&5GRp?3btFR,@gxI/IoutΎ~O0E)5)g9|=4gJT^U࿏l"#L zrTRգӃ aЈlT_ ULS);h+t)д! N)C2ߎrJv$'L<8|KUV5MY^# APOgk(jyx7+AlPnzMĺ-[=t4dUxn^Y Uh%8;Ϋl~mwM֞)H+_LM(*P\Cr擗a|㧩ի?/L u0 += [Ve#]Qt-PWI6SU`yyRhzެӑmJ'$!In]aRb<\\1۟C +48P[dPX&ĂLoĒlŀ73DO1 ^Rjc1wY+ùMUm~ŰS B3%.uOֿr|?`+aL% 6dk00+qj Aw_I` Gȧ<9" +6&3`4ٯiה;ï5q}_k2pvI _LH liW7d{gs z3 > 4~j~~L zS䄾 ;A.D줍kr5pWwi'?g}.MD,}|u>ݠ{֤[2EwՕ}.QwJ|׶]Y̞rv1mMhvZ6䏬Vqm<oַƚ\<)6Ѥx {3ٜ SA#29?Ck&f 7K>JmB3[⧿N( B̰Ik.+|^G !kZ!?ߚ^}nu?MYcҾJl}z3q}K%BgyawD7jx.ԲvMXokl|dt&֫ui Jk^OzZ{W~@5n|F mͼjB i+MT[lx]ךAюvl߮77+ِ\4Bg:0΋uvG+1]:CuwҮZ{^]vzP^uCW;^=F^zu.I>jSϯ+}5[~GB\G?^ߣ l17a &lՉCb40[6T6{C`a=agzn h;ߌa 7ŸT1k{k8ƅo5*őIġ44Ƣɱ|A_\L\T2޷ 4++m +.lLͧfgZ7+9><;\wlfRg:`e:ֆ`?ۀעX-Çd~,gc)usKp[Of=\ټX].h&2[kv5gpX/Fڔ͕+"Ç-{έmYkn˝&]oov}jI{g/o{}_Ú:|֑X!Gk91.w +miujJlNL_ Cg-tq;;SNp){9tk2' h0jvіyEx.bBľP?5Z]Cv*}כőw5hnswdp;3g1|nz#:R_MqYE~:juuul)LײW! Ď!n7i+8gOq0坉mLLz,1mIl8$u2;/eo6W)cd +6TRT^:V&xIW%n].bftoY_{=gJ5#3譒Wubl*`1g;X29c ;W|Mjs y=0ͯL~V?VB~VXag.v9VL4]O%Z͵RVyl}a , K8+ё ^(Yiʪ]5X[vg~ثտ\&}ُמW|fjj.\ qWCs~cݵk> / l6Q Lca&gof77wR97tZygs6x]je1X]m'Qu[FoaVWjwa~̩%N=NXW|լrS_:?3LWU%޶ﻬA,Ζ{9 'Ost}163&NU[3i5}1\-Ɏ&?_fOe:'x'XoLz}ҝu듓eҜ^/]f:C8lYcfy)y=o].jbHn |V>;wvs:= }~wӺ>.Mаms>l#_Nn7tJ{)hg~ģq9 +a5E.=[W+5'혟A4g~1*w1A3TKdAm& ڲR8&V]җ_+yJz'X茂.ؖA_l{q܆>Gh(q5%ڒ[1Ko>ن7wP{5?_Ƨe5"^ |rka/*"LIk/3vr܂l dwKXyd{W`/b:Y v7v=%8_+8>anHh{@)W0Â>\X!O]OOg +~u`H'u$XnΗ~ٓ0Rms[x9/ܚO{r]r,I:`-tG$Ȁǀ)eUȎ&=ҼMza72#wG*?\w q2(v,)jpS% $r粲\65+>۫n7BQ ++o6ތ 2ddN4NFg">s=z[AqX9@|@pڰCsl߬Z@T^t ; U$y֢DIlRK h`4aE;R"sWB&uZ,9RcA0\Y?F*Ś>\o\QwDKb!{>+ .4HD6A"tv3g +3-H.`ːD$nA>~Y uɖ]'6]:N Sz#I`Mھ!`'_-nI +O f ci +Yj'5Yi~̉pi} O;kvw5@2D]N3s`o{n|ZcD5j橭#ЯbDڂ^x)~10Fk~c{=Rge|=DU,1vgh8ba'}rS-Ӷٛ* +~íXėQ^4}j#R*U0`@MjAyTf ύ=w4|[3 g';u)ߤӈ2X^uݴlܐA_ ^g߾WK$ruaz lm̢!~fkL{`#MyJxB^R> +Zl!8c;u6o"vՔl^<&;&D}$ۥUMjP[1:GյL95߳GHH䓣h.bA-;2oZ?r6LUsE),HC>E{LK%*Q `m^79Ո|巛OKְ畇p|XNGe ̼axiDVFK'inT_/.*e)Gv0|vLtq.h0+1rz  f bӆcʨ1-vqB\Yr/WeI[Q7ҕ6B,"irf.72c+4 sZ ` ganjQF)^n0ؤ) \$5h6K˞l߮&m w`-Dw &1vn˺̺aȧ+ )(I(NL8ts_"i&kU\ 4^!o)dĞ{홦k~&(a{K^H0 =F5é1Aـ`L9aAoaT4ePR>E3$ё/  -RL=fcz1x+| M(uE3̜첐FyP +ڀ+zS.5I|Eɹ"a$6Dz (o4ZB>ᤙwS0 ȢE!2",XIlr_f Bh! @\ҝĤ!ͣ#Z +ie 5\z~_ױMm>\,@,yG-s }~5X˵F%5}+6c4n  Rve+x#)|F?$-K7S" J?k-xmuso:^)O%wN7pmҫO,dG} `Ԧj}0oze+1h8S^!4 +E?4@hX B۟`b0nd+?k=,-Btu!-(tD,3>HLr[Mxq'۝q@mg SӲmnھ17l8!9w3o)1k$s6x]X`vbTeB(kGsfGir4M$h?[8qCq\͇_gjtLԮGyjW;@wZ| P$(NHz?0Y&Bo9vW&M.}9k?&`_` ÙVA:O[hoL]0m v.;̢P&GvZߚyZ\7@5lWv#&M `{6,ܟN@7 P[>;QxCu~v`~3z]q +]Xfu?4JAopЯv/l}PprT;6?8X$[&l#lnwc.=m̘HmZc-lN7ӷiwo#S6)OuPoLx%X](ZH /\%v񇎎4P+R-H{N?DNGZN-6Z3~fߗ)r[ +Qd') LtUdm8$/V(ER$߾~JPIAK'@wLw0op̶`s|v`)\ Ж26;ݍo{ <1^J)O_)D?=FUs?N/&')${`3/qc Nvkd5g/rF˯lqN ůYKIJ+_H.kG4`5̔'ù-{.K,LPj6%c)[@mfA +VG\@G"kKX$Yls; p c[a=s#¹tŏ|e'еXxz|7Sfa8j}%mN.ж+rCCk (Yk6@I$zeg2?_`PɔK<20FZ_m/@|:}I͠unX_Bnts =!~n:a_@[>hf'd/@g1 +Ĺ=8P@`RhLK0] UE| Xc M[-pGY$W!^tcHi9rYVhu|S?^v8ws!R`NaVpn[)ٺ!lo+v^rFx '?g' e뾖s͈Piܭ2( WsƼ>Wvz:Gdz٠]v:PXB~i pGPp|"<_,0ĝqёMp"UCocʪXyC߼T9*ٷqݮiW'ѷxY)Ϸb.V .@oLczKا]4h\~(euq)\0bpBЯ1J͗=ROa֘ow'Ni|qO.Or' 쎷/)`C7m3:ߏO]hӷB;PI&_߃mϾz 0[- wF&09U00W̝ +`ܽx,t< 9!OXt_Kr "=GfOx=o蔰${Yw Յ4^:]_XF،.{$[~J>$2X 18@k~ >wO@wzZ$[͝hLMbSc0}- Zcy`A!D`j19P_:ץۉ󝁊!p8k8F ?JbP'" .Os* +UtU *k2 T6l>J&~y{Gjޞ0'71pm>> +ph-;|T@,wi +Di+GqFrPۚ"u|]1 x97>7̇zNRP G 51~ A% o6ЛMTk$ܔOŠ"ar.dMn{^3 1x.IB5Tk< +#Lx}_8-A=vA?Vns@}b Zs-l~T#sN`5P'RPxm[kC-ă + /6]=P?Ow+X\H?uOvp[Ƣ@NOldf=mEM%Q5 +}<}MV!Ci~S!*~<$.6m$t5s]O{NBH*#a~:ؘjS M7ׇ\)c17},zԱH?M=駯1AiD'|2#ɧޞNBajlsk0 B̅gvܧG%N6T 73*v4G@VU 7SԧR6 +vfCSR/Z1|΋1{t55u3XKMx#O1Z:XsiFyi,=Y V~m""! AP}ЙM) hG4Thq B 5P=-uuf0h ́]A`+cHCl dH*&INP&R9H~-wBEKP:FSZ%DSwW%e %D'R]N%ˈĸpb[eE7?eI?'miklߚ WzJ|@C(S_ z"ZnHQp5i%؞* WgĎYҦQ/h![qTO Z|lrCyo> Bӿ7<07)E<Ѫ6{$[=XPx 8\)$UkmȆPphF`#UeCfmP>tMfQk.i:WkQXHYR,K^I;H!JD,OXR9q%"%UI \w*b#W*UyވogkÚkv=UcYkҢ@a3Ak()[5zڟF]R "MrTCHsiNMMh. 8-$A+iήLT麒2Tk,p _%ӜF'┖z 8o8z4N +DZٳ +筂^IQU$2nʜYIi6 .nC8Gr$) +JQZLHi6vZ_!`}IWB/g BƟ;<;` ㉤ QAhV/U.Nӹgkqt1[<^8_Lٕ tÁH(#Wt#ؕcqS"6N-af#{t嚝M)+~=Y'Q pQ$ E]"A @{GL(p\\5)Te4ABv;V(ֱ,s/rPè+% 'zhWTK4P3/9r/3!D鶓p¸gq˟&e5,$ ZFͅ]ܠ,A%" Y}0#/nYߨޢKփGCDwlTX/tѯ9P/Xc/zK;>n. P'&& +ssϭf#'~}-J[#P6ֆJA(o"ҍ; #@y&~j<.@W+ +{쭎^zFX'H7؋PRbGwK7Fu{{I7>i0Awl.G6|Bu(WmDWM%UUjO&`WsWxe {s8thcg??-#\ݽB ٠TYފib{YyiG?%B(Ӝ5 +@]Br{[Sǚɣlozʉllk#9_:9G۩舫KmDl~7GI.? uY +$?ToгWHF(ÙR4:5&`T9m3~m7v::R08Qd +?ÒfP659)/14;E}7u,PdJd =v#:8Q<ə29CVu Tz;`.吅aoLdA({Ũ Nޞgsp^Ri&O>='G>O>|B:|.n||ssRL僢)||q~';>=Χ|t>^p #-Q8f+|y`}{7&޶8ۘ)MZbu9bbeTI]>m +U-86>NawF-ly'*(/r*2Vΐ@e;X!`E5]|sOJ-J^yspYAz =ࣥL"Iar]!&Riw +KŴ;%O1Lgo;[q#}Z!otF:G ܔ[f1)Eg9ShLj^YZ*uӝT9X*LG r3z8=Gzo?|4uLr(roWxP΢o Tq)w:1i%,o "^O#z=ffY^>':MDao530<i;w#M$}/2zާR%JIhIC)oׁ%.z (ۏw=X9k*@$,Af%2|*N +,Ut9 fکQe;e"[Tj"gE^rC{5L\#%E +)P +z\$,i=-nJi%l=./}$.VoPd@r*dDc:a彑_JFʌzoܺޔ/=+ @ **t!Xk ̇P%|zoP {#S]A0F"$ tWvM~;z054OȐ,`GtIY5=c6P|8@ PC LU0J9pU˥ tU{E(U ,;~u2V$r Rq6y|RfuO{vfHfh0,̩{nfޯagthgdgDTo̻iD̓rf6@fUBim"u,g^vcWR{PFd}P^:cW;};Ry x*0U`XB~qQ4,A?%q;!sBQW̳+ +}qQn=?^xKvtO^Զ:>u?ٛ2m}0Ӭfݨ,$$YBp$YB7-"Ixdǚy$޳d{J=%I'v5(TI]4Eל%a* E.aMuhs$g%y=E{{VRT'n{>=)O1?DF'f%Іcs\tĢJOspw&O+p>>HGy(w3Ĭk>h*z>>ɌۛsڳVxVbXV(׉}bY}yjb|>%I}b]I?' +&=i]H{,O +iON:KubZXt7Xe'SG!=,xWi;)4GF9g*Jp}!3C@o +ԆZ瘌Gj*g*QZ妓&SI̖_wrOJSeGv?dKDl_p#|k?zCЛl?"ᥭR73.6k33T +b)U0!:ga˻)Z1ۛ*;7!7u~<C&|sb31@ݙB!2:/5@ +%TPʢPGaL +LT|SAw{XUN-uoYH)Ki6*~ Q;4U\(z!7gm?jcjwڟ zs~EvtSU';}/EnaACm*d +QK5: JTdz5Tm-dRyADI6ߞzSi]@T}\c{=%]T70}etv~-4?TŒnYǽy|.6yPnբ갷 +MfK;RW@Pm1L+&`ädwh0rL4L-Ya% fژOu*ϰ<ηڈ {t7_.F0tA@݉Q-g읏S457#)WohRTwGR)Cy+ 5?َ.~Jwʑ 4MT:G" %]@ʒYRYKGVqv'RPbPd<$6]*uYJB>m*[4Gn)יv9PtE1NFrP-HoL ^x}Ukl"&Nޥs!߈B#˓[}'{ M6{/|a9Ȕ_ +Varm߸n4+XРG-:ե$>t.oy+:Lg9]Sx]-~J،5ogYҹI?9gj|3;0"aF6. ]XM`g?~%13\K$˩QչD٤DTn^uUB*(?,%N?)4@|D:$D0)jׯRբܤNIT(KI"S祢X~كb!㏺[ qo +2{ƭV]HтŜ)@i<K|JF>|//]C_<#~mm9 +!"UCyl$WP>ě병O-xxWzܳR.; J +/x(iuG0D,xxGߧ\',x(_퐒/x(SgO)x(EdCnؕ?i<+,maC >d_PȌ*5ueeF^WQK-x(ov342֗-VP^!:PPcT')x*o%RP]W %6sڡL|CA +[ksCjur2d2$-x(:&Fu?+ Ϭ_1|e`;N}m]m"c@D]mn= NWF"0l\cxy~:z3$zmkiG:5dd6LjXr6e&9c| ++@wLrx+ t g9ࠗN43[;.u4o ++JC” ;DÕׯWpi'gЃ>:܆H6KBhw($"d23Q.$r`rʫ- $9R CR1M:o6/lH>8tz$j@ ,2!Fr}Ȫc*Ωn@W;,4UY Qq|\<;g.9٥#?kzOսwEĪ[+wPg\[\l~`Ǿ <JDgP?n>eŊokS%7HjoUS{鸽2S;{}A +|۟͋|r (WYcNn>9@tsֿÜ-ǾXa3gهϮ,6ōpu}N17;>+w>_Dmل&1Sdeܖ}ؼ WjW.`^̰G^(k7@Ĝ0( /-C;=ٯ{pt[w^ȳ>|oi'jydZ/i_vGi_YN;=m|.Y}Kv90'rJMx^SZWkݱݬQNx>D7&3^f#ȮݾK/N<=PIU TkܴgLf"VDE5&Ϛܤ̔˾Etu-c;Ou|o:ZJJg+CNKI뻟vqh8?SS3>/qkg'_|v1wk]xzꋓ+:ٲ2>!,\bNW-QZF$4EџQ"4%5٧.,.NŸj_}w}<_2sgKeHCNVX6F/O8U^Z~_Hkҵ,9wq¨/O8jγy!K)wvi~r_?qxNqmEg#4O{9֟5\}WDr1|]7/ͅ/Wlͫc0؎9q4n;69+$gi}6_tk~qOm9 },Jr@˘IĂ *G ~A_<^&`y_FtSLE߮ Jg9)Ds)]~",VVH6K^3.Q2ufo繹*\^s[yo|yȷ}o"j?NS~~Kas,p.BuM{曝L}ľa7JB=C^kO.b%l~7_{T>Q?sp-_m}G6y}ه'i8\yXǯ_9ڧn,34w>?:xj%MvGr6?9r ϛwr~o?\ls[ݟ̯L x[EW[.Ko{/ǯ.xjknq=| $Ba}'}}?|ʑU~ޢڽEU "Y|sW>ľgwXJ\+#ѹኾߺ{~ze!)Yr)q {NItD'.xȇj6,n$LI+~})q΍"iGsDtVjǷ~~+φ$k7˪ό0#R\;vt/Nˇ~>aOS6B)sUǞDgZK\PgL*5Ei;ENu{$V/%/~_=}dzSG잻=}ӳsW._NOݟ@K\)_Yv,*O>Ou0UT)s7LKGOWze_nst'ꟷi`6볱֭J>b[g?<}9v/w%m ObMcz&߾Oܻǻ?g> ?~l3>Op+p]v^U1jRs孊[ZЗtT֬]_WmOddKV~; [?`H6"їv.RooN[5w~=K,}'v_ʳr|P7}q2ww^k(r)jُ?McaQB1.~+vrY(#22ECystҏwdi_$?,??}Yϟ?Ry'"vHW8~v9sqYuiJ][1cߚwܧ\F>ŝe _R_N}_k;Vk̅CjؾcoN-ԻZƑ[!/ӗ<=9-yóWVpS6y !։ 2e0o\7 `ԅZ>JY<3.wi5讔I'r`/ gľt/~V{HlfJ!|g$wZ?xچ +!UyQb}̚]_b<^+!'\ +0ۄ뫛,7A^#Ȳ_.xrJ[Fi |ͦ}xT/d$urQ6Ս\妫<3)ȑe\jk|#ϸt,f @{Š0ߴ'Oڝ_֪(jVxh"ws})h ukwת~Z_e +Ksڕ/i}yNsڵ'/#;O[׬mMaws~r'rc32v~sCȵߚwWGJ%%}2txGZcx-oOPOg}߮>Ν*asme@tN݌;?{iy󭧺Q/sx6&__{{sߟ񳵿|_k+y[ bz/5`zF2.y hգc/~UVI0lvG{Ϝ,<[O8Ε?IzD J%v9nܿ}Xݗ.])GzNb]8peKcmjskmiT񴱖W(_3Ҩ^]?u଺3YbuV-mi|=GZ_a?#f3׌4|?#lTEy_]7~mO]??+R[_WnDֳ^˜y׍ǻX?}t^:/!K1Q璙e +vG@n.Jw=}}J=e9х4]x>R1:Z I}i#г +yMϟ9cB=g;{>3#-~8;vKn/|~vzVt `v U SGȏujvu]0!sLYGmt35er6>)|z\>3_ϴ}1qS?~{&7QgvL2|2Fёkܘl}9XS8S4Αz$9v"o}~~W?}י?}G>Q-}ɧO+/_J:2G.o;:;۽yEsn?{Oo?ޓ_|Ǽ/O^<X-+/e+/R2̗\iI+vF {`s`[ףvMhlk@׷~l䟇b\uzŦŵ\1 "k[H5*;&KP^FoD;*rS/r4DsO`3Cj>Qg za/nBk& 銬-[u7B~#MBZ+*#xD"!Ԓ1h.ur85ufHTLjwYF촎E|YO:I蹉&cnOsR2P3X$"1ql-~)FϕrFiyJTUw˻\yF8-xOTI<,37)yvJL̽Xj@]qn\M`T*g8VnOHsUH eMy2=m$]:cЇ`ex;8#unFGOVI1̖GJc39YoMʁcskaTCWSqNUMN7.ĖuWL߯J X!rAXpQlox,Co+0WV +7=ju^RybH& +;+$$ I-$n&OCH. +^8~OeT.*Ge`mkcw'\望$ +y#`^.hv˂2 8=ې#T>#NDjϑ B~Tav@T7Jo^*M-9%*ܿ MG[b'TQlBQPSu , RYp6PF݂7O&IS!D')=OӨܸbe5yTTͅꊫ'Ra`G5퍓{o4i ׾:TTM&a;6oLjlӨ7vYyvꏫg_G8d=]-! Ct1+8t(1 Q;FmK@;zˇ `ƨ0@Ƴ@b]^qpm +82ɨf*B"R8 B3rlh?pݷgadP*+Te yMC]¯ap{K5srN6Es 8Cΐg!JmMR +V xb ׼o ?M({bu[\B[aD6)YM`hIT v6.)'#"y^A(5Il/p/0]y +6${3.uQ˘b)<Ȋ2xp ap[&RXW# Ě`;sO~BH.$e G0$[NeԠs5$ͺI1haȊjpfm":h +槱_knfTF~hy?;#QxLGéqW@wV/3w5{|&;eHB.^qbw>qxO +g![,($[h@ $-Y{ka1SXehoޔ۳1^V{!2$5/`BtE;a@/+'h8<7OCHѰոBtE~Zla๱>ȫ Ra Ɲfx7@\ + 6q.A6c |Uj+ Or&qi088`Dm{>y|y p?9 +W?Ck(TpUܮj0q9 +tAӞR?x crK˻**-!8Rl56Z/^,/rgS 2A=dvl4ǚD{1p z [b`XH()McfVlB +!Bzah}}pjr =?Fi$PX8-ď8~-~& IEU*BG9G2++ ҿpز j60ϛ!`JXmЪ,V+K sM;Y9B)$5,W n>缅`b4d,A7@ju'EQ9Y USUNQD6[NV}&-'A:7C5)T+bA^C lb9OcEX0LgFT  T +>#.dx&Kt'&\X&oOp9wG>y~cwo[0Z*Mwp`"kr2f_Q8G1$1Ppscd`oPID.FSRCi_ ;H(c}T%7 TQA͖DCpЈL0O&Lp1 G = +/d[/,NJ84) e2sJ[~!"ǐfӎcj R  +, +fV1 }"@Ȱ,ntҦ#!;psɲ>(Z8[Vodpn06,GW} Gpd Lz}GQA#TAOh{샘H1C#MiPaXЇ+(&C哧n?<\7!~wџpγ'_<9ҙ7`X~Ʌۗx]h7n7GZ%Ukf,ieŎR{ۭ/+%?}XG/ +"3[)vsN2z+bbBBQ5@ + b*0` z]Adnjh MԠl ,iezlXJ\ScLè72V?#2SWYsU|LTt>-(+XNlq\!s{{p$;juuYCt$bSC(ʊxx!fM4®/e,Ljtc49֙IX,-foap|"pW*qݢaK;*X%8Bqu|WovfZG>Cئh,QQ&7c6ރ'٠)!2H4dL$ZZ8P 1sr+0Wj,ap7zb8"$ \ F_aAѯչMK N5ZsenHrc0t.%QZ5ܠbd6*?]au^C]Fn#S, >(T qxcߥ3yXO护y^U}rjɇ j1`[bdh[ZM{k`;i^zkGWD7sL4*ٱJkP;NE}B +#5 +fMf1yj'̜VU-vjh ӽoV)֊Zj$0^52\ ڴeA6[am"#u=ʎd سD١r+ ;RaX㽂C%Ȏ^!+(;DZYAD 5U eAKi]K$Ѭ,T}ܣID&Rĩ 9$Aɇ MDpp6Շ JS^74Ő`ǣPuex1DlM|LlT|vi9欁ip AFH"5ZPeT g ^J AdΚ%LXY +QX05QLrltt3۸[2B[lm[PLk :jTw+)2IBւ I)=XlE$J)uZH4R%U])7vp+v:vD@a2˫*H۠Iq(z$*0C4ؠ&^F09,_PtBZǵ7&.u3`mHx#Zs=&Zr /\XQ%;gy{P +8)3-Op'n +@H!$d:RA.  )i @|񯬫c TβQ8㢗@HJ# X/bD^ $8Fj9X8EW攭lHoQĢݶ3fʨUm`מW%0N)33^. G*Sj` LLo0RQihCX8*50&Zm'LHIVF"1#='yD`Կ!@E+u!+4δD]+HI2<ͪiD90,Ș5jubC汶uk1 `j۸,ƴY(43c؆v8%)+5&%b1K1P(ehF`[!3#R6_a8c  ?ƠU! +#b} EY^XOj\C ĉ;#S4hV(ji EbDKIk¯3(;:#v<k&dN ["AmAcΊbre'6ɌzfXXꀷR0! + c{&Esc 5?(qnޏENp VwvPtTXa9؈5HP0G\ DkCWm͒z T$(̿ _K m@A9(iV#+؍ ]g1)&(lPe!:a-pkl{PFS2iJ%ihx)-;fBP38)\v=F$MOoa\T"/SJʹTY`M$3K,v#lLf"ou|9rF`X=2ԯ,a?@ шXBNrPU!g0,XbVF#NpsQu辌9o*DmPT,jf=G% z`aE JNɊ5 q@PqhFG |:}=wQyjG BgFX_X%[.6H¨Zӧhs&0DvPISDt<uyvv?F1u +yJw[0>YW]T猊-͵/wզmӬ*aT + 2[&kF&Q9X U ]FEKBGwh$8+VtJ=+6! MF`i@*f^{1qE/N*%$R]7 $$Ĉb7"%IMO`)r*g$Ų2$8¯Noe] _8M>[D3R4rJ[ 5dvݑ"9p[f8hQ +Ob[`TqwRH( ~J`\Nf7x"2o1:*k+0UK#(!)Vpb5#X>R6¸&p;u7<25d΃Y13,+'c +`UlД=bUj?J̪BCĶ x  كV/kkYc( Ъ1TQ +j,U3NKo?dљJM3ikDZa4bsVTq]S{Qa42RP K{ӹz%lF0%9-jWzjO0Ŝ` ETQC$r@g0iVP% 4}qHѴ1BC݋[{ ֽNi"'1 +Τ`Zvzb]b 2P )G XC ا s4(ix 6AGC(W' fA`c!]?0:1ļL9ڝK^HZ(S沒S8/rԃ.4bH8S ؃Ĭm2{ dmfjK$yy#f4b0ӊiZ܃ )NtUIƉ%t;.ȎLr3ʦ<4GZ6.,`89N0qB.@n'I[beز9)PrY:x8]JItȱ]߇4qX [78)%!Kݣ~`sFFZ! 40rľdžR +EB(V.GFg~DwDG"`[l;Չ(ڛ&6ԉ01f;$ *TmvlCԿs"6D$ agE﬷ٷ&[P[u." m{ɚ3<ϩEXvhBօZ.bȺzElд\Ě"."HgZ>bM1z>bq#"QHXڎkGTr7> mGa >"@ߑa饏tl$baM?}ĉ/f+p]HB1SOeE8V@Tl ȜXI#:UɇFC[dL$XTjuJS9rA#@A*c9|TJE38ȪoI-$<,Wh&`ϱIW%^ =NLo1tN1%%Fp/lIL2-4D}ic hS>x"IB8Ə؛Xt:R/JcHq-Q' ;J?d2hJZV˳G #O)d||8G!;/勳==$ +dh;ѯA Yكjyg{vg3Um:2 +i2$u]1zKg<-skЈ溸(04#8lKtx=gȔEY照j +&1Z1[,ExF)߇Zp#]_1tMB9H{*]R }Gc1#$h8h}J7*@\E,Ov Jc ^p:ug85 >.ԩT8U˞̓d^u^c^jskM$|/54_S ?gO;;nqx/5g陵n+{){,CTO*mix4r!&lBNfO@eq^%s_tQt`!d,/HeasKFQ9SpbcPYFPh=tIcCJY"vqF[D& #5卜i`?CV4sb~V#71 d@Ң,*+sz +] ""Ӳue.-S^H&hMYx"hNζbAYPmh&bm.Y$ۊlLĠY4mhW ƾmإ!Wkkֱ5*T3Rmmvjd}Xb6 c#,&UdRT +,XYZ‚oÂB3#\l'[7.qf ,/8j,hXbPL% mض, JX0m`[`DfmȺE `dQKX4k`y:75H8 b$&i g#,؈}&koJ}&&l+T2mLt"YwuXsq <7Յl 6rY5x_S/}M&k1zcC&/puS`sïmFa[a;] 9$Ѳ"l+gzlklU5@Ҵ&aqg{ܫ^mS`Q݆UѴQ*1SXqNl%d[PL DJmclEYzT/N_`5r{66 ĉv[N|HG +ZcdE"gŕ0Azp(4UD"l?omGP V@=)3GhNb2D2>g/wCTzcKuWi;FoL=,YkgdaKKzU4mX֣^XAykBIҷR Ħ + {?؁.~^p8tPqeׇ]F2 RAݦly֫u2>b11`ľ0*1 S@^“Aqr.3ļ2 VCD$`CL/Cp(*4CL/Q^xt!ˀxF0 Ax1v±lkHFjpyրB3ߩz58/űݵ̟y͒`*C5  + oRn9tKTh^ @1@=.yeag``^T?s,@:א U?BK)Yɋ٭9kd8-p钬p >--Sr0^CFRmv`!RVeAF,ۊU ^Fy&ۭ@gQ!ѹ_" ^ x5<IVxa U9-!lP!1j-jz-ƅ1d5ךG8e$Ck +WZ8W kWDn3DŽO]w U5 7iÙm@KBd<4K֓8%nXhT^, 5 +M6 *6WP^QfqmjGFUP`nW3k-(^OlTa"D݆37%nvl䔽oV$!*&`a h0^op2S+Ls#CH:m/볌tӆ5|~ä1k/]L(D6 eycq6D@HxB89~((@CΦ^@d+*bUP,x,|rĬ໤HK'oq!|DA F"o=%yiixs>әi mwm9 %7lGL (]828+Ё}X]BFЪqzH+iY $`G^wI" 4e /x.Ce. fcF**.:x$DKdpzW7 ]!U]X(w%f6X'#]%gb$ +YG@F ]^zM+ml&Qm݀53jv:Xm:(gr쮉[-.m2HۅyQaSAcIr·`2.!c*ޤ&02{YK5k@w1>طƙ.F2=N.ݰ#]vk1d$Tlυ.6F@h5qUk+0dӌLY֜bل1*Cф_(!bu0%f{M߽&ӌ]bq`ט`z'!viwإv><`YI؊V8x;oJ."bb>e^.EGikss@Cę1f K +&;!4ٱ5.Kj. T!ZPd/!u^j.:/TKM_(cZu^f%^Şf KOHb.Pei\#P]P]a&q-&: u`"u|0呺D%% +2{H]f\+Tɲ$BN%$qpXF +lմ EXR>WZ@]RC'l-.Vϡȓ#]%-Y#8]R s2\Gت0bMς!H|vޮG0%t-RϢXlGz K(tV G4=>W)Ղ,e)5>p +++ӓ[\3Mph˜6}LOs4ƶsE Yf^M1N7=<*ʏN9H!3Q^ WIW\Jaɣ_LL*2 œuxDp&Id!T%u7!/ U+KӬLx̢RAϱLӠ7(%CFOIXEBΐ^T : *{`%L^Tm S$ݫP_IuNyixk?6,VBհR7'>K% IdX [/A8MWmO/ö;ɞemRx0^B@L߳I&u}Yfb$SӍՅ)J]6T Z)͒ńe-$Xl7L2/!S_+ AB%ƹ"d~j"M!Yu){BApfyɠ=!n&If2 j#yLO˒ \q,vv@Y|g{gG 8W{؞"fIWOz t+$%pe>Kʷ.E^o7ɛ~GS;)fר./ltII [FW78IӨ +endstream endobj 29 0 obj <>stream +;. 6}n2+ lcO[ٳ"j퇢J *U]IT!w(8Sljx&MM )8_i E5$TM6 DvOjqtӨ7&e(۝h zQ[ncyЁA6Cn,d_jE RQK)ctd2#T79ہW# yU$YQmlb9$?1<){j*Jvݲ1pYQMS))p0E/ O9~ւ듉KM Mex E= ˈqbe)(mc) f^$;h]MAU7 R=SL|*IRdhx^-r#p%>"LdXc*7Qp!t-7%޷|e?[0BbYg(\A(\ +A5Sݹ;'J-ֳ"PH&)Lٸ"o'C FE$Jf:^12;n'$ch6HR1`/j Bܐ8qe>i&VbPYv#q,NTDש e)FSCj0';:&&3\s%\.bTΒ 37HD x_ZL00(wdذClP#Y@ fmHl :RrL.1.0"xFwR$#7'P*F +惼)16Kkp(/[ޝL3ϩ*l8uwPY KIlb^B#i:t Tb#I=MEٛ"6i-q[/9D\[+nIvZ}ӨFWqomܞ%lm۬Tn5UQ!7M8IB[&jlU9$T*B5HWLoLy$"_of},W 6ݯCj3Z̗'ך2a +D57`Q3ʰ 7K<,QU&p#R*R'j*$a </TTʆbCDD5Q=|aزH-&JEnJAUZ}#bveA5 9,ffU8RV)\~mÞXu ڧFky,HBC&,*]ÎSX|$j(G*;]QRHuM,ajyؙ7Qvz OsS_m8̱˾pROI2 +ysިR&""d 8H jNwgKQ9s_brΐq @Py*GxحC/p(}9D!b4ӵ`m<3}aGTJa%h8'ܙ2]t5dt9[Yk?5@-]DoS9 + +׌{rp>p`RCD>e@ҵl>IIBSS2#4Z IY +7`VFqE +t cIn||9v$shʝ#maֳ*۾itc=H7l4&h6Ep&gWԂM 2"u.[ 8,pYR((`dA\( +ɥlK6ɖ+yZyg=NIB("' + hNA"=d^f5x Ӹ\Jw +U""3Str!d Z\Ɋh1 9&kRJYFu*rk%Vd[0-$Z " )(Lrqʓ.q4[lKs[zO)"p?5 >c%.l8Ec4,|,X $ìf9f: +%Aփ}QV&lF) +LfjŜ 2 fLɔ3UlehdInt5~'L7QGImА={$FA9q޶RolPIU-/ۺQwqFUm;19knlQC/>T18KQ#B( a**V ;-& fET<(>kDx)$ ?`sJd)>Z RJ3x#1 j.8l=(7ogktYcxx`Kj[:7%-mSä&"(utIjsԢǠ26eeJqFS. ƩO2~"ᙧiId~bѴW9ML&ೄ+*%8e\49Y-*gA3IIkNHw0Xfe#DO)B-xis\N kő۱y$KfoI Ѣ7nC YJΕuùbՀJ?\I%YE?4a{=摵[Vfht_c^ZeDf?6p=OCtPmJYEFH2YsS3Q"h+Ջ 7+{!"3)Fd2[O /@!QN( +Lr)[o \#8&D9AJ3J S͠ +q?=ޛP BtsBPҬ&̧ݥ}w(:ҿKѮL/ iE^)5b.>oqφV &p?ӭ]Nh|#%h7OB$zZYs 3}2 +R1!k%C6b@65 l 7q'q ^"&7hl;h z5G|\9NRa8qr4>fmC| am _a"Njrz6t/FezD!IZM5= GEuq Mw I\^n &%>7"miG"N|Y"BS\iHAt| ae紗I6;ffKX7H c}IF)A+ +|#8&ҡ$` 7E;fzg2rW)ЅAL"2K7񹦢 r>tOn\ҧ[{v및}n |pFgUJ \}nDan0>v5*c5"x)%\%j#5+TOp'`+ybQiOƴGSԩ?S"Ot5G`c`jrcLL2>E9gd&]M[N+tto/o<.eM/E pi ǫ:%aw)@S„34gMPY jS"pq.JhFP#\>lr0xh4xLF'g8FB"+''uZj&&[O ,"2Eꐚ}v\4JF"Mܮ gnBy/|_xtqS]&%?L7sLh炍-Ҷ]NA ̭`'9ݮsJȋ͡N7~UJvI{M*y-]- x)upFp::e!0rZ;~$2QiQJ)z4^Lt3PHY϶7~[^N+4k k36ꑢ=I`ˍFMu}S1hɳg wX/ӯ@f Dn{l}>rUbμJp9ԊBˣ02-lu˛v89(ͬ57M^\+H *\kĜ&Q/T^ھXC5t+ڻeQ~EVυޙ˫˃CBAg9r9HknZO)O.4-K?MN Rr$*'|0GLQ^:d"G7B#)=4"J`24ZL 4Ip + + *r v]C,w:0)Vb~JW̷џr e4dJ'ZɛXA@ř`P/0? DBx +P62IPM8XӾζxV>>rGywwG>ֈkY:9 yJ]u3c܍1npiu͜F8}ͬ7Μ&/*,XhFjl"V~|}9]=+g]Z{Ff*n9U53Ӫ%fvBkod3Iy,q,n{̲}e3 S6AJ*9\\_u3_灱^cV> >t$H Iݓ `܌ܱțCV>p6߸)PUA{CFy>'VQǨ xzleGp7rΆ7:Y99w9#Bjvm ٳR*yLht=Wo>#U*3߲jTfrRXӄ ܞ ;ֵ[!G9U04~ʴ[Ɯ֗>~Je;+?w_㽬 +:da`GI1='Kc"v~mV(/F2 V 0#@`Ħwy%=UN|`|5{5f> r;Y)CщH]+-:n8Yj409hef)je&P!K&@^2 WJ!xW^2M 2w>^2'H1ʫѝ9VɲIwujg +颌\ MP)U/3VDŽg _2ge3.X{d3"i` UL$)e cG9Mzƹ9'_2LLF5\V45QnLq0hx +<4&P)j$leUhm*bn4j&4il8I<=$r "%0qܟX)T#gJFOLMӳ85t.bBHcQ‹ϫ.8!Ky2{&J_MI4@w;z_LĘ>FSyi)KFk*W'w8w)AO6F]O)3.(DGM%C #89J +PɳeI +˄v~.D&rUOX -|zG2X4E0NNۇL{(֯q@,SN 9Oa[ҦM} ?cW o=&u?ê4)AWnˡ9(&VYR\ Hă 輜&8Pt<#TPcl@SzP1,?_:><+TG$vbdxO O7+7>1<C}/`@_⽬*|nuL5Sg̻jufqYogz*ih ׊f9E396oIc$qyVKg/*ٜkz/s&I+g5b!M+(;?SmaҿaK'vp1\s8;ެ^r8&+Y!I9\7oT2P0I9@N0{-8&}NgRp:nWAg( Q}$ n԰qfD5?R{̴L1& 猿HfZĻX،C"L>qad&o4B9ܠyV4Jx诊y6|NU0y/evg'+SX<9*!uxgh~aLh[;'S;*͉užսv2[ؤ]n>Rd ? א*vQ|Wg$ԋbfeUҫbx56ukȷYͰUQRW$3Vbg 9S'yq5Fa:&:mǘ]4d֐t,4loZsƾ'e!'JWEy3^jf1hV#>cG#hY|*'VzYz~5x[Icu]Q~V#F#Ѵ'Z-?FR-G4G79~i6=<0;owo*F#cꆞ79"FwG4:_6}MgԉVj<.GKq]jF:htZV0%Y&/ӣՠZ+YuDbzf^yp /vlV\GXE;'< OD69 Pxi:Xc5mdv +C{:MPlȳXX.3 X\/k)"gxɃ(P޲tj,G8Lm2M\[*hNO+9ƵdX5n@8IlrVڡ<#Epė#ν\cj7={P1GuZ\\ez|,/q$ӹXs)Ge(#\].waN~uڟbU>j JH5 ȁ9 S +,[f+cΐ.QK+!%:)z4% +fm SZtߤ!^t( T79)>NoK05/ ̰2_\nvB[8пSQDKÁ&bm)3V4gf4 de=yH)tjo 2\o10AˠHff.p_5b6Uo,]s%AV)$|Jwm53gn 렑FZo'oɤR s*cuK&IxaG #fNIhJۈEQqzM3u՘dƿwon'o +_OCs-8햷nՓsOr )!1:fs8>Vw+U,_%CƏD|(%8ޏ^1(:b2 +.*"]DFAS'X0T`adff`2-1JO qenxPDrp$DAcO64F2GwbcJ[9c1;i@cb,˝~A1P F4E7,غ %6m.N'cSpE뀊yaN6"Gd ?7?]\Tk`,~lv|x ++d.-b1[{|Ml+GB8dAWd.{U/ +:!Z< Y`+2γ]h\]]T+|d6I+`Yq O;b[+w~X-WV`-u*گN`>o*nޞ$jɅˊ`{b@UP^tRd.#FkD +.-FfX!X3e|2t)roqHHS@` +"Ƞ{RNb)ck0%Z' H +cA?D.$(PZKFAQ Gr(H! +o\DŏQ,Dq3fY~w&"Ə~4.M"FUPg(A'rGA4q{Q 0"na:Wš +ύ(m:Cp1̖n0gidG)t8CsgXB*cڐCBo~CUjJJJei|ǝ1ɺ4p䬢!N'SʝQ7";C <9.yFTN@gD!gp3"b].(cx`}ܠ#7~nQW:8$;-ٔՠh0_JQj.X*X6-hdRrA:!Xc /P.&` ê ܄a;-j$ѴP#Ӣ |~WT& V(|M&A|*t 4x4~ѐ[㖌0so0p.E]yVδu*7v8=mpƅcx>NryvwO _; =FT D?P_~kw۷0pn=}򑎛N5G0Ei_lrm[A4T{i?~AYada&x!)nZ~+ t˗P'GJfa/0tVzgƍEg"W/?@ZWUg•H"7hv=+C[hz_䦪iT)k50 +ts4)nhF 2%ёy* +qj!1mA*ejg]#c|%(@9nF3q=bA̸[ +>Mܬ?MۛۇmGuh?2+gAT׿8biNiG0Q:܅b^*磮 R H=Uђ;/ŏb =C!VQ,\~2$`lQW)ݻ(z۠ë(*ƊPXUݕPԙ邫,Bl쀫QtqRbPN#WM.9h-ejhn*H~2X}Vm*Xmv5Ye#yV)W@1=p3ׇ8--B~hG6\)zŅUBO0g2Z*#A:ov7H9Ubs)a8׃9v +"@4NG"f94 +:PZaMoq/aJhp#mP*otd 9Hn\`J;5tOwWo~?V>EPT^ZoW8юH] Ջ"tsfC`2I+,1PT"?䂨0j#UqP͠Vamr7D$7zTDkf|;b @U(zv&u+ ^S|Ax;J;{ 76.}bhFz,hOіa.xĢ)Ju;u@!p۱SY|7s?ANm7'pSS&TGOm +zapY@VT7Q}yýd*(\䔑hm)OLȑo)cQfAd 8E'? 5v)@>l +  i착僾zl,,'c䁦(/A@#[@S# ?ܰ)o*ͅy0<*G$^jR1+Їåe8.iM?pGىnn?j~βyx>rqlRд(pRfC~tEK : Y2,e3bRxJ2tlݐR[zYx"+>ҎSH~c yC-}y x&j:2y%CIhf&%s#ɧs]aR\'XWCsQRh0wGIQQRjMnEPR? %Q?'J*jFI}%~q-r5xY]=q 3ҺqT.i32kQ(.z [ƨՇ8ǭ h8m63+Vގ(+M|Iot%cpowC6mFthhCaTEgW +2(b` HE4U=2MrL hR`/i)jąnk.6gUHwZD~ bZS%&+#9&WAi\M]i%'MS*xAPRQɂ6!&諧_ðR< +YbXi1Ԧ[^A@]k=[?o=5n9ʛ,R4X*0b1gi!"vF<, +0sτG/~=/S y]="GkT_bS ׆ؕ6DŮ!*v/SKU_b@صvTŮ*v Vrip(ՏSȗ):O]ˌ t'4Ɨ]k|-9_ +4df HJy5ljg4- R2]LJ{̨3%̈a&lAvQ, B>$d/\xWĕp%h#=/[!3B,$^nOa4w{bOL%_vքڌAb])~+@3XW/eV(rVqJ? + y4gG>Na]P4KE2R' ܅ 0ji"qh+^J^e:ȉ=S-W2+X[w%H \mJ^ѪĤV,󕼬2k?/e"t 7-/+ + p&n^I +ד;ؿMI}|A,UV'7Eb%ש n^tW+*Sts]fTLW~pdV9.eQTPted2?OvdPE֊vqr7Cd$AovٗUHtCK}L]SuT +Eno]/T5_K}\EVZgv*8vȠTM|wv boO;]F'AX>XotbM䁢c]+VNo%1.eÚ  +]vVVe=7Cs2 +]4ЋQt'cW芕nĪHBWM '}dOL7OK_~ࡕ/Oˌrjeìl#B0taiҔri~_KVY#W4FU˖WoX斗C@sf\_VO߉~1/usnH}~<3\ nG_Nkz3cظgO(ptvݐӢm{(ZrZѮo%DeJgҭU|D4oiqݹ:nɗ2ϘijiG{ǯ&fO6>bqve**\tnV|4<ŭ=3 9-J.?{CN+Dim+2!.i[\$ TJiYqvL/Fd7*t=aSZDEFMRzEecĺĔvA\@S~`;#M %]t¼mP}BZBh} me8ǠPg1ː -lIoaQ*D)-}vV { "d( s$"E8ʍ.Us @¤ۑ fMfM*JپYn˰kv}>3,\jM*\$,U4t-{FJn,(CQS 3κ\NNqW+voСP3w2/04׸6 lyEB| vh9{FϧQ'L$ +6wcth'`Xwp]S}HscZ=72RLcADei&KVuM?owpDnS2߁l-i6/ ,g,ziTQ:NhoC>V%|c>D6 ]t._(/!#oR-Y[/rc>n]Eq{zFqtZk. vzFXG;J({)cUB xPk 5s=P +"ׄ(@cmהh,vޡ&/u:Y̲X7p zG%~3JU [^+&Ȯ[DWV=P"hF@}'I: +eU l)Iλ|#̼%%@Yq|ύAw[fUwhy9Mw_|3D&84:&RZg !sS"onw:im@;1A|FN/CSLܡfTqt˞'{ָ~Vh6K灒ˇd;f~c[Q @{_ =18tw?>#ײ=(L1Q {E64 +2)Z[EPug Wvȝ;/Ҽ>iιc$flf#]9)SNb|)sn>앋nWSN!]~:!7 i/%X^}rys]Nvm\HYq!S3voxq\)c ʯMLF涹o-"Ǯ4+sV#"tdtJ`̞oMŻH#q3%n}/o(ۄy#SqRaԌܵrӣC)Qfz:|\qQz{|D "}?ʦv҇8_e훇ݷ-CXV#%BjE( gm"(y{ȨxNCFxI}Q?6zve18V#q&i#4چ!#qA $ͮ:o6MAc%ZT:GUFFmϔ)9`p z(;UTv#?tX +UˁnPJ!PVEmogLXw5#Ly.E'(sB7-{FPmLf3k9GӁ{7l gIY{}">_f(rYu~>9ȋd72<ljoz/K@@CzG|xpTrI|'n،8! +C|gFPEC{RGf`%La%"Gy4>ͻHB5yfkyS 11T,Q뢸gM7S0 80 =PE {FY)5 L#^WK 1Tb GC +'W x+Jͫ6KϞwR% +&?0mFC`~c}~됎m9HJ2 'GjM` +Ebzþڃ #p&h}g7ԍXaU +Yc OץisqŻx~}d|tt'K &v& +CRCt xE +Zݥ +O_(P a69rżUmU}A]eZEIc|eOD\{h̀;˜mkrdiwͬ leB;'#!1 uiHq%.`OȆ+JXk@%.5 zt~N!(M-epI-0=>ڨR[C[)?zhS$>wYVQXsHlm٘/;q W#7_}E`t>7 ٬aRJp!7ߌ<',&3Zn9kq +s"._nH|hɇ6-׵|qOd!wYh:H @F$`C>]5W3$4mk:Tqp>F86pR]ӡʴ'{[.ju'+\:tT T`Z> BHS"#hXHf(WQt,; 6S :\gH3Uԡ IgGY4ZcH4` j& +A@xG޿f~߼mi|+ E#T"Sto?L*[zd3Ϩ동gX{4<ɨ%vmp4hڡn8Znv"m +xD6 &A3FLYjoqy3f|-ņ 1LСv3w(G͔ >__?cZEj' /K}Ydn;T1=cq"8~}uhAYz\|q<K┆![š-o38:j]:ZP&:I'Q bvWOnn(GawAo7jdzk0&A_ :#x#aѾFʡ tgVgFh㰴ش|ڧTDFʍ IZz^"y#mJ6#o8SP־QZЪ"=,Z|}?/${:HN(WxxQ$}"аtLzm;&*є+:pi +Σ-Y +FbSe(35d;ܡle*2zU§@Ep>37MfY zam>APµdXÝEeA!S fAPǛg5p5)kP8N3zFNd3/htP)TP}^DOCI:ʿ=O6 %]:_2fIFmw͇3?,(JPx;KkY1 2r'D-}&&l +/j34w@}pW hT:gOs/Y #@;Ve:91vU\@%S;Ml,ȌQT"{Xki߲}[iaFfM$ aIj^@+R/SJ/rg22^(ecE1PF[)g{pLfh(C`esFA:*]\9 "!mOS  SR6piai>g>{Hn]P +M2N ܆o4ibdY G<ķo5'sq;uP'W/ ?V)h>FmLk"\|Px^+XгUL4b1tz$TwsjWy$T V`+*$'֒^-ݯЧPz7T`)ZS9Ɩhdm\^ :η9.z$rof]Fײ=EJ`30uʽN ii6%Z drm}(zw ?zekP퓎DG +h[D2E< eID7h# YΖBY6ѐ˺P-(M I׵@bHϷ^A~Dk;MZw(CPbwwٞNw +$bHaѲeYE"UtB'uA oƉc0B]<;2p e`c=˗\ , '''e/zoE*+@q5eaRQunKBAc6G=Hv3JhU si8/%(I(# LQ{mp7 o6mWٖE[f_>2Fad`q7z65+tO$oX'1+}F5YJñx`m-x-Ę+kۍO +fl0)XCy`U2Y餓یtw}Sq%aM7lpRno8XO.q&-+}`v3jղx*[ghXx=Fo9XzGP{ݢƍu3p9ysoJ0U7e^x6g}ɼ?f=nm"~>|C{([!qΌびon[OϨ>v6 (y CF7G*_&mmNEWf)(k.Q~oB3]6dیjǗ kaִt Q-&2M³͈ơ0f3:0D}_XXAֲٓu_،*3Zoj[;qkr_XXC,a~jLZJ@/-{0j=x. +]Pb`'jT5{7I>{F5ta3n++U6 T@!6i++hmz4q&<7Bš4)^@c<_N; +*rFQ啃Ut@Օ + %9_9MUAg^W,Wm=#XA܂lK"4+q,L=rvf^9Xa +GF6bP++_5SJsS}e`1[V=`s7 O`{l1Iʓ[ Vbܯl]Z%$<(JSe\z +l7eèbAl~21ȦS8O~MZ՚zP;D]IlFnw)Q7q :Tɪ͙yEM@s) +FOu|)+~)l 0y3%] ʀLOZ[5IeQTF}XGL@W +|kwoԉ{nF)Tj#PS4S4qK[DkF0n&wf΀ȉ\RQ3`A ==)\xD\L[j^~*7` ++}\)@R,xu)]ՠ ֚qكؔW'+dي"\ +4Q':+?ϴ׈w`!HuLLJԐ/޹|zf}&:L8*0HZևQ9n_3 p;Ȏy"a-nJ {.t#-.f:?@8 {d;im|.تk-3n2΂x8m;^u/_9>wӳf8:א`M{;}n r:3 +!\w3+qomWt͊{F^“)vÈrҌ啩`ˊv '+ڌ1Z"{Wֶ3g{Y6^V9+͖N-# :r+6)jyoDj-9,a!lF/I,}A7@,EjTB .擅'>ãӑJ9fwHtRoaVX(Bzy>-gIHi6R(L}a`u/LVzau7^Xw[$9 X<,jloYWF<{FCP$};ez=|a0L צ>?"J>ŲyFn<=rmJzN.zBJ@Zt_W3kʃP%kwcӹ?zT45fz*+hWUrXp8+)3B|^ HebEN`w6tl󤾻 >]{s+ sK [Cy +*cn&ǵpqe_FԪ)S9M=sCu{)<@7eci{_7Żүވ W9 7VURny~|נ&}[eRpbOyݴ-ьwu!FĹqFhWb>sV|M* +A)DÓޭ6ai܃=fL8̀zr @݆z失H"|Oqzm7if@j? Tu +]tz:ri:ƻ7Gvw,27>k{gьvرpʱZ|m孿mxun+?hWz3zv$\*NުAU ԪBES 5& }ud7gGz>ohDaaM5 җѹ,?KAQRԾ"E͎]EʖBҦZԘЍgG"F=;b} +ƾkM\{4*;WmoyF=ϏewBӛѳo*4QpЇm2ʏph~!S#:f&r׊ZQ1V\TW\5 /[eHpwjLw^D771kWpg@O#q"cړ6%sdpf$kLZ] pp$Gz[l8cS# jW3|Ux&gť; ՘^un*_ͳX\3%W}t\LtMHّf8;i=X%6z'v #bЪ,=WuGm}:=VZfYoծd|sZg۠$wRU̚t/mj3@ڄzjr3fX782s\e>>USIMk8 +*Z:J^[UjPU} +>U-|}]kuE_2-UzDm3y]Czn6y^pxt]zzW2H^#yТy= *GeVkF:@V!<, TN!m׽x!bH c]Ѫ3U6Yk7܍$dվ;}ra>V_/ڔ7w9e^m֯9:V{)AFfR9gXcyVK`oɑ{MȖcyF{E5/"ؾ">ǒ5X5X±zɱ2ӫBorNJد+upR7Ǻԑg|bm 4Na/}pX_ {:r'UgusQw9mȦ|s[c:j=VYx֨(>_wSB쪁<vԊ=K* YDiM9juț#WGLh"7T4/rOSX`v}~pZ]{7d{?!tqLEY٭c,MVȧq=d/NUd,*hԼeo*FfZmؽ#F1z/-Bٮ^}E({6όQʞ9 tl1q hdOq˞Sȟ?'.]5}E +{^w兜-_m{P|#&%6гػ& Y4њ6v1iLjE)4JW -` +=.I&l@jW"'mDkI˫Q: Y:DNW$[Fה!ck({dolg=h]JhS{C\{83XSκp>:Jއx64}ߟۛ`ffQ\\:- +b6Vʫ^st6wlLJNJ+kfcSyތaQ3he$0Ͱ䚔bɺЇc(b?hٝWlhfkwh~7.XhhR +& /GvyP^9{yޕhm(ItWbtTHUW=m=dO}lwٍ\5JQ\nj]F=bԶ`ΛUz8O=[Ci7&>,v}9CGԳzntl˛ѳkD2uZm "Dl Ei;Imhތ6k.BePy/)>#ͧERDUxtî-γoڛWB 8FUH1ZM(C,wlLʭ7k ŅP_l# r| +V925[e]ڮ-U169\w{vYQP8`?1Ua֬"Z ڬ<\b9 $6mY0+"Oү]d;)15E|VbKm*ۻ7GfUz.}N + R:_}ʞ]+{K#B3ˈFjۻ7GnEoO5j.Q/ +ne@"tʳ=0W'ez 9rϣ*̻ < @G{7Hr;?jrBAB\mr3tӯI]UnV BxF;rU*75,NsG=&4UFNJsht>7Ymޛ;F̊FSoU~MrM5Z7M0{O<=ʳZM{ˈOM{K}mݮ{yEJ!lV%_+VP /1_g~VuZOUmLO12/-x Я+m:$O{ij'7vvʝ2˓Rgd='ًblqNK2inrݻ7GUFyd>FϮ]u Mݹz?)5q@7l}l+2U\fHuzߪwongg.v.V壾:gLNTceA\fɚHv oLkx&oVmL* 7]dWudg1-O#ŗ)bnifv`&nkԼ{SM v#I9W-oyF7f`e<9o,~Ȓ֙Xkڮ~{8_ӛ4gMI̞R۾ѢؽǮB΃|=DBN{n&Us^*lVX]xw"!!gR.w/mzB2g@C#tP >"$ZG/0EI|F4 RR*th(>KF]K|biHSFg!OLRyUbDUudڑ2vu6ja2hYĿR-@DWMACd]Ru{3ěҾ~ )%NsBicOqLtg90O D)8&iF𷺲 u982ώ8V#TdbBKf% :XejzM) +*Q*pF]^ WHxi +mo=+0>?->0>LiX[.1>ۚi3,X$'"2zUABBd0<< 7bpDŽzj*tOh|֠t{q' S|V O%-li]mXAr^nt8v8A+&&Zy?|XZMdWs|RMЮg_t|Z٢z;]u|6{5*;>[ׇͅVѳ/%Yu#F4Vߟ  9&8*-$LjbKtZ$YN?uJ}W%DŽkꃫ[I,9oԁ$`;U|Z)і"L>(񌌸+cb3Ip@{7eq\r&=LG(~-&@>?·0)o(|d"k$Nȫo(PW +nR- o޵1Io8|1yToF)W_8_8|]r:V n{!}^龎s*wn*TQ{Flo;t*hl12Aq;Gf|l/Yof-!#q#o,}2sn}M{3zrN<&d>K"/D'^'`;h +TtMș_+x>^|UuRKke;/WwB#H۶^gO+*ip-3apʞ3gwF UlﵟlzPsT }jtXHw6)#Ѹ:״˟):γ2Ge=>Et3l%gqM 妁֬suRLe֞]+?8t^Z*&I&|{1t`j4SL,Z_9R'0Iq*f@|q:DgOc;DŽ&`i올 +4񬸷QȮ$:VLϯ4VVwQWQ=пWUR 3.zȢ8`\h@&0EIgMvFEpUu2:]&f(+JN6nmGߍ6Ys) m_>( + ^P&W')z+ON1L`\%m].=-;:Zitq[2vItPV\Hwdҿ([ޮ> +l\L1M(} p&|QJGU&GM+}wڰmb黉E-E..dʮ>o}3G=]X6~Li,pΪU1cCZۆ(K`{q'ۑB!:[ 3~::'2ZZ˒ZW[يϧ^԰ik +hTtiYH>4mv v ZyWV-WڻPkBk5ޕ޻Jnq"~ytʜ+%aJػR*ϻ"t:E]aJFt-jΕ5~\%+ٹBj[R5Um~s.~`7{oAUҵ ڕy`Oc{MZs_=:m"žpRJ9PUk!DBԫ! 쒷nt"`,}f? ~kXw *m +DEP (M.F{mAIa}a  F>ul{z`:K tu"A@NSꕔxr+Ra9`]ԉ +n- +1VX^. 6q!#{*D'YZr%fryCd$q%k,Iw߻d:M߄ZWV{'_<͏de?e'+h ?(A&/Q% +endstream endobj 8 0 obj [7 0 R 6 0 R] endobj 30 0 obj <> endobj xref +0 31 +0000000000 65535 f +0000000016 00000 n +0000000156 00000 n +0000040398 00000 n +0000000000 00000 f +0000050472 00000 n +0000050100 00000 n +0000050170 00000 n +0000228217 00000 n +0000040449 00000 n +0000040888 00000 n +0000055729 00000 n +0000053045 00000 n +0000052932 00000 n +0000048917 00000 n +0000049538 00000 n +0000049586 00000 n +0000050356 00000 n +0000050387 00000 n +0000050240 00000 n +0000050271 00000 n +0000050843 00000 n +0000051159 00000 n +0000053080 00000 n +0000055803 00000 n +0000056021 00000 n +0000057345 00000 n +0000061673 00000 n +0000127262 00000 n +0000192851 00000 n +0000228246 00000 n +trailer +<<790998C9BE9E5F4B86F18795B5F904FB>]>> +startxref +228431 +%%EOF diff --git a/docs/marketing/logo/SVG/Combinationmark White Background.svg b/docs/marketing/logo/SVG/Combinationmark White Background.svg new file mode 100644 index 0000000..4d5eb14 --- /dev/null +++ b/docs/marketing/logo/SVG/Combinationmark White Background.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/marketing/logo/SVG/Combinationmark White Border.svg b/docs/marketing/logo/SVG/Combinationmark White Border.svg new file mode 100644 index 0000000..37a84a6 --- /dev/null +++ b/docs/marketing/logo/SVG/Combinationmark White Border.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/marketing/logo/SVG/Combinationmark White.svg b/docs/marketing/logo/SVG/Combinationmark White.svg new file mode 100644 index 0000000..4199e11 --- /dev/null +++ b/docs/marketing/logo/SVG/Combinationmark White.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/marketing/logo/SVG/Combinationmark.svg b/docs/marketing/logo/SVG/Combinationmark.svg new file mode 100644 index 0000000..b7bfe60 --- /dev/null +++ b/docs/marketing/logo/SVG/Combinationmark.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/marketing/logo/SVG/Logomark Full Black.svg b/docs/marketing/logo/SVG/Logomark Full Black.svg new file mode 100644 index 0000000..b59aedb --- /dev/null +++ b/docs/marketing/logo/SVG/Logomark Full Black.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/marketing/logo/SVG/Logomark Full Purple.svg b/docs/marketing/logo/SVG/Logomark Full Purple.svg new file mode 100644 index 0000000..6777fc8 --- /dev/null +++ b/docs/marketing/logo/SVG/Logomark Full Purple.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/marketing/logo/SVG/Logomark Purple.svg b/docs/marketing/logo/SVG/Logomark Purple.svg new file mode 100644 index 0000000..c9b08d1 --- /dev/null +++ b/docs/marketing/logo/SVG/Logomark Purple.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/marketing/logo/SVG/Logomark White.svg b/docs/marketing/logo/SVG/Logomark White.svg new file mode 100644 index 0000000..669ba10 --- /dev/null +++ b/docs/marketing/logo/SVG/Logomark White.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/toc.yml b/docs/toc.yml new file mode 100644 index 0000000..56c64f1 --- /dev/null +++ b/docs/toc.yml @@ -0,0 +1,13 @@ +- name: Home + href: index.md +- name: Guides + href: guides/ + topicUid: Guides.Introduction +- name: API Reference + href: api/ + topicUid: API.Docs +- name: FAQ + href: faq/ + topicUid: FAQ.Basics.GetStarted +- name: Changelog + href: ../CHANGELOG.md