diff --git a/BUILDING.md b/BUILDING.md
index 4bb2915b5..0b7e66464 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -1,74 +1,74 @@
# Building DisCatSharp
These are detailed instructions on how to build the DisCatSharp library under various environmnets.
It is recommended you have prior experience with multi-target .NET Core/Standard projects, as well as the `dotnet` CLI utility, and MSBuild.
## Requirements
In order to build the library, you will first need to install some software.
### Windows
On Windows, we only officially support Visual Studio 2019 16.10 or newer. Visual Studio Code and other IDEs might work, but are generally not supported or even guaranteed to work properly.
* **Windows 10** - while we support running the library on Windows 7 and above, we only support building on Windows 10 and better.
* [**Git for Windows**](https://git-scm.com/download/win) - required to clone the repository.
* [**Visual Studio 2021**](https://www.visualstudio.com/downloads/) - community edition or better. We do not support Visual Studio 2017 and older. Note that to build the library, you need Visual Studio 2019 version 16.10 or newer.
* **Workloads**:
* **.NET Framework Desktop** - required to build .NETFX (4.5, 4.6, and 4.7 targets)
* **.NET Core Cross-Platform Development** - required to build .NET Standard targets (1.1, 1.3, and 2.0) and the project overall.
* **Individual Components**:
* **.NET Framework 4.5 SDK** - required for .NETFX 4.5 target
* **.NET Framework 4.6 SDK** - required for .NETFX 4.6 target
* **.NET Framework 4.7 SDK** - required for .NETFX 4.7 target
* [**.NET Core SDK 3.1**](https://www.microsoft.com/net/download) - required to build the project.
* **Windows PowerShell** - required to run the build scripts. You need to make sure your script execution policy allows execution of unsigned scripts.
### GNU/Linux
On GNU/Linux, we support building via Visual Studio Code and .NET Core SDK. Other IDEs might work, but are not supported or guaranteed to work properly.
While these should apply to any modern distribution, we only test against Debian 10. Your mileage may vary.
-When installing the below, make sure you install all the dependencies properly. We might ship a build environmnent as a docker container in the future.
+When installing the below, make sure you install all the dependencies properly. We might ship a build environment as a docker container in the future.
* **Any modern GNU/Linux distribution** - like Debian 10.
* **Git** - to clone the repository.
* [**Visual Studio Code**](https://code.visualstudio.com/Download) - a recent version is required.
* **C# for Visual Studio Code (powered by OmniSharp)** - required for syntax highlighting and basic Intellisense
* [**.NET SDK 6.0**](https://www.microsoft.com/net/download) - required to build the project.
* [**Mono 5.x**](http://www.mono-project.com/download/#download-lin) - required to build the .NETFX 4.5, 4.6, and 4.7 targets, as well as to build the docs.
* [**PowerShell Core**](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-7.1) - required to execute the build scripts.
* **p7zip-full** - required to package docs.
## Instructions
Once you install all the necessary prerequisites, you can proceed to building. These instructions assume you have already cloned the repository.
### Windows
Building on Windows is relatively easy. There's 2 ways to build the project:
#### Building through Visual Studio
Building through Visual Studio yields just binaries you can use in your projects.
1. Open the solution in Visual Studio.
2. Set the configuration to Release.
3. Select Build > Build Solution to build the project.
4. Select Build > Publish DisCatSharp to publish the binaries.
#### Building with the build script
Building this way outputs NuGet packages, and a documentation package. Ensure you have an internet connection available, as the script will install programs necessary to build the documentation.
1. Open PowerShell and navigate to the directory which you cloned DisCatSharp to.
2. Execute `.\s_oneclick-rebuild-all.ps1 -configuration Release` and wait for the script to finish execution.
3. Once it's done, the artifacts will be available in *dcs-artifacts* directory, next to the directory to which the repository is cloned.
### GNU/Linux
When all necessary prerequisites are installed, you can proceed to building. There are technically 2 ways to build the library, though both of them perform the same steps, they are just invoked slightly differently.
#### Through Visual Studio Code
1. Open Visual Studio Code and open the folder to which you cloned DisCatSharp as your workspace.
2. Select Build > Run Task...
3. Select `buildRelease` task and wait for it to finish.
4. The artifacts will be placed in *dcs-artifacts* directory, next to where the repository is cloned.
#### Through PowerShell
1. Open PowerShell (`pwsh`) and navigate to the directory which you cloned DisCatSharp to.
2. Execute `.\s_oneclick-rebuild-all.ps1 -configuration Release` and wait for the script to finish execution.
3. Once it's done, the artifacts will be available in *dcs-artifacts* directory, next to the directory to which the repository is cloned.
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..bd102d549
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,26 @@
+# v10.1.0
+
+## What's Changed
+* Bump 10.0.1 by @Lulalaby in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/165
+* Proper exception log when registering app commands fails by @TheXorog in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/167
+* Fix ChannelFlagsChange Cast & NullReference by @TheXorog in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/168
+* Update DiscordGuild.cs by @OoLunar in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/171
+* Bypass message cache by @Lulalaby in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/172
+* Support for shard slash commands! by @Lulalaby in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/173
+* Stuff fixed while working on Hatsune Miku by @Lulalaby in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/174
+* Miku fix by @Lulalaby in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/175
+* Bump Microsoft.NET.Test.Sdk from 17.2.0 to 17.3.0 by @dependabot in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/181
+* Bump xunit from 2.4.1 to 2.4.2 by @dependabot in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/180
+* Implementation of missing features by @Lulalaby in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/177
+* Added Forum Tags by @TheXorog in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/182
+* Forum tag changes by @TheXorog in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/183
+* Translation Generator & Exporter by @Lulalaby in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/184
+* Fix tags initialization by @Saalvage in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/185
+* Bump Microsoft.NET.Test.Sdk from 17.3.0 to 17.3.1 by @dependabot in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/186
+* Fix webhooks for threads by @Saalvage in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/188
+* Fix Default Help if Auto Defer is enabled by @TheXorog in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/190
+* Fix threads by removing the shadow properties by @Saalvage in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/189
+* Add Avatar Decorations by @TheXorog in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/191
+* Add Theme Colors by @TheXorog in https://github.com/Aiko-IT-Systems/DisCatSharp/pull/192
+
+**Full Changelog**: https://github.com/Aiko-IT-Systems/DisCatSharp/compare/10.0.0...v10.1.0
diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
index a210b4524..71eb61fb2 100644
--- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
+++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
@@ -1,2095 +1,2094 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// 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.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using DisCatSharp.ApplicationCommands.Attributes;
using DisCatSharp.ApplicationCommands.Context;
using DisCatSharp.ApplicationCommands.Entities;
using DisCatSharp.ApplicationCommands.EventArgs;
using DisCatSharp.ApplicationCommands.Exceptions;
using DisCatSharp.ApplicationCommands.Workers;
using DisCatSharp.Common;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Exceptions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace DisCatSharp.ApplicationCommands;
///
/// A class that handles slash commands for a client.
///
public sealed class ApplicationCommandsExtension : BaseExtension
{
///
/// A list of methods for top level commands.
///
private static List s_commandMethods { get; set; } = new();
///
/// List of groups.
///
private static List s_groupCommands { get; set; } = new();
///
/// List of groups with subgroups.
///
private static List s_subGroupCommands { get; set; } = new();
///
/// List of context menus.
///
private static List s_contextMenuCommands { get; set; } = new();
///
/// List of global commands on discords backend.
///
internal static List GlobalDiscordCommands { get; set; }
///
/// List of guild commands on discords backend.
///
internal static Dictionary> GuildDiscordCommands { get; set; }
///
/// Singleton modules.
///
private static List s_singletonModules { get; set; } = new();
///
/// List of modules to register.
///
private readonly List> _updateList = new();
///
/// Configuration for Discord.
///
internal static ApplicationCommandsConfiguration Configuration;
///
/// Set to true if anything fails when registering.
///
private static bool s_errored { get; set; }
///
/// Gets a list of registered commands. The key is the guild id (null if global).
///
public IReadOnlyList>> RegisteredCommands
=> s_registeredCommands;
private static readonly List>> s_registeredCommands = new();
///
/// Gets a list of registered global commands.
///
public IReadOnlyList GlobalCommands
=> GlobalCommandsInternal;
internal static readonly List GlobalCommandsInternal = new();
///
/// Gets a list of registered guild commands mapped by guild id.
///
public IReadOnlyDictionary> GuildCommands
=> GuildCommandsInternal;
internal static readonly Dictionary> GuildCommandsInternal = new();
///
/// Gets the registration count.
///
private static int s_registrationCount { get; set; }
///
/// Gets the expected count.
///
private static int s_expectedCount { get; set; }
///
/// Gets the guild ids where the applications.commands scope is missing.
///
private IReadOnlyList _missingScopeGuildIds;
///
/// Gets whether debug is enabled.
///
internal static bool DebugEnabled { get; set; }
internal static LogLevel ApplicationCommandsLogLevel
=> DebugEnabled ? LogLevel.Debug : LogLevel.Trace;
///
/// Gets whether check through all guilds is enabled.
///
internal static bool CheckAllGuilds { get; set; }
///
/// Gets whether the registration check should be manually overridden.
///
internal static bool ManOr { get; set; }
///
/// Gets whether interactions should be automatically deffered.
///
internal static bool AutoDeferEnabled { get; set; }
///
/// Whether this module finished the startup.
///
internal bool StartupFinished { get; set; } = false;
///
/// Gets the service provider this module was configured with.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "")]
public IServiceProvider Services
=> Configuration.ServiceProvider;
///
/// Gets a list of handled interactions. Fix for double interaction execution bug.
///
internal static List HandledInteractions = new();
///
/// Initializes a new instance of the class.
///
/// The configuration.
internal ApplicationCommandsExtension(ApplicationCommandsConfiguration configuration = null)
{
Configuration = configuration;
DebugEnabled = configuration?.DebugStartup ?? false;
CheckAllGuilds = configuration?.CheckAllGuilds ?? false;
ManOr = configuration?.ManualOverride ?? false;
AutoDeferEnabled = configuration?.AutoDefer ?? false;
}
///
/// Runs setup.
/// DO NOT RUN THIS MANUALLY. DO NOT DO ANYTHING WITH THIS.
///
/// The client to setup on.
protected internal override void Setup(DiscordClient client)
{
if (this.Client != null)
throw new InvalidOperationException("What did I tell you?");
this.Client = client;
this._slashError = new AsyncEvent("SLASHCOMMAND_ERRORED", TimeSpan.Zero, null);
this._slashExecuted = new AsyncEvent("SLASHCOMMAND_EXECUTED", TimeSpan.Zero, null);
this._contextMenuErrored = new AsyncEvent("CONTEXTMENU_ERRORED", TimeSpan.Zero, null);
this._contextMenuExecuted = new AsyncEvent("CONTEXTMENU_EXECUTED", TimeSpan.Zero, null);
this._applicationCommandsModuleReady = new AsyncEvent("APPLICATION_COMMANDS_MODULE_READY", TimeSpan.Zero, null);
this._applicationCommandsModuleStartupFinished = new AsyncEvent("APPLICATION_COMMANDS_MODULE_STARTUP_FINISHED", TimeSpan.Zero, null);
this._globalApplicationCommandsRegistered = new AsyncEvent("GLOBAL_COMMANDS_REGISTERED", TimeSpan.Zero, null);
this._guildApplicationCommandsRegistered = new AsyncEvent("GUILD_COMMANDS_REGISTERED", TimeSpan.Zero, null);
this.Client.GuildDownloadCompleted += async (c, e) => await this.UpdateAsync();
this.Client.InteractionCreated += this.CatchInteractionsOnStartup;
this.Client.ContextMenuInteractionCreated += this.CatchContextMenuInteractionsOnStartup;
}
private async Task CatchInteractionsOnStartup(DiscordClient sender, InteractionCreateEventArgs e)
{
if (!this.StartupFinished)
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Application commands are unavailable for now."));
else
await Task.Delay(1);
}
private async Task CatchContextMenuInteractionsOnStartup(DiscordClient sender, ContextMenuInteractionCreateEventArgs e)
{
if (!this.StartupFinished)
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Context menu commands are unavailable for now."));
else
await Task.Delay(1);
}
private void FinishedRegistration()
{
this.Client.InteractionCreated -= this.CatchInteractionsOnStartup;
this.Client.ContextMenuInteractionCreated -= this.CatchContextMenuInteractionsOnStartup;
this.StartupFinished = true;
this.Client.InteractionCreated += this.InteractionHandler;
this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler;
}
///
/// Cleans the module for a new start of the bot.
/// DO NOT USE IF YOU DON'T KNOW WHAT IT DOES.
///
public void CleanModule()
{
this._updateList.Clear();
s_singletonModules.Clear();
s_errored = false;
s_expectedCount = 0;
s_registrationCount = 0;
s_commandMethods.Clear();
s_groupCommands.Clear();
s_contextMenuCommands.Clear();
s_subGroupCommands.Clear();
s_singletonModules.Clear();
s_registeredCommands.Clear();
GlobalCommandsInternal.Clear();
GuildCommandsInternal.Clear();
}
///
/// Cleans all guild application commands.
/// You normally don't need to execute it.
///
internal async Task CleanGuildCommandsAsync()
{
foreach (var guild in this.Client.Guilds.Values)
await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guild.Id, Array.Empty());
}
///
/// Cleans the global application commands.
/// You normally don't need to execute it.
///
internal async Task CleanGlobalCommandsAsync()
=> await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty());
///
/// Registers a command class with optional translation setup for a guild.
///
/// The command class to register.
/// The guild id to register it on.
/// A callback to setup translations with.
public void RegisterGuildCommands(ulong guildId, Action translationSetup = null) where T : ApplicationCommandsModule
=> this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup)));
-
///
/// Registers a command class with optional translation setup for a guild.
///
/// The of the command class to register.
/// The guild id to register it on.
/// A callback to setup translations with.
public void RegisterGuildCommands(Type type, ulong guildId, Action translationSetup = null)
{
if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type))
throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type));
this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type, translationSetup)));
}
///
/// Registers a command class with optional translation setup globally.
///
/// The command class to register.
/// A callback to setup translations with.
public void RegisterGlobalCommands(Action translationSetup = null) where T : ApplicationCommandsModule
=> this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup)));
///
/// Registers a command class with optional translation setup globally.
///
/// The of the command class to register.
/// A callback to setup translations with.
public void RegisterGlobalCommands(Type type, Action translationSetup = null)
{
if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type))
throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type));
this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, translationSetup)));
}
///
/// Fired when the application commands module is ready.
///
public event AsyncEventHandler ApplicationCommandsModuleReady
{
add => this._applicationCommandsModuleReady.Register(value);
remove => this._applicationCommandsModuleReady.Unregister(value);
}
private AsyncEvent _applicationCommandsModuleReady;
///
/// Fired when the application commands modules startup is finished.
///
public event AsyncEventHandler ApplicationCommandsModuleStartupFinished
{
add => this._applicationCommandsModuleStartupFinished.Register(value);
remove => this._applicationCommandsModuleStartupFinished.Unregister(value);
}
private AsyncEvent _applicationCommandsModuleStartupFinished;
///
/// Fired when guild commands are registered on a guild.
///
public event AsyncEventHandler GuildApplicationCommandsRegistered
{
add => this._guildApplicationCommandsRegistered.Register(value);
remove => this._guildApplicationCommandsRegistered.Unregister(value);
}
private AsyncEvent _guildApplicationCommandsRegistered;
///
/// Fired when the global commands are registered.
///
public event AsyncEventHandler GlobalApplicationCommandsRegistered
{
add => this._globalApplicationCommandsRegistered.Register(value);
remove => this._globalApplicationCommandsRegistered.Unregister(value);
}
private AsyncEvent _globalApplicationCommandsRegistered;
///
/// Used for RegisterCommands and the event.
///
internal async Task UpdateAsync()
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Request to register commands on shard {shard}", this.Client.ShardId);
if (this.StartupFinished)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Shard {shard} already setup, skipping", this.Client.ShardId);
this.FinishedRegistration();
return;
}
GlobalDiscordCommands = new();
GuildDiscordCommands = new();
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Expected Count: {count}", s_expectedCount);
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Shard {shard} has {guilds} guilds.", this.Client.ShardId, this.Client.Guilds?.Count);
List failedGuilds = new();
List globalCommands = null;
globalCommands = (await this.Client.GetGlobalApplicationCommandsAsync(Configuration?.EnableLocalization ?? false)).ToList() ?? null;
var updateList = this._updateList;
var guilds = CheckAllGuilds ? this.Client.Guilds?.Keys.ToList() : updateList.Where(x => x.Key != null)?.Select(x => x.Key.Value).Distinct().ToList();
var wrongShards = guilds.Where(x => !this.Client.Guilds.ContainsKey(x)).ToList();
if (wrongShards.Any())
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Some guilds are not on the same shard as the client. Removing them from the update list.");
foreach (var guild in wrongShards)
{
updateList.RemoveAll(x => x.Key == guild);
guilds.Remove(guild);
}
}
var commandsPending = updateList.Select(x => x.Key).Distinct().ToList();
s_expectedCount = commandsPending.Count;
foreach (var guild in guilds)
{
List commands = null;
var unauthorized = false;
try
{
commands = (await this.Client.GetGuildApplicationCommandsAsync(guild, Configuration?.EnableLocalization ?? false)).ToList() ?? null;
}
catch (UnauthorizedException)
{
unauthorized = true;
}
finally
{
if (!unauthorized && commands != null && commands.Any())
GuildDiscordCommands.Add(guild, commands.ToList());
else if (unauthorized)
failedGuilds.Add(guild);
}
}
//Default should be to add the help and slash commands can be added without setting any configuration
//so this should still add the default help
if (Configuration is null || (Configuration is not null && Configuration.EnableDefaultHelp))
{
updateList.Add(new KeyValuePair
(null, new ApplicationCommandsModuleConfiguration(typeof(DefaultHelpModule))));
commandsPending = updateList.Select(x => x.Key).Distinct().ToList();
}
if (globalCommands != null && globalCommands.Any())
GlobalDiscordCommands.AddRange(globalCommands);
foreach (var key in commandsPending)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, key.HasValue ? $"Registering commands in guild {key.Value}" : "Registering global commands.");
if (key.HasValue)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Found guild {guild} in shard {shard}!", key.Value, this.Client.ShardId);
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Registering");
}
await this.RegisterCommands(updateList.Where(x => x.Key == key).Select(x => x.Value).ToList(), key);
}
this._missingScopeGuildIds = failedGuilds;
await this._applicationCommandsModuleReady.InvokeAsync(this, new ApplicationCommandsModuleReadyEventArgs(Configuration?.ServiceProvider)
{
GuildsWithoutScope = failedGuilds
});
this.Client.GuildDownloadCompleted -= async (c, e) => await this.UpdateAsync();
}
///
/// Method for registering commands for a target from modules.
///
/// The types.
/// The optional guild id.
private async Task RegisterCommands(List types, ulong? guildId)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Registering commands on shard {shard}", this.Client.ShardId);
//Initialize empty lists to be added to the global ones at the end
var commandMethods = new List();
var groupCommands = new List();
var subGroupCommands = new List();
var contextMenuCommands = new List();
var updateList = new List();
var commandTypeSources = new List>();
var groupTranslation = new List();
var translation = new List();
//Iterates over all the modules
foreach (var config in types)
{
var type = config.Type;
try
{
var module = type.GetTypeInfo();
var classes = new List();
var ctx = new ApplicationCommandsTranslationContext(type, module.FullName);
config.Translations?.Invoke(ctx);
//Add module to classes list if it's a group
var extremeNestedGroup = false;
if (module.GetCustomAttribute() != null)
{
classes.Add(module);
}
else if (module.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).Any(x => x.IsDefined(typeof(SlashCommandGroupAttribute))))
{
//Otherwise add the extreme nested groups
classes = module.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)
.Where(x => x.IsDefined(typeof(SlashCommandGroupAttribute)))
.Select(x => module.GetNestedType(x.Name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).GetTypeInfo()).ToList();
extremeNestedGroup = true;
}
else
{
//Otherwise add the nested groups
classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList();
}
if (module.GetCustomAttribute() != null || extremeNestedGroup)
{
List groupTranslations = null;
if (!string.IsNullOrEmpty(ctx.Translations))
{
groupTranslations = JsonConvert.DeserializeObject>(ctx.Translations);
}
var slashGroupsTuple = await NestedCommandWorker.ParseSlashGroupsAsync(type, classes, guildId, groupTranslations);
if (slashGroupsTuple.applicationCommands != null && slashGroupsTuple.applicationCommands.Any())
{
updateList.AddRange(slashGroupsTuple.applicationCommands);
if (Configuration.GenerateTranslationFilesOnly)
{
var cgwsgs = new List();
var cgs2 = new List();
foreach (var cmd in slashGroupsTuple.applicationCommands)
{
if (cmd.Type == ApplicationCommandType.ChatInput)
{
if (cmd.Options.First().Type == ApplicationCommandOptionType.SubCommandGroup)
{
var cgs = new List();
foreach (var scg in cmd.Options)
{
var cs = new List();
foreach (var sc in scg.Options)
{
if (sc.Options == null || !sc.Options.Any())
cs.Add(new Command(sc.Name, sc.Description, null, null));
else
cs.Add(new Command(sc.Name, sc.Description, sc.Options.ToList(), null));
}
cgs.Add(new CommandGroup(scg.Name, scg.Description, cs, null));
}
cgwsgs.Add(new CommandGroupWithSubGroups(cmd.Name, cmd.Description, cgs, ApplicationCommandType.ChatInput));
}
else if (cmd.Options.First().Type == ApplicationCommandOptionType.SubCommand)
{
var cs2 = new List();
foreach (var sc2 in cmd.Options)
{
if (sc2.Options == null || !sc2.Options.Any())
cs2.Add(new Command(sc2.Name, sc2.Description, null, null));
else
cs2.Add(new Command(sc2.Name, sc2.Description, sc2.Options.ToList(), null));
}
cgs2.Add(new CommandGroup(cmd.Name, cmd.Description, cs2, ApplicationCommandType.ChatInput));
}
}
}
if (cgwsgs.Any())
foreach (var cgwsg in cgwsgs)
groupTranslation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(cgwsg)));
if (cgs2.Any())
foreach (var cg2 in cgs2)
groupTranslation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(cg2)));
}
}
if (slashGroupsTuple.commandTypeSources != null && slashGroupsTuple.commandTypeSources.Any())
commandTypeSources.AddRange(slashGroupsTuple.commandTypeSources);
if (slashGroupsTuple.singletonModules != null && slashGroupsTuple.singletonModules.Any())
s_singletonModules.AddRange(slashGroupsTuple.singletonModules);
if (slashGroupsTuple.groupCommands != null && slashGroupsTuple.groupCommands.Any())
groupCommands.AddRange(slashGroupsTuple.groupCommands);
if (slashGroupsTuple.subGroupCommands != null && slashGroupsTuple.subGroupCommands.Any())
subGroupCommands.AddRange(slashGroupsTuple.subGroupCommands);
}
//Handles methods and context menus, only if the module isn't a group itself
if (module.GetCustomAttribute() == null)
{
List commandTranslations = null;
if (!string.IsNullOrEmpty(ctx.Translations))
{
commandTranslations = JsonConvert.DeserializeObject>(ctx.Translations);
}
//Slash commands
var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null);
var slashCommands = await CommandWorker.ParseBasicSlashCommandsAsync(type, methods, guildId, commandTranslations);
if (slashCommands.applicationCommands != null && slashCommands.applicationCommands.Any())
{
updateList.AddRange(slashCommands.applicationCommands);
if (Configuration.GenerateTranslationFilesOnly)
{
var cs = new List();
foreach (var cmd in slashCommands.applicationCommands)
if (cmd.Type == ApplicationCommandType.ChatInput && (cmd.Options == null || !cmd.Options.Any(x => x.Type == ApplicationCommandOptionType.SubCommand || x.Type == ApplicationCommandOptionType.SubCommandGroup)))
{
if (cmd.Options == null || !cmd.Options.Any())
cs.Add(new Command(cmd.Name, cmd.Description, null, ApplicationCommandType.ChatInput));
else
cs.Add(new Command(cmd.Name, cmd.Description, cmd.Options.ToList(), ApplicationCommandType.ChatInput));
}
if (cs.Any())
foreach (var c in cs)
translation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c)));
}
}
if (slashCommands.commandTypeSources != null && slashCommands.commandTypeSources.Any())
commandTypeSources.AddRange(slashCommands.commandTypeSources);
if (slashCommands.commandMethods != null && slashCommands.commandMethods.Any())
commandMethods.AddRange(slashCommands.commandMethods);
//Context Menus
var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null);
var contextCommands = await CommandWorker.ParseContextMenuCommands(type, contextMethods, commandTranslations);
if (contextCommands.applicationCommands != null && contextCommands.applicationCommands.Any())
{
updateList.AddRange(contextCommands.applicationCommands);
if (Configuration.GenerateTranslationFilesOnly)
{
var cs = new List();
foreach (var cmd in contextCommands.applicationCommands)
if (cmd.Type == ApplicationCommandType.Message || cmd.Type == ApplicationCommandType.User)
cs.Add(new Command(cmd.Name, null, null, cmd.Type));
if (cs.Any())
foreach (var c in cs)
translation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c)));
}
}
if (contextCommands.commandTypeSources != null && contextCommands.commandTypeSources.Any())
commandTypeSources.AddRange(contextCommands.commandTypeSources);
if (contextCommands.contextMenuCommands != null && contextCommands.contextMenuCommands.Any())
contextMenuCommands.AddRange(contextCommands.contextMenuCommands);
//Accounts for lifespans
if (module.GetCustomAttribute() != null && module.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton)
s_singletonModules.Add(CreateInstance(module, Configuration?.ServiceProvider));
}
}
catch (Exception ex)
{
if (ex is BadRequestException brex)
{
this.Client.Logger.LogCritical(brex, @"There was an error registering application commands: {res}", brex.WebResponse.Response);
}
else
{
if (ex.InnerException is not null && ex.InnerException is BadRequestException brex1)
this.Client.Logger.LogCritical(brex1, @"There was an error registering application commands: {res}", brex1.WebResponse.Response);
else
this.Client.Logger.LogCritical(ex, @"There was an error parsing the application commands");
}
s_errored = true;
}
}
if (!s_errored)
{
updateList = updateList.DistinctBy(x => x.Name).ToList();
if (Configuration.GenerateTranslationFilesOnly)
{
s_registrationCount++;
this.CheckRegistrationStartup(ManOr, translation, groupTranslation);
}
else
{
try
{
List commands = new();
try
{
if (guildId == null)
{
if (updateList != null && updateList.Any())
{
var regCommands = await RegistrationWorker.RegisterGlobalCommandsAsync(this.Client, updateList);
var actualCommands = regCommands.Distinct().ToList();
commands.AddRange(actualCommands);
GlobalCommandsInternal.AddRange(actualCommands);
}
else
{
foreach (var cmd in GlobalDiscordCommands)
{
try
{
await this.Client.DeleteGlobalApplicationCommandAsync(cmd.Id);
}
catch (NotFoundException)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Could not delete global command {cmdId}. Please clean up manually", cmd.Id);
}
}
}
}
else
{
if (updateList != null && updateList.Any())
{
var regCommands = await RegistrationWorker.RegisterGuildCommandsAsync(this.Client, guildId.Value, updateList);
var actualCommands = regCommands.Distinct().ToList();
commands.AddRange(actualCommands);
GuildCommandsInternal.Add(guildId.Value, actualCommands);
/*
if (client.Guilds.TryGetValue(guildId.Value, out var guild))
{
guild.InternalRegisteredApplicationCommands = new();
guild.InternalRegisteredApplicationCommands.AddRange(actualCommands);
}
*/
}
else
{
foreach (var cmd in GuildDiscordCommands.First(x => x.Key == guildId.Value).Value)
{
try
{
await this.Client.DeleteGuildApplicationCommandAsync(guildId.Value, cmd.Id);
}
catch (NotFoundException)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Could not delete guild command {cmdId} in guild {guildId}. Please clean up manually", cmd.Id, guildId.Value);
}
}
}
}
}
catch (UnauthorizedException ex)
{
this.Client.Logger.LogError("Could not register application commands for guild {guildId}.\nError: {exc}", guildId, ex.JsonMessage);
return;
}
//Creates a guild command if a guild id is specified, otherwise global
//Checks against the ids and adds them to the command method lists
foreach (var command in commands)
{
if (commandMethods.GetFirstValueWhere(x => x.Name == command.Name, out var com))
com.CommandId = command.Id;
else if (groupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var groupCom))
groupCom.CommandId = command.Id;
else if (subGroupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var subCom))
subCom.CommandId = command.Id;
else if (contextMenuCommands.GetFirstValueWhere(x => x.Name == command.Name, out var cmCom))
cmCom.CommandId = command.Id;
}
//Adds to the global lists finally
s_commandMethods.AddRange(commandMethods.DistinctBy(x => x.Name));
s_groupCommands.AddRange(groupCommands.DistinctBy(x => x.Name));
s_subGroupCommands.AddRange(subGroupCommands.DistinctBy(x => x.Name));
s_contextMenuCommands.AddRange(contextMenuCommands.DistinctBy(x => x.Name));
s_registeredCommands.Add(new KeyValuePair>(guildId, commands.ToList()));
foreach (var command in commandMethods)
{
var app = types.First(t => t.Type == command.Method.DeclaringType);
}
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Expected Count: {exp}\nCurrent Count: {cur}", s_expectedCount, s_registrationCount);
if (guildId.HasValue)
{
await this._guildApplicationCommandsRegistered.InvokeAsync(this, new GuildApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider)
{
Handled = true,
GuildId = guildId.Value,
RegisteredCommands = GuildCommandsInternal.Any(c => c.Key == guildId.Value) ? GuildCommandsInternal.FirstOrDefault(c => c.Key == guildId.Value).Value : null
});
}
else
{
await this._globalApplicationCommandsRegistered.InvokeAsync(this, new GlobalApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider)
{
Handled = true,
RegisteredCommands = GlobalCommandsInternal
});
}
s_registrationCount++;
this.CheckRegistrationStartup(ManOr);
}
catch (Exception ex)
{
if (ex is BadRequestException brex)
{
this.Client.Logger.LogCritical(brex, @"There was an error registering application commands: {res}", brex.WebResponse.Response);
}
else
{
if (ex.InnerException is not null && ex.InnerException is BadRequestException brex1)
this.Client.Logger.LogCritical(brex1, @"There was an error registering application commands: {res}", brex1.WebResponse.Response);
else
this.Client.Logger.LogCritical(ex, @"There was an general error registering application commands");
}
s_errored = true;
}
}
}
}
private async void CheckRegistrationStartup(bool man = false, List translation = null, List groupTranslation = null)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Checking counts...\n\nExpected Count: {exp}\nCurrent Count: {cur}", s_expectedCount, s_registrationCount);
if ((s_registrationCount == s_expectedCount) || man)
{
await this._applicationCommandsModuleStartupFinished.InvokeAsync(this, new ApplicationCommandsModuleStartupFinishedEventArgs(Configuration?.ServiceProvider)
{
Handled = true,
RegisteredGlobalCommands = GlobalCommandsInternal,
RegisteredGuildCommands = GuildCommandsInternal,
GuildsWithoutScope = this._missingScopeGuildIds
});
if (Configuration.GenerateTranslationFilesOnly)
{
try
{
if (translation != null && translation.Any())
{
var file_name = $"translation_generator_export-shard{this.Client.ShardId}-SINGLE-{s_registrationCount}_of_{s_expectedCount}.json";
var fs = File.Create(file_name);
var ms = new MemoryStream();
var writer = new StreamWriter(ms);
await writer.WriteAsync(JsonConvert.SerializeObject(translation.DistinctBy(x => x.Name), Formatting.Indented));
await writer.FlushAsync();
ms.Position = 0;
await ms.CopyToAsync(fs);
await fs.FlushAsync();
fs.Close();
await fs.DisposeAsync();
ms.Close();
await ms.DisposeAsync();
this.Client.Logger.LogInformation("Exported base translation to {exppath}", file_name);
}
if (groupTranslation != null && groupTranslation.Any())
{
var file_name = $"translation_generator_export-shard{this.Client.ShardId}-GROUP-{s_registrationCount}_of_{s_expectedCount}.json";
var fs = File.Create(file_name);
var ms = new MemoryStream();
var writer = new StreamWriter(ms);
await writer.WriteAsync(JsonConvert.SerializeObject(groupTranslation.DistinctBy(x => x.Name), Formatting.Indented));
await writer.FlushAsync();
ms.Position = 0;
await ms.CopyToAsync(fs);
await fs.FlushAsync();
fs.Close();
await fs.DisposeAsync();
ms.Close();
await ms.DisposeAsync();
this.Client.Logger.LogInformation("Exported base translation to {exppath}", file_name);
}
}
catch (Exception ex)
{
this.Client.Logger.LogError(@"{msg}", ex.Message);
this.Client.Logger.LogError(@"{stack}", ex.StackTrace);
}
this.FinishedRegistration();
await this.Client.DisconnectAsync();
}
else
{
this.FinishedRegistration();
}
}
}
///
/// Interaction handler.
///
/// The client.
/// The event args.
private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Got slash interaction on shard {shard}", this.Client.ShardId);
if (HandledInteractions.Contains(e.Interaction.Id))
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Ignoring, already received");
return Task.FromResult(true);
}
else
HandledInteractions.Add(e.Interaction.Id);
_ = Task.Run(async () =>
{
if (e.Interaction.Type == InteractionType.ApplicationCommand)
{
//Creates the context
var context = new InteractionContext
{
Interaction = e.Interaction,
Channel = e.Interaction.Channel,
Guild = e.Interaction.Guild,
User = e.Interaction.User,
Client = client,
ApplicationCommandsExtension = this,
CommandName = e.Interaction.Data.Name,
InteractionId = e.Interaction.Id,
Token = e.Interaction.Token,
Services = Configuration?.ServiceProvider,
ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList(),
ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList(),
ResolvedChannelMentions = e.Interaction.Data.Resolved?.Channels?.Values.ToList(),
ResolvedAttachments = e.Interaction.Data.Resolved?.Attachments?.Values.ToList(),
Type = ApplicationCommandType.ChatInput,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale,
AppPermissions = e.Interaction.AppPermissions
};
try
{
if (s_errored)
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Application commands failed to register properly on startup."));
throw new InvalidOperationException("Application commands failed to register properly on startup.");
}
var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id);
var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
var subgroups = s_subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
if (!methods.Any() && !groups.Any() && !subgroups.Any())
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("A application command was executed, but no command was registered for it."));
throw new InvalidOperationException("A application command was executed, but no command was registered for it.");
}
if (methods.Any())
{
var method = methods.First().Method;
this.Client.Logger.LogDebug("Executing {cmd}", method.Name);
var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options);
await this.RunCommandAsync(context, method, args);
}
else if (groups.Any())
{
var command = e.Interaction.Data.Options[0];
var method = groups.First().Methods.First(x => x.Key == command.Name).Value;
this.Client.Logger.LogDebug("Executing {cmd}", method.Name);
var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options[0].Options);
await this.RunCommandAsync(context, method, args);
}
else if (subgroups.Any())
{
var command = e.Interaction.Data.Options[0];
var group = subgroups.First().SubCommands.First(x => x.Name == command.Name);
var method = group.Methods.First(x => x.Key == command.Options.First().Name).Value;
this.Client.Logger.LogDebug("Executing {cmd}", method.Name);
var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options[0].Options.First().Options);
await this.RunCommandAsync(context, method, args);
}
await this._slashExecuted.InvokeAsync(this, new SlashCommandExecutedEventArgs(this.Client.ServiceProvider) { Context = context });
}
catch (Exception ex)
{
await this._slashError.InvokeAsync(this, new SlashCommandErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex });
}
}
else if (e.Interaction.Type == InteractionType.AutoComplete)
{
if (s_errored)
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Application commands failed to register properly on startup."));
throw new InvalidOperationException("Application commands failed to register properly on startup.");
}
var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id);
var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
var subgroups = s_subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
if (!methods.Any() && !groups.Any() && !subgroups.Any())
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("An autocomplete interaction was created, but no command was registered for it"));
throw new InvalidOperationException("An autocomplete interaction was created, but no command was registered for it");
}
try
{
if (methods.Any())
{
var focusedOption = e.Interaction.Data.Options.First(o => o.Focused);
var method = methods.First().Method;
var option = method.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name);
var provider = option.GetCustomAttribute().ProviderType;
var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider));
var providerInstance = Activator.CreateInstance(provider);
var context = new AutocompleteContext
{
Interaction = e.Interaction,
Client = client,
Services = Configuration?.ServiceProvider,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
Channel = e.Interaction.Channel,
User = e.Interaction.User,
Options = e.Interaction.Data.Options.ToList(),
FocusedOption = focusedOption,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale,
AppPermissions = e.Interaction.AppPermissions
};
var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context });
await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices));
}
else if (groups.Any())
{
var command = e.Interaction.Data.Options[0];
var group = groups.First().Methods.First(x => x.Key == command.Name).Value;
var focusedOption = command.Options.First(o => o.Focused);
var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name);
var provider = option.GetCustomAttribute().ProviderType;
var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider));
var providerInstance = Activator.CreateInstance(provider);
var context = new AutocompleteContext
{
Client = client,
Interaction = e.Interaction,
Services = Configuration?.ServiceProvider,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
Channel = e.Interaction.Channel,
User = e.Interaction.User,
Options = command.Options.ToList(),
FocusedOption = focusedOption,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale,
AppPermissions = e.Interaction.AppPermissions
};
var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context });
await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices));
}
else if (subgroups.Any())
{
var command = e.Interaction.Data.Options[0];
var group = subgroups.First().SubCommands.First(x => x.Name == command.Name).Methods.First(x => x.Key == command.Options.First().Name).Value;
var focusedOption = command.Options.First().Options.First(o => o.Focused);
var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name);
var provider = option.GetCustomAttribute().ProviderType;
var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider));
var providerInstance = Activator.CreateInstance(provider);
var context = new AutocompleteContext
{
Client = client,
Interaction = e.Interaction,
Services = Configuration?.ServiceProvider,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
Channel = e.Interaction.Channel,
User = e.Interaction.User,
Options = command.Options.First().Options.ToList(),
FocusedOption = focusedOption,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale,
AppPermissions = e.Interaction.AppPermissions
};
var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context });
await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices));
}
}
catch (Exception ex)
{
this.Client.Logger.LogError(ex, "Error in autocomplete interaction");
}
}
});
return Task.CompletedTask;
}
///
/// Context menu handler.
///
/// The client.
/// The event args.
private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreateEventArgs e)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Got context menu interaction on shard {shard}", this.Client.ShardId);
if (HandledInteractions.Contains(e.Interaction.Id))
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Ignoring, already received");
return Task.FromResult(true);
}
else
HandledInteractions.Add(e.Interaction.Id);
_ = Task.Run(async () =>
{
//Creates the context
var context = new ContextMenuContext
{
Interaction = e.Interaction,
Channel = e.Interaction.Channel,
Client = client,
Services = Configuration?.ServiceProvider,
CommandName = e.Interaction.Data.Name,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
InteractionId = e.Interaction.Id,
User = e.Interaction.User,
Token = e.Interaction.Token,
TargetUser = e.TargetUser,
TargetMessage = e.TargetMessage,
Type = e.Type,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale,
AppPermissions = e.Interaction.AppPermissions
};
try
{
if (s_errored)
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Context menus failed to register properly on startup."));
throw new InvalidOperationException("Context menus failed to register properly on startup.");
}
//Gets the method for the command
var method = s_contextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id);
if (method == null)
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("A context menu command was executed, but no command was registered for it."));
throw new InvalidOperationException("A context menu command was executed, but no command was registered for it.");
}
await this.RunCommandAsync(context, method.Method, new[] { context });
await this._contextMenuExecuted.InvokeAsync(this, new ContextMenuExecutedEventArgs(this.Client.ServiceProvider) { Context = context });
}
catch (Exception ex)
{
await this._contextMenuErrored.InvokeAsync(this, new ContextMenuErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex });
}
});
return Task.CompletedTask;
}
///
/// Runs a command.
///
/// The base context.
/// The method info.
/// The arguments.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "")]
internal async Task RunCommandAsync(BaseContext context, MethodInfo method, IEnumerable args)
{
object classInstance;
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Executing {cmd}", method.Name);
//Accounts for lifespans
var moduleLifespan = (method.DeclaringType.GetCustomAttribute() != null ? method.DeclaringType.GetCustomAttribute()?.Lifespan : ApplicationCommandModuleLifespan.Transient) ?? ApplicationCommandModuleLifespan.Transient;
switch (moduleLifespan)
{
case ApplicationCommandModuleLifespan.Scoped:
//Accounts for static methods and adds DI
classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider.CreateScope().ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider.CreateScope().ServiceProvider);
break;
case ApplicationCommandModuleLifespan.Transient:
//Accounts for static methods and adds DI
classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider);
break;
//If singleton, gets it from the singleton list
case ApplicationCommandModuleLifespan.Singleton:
classInstance = s_singletonModules.First(x => ReferenceEquals(x.GetType(), method.DeclaringType));
break;
default:
throw new Exception($"An unknown {nameof(ApplicationCommandModuleLifespanAttribute)} scope was specified on command {context.CommandName}");
}
ApplicationCommandsModule module = null;
if (classInstance is ApplicationCommandsModule mod)
module = mod;
// Slash commands
if (context is InteractionContext slashContext)
{
await this.RunPreexecutionChecksAsync(method, slashContext);
var shouldExecute = await (module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true));
if (shouldExecute)
{
if (AutoDeferEnabled)
await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource);
await (Task)method.Invoke(classInstance, args.ToArray());
await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask);
}
}
// Context menus
if (context is ContextMenuContext contextMenuContext)
{
await this.RunPreexecutionChecksAsync(method, contextMenuContext);
var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(contextMenuContext) ?? Task.FromResult(true));
if (shouldExecute)
{
if (AutoDeferEnabled)
await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource);
await (Task)method.Invoke(classInstance, args.ToArray());
await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask);
}
}
}
///
/// Property injection
///
/// The type.
/// The services.
internal static object CreateInstance(Type t, IServiceProvider services)
{
var ti = t.GetTypeInfo();
var constructors = ti.DeclaredConstructors
.Where(xci => xci.IsPublic)
.ToArray();
if (constructors.Length != 1)
throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor.");
var constructor = constructors[0];
var constructorArgs = constructor.GetParameters();
var args = new object[constructorArgs.Length];
if (constructorArgs.Length != 0 && services == null)
throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors.");
// inject via constructor
if (constructorArgs.Length != 0)
for (var i = 0; i < args.Length; i++)
args[i] = services.GetRequiredService(constructorArgs[i].ParameterType);
var moduleInstance = Activator.CreateInstance(t, args);
// inject into properties
var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic);
foreach (var prop in props)
{
if (prop.GetCustomAttribute() != null)
continue;
var service = services.GetService(prop.PropertyType);
if (service == null)
continue;
prop.SetValue(moduleInstance, service);
}
// inject into fields
var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic);
foreach (var field in fields)
{
if (field.GetCustomAttribute() != null)
continue;
var service = services.GetService(field.FieldType);
if (service == null)
continue;
field.SetValue(moduleInstance, service);
}
return moduleInstance;
}
///
/// Resolves the slash command parameters.
///
/// The event arguments.
/// The interaction context.
/// The method info.
/// The options.
private async Task> ResolveInteractionCommandParameters(InteractionCreateEventArgs e, InteractionContext context, MethodInfo method, IEnumerable options)
{
var args = new List { context };
var parameters = method.GetParameters().Skip(1);
foreach (var parameter in parameters)
{
//Accounts for optional arguments without values given
if (parameter.IsOptional && (options == null || (!options?.Any(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()) ?? true)))
args.Add(parameter.DefaultValue);
else
{
var option = options.Single(x => x.Name == parameter.GetCustomAttribute().Name.ToLower());
if (parameter.ParameterType == typeof(string))
args.Add(option.Value.ToString());
else if (parameter.ParameterType.IsEnum)
args.Add(Enum.Parse(parameter.ParameterType, (string)option.Value));
else if (parameter.ParameterType == typeof(ulong) || parameter.ParameterType == typeof(ulong?))
args.Add((ulong?)option.Value);
else if (parameter.ParameterType == typeof(int) || parameter.ParameterType == typeof(int?))
args.Add((int?)option.Value);
else if (parameter.ParameterType == typeof(long) || parameter.ParameterType == typeof(long?))
if (option.Value == null)
args.Add(null);
else
args.Add(Convert.ToInt64(option.Value));
else if (parameter.ParameterType == typeof(bool) || parameter.ParameterType == typeof(bool?))
args.Add((bool?)option.Value);
else if (parameter.ParameterType == typeof(double) || parameter.ParameterType == typeof(double?))
args.Add((double?)option.Value);
else if (parameter.ParameterType == typeof(int) || parameter.ParameterType == typeof(int?))
args.Add((int?)option.Value);
else if (parameter.ParameterType == typeof(DiscordAttachment))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Attachments != null &&
e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment))
args.Add(attachment);
else
args.Add(new DiscordAttachment() { Id = (ulong)option.Value, Discord = this.Client.ApiClient.Discord });
}
else if (parameter.ParameterType == typeof(DiscordUser))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Members != null &&
e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member))
args.Add(member);
else if (e.Interaction.Data.Resolved.Users != null &&
e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user))
args.Add(user);
else
args.Add(await this.Client.GetUserAsync((ulong)option.Value));
}
else if (parameter.ParameterType == typeof(DiscordChannel))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Channels != null &&
e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel))
args.Add(channel);
else
args.Add(e.Interaction.Guild.GetChannel((ulong)option.Value));
}
else if (parameter.ParameterType == typeof(DiscordRole))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Roles != null &&
e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role))
args.Add(role);
else
args.Add(e.Interaction.Guild.GetRole((ulong)option.Value));
}
else if (parameter.ParameterType == typeof(SnowflakeObject))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Channels != null && e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel))
args.Add(channel);
if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role))
args.Add(role);
else if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member))
args.Add(member);
else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user))
args.Add(user);
else
throw new ArgumentException("Error resolving mentionable option.");
}
else
throw new ArgumentException($"Error resolving interaction.");
}
}
return args;
}
///
/// Runs the pre-execution checks.
///
/// The method info.
/// The base context.
private async Task RunPreexecutionChecksAsync(MethodInfo method, BaseContext context)
{
if (context is InteractionContext ctx)
{
//Gets all attributes from parent classes as well and stuff
var attributes = new List();
attributes.AddRange(method.GetCustomAttributes(true));
attributes.AddRange(method.DeclaringType.GetCustomAttributes());
if (method.DeclaringType.DeclaringType != null)
{
attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes());
if (method.DeclaringType.DeclaringType.DeclaringType != null)
{
attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes());
}
}
var dict = new Dictionary();
foreach (var att in attributes)
{
//Runs the check and adds the result to a list
var result = await att.ExecuteChecksAsync(ctx);
dict.Add(att, result);
}
//Checks if any failed, and throws an exception
if (dict.Any(x => x.Value == false))
throw new SlashExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() };
}
if (context is ContextMenuContext cMctx)
{
var attributes = new List();
attributes.AddRange(method.GetCustomAttributes(true));
attributes.AddRange(method.DeclaringType.GetCustomAttributes());
if (method.DeclaringType.DeclaringType != null)
{
attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes());
if (method.DeclaringType.DeclaringType.DeclaringType != null)
{
attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes());
}
}
var dict = new Dictionary();
foreach (var att in attributes)
{
//Runs the check and adds the result to a list
var result = await att.ExecuteChecksAsync(cMctx);
dict.Add(att, result);
}
//Checks if any failed, and throws an exception
if (dict.Any(x => x.Value == false))
throw new ContextMenuExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() };
}
}
///
/// Gets the choice attributes from choice provider.
///
/// The custom attributes.
/// The optional guild id
private static async Task> GetChoiceAttributesFromProvider(IEnumerable customAttributes, ulong? guildId = null)
{
var choices = new List();
foreach (var choiceProviderAttribute in customAttributes)
{
var method = choiceProviderAttribute.ProviderType.GetMethod(nameof(IChoiceProvider.Provider));
if (method == null)
throw new ArgumentException("ChoiceProviders must inherit from IChoiceProvider.");
else
{
var instance = Activator.CreateInstance(choiceProviderAttribute.ProviderType);
// Abstract class offers more properties that can be set
if (choiceProviderAttribute.ProviderType.IsSubclassOf(typeof(ChoiceProvider)))
{
choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.GuildId))
?.SetValue(instance, guildId);
choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.Services))
?.SetValue(instance, Configuration.ServiceProvider);
}
//Gets the choices from the method
var result = await (Task>)method.Invoke(instance, null);
if (result.Any())
{
choices.AddRange(result);
}
}
}
return choices;
}
///
/// Gets the choice attributes from enum parameter.
///
/// The enum parameter.
private static List GetChoiceAttributesFromEnumParameter(Type enumParam)
{
var choices = new List();
foreach (Enum enumValue in Enum.GetValues(enumParam))
{
choices.Add(new DiscordApplicationCommandOptionChoice(enumValue.GetName(), enumValue.ToString()));
}
return choices;
}
///
/// Gets the parameter type.
///
/// The type.
private static ApplicationCommandOptionType GetParameterType(Type type)
{
var parameterType = type == typeof(string)
? ApplicationCommandOptionType.String
: type == typeof(long) || type == typeof(long?) || type == typeof(int) || type == typeof(int?)
? ApplicationCommandOptionType.Integer
: type == typeof(bool) || type == typeof(bool?)
? ApplicationCommandOptionType.Boolean
: type == typeof(double) || type == typeof(double?)
? ApplicationCommandOptionType.Number
: type == typeof(DiscordAttachment)
? ApplicationCommandOptionType.Attachment
: type == typeof(DiscordChannel)
? ApplicationCommandOptionType.Channel
: type == typeof(DiscordUser)
? ApplicationCommandOptionType.User
: type == typeof(DiscordRole)
? ApplicationCommandOptionType.Role
: type == typeof(SnowflakeObject)
? ApplicationCommandOptionType.Mentionable
: type.IsEnum
? ApplicationCommandOptionType.String
: throw new ArgumentException("Cannot convert type! Argument types must be string, int, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, SnowflakeObject, DiscordAttachment or an Enum.");
return parameterType;
}
///
/// Gets the choice attributes from parameter.
///
/// The choice attributes.
private static List GetChoiceAttributesFromParameter(IEnumerable choiceAttributes) =>
!choiceAttributes.Any()
? null
: choiceAttributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList();
///
/// Parses the parameters.
///
/// The parameters.
/// The optional guild id.
internal static async Task> ParseParametersAsync(IEnumerable parameters, ulong? guildId)
{
var options = new List();
foreach (var parameter in parameters)
{
//Gets the attribute
var optionAttribute = parameter.GetCustomAttribute();
if (optionAttribute == null)
throw new ArgumentException("Arguments must have the Option attribute!");
var minimumValue = parameter.GetCustomAttribute()?.Value ?? null;
var maximumValue = parameter.GetCustomAttribute()?.Value ?? null;
var minimumLength = parameter.GetCustomAttribute()?.Value ?? null;
var maximumLength = parameter.GetCustomAttribute()?.Value ?? null;
var channelTypes = parameter.GetCustomAttribute()?.ChannelTypes ?? null;
var autocompleteAttribute = parameter.GetCustomAttribute();
if (optionAttribute.Autocomplete && autocompleteAttribute == null)
throw new ArgumentException("Autocomplete options must have the Autocomplete attribute!");
if (!optionAttribute.Autocomplete && autocompleteAttribute != null)
throw new ArgumentException("Setting an autocomplete provider requires the option to have autocomplete set to true!");
//Sets the type
var type = parameter.ParameterType;
var parameterType = GetParameterType(type);
if (parameterType == ApplicationCommandOptionType.String)
{
minimumValue = null;
maximumValue = null;
}
else if (parameterType == ApplicationCommandOptionType.Integer || parameterType == ApplicationCommandOptionType.Number)
{
minimumLength = null;
maximumLength = null;
}
if (parameterType != ApplicationCommandOptionType.Channel)
channelTypes = null;
//Handles choices
//From attributes
var choices = GetChoiceAttributesFromParameter(parameter.GetCustomAttributes());
//From enums
if (parameter.ParameterType.IsEnum)
{
choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType);
}
//From choice provider
var choiceProviders = parameter.GetCustomAttributes();
if (choiceProviders.Any())
{
choices = await GetChoiceAttributesFromProvider(choiceProviders, guildId);
}
options.Add(new DiscordApplicationCommandOption(optionAttribute.Name, optionAttribute.Description, parameterType, !parameter.IsOptional, choices, null, channelTypes, optionAttribute.Autocomplete, minimumValue, maximumValue, minimumLength: minimumLength, maximumLength: maximumLength));
}
return options;
}
/*
///
/// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client.
/// Not recommended and should be avoided since it can make slash commands be unresponsive for a while.
///
public async Task RefreshCommandsAsync()
{
s_commandMethods.Clear();
s_groupCommands.Clear();
s_subGroupCommands.Clear();
s_registeredCommands.Clear();
s_contextMenuCommands.Clear();
GlobalDiscordCommands.Clear();
GuildDiscordCommands.Clear();
GuildCommandsInternal.Clear();
GlobalCommandsInternal.Clear();
GlobalDiscordCommands = null;
GuildDiscordCommands = null;
s_errored = false;
/*if (Configuration != null && Configuration.EnableDefaultHelp)
{
this._updateList.RemoveAll(x => x.Value.Type == typeof(DefaultHelpModule));
}*/
/*
await this.UpdateAsync();
}*/
///
/// Fires when the execution of a slash command fails.
///
public event AsyncEventHandler SlashCommandErrored
{
add => this._slashError.Register(value);
remove => this._slashError.Unregister(value);
}
private AsyncEvent _slashError;
///
/// Fires when the execution of a slash command is successful.
///
public event AsyncEventHandler SlashCommandExecuted
{
add => this._slashExecuted.Register(value);
remove => this._slashExecuted.Unregister(value);
}
private AsyncEvent _slashExecuted;
///
/// Fires when the execution of a context menu fails.
///
public event AsyncEventHandler ContextMenuErrored
{
add => this._contextMenuErrored.Register(value);
remove => this._contextMenuErrored.Unregister(value);
}
private AsyncEvent _contextMenuErrored;
///
/// Fire when the execution of a context menu is successful.
///
public event AsyncEventHandler ContextMenuExecuted
{
add => this._contextMenuExecuted.Register(value);
remove => this._contextMenuExecuted.Unregister(value);
}
private AsyncEvent _contextMenuExecuted;
}
///
/// Holds configuration data for setting up an application command.
///
internal class ApplicationCommandsModuleConfiguration
{
///
/// The type of the command module.
///
public Type Type { get; }
///
/// The translation setup.
///
public Action Translations { get; }
///
/// Creates a new command configuration.
///
/// The type of the command module.
/// The translation setup callback.
public ApplicationCommandsModuleConfiguration(Type type, Action translations = null)
{
this.Type = type;
this.Translations = translations;
}
}
///
/// Links a command to its original command module.
///
internal class ApplicationCommandSourceLink
{
///
/// The command.
///
public DiscordApplicationCommand ApplicationCommand { get; set; }
///
/// The base/root module the command is contained in.
///
public Type RootCommandContainerType { get; set; }
///
/// The direct group the command is contained in.
///
public Type CommandContainerType { get; set; }
}
///
/// The command method.
///
internal class CommandMethod
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the method.
///
public MethodInfo Method { get; set; }
}
///
/// The group command.
///
internal class GroupCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the methods.
///
public List> Methods { get; set; } = null;
}
///
/// The sub group command.
///
internal class SubGroupCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the sub commands.
///
public List SubCommands { get; set; } = new();
}
///
/// The context menu command.
///
internal class ContextMenuCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the method.
///
public MethodInfo Method { get; set; }
}
#region Default Help
///
/// Represents the default help module.
///
internal class DefaultHelpModule : ApplicationCommandsModule
{
public class DefaultHelpAutoCompleteProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext context)
{
var options = new List();
IEnumerable slashCommands = null;
var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync();
if (context.Guild != null)
{
var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First())
.Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase))
.ToList();
}
else
{
await Task.WhenAll(globalCommandsTask);
slashCommands = globalCommandsTask.Result
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First())
.Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase))
.ToList();
}
foreach (var sc in slashCommands.Take(25))
{
options.Add(new DiscordApplicationCommandAutocompleteChoice(sc.Name, sc.Name.Trim()));
}
return options.AsEnumerable();
}
}
public class DefaultHelpAutoCompleteLevelOneProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext context)
{
var options = new List();
IEnumerable slashCommands = null;
var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync();
if (context.Guild != null)
{
var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
}
else
{
await Task.WhenAll(globalCommandsTask);
slashCommands = globalCommandsTask.Result
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
}
var command = slashCommands.FirstOrDefault(ac =>
ac.Name.Equals(context.Options[0].Value.ToString().Trim(),StringComparison.OrdinalIgnoreCase));
if (command is null || command.Options is null)
{
options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command"));
}
else
{
var opt = command.Options.Where(c => c.Type is ApplicationCommandOptionType.SubCommandGroup or ApplicationCommandOptionType.SubCommand
&& c.Name.StartsWith(context.Options[1].Value.ToString(), StringComparison.InvariantCultureIgnoreCase)).ToList();
foreach (var option in opt.Take(25))
{
options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim()));
}
}
return options.AsEnumerable();
}
}
public class DefaultHelpAutoCompleteLevelTwoProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext context)
{
var options = new List();
IEnumerable slashCommands = null;
var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync();
if (context.Guild != null)
{
var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
}
else
{
await Task.WhenAll(globalCommandsTask);
slashCommands = globalCommandsTask.Result
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
}
var command = slashCommands.FirstOrDefault(ac =>
ac.Name.Equals(context.Options[0].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase));
if (command.Options is null)
{
options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command"));
return options.AsEnumerable();
}
var foundCommand = command.Options.FirstOrDefault(op => op.Name.Equals(context.Options[1].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase));
if (foundCommand is null || foundCommand.Options is null)
{
options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command"));
}
else
{
var opt = foundCommand.Options.Where(x => x.Type == ApplicationCommandOptionType.SubCommand &&
x.Name.StartsWith(context.Options[2].Value.ToString(), StringComparison.OrdinalIgnoreCase)).ToList();
foreach (var option in opt.Take(25))
{
options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim()));
}
}
return options.AsEnumerable();
}
}
[SlashCommand("help", "Displays command help")]
internal async Task DefaultHelpAsync(InteractionContext ctx,
[Autocomplete(typeof(DefaultHelpAutoCompleteProvider))]
[Option("option_one", "top level command to provide help for", true)] string commandName,
[Autocomplete(typeof(DefaultHelpAutoCompleteLevelOneProvider))]
[Option("option_two", "subgroup or command to provide help for", true)] string commandOneName = null,
[Autocomplete(typeof(DefaultHelpAutoCompleteLevelTwoProvider))]
[Option("option_three", "command to provide help for", true)] string commandTwoName = null)
{
List applicationCommands = null;
var globalCommandsTask = ctx.Client.GetGlobalApplicationCommandsAsync();
if (ctx.Guild != null)
{
var guildCommandsTask= ctx.Client.GetGuildApplicationCommandsAsync(ctx.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
applicationCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First())
.ToList();
}
else
{
await Task.WhenAll(globalCommandsTask);
applicationCommands = globalCommandsTask.Result
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First())
.ToList();
}
if (applicationCommands.Count < 1)
{
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder()
.WithContent($"There are no slash commands"));
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
.WithContent($"There are no slash commands").AsEphemeral(true));
return;
}
if (commandTwoName is not null && !commandTwoName.Equals("no_options_for_this_command"))
{
var commandsWithSubCommands = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.Any(op => op.Type == ApplicationCommandOptionType.SubCommandGroup));
var subCommandParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Name.Equals(commandName,StringComparison.OrdinalIgnoreCase));
var cmdParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Options.Any(op => op.Name.Equals(commandOneName))).Options
.FirstOrDefault(opt => opt.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase));
var cmd = cmdParent.Options.FirstOrDefault(op => op.Name.Equals(commandTwoName,StringComparison.OrdinalIgnoreCase));
var discordEmbed = new DiscordEmbedBuilder
{
Title = "Help",
Description = $"{subCommandParent.Mention.Replace(subCommandParent.Name, $"{subCommandParent.Name} {cmdParent.Name} {cmd.Name}")}: {cmd.Description ?? "No description provided."}"
};
if (cmd.Options is not null)
{
var commandOptions = cmd.Options.ToList();
var sb = new StringBuilder();
foreach (var option in commandOptions)
sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n');
sb.Append('\n');
discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim()));
}
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder()
.AddEmbed(discordEmbed));
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true));
}
else if (commandOneName is not null && commandTwoName is null && !commandOneName.Equals("no_options_for_this_command"))
{
var commandsWithOptions = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.All(op => op.Type == ApplicationCommandOptionType.SubCommand));
var subCommandParent = commandsWithOptions.FirstOrDefault(cm => cm.Name.Equals(commandName,StringComparison.OrdinalIgnoreCase));
var subCommand = subCommandParent.Options.FirstOrDefault(op => op.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase));
var discordEmbed = new DiscordEmbedBuilder
{
Title = "Help",
Description = $"{subCommandParent.Mention.Replace(subCommandParent.Name, $"{subCommandParent.Name} {subCommand.Name}")}: {subCommand.Description ?? "No description provided."}"
};
if (subCommand.Options is not null)
{
var commandOptions = subCommand.Options.ToList();
var sb = new StringBuilder();
foreach (var option in commandOptions)
sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n');
sb.Append('\n');
discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim()));
}
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed));
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true));
}
else
{
var command = applicationCommands.FirstOrDefault(cm => cm.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase));
if (command is null)
{
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder()
.WithContent($"No command called {commandName} in guild {ctx.Guild.Name}"));
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
.WithContent($"No command called {commandName} in guild {ctx.Guild.Name}").AsEphemeral(true));
return;
}
var discordEmbed = new DiscordEmbedBuilder
{
Title = "Help",
Description = $"{command.Mention}: {command.Description ?? "No description provided."}"
}.AddField(new DiscordEmbedField("Command is NSFW", command.IsNsfw.ToString()));
if (command.Options is not null)
{
var commandOptions = command.Options.ToList();
var sb = new StringBuilder();
foreach (var option in commandOptions)
sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n');
sb.Append('\n');
discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim()));
}
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed));
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true));
}
}
}
#endregion
diff --git a/DisCatSharp.Docs/articles/basics/bot_account.md b/DisCatSharp.Docs/articles/basics/bot_account.md
index 85fd0f51b..5ef2c15c0 100644
--- a/DisCatSharp.Docs/articles/basics/bot_account.md
+++ b/DisCatSharp.Docs/articles/basics/bot_account.md
@@ -1,91 +1,95 @@
---
uid: basics_bot_account
title: Creating a Bot Account
---
# Creating a Bot Account
## Create an Application
Before you're able to create a [bot account](https://discord.com/developers/docs/topics/oauth2#bots) to interact with the Discord API, you'll need to create a new OAuth2 application.
Go to the [Discord Developer Portal](https://discord.com/developers/applications) and click `New Application` at the top right of the page.
![Discord Developer Portal](/images/basics_bot_account_01.png)
You'll then be prompted to enter a name for your application.
![Naming Application](/images/basics_bot_account_02.png "Naming Application")
The name of your application will be the name displayed to users when they add your bot to their Discord server.
With that in mind, it would be a good idea for your application name to match the desired name of your bot.
-Enter your desired application name into the text box, then hit the `Create` button.
+Enter your desired application name into the text box, accept the [Developer Terms of Service](https://discord.com/developers/docs/policies-and-agreements/terms-of-service) and [Developer Policy](https://discord.com/developers/docs/policies-and-agreements/developer-policy) and hit the `Create` button.
After you hit `Create`, you'll be taken to the application page for your newly created application.
![Application Page](/images/basics_bot_account_03.png)
That was easy, wasn't it?
Before you move on, you may want to upload an icon for your application and provide a short description of what your bot will do.
As with the name of your application, the application icon and description will be displayed to users when adding your bot.
## Add a Bot Account
Now that you have an application created, you'll be able to add a brand new bot account to it.
Head on over to the bot page of your application by clicking on `Bot` in the left panel.
From there, click on the `Add Bot` button at the top right of the page.
![Bot Page](/images/basics_bot_account_04.png)
Then confirm the creation of the bot account.
![Creation Confirmation](/images/basics_bot_account_05.png)
# Using Your Bot Account
## Invite Your Bot
Now that you have a bot account, you'll probably want to invite it to a server!
A bot account joins a server through a special invite link that'll take users through the OAuth2 flow;
you'll probably be familiar with this if you've ever added a public Discord bot to a server.
-To get the invite link for your bot, head on over to the OAuth2 page of your application.
+To get the invite link for your bot, head on over to the `OAuth2` page of your application and select the `URL Generator` page.
-![OAuth2](/images/basics_bot_account_06.png)
+![OAuth2 URL Generator](/images/basics_bot_account_06.png "OAuth2 URL Generator")
We'll be using the *OAuth2 URL Generator* on this page.
Simply tick `bot` under the *scopes* panel; your bot invite link will be generated directly below.
-![OAuth2 Scopes](/images/basics_bot_account_07.png)
+![OAuth2 Scopes](/images/basics_bot_account_07.png "OAuth2 Scopes")
By default, the generated link will not grant any permissions to your bot when it joins a new server.
If your bot requires specific permissions to function, you'd select them in the *bot permissions* panel.
![Permissions](/images/basics_bot_account_08.png "Permissions Panel")
The invite link in the *scopes* panel will update each time you change the permissions.
Be sure to copy it again after any changes!
## Get Bot Token
Instead of logging in to Discord with a username and password, bot accounts use a long string called a *token* to authenticate.
You'll want to retrieve the token for your bot account so you can use it with DisCatSharp.
-Head back to the bot page and click on `Click to Reveal Token` just above the `Copy` and `Regenerate` buttons to take a peek at your token.
+Head back to the bot page and click on `Reset Token` just below the bot's username field.
-![Token Reveal](/images/basics_bot_account_09.png "Token Reveal")
+![Token Reset](/images/basics_bot_account_09.png "Token Reset")
+
+Click on `Yes, do it!` to confirm the reset.
+
+![Token Reset Confirm](/images/basics_bot_account_10.png "Token Reset Confirmation")
Go ahead and copy your bot token and save it somewhere. You'll be using it soon!
>[!IMPORTANT]
> Handle your bot token with care! Anyone who has your token will have access to your bot account.
> Be sure to store it in a secure location and *never* give it to *anybody*.
>
- > If you ever believe your token has been compromised, be sure to hit the `Regenerate` button (as seen above) to invalidate your old token and get a brand new token.
+ > If you ever believe your token has been compromised, be sure to hit the `Reset Token` button (as seen above) to invalidate your old token and get a brand new token.
## Write Some Code
You've got a bot account set up and a token ready for use.
-Sounds like it's time for you to [write your first bot](xref:basics_first_bot)!
\ No newline at end of file
+Sounds like it's time for you to [write your first bot](xref:basics_first_bot)!
diff --git a/DisCatSharp.Docs/articles/basics/first_bot.md b/DisCatSharp.Docs/articles/basics/first_bot.md
index 1cc5e1b70..8a36d5c9c 100644
--- a/DisCatSharp.Docs/articles/basics/first_bot.md
+++ b/DisCatSharp.Docs/articles/basics/first_bot.md
@@ -1,226 +1,234 @@
---
uid: basics_first_bot
title: Your First Bot
---
# Your First Bot
>[!NOTE]
> This article assumes the following:
> * You have [created a bot account](xref:basics_bot_account "Creating a Bot Account") and have a bot token.
- > * You have [Visual Studio 2019](https://visualstudio.microsoft.com/vs/) installed on your computer.
+ > * You have [Visual Studio 2022](https://visualstudio.microsoft.com/vs/) installed on your computer.
## Create a Project
Open up Visual Studio and click on `Create a new project` towards the bottom right.
![Visual Studio Start Screen](/images/basics_first_bot_01.png)
-Select `Console App (.NET Core)` then click on the `Next` button.
+Select `Console App` then click on the `Next` button.
![New Project Screen](/images/basics_first_bot_02.png)
Next, you'll give your project a name. For this example, we'll name it `MyFirstBot`.
If you'd like, you can also change the directory that your project will be created in.
Enter your desired project name, then click on the `Create` button.
![Name Project Screen](/images/basics_first_bot_03.png)
-Voilà! Your project has been created!
+Now select `.NET 6.0 (Long-term support)` from the dropdown menu, tick the `Do not use top-level statements` checkbox and click on the `Next` button.
+
+![Framework Project Screen](/images/basics_first_bot_04.png)
-![Visual Studio IDE](/images/basics_first_bot_04.png)
+
+Voilà! Your project has been created!
+![Visual Studio IDE](/images/basics_first_bot_05.png)
## Install Package
Now that you have a project created, you'll want to get DisCatSharp installed.
Locate the *solution explorer* on the right side, then right click on `Dependencies` and select `Manage NuGet Packages` from the context menu.
-![Dependencies Context Menu](/images/basics_first_bot_05.png)
+![Dependencies Context Menu](/images/basics_first_bot_06.png)
You'll then be greeted by the NuGet package manager.
Select the `Browse` tab towards the top left, then type `DisCatSharp` into the search text box with the Pre-release checkbox checked **ON**.
-![NuGet Package Search](/images/basics_first_bot_06.png)
+![NuGet Package Search](/images/basics_first_bot_07.png)
The first results should be the DisCatSharp packages.
Package|Description
:---: |:---:
`DisCatSharp`|Main package; Discord API client.
`DisCatSharp.CommandsNext`|Add-on which provides a command framework.
`DisCatSharp.Common`|Common tools & converters
`DisCatSharp.Interactivity`|Add-on which allows for interactive commands.
-`DisCatSharp.Lavalink`|Client implementation for [Lavalink](xref:audio_lavalink_setup). Useful for music bots.
+`DisCatSharp.Lavalink`|Client implementation for [Lavalink](xref:modules_audio_lavalink_setup). Useful for music bots.
`DisCatSharp.ApplicationCommands`|Add-on which makes dealing with application commands easier.
`DisCatSharp.VoiceNext`|Add-on which enables connectivity to Discord voice channels.
`DisCatSharp.VoiceNext.Natives`|Voice next natives.
We'll only need the `DisCatSharp` package for the basic bot we'll be writing in this article.
-Select it from the list then click the `Install` button to the right (after verifying that you will be installing the **latest 4.0 version**).
+Select it from the list then click the `Install` button to the right.
![Install DisCatSharp](/images/basics_first_bot_08.png)
You're now ready to write some code!
## First Lines of Code
DisCatSharp implements [Task-based Asynchronous Pattern](https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern).
Because of this, the majority of DisCatSharp methods must be executed in a method marked as `async` so they can be properly `await`ed.
Due to the way the compiler generates the underlying [IL](https://en.wikipedia.org/wiki/Common_Intermediate_Language) code,
marking our `Main` method as `async` has the potential to cause problems. As a result, we must pass the program execution to an `async` method.
Head back to your *Program.cs* tab and empty the `Main` method by deleting line 9.
![Code Editor](/images/basics_first_bot_09.png)
Now, create a new `static` method named `MainAsync` beneath your `Main` method. Have it return type `Task` and mark it as `async`.
After that, add `MainAsync().GetAwaiter().GetResult();` to your `Main` method.
```cs
static void Main(string[] args)
{
MainAsync().GetAwaiter().GetResult();
}
static async Task MainAsync()
{
}
```
If you typed this in by hand, Intellisense should have generated the required `using` directive for you.
However, if you copy-pasted the snippet above, VS will complain about being unable to find the `Task` type.
Hover over `Task` with your mouse and click on `Show potential fixes` from the tooltip.
![Error Tooltip](/images/basics_first_bot_10.png)
Then apply the recommended solution.
![Solution Menu](/images/basics_first_bot_11.png)
We'll now create a new `DiscordClient` instance in our brand new asynchronous method.
Create a new variable in `MainAsync` and assign it a new `DiscordClient` instance, then pass an instance of `DiscordConfiguration` to its constructor.
Create an object initializer for `DiscordConfiguration` and populate the `Token` property with your bot token then set the `TokenType` property to `TokenType.Bot`.
-Next add the `Intents` Property and Populated it with the @DisCatSharp.DiscordIntents.AllUnprivileged value. These Intents
+Next add the `Intents` Property and Populated it with the @DisCatSharp.DiscordIntents.AllUnprivileged and DiscordIntents.MessageContent values.
+The message content intent must be enabled in the developer portal as well.
+These Intents
are required for certain Events to be fired. Please visit this [article](xref:beyond_basics_intents) for more information.
```cs
var discord = new DiscordClient(new DiscordConfiguration()
{
Token = "My First Token",
TokenType = TokenType.Bot,
- Intents = DiscordIntents.AllUnprivileged
+ Intents = DiscordIntents.AllUnprivileged | DiscordIntents.MessageContent
});
```
>[!WARNING]
> We hard-code the token in the above snippet to keep things simple and easy to understand.
>
> Hard-coding your token is *not* a smart idea, especially if you plan on distributing your source code.
> Instead you should store your token in an external medium, such as a configuration file or environment variable, and read that into your program to be used with DisCatSharp.
Follow that up with `await discord.ConnectAsync();` to connect and login to Discord, and `await Task.Delay(-1);` at the end of the method to prevent the console window from closing prematurely.
```cs
var discord = new DiscordClient();
await discord.ConnectAsync();
await Task.Delay(-1);
```
As before, Intellisense will have auto generated the needed `using` directive for you if you typed this in by hand.
If you've copied the snippet, be sure to apply the recommended suggestion to insert the required directive.
If you hit `F5` on your keyboard to compile and run your program, you'll be greeted by a happy little console with a single log message from DisCatSharp. Woo hoo!
![Program Console](/images/basics_first_bot_12.png)
## Spicing Up Your Bot
Right now our bot doesn't do a whole lot. Let's bring it to life by having it respond to a message!
Hook the `MessageCreated` event fired by `DiscordClient` with a
[lambda](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions).
Mark it as `async` and give it two parameters: `s` and `e`.
```cs
discord.MessageCreated += async (s, e) =>
{
};
```
Then, add an `if` statement into the body of your event lambda that will check if `e.Message.Content` starts with your desired trigger word and respond with
a message using `e.Message.RespondAsync` if it does. For this example, we'll have the bot to respond with *pong!* for each message that starts with *ping*.
```cs
discord.MessageCreated += async (s, e) =>
{
if (e.Message.Content.ToLower().StartsWith("ping"))
await e.Message.RespondAsync("pong!");
};
```
## The Finished Product
Your entire program should now look like this:
```cs
using System;
using System.Threading.Tasks;
using DisCatSharp;
namespace MyFirstBot
{
class Program
{
static void Main(string[] args)
{
MainAsync().GetAwaiter().GetResult();
}
static async Task MainAsync()
{
var discord = new DiscordClient(new DiscordConfiguration()
{
- Token = "My First Token",
- TokenType = TokenType.Bot
+ Token = "My First Token",
+ TokenType = TokenType.Bot,
+ Intents = DiscordIntents.AllUnprivileged | DiscordIntents.MessageContent
});
discord.MessageCreated += async (s, e) =>
{
if (e.Message.Content.ToLower().StartsWith("ping"))
await e.Message.RespondAsync("pong!");
};
await discord.ConnectAsync();
await Task.Delay(-1);
}
}
}
```
Hit `F5` to run your bot, then send *ping* in any channel your bot account has access to.
Your bot should respond with *pong!* for each *ping* you send.
Congrats, your bot now does something!
![Bot Response](/images/basics_first_bot_13.png)
## Further Reading
Now that you have a basic bot up and running, you should take a look at the following:
* [Events](xref:beyond_basics_events)
-* [CommandsNext](xref:commands_intro)
+* [CommandsNext](xref:modules_commandsnext_intro)
+* [ApplicationCommands](xref:modules_application_commands_intro)
diff --git a/DisCatSharp.Docs/articles/basics/templates.md b/DisCatSharp.Docs/articles/basics/templates.md
index 9b769762a..20b88b762 100644
--- a/DisCatSharp.Docs/articles/basics/templates.md
+++ b/DisCatSharp.Docs/articles/basics/templates.md
@@ -1,60 +1,60 @@
---
uid: basics_templates
title: Project Templates
---
# Prerequisites
Install the following packages:
- DisCatSharp.ProjectTemplates
To Install the latest:
```powershell
dotnet new --install DisCatSharp.ProjectTemplates
```
-To install a specific version (example uses 9.8.4.1):
+To install a specific version (example uses 10.0.0):
```powershell
-dotnet new --install DisCatSharp.ProjectTemplates::9.8.4.1
+dotnet new --install DisCatSharp.ProjectTemplates::10.0.0
```
# Steps
![Install Setup](/images/pt_nuget_install.png)
-----
If you're using Visual Studio, the templates will show up when creating a new project/solution
![Install Setup](/images/pt_project_new.png)
To easily find the DCS templates, you can search for either `Bot` or `Discord`. These tags are generic so if anyone else creates their own
discord or bot template our DCS templates will still be discoverable. We shall be using the solution template for our example.
![Classification](/images/pt_project_new_classification.png)
For example sake, the project name is DCSTest
![Project Name](/images/pt_project_new_name.png)
Input your Discord Token which can be retrieved via Discord's Developer Portal. The checkboxes represent the various modules in the DCS library. Checking it,
will include it in your project. If it's an extension, it automatically gets configured/included.
![Parameters](/images/pt_project_new_options.png)
You should see something similar to the following image. It's worth noting that you need to set the Web project as the `Startup` project. Due to the web being
list last, the `Bot` project is considered the startup. You would think that a class-library which doesn't have an exe could be considered a startup project....
![Project Structure](/images/pt_scaffolded.png)
At this point in time the template is ready to run!
-----
# Templates
## Bot Template
This is a class library in which you place bot related code. It contains its own json file where you can
configure your bot accordingly!
An extension class provides easy to call methods for adding the Bot's services/configuration into the dependency injection (DI) pipeline.
## Web Template
This is a very minimal project. By itself it only has a default endpoint which displays "Hello World".
## Solution Template
Combines the bot and web templates. Includes the appropriate references/calls to get your bot up and running with minimal
effort.
diff --git a/DisCatSharp.Docs/articles/beyond_basics/components/buttons.md b/DisCatSharp.Docs/articles/beyond_basics/components/buttons.md
index 32b43b0bb..9aa57267e 100644
--- a/DisCatSharp.Docs/articles/beyond_basics/components/buttons.md
+++ b/DisCatSharp.Docs/articles/beyond_basics/components/buttons.md
@@ -1,170 +1,170 @@
---
uid: advanced_topics_buttons
title: Buttons
---
# Introduction
Buttons are a feature in Discord based on the interaction framework appended to the bottom of a message which come in several colors.
You will want to familarize yourself with the [message builder](xref:beyond_basics_messagebuilder) as it and similar builder objects will be used throughout this article.
With buttons, you can have up to five buttons in a row, and up to five (5) rows of buttons, for a maximum for 25 buttons per message.
Furthermore, buttons come in two types: regular, and link. Link buttons contain a Url field, and are always grey.
# Buttons Continued
> [!WARNING]
> Component (Button) Ids on buttons should be unique, as this is what's sent back when a user presses a button.
>
> Link buttons do **not** have a custom id and do **not** send interactions when pressed.
Buttons consist of five parts:
- Id
- Style
- Label
- Emoji
- Disabled
The id of the button is a settable string on buttons, and is specified by the developer. Discord sends this id back in the [interaction object](https://discord.dev/interactions/slash-commands#interaction).
Non-link buttons come in four colors, which are known as styles: Blurple, Grey, Green, and Red. Or as their styles are named: Primary, Secondary, Success, and Danger respectively.
How does one construct a button? It's simple, buttons support constructor and object initialization like so:
```cs
var myButton = new DiscordButtonComponent()
{
CustomId = "my_very_cool_button",
Style = ButtonStyle.Primary,
Label = "Very cool button!",
Emoji = new DiscordComponentEmoji("😀")
};
```
This will create a blurple button with the text that reads "Very cool button!". When a user pushes it, `"my_very_cool_button"` will be sent back as the `Id` property on the event. This is expanded on in the [how to respond to buttons](#responding-to-button-presses).
The label of a button is optional *if* an emoji is specified. The label can be up to 80 characters in length.
The emoji of a button is a [partial emoji object](https://discord.dev/interactions/message-components#component-object), which means that **any valid emoji is usable**, even if your bot does not have access to it's origin server.
The disabled field of a button is rather self explanatory. If this is set to true, the user will see a greyed out button which they cannot interact with.
# Adding buttons
> [!NOTE]
> This article will use underscores in button ids for consistency and styling, but spaces are also usable.
Adding buttons to a message is relatively simple. Simply make a builder, and sprinkle some content and the buttons you'd like.
```cs
var builder = new DiscordMessageBuilder();
builder.WithContent("This message has buttons! Pretty neat innit?");
```
Well, there's a builder, but no buttons. What now? Simply make a new button object (`DiscordButtonComponent`) and call `.AddComponents()` on the MessageBuilder.
```cs
var myButton = new DiscordButtonComponent
{
CustomId = "my_custom_id",
Label = "This is a button!",
Style = ButtonStyle.Primary,
};
var builder = new DiscordMessageBuilder()
.WithContent("This message has buttons! Pretty neat innit?")
.AddComponents(myButton);
```
Now you have a message with a button. Congratulations! It's important to note that `.AddComponents()` will create a new row with each call, so **add everything you want on one row in one call!**
Buttons can be added in any order you fancy. Lets add 5 to demonstrate each color, and a link button for good measure.
```cs
var builder = new DiscordMessageBuilder()
.WithContent("This message has buttons! Pretty neat innit?")
.AddComponents(new DiscordComponent[]
{
new DiscordButtonComponent(ButtonStyle.Primary, "1_top", "Blurple!"),
new DiscordButtonComponent(ButtonStyle.Secondary, "2_top", "Grey!"),
new DiscordButtonComponent(ButtonStyle.Success, "3_top", "Green!"),
new DiscordButtonComponent(ButtonStyle.Danger, "4_top", "Red!"),
new DiscordLinkButtonComponent("https://some-super-cool.site", "Link!")
});
```
As promised, not too complicated. Links however are `DiscordLinkButtonComponent`, which takes a URL as it's first parameter, and the label. Link buttons can also have an emoji, like regular buttons.
Lets also add a second row of buttons, but disable them, so the user can't push them all willy-nilly.
```cs
builder.AddComponents(new DiscordComponent[]
{
new DiscordButtonComponent(ButtonStyle.Primary, "1_top_d", "Blurple!", true),
new DiscordButtonComponent(ButtonStyle.Secondary, "2_top_d", "Grey!", true),
new DiscordButtonComponent(ButtonStyle.Success, "3_top_d", "Green!", true),
new DiscordButtonComponent(ButtonStyle.Danger, "4_top_d", "Red!", true),
new DiscordLinkButtonComponent("https://some-super-cool.site", "Link!", true)
});
```
Practically identical, but now with `true` as an extra parameter. This is the `Disabled` property.
Produces a message like such: ![Buttons](/images/advanced_topics_buttons_01.png)
Well, that's all neat, but lets say you want to add an emoji. Being able to use any emoji is pretty neat, after all. That's also very simple!
```cs
var myButton = new DiscordButtonComponent
(
ButtonStyle.Primary,
"emoji_button",
null,
false,
new DiscordComponentEmoji(595381687026843651)
);
```
And you're done! Simply add that to a builder, and when you send, you'll get a message that has a button with a little Pikachu enjoying a lolipop. Adorable. ![PikaLolipop](/images/advanced_topics_buttons_02.png)
# Responding to button presses
When any button is pressed, it will fire the [ComponentInteractionCreated](xref:DisCatSharp.DiscordClient#DisCatSharp_DiscordClient_ComponentInteractionCreated) event.
In the event args, `Id` will be the id of the button you specified. There's also an `Interaction` property, which contains the interaction the event created. It's important to respond to an interaction within 3 seconds, or it will time out. Responding after this period will throw a `NotFoundException`.
With buttons, there are two new response types: `DeferredMessageUpdate` and `UpdateMessage`.
Using `DeferredMessageUpdate` lets you create followup messages via the [followup message builder](xref:DisCatSharp.Entities.DiscordFollowupMessageBuilder). The button will return to being in it's 'dormant' state, or it's 'unpushed' state, if you will.
You have 15 minutes from that point to make followup messages. Responding to that interaction looks like this:
```cs
client.ComponentInteractionCreated += async (s, e) =>
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate);
// Do things.. //
}
```
If you would like to update the message when a button is pressed, however, you'd use `UpdateMessage` instead, and pass a `DiscordInteractionResponseBuilder` with the new content you'd like.
```cs
client.ComponentInteractionCreated += async (s, e) =>
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().WithContent("No more buttons for you >:)"));
}
```
This will update the message, and without the infamous (edited) next to it. Nice.
# Interactivity
Along with the typical `WaitForMessageAsync` and `WaitForReactionAsync` methods provided by interactivity, there are also button implementations as well.
-More information about how interactivity works can be found in [the interactivity article](xref:interactivity)
+More information about how interactivity works can be found in [the interactivity article](xref:modules_interactivity)
Since buttons create interactions, there are also two additional properties in the configuration:
- ResponseBehavior
- ResponseMessage
ResponseBehavior is what interactivity will do when handling something that isn't a valid valid button, in the context of waiting for a specific button. It defaults to `Ignore`, which will cause the interaction fail.
Alternatively, setting it to `Ack` will acknowledge the button, and continue waiting.
Respond will reply with an ephemeral message with the aforementioned response message.
ResponseBehavior only applies to the overload accepting a string id of the button to wait for.
diff --git a/DisCatSharp.Docs/articles/beyond_basics/components/select_menus.md b/DisCatSharp.Docs/articles/beyond_basics/components/select_menus.md
index a3bc9d190..f0c1a7893 100644
--- a/DisCatSharp.Docs/articles/beyond_basics/components/select_menus.md
+++ b/DisCatSharp.Docs/articles/beyond_basics/components/select_menus.md
@@ -1,139 +1,139 @@
---
uid: advanced_topics_select_menus
title: Select Menus
---
# Introduction
The select menus, like the [buttons](xref:advanced_topics_buttons), are message components.
You will want to familarize yourself with the [message builder](xref:beyond_basics_messagebuilder) as it and similar builder objects will be used throughout this article.
A row can only have one select menu. An row containing a select menu cannot also contain buttons.
Since a message can have up to 5 rows, you can add up to 5 select menus to a message.
# Select Menus
> [!WARNING]
> Component Ids and option values should be unique, as this is what's sent back when a user selects one (or more) option.
Select menus consist of five parts:
- Id
- Placeholder
- Options
- MinOptions
- MaxOptions
- Disabled
The id of the select menu is a settable string, and is specified by the developer. Discord sends this id back in the [interaction object](https://discord.dev/interactions/slash-commands#interaction).
**Placeholder** is a settable string that appears in the select menu when nothing is selected.
**Options** is an array of options for the user to select. Their maximum number in one select menu is 25.
You can let users choose 1 or more options using **MinOptions** and **MaxOptions**.
Options consist of five parts:
- Label
- Value
- Description
- IsDefault
- Emoji
Menu creation, for easier understanding, can be divided into two stages:
```cs
// First, create an array of options.
var options = new DiscordSelectComponentOption[]
{
new DiscordSelectComponentOption("First option", "first_option", "This is the first option, you can add your description of it here.", false, new DiscordComponentEmoji("😀")),
new DiscordSelectComponentOption("Second option", "second_option", "This is the second option, you can add your description of it here.", false, new DiscordComponentEmoji("😎"))
};
// Now let's create a select menu with the options created above.
var selectMenu = new DiscordSelectComponent("my_select_menu", "Please select one of the options", options);
```
This will create a select menu with two options and the text "Please select one of the options".
When a user select **one** option, `"my_select_menu"` will be sent back as the `Id` property on the event.
This is expanded on in the [how to respond to select menus](#responding-to-select-menus).
You can increase the maximum/minimum number of selections in the select menu constructor. You can also block the select menu, or options.
Description and emoji of options are optional. The label, value and description can be up to 100 characters in length.
The emoji of a option is a [partial emoji object](https://discord.dev/interactions/message-components#component-object), which means that **any valid emoji is usable**, even if your bot does not have access to it's origin server.
# Adding Select Menu
Adding a select menu is no different than adding a button.
We have already created the select menu above, now we will just create a new message builder add the select menu to it.
```cs
var builder = new DiscordMessageBuilder()
.WithContent("This message has select menu! Pretty neat innit?")
.AddComponents(selectMenu);
```
Now you have a message with a select menu. Congratulations! It's important to note that `.AddComponents()` will create a new row with each call, so **add everything you want on one row in one call!**
Lets also add a second row with select menu with the ability to choose any number of options.
```cs
var secondOptions = new DiscordSelectComponentOption[]
{
new DiscordSelectComponentOption("First option", "first_option", "This is the first option, you can add your description of it here.", false, new DiscordComponentEmoji("😀")),
new DiscordSelectComponentOption("Second option", "second_option", "This is the second option, you can add your description of it here.", false, new DiscordComponentEmoji("😎"))
new DiscordSelectComponentOption("Third option", "third_option", "This is the third option, you can add your description of it here.", false, new DiscordComponentEmoji("😘"))
};
var secondSelectMenu = new DiscordSelectComponent("my_second_select_menu", "Please select up to 3 options", secondOptions, 1, 3);
builder.AddComponents(secondSelectMenu);
```
And you're done! The select menu will now be sent when the user closes the select menu with 1 to 3 options selected.
# Responding to select menus
When any select menu is pressed, it will fire the [ComponentInteractionCreated](xref:DisCatSharp.DiscordClient#DisCatSharp_DiscordClient_ComponentInteractionCreated) event.
In the event args, `Id` will be the id of the select menu you specified. There's also an `Interaction` property, which contains the interaction the event created. It's important to respond to an interaction within 3 seconds, or it will time out. Responding after this period will throw a `NotFoundException`.
With select menus, there are two new response types: `DefferedMessageUpdate` and `UpdateMessage`.
Using `DeferredMessageUpdate` lets you create followup messages via the [followup message builder](xref:DisCatSharp.Entities.DiscordFollowupMessageBuilder).
You have 15 minutes from that point to make followup messages. Responding to that interaction looks like this:
```cs
client.ComponentInteractionCreated += async (s, e) =>
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.DefferedMessageUpdate);
// Do things.. //
}
```
If you would like to update the message when an select menu option selected, however, you'd use `UpdateMessage` instead, and pass a `DiscordInteractionResponseBuilder` with the new content you'd like.
```cs
client.ComponentInteractionCreated += async (s, e) =>
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().WithContent("No more select menu for you >:)"));
}
```
This will update the message, and without the infamous (edited) next to it. Nice.
# Interactivity
Along with the typical `WaitForMessageAsync` and `WaitForReactionAsync` methods provided by interactivity, there are also select menus implementations as well.
-More information about how interactivity works can be found in [the interactivity article](xref:interactivity)
+More information about how interactivity works can be found in [the interactivity article](xref:modules_interactivity)
Since select menus create interactions, there are also two additional properties in the configuration:
- ResponseBehavior
- ResponseMessage
ResponseBehavior is what interactivity will do when handling something that isn't a valid select menu, in the context of waiting for a specific select menu. It defaults to `Ignore`, which will cause the interaction fail.
Alternatively, setting it to `Ack` will acknowledge the select menu, and continue waiting.
Respond will reply with an ephemeral message with the aforementioned response message.
ResponseBehavior only applies to the overload accepting a string id of the select menu to wait for.
diff --git a/DisCatSharp.Docs/articles/important_changes/10_1_0.md b/DisCatSharp.Docs/articles/important_changes/10_1_0.md
new file mode 100644
index 000000000..53f0208f0
--- /dev/null
+++ b/DisCatSharp.Docs/articles/important_changes/10_1_0.md
@@ -0,0 +1,4 @@
+---
+uid: important_changes_10_1_0
+title: Version 10.1.0
+---
diff --git a/DisCatSharp.Docs/articles/hosting.md b/DisCatSharp.Docs/articles/misc/hosting.md
similarity index 99%
rename from DisCatSharp.Docs/articles/hosting.md
rename to DisCatSharp.Docs/articles/misc/hosting.md
index 884443923..f55bdc431 100644
--- a/DisCatSharp.Docs/articles/hosting.md
+++ b/DisCatSharp.Docs/articles/misc/hosting.md
@@ -1,78 +1,78 @@
---
-uid: hosting
+uid: misc_hosting
title: Hosting Solutions
---
## 24/7 Hosting Solutions
### Free hosting
If you're looking for free hosts, you've likely considered using [Heroku](https://www.heroku.com/) or [Glitch](https://glitch.com/).
We advise against using these platforms as they are designed to host web services, not Discord bots, and instances from either of these companies will shut down if there isn't enough internet traffic.
Save yourself the headache and don't bother.
Outside of persuading somebody to host your bot, you won't find any good free hosting solutions.
### Self Hosting
If you have access to an unused machine, have the technical know-how, and you also have a solid internet connection, you might consider hosting your bot on your own.
Even if you don't have a space PC on hand, parts to build one are fairly cheap in most regions. You could think of it as a one time investment with no monthly server fees.
Any modern hardware will work just fine, new or used.
Depending on how complex your bot is, you may even consider purchasing a Raspberry Pi ($35).
### Third-Party Hosting
The simplest, and probably most hassle-free (and maybe cheapest in the long run for dedicated machines) option is to find a provider
that will lend you their machine or a virtual host so you can run your bot in there.
Generally, cheapest hosting options are all GNU/Linux-based, so it's highly recommended you familiarize yourself with the OS and its
environment, particularly the shell (command line), and concepts such as SSH.
There are several well-known, trusted, and cheap providers:
* [Host Pls](https://host-pls.com/) - A hosting solution made by Discord bot developers. Based in America, starting from $2.49/mo.
* [Vultr](https://www.vultr.com/products/cloud-compute/) - Based in the US with datacenters in many regions, including APAC. Starting at $2.50/mo.
* [DigitalOcean](https://www.digitalocean.com/products/droplets/) - The gold standard, US based. Locations available world wide. Starting from $5.00/mo.
* [Linode](https://www.linode.com/products/shared/) - US based host with many datacenters around the world. Starting at $5.00/mo.
* [OVH](https://www.ovhcloud.com/en/vps/) - Very popular VPS host. Based in Canadian with French locations available. Starting from $6.00/mo.
* [Contabo](https://contabo.com/?show=vps) - Based in Germany; extremely good value for the price. Starting from 4.99€/mo.
Things to keep in mind when looking for a VPS host:
* The majority of cheap VPS hosts will be running some variant of Linux, and not Windows.
* The primary Discord API server is located in East US.
* If latency matters for you application, choose a host that is closer to this location.
In addition to these, there are several hosting providers that offer free trials or in-service credit:
* [**Microsoft Azure**](https://azure.microsoft.com/en-us/free/?cdn=disable "Microsoft Azure"): $200 in-service credit,
to be used within month of registration. Requires credit or debit card for validation. Azure isn't cheap, but it supports
both Windows and GNU/Linux-based servers. If you're enrolled in Microsoft Imagine, it's possible to get these cheaper or
free.
* [**Amazon Web Services**](https://aws.amazon.com/free/ "AWS"): Free for 12 months (with 750 compute hours per month). Not
cheap once the trial runs out, but it's also considered industry standard in cloud services.
* [**Google Cloud Platform**](https://cloud.google.com/free/ "Google Cloud Platform"): $300 in-service credit, to be used
within year of registration. GCP is based in the US, and offers very scalable products. Like the above, it's not the
cheapest of offerings.
### Hosting on Cloud Native Services
With most bots, unless if you host many of them, they dont require a whole machine to run them, just a slice of a machine. This is
where Docker and other cloud native hosting comes into play. There are many different options available to you and you will need
to chose which one will suit you best. Here are a few services that offer Docker or other cloud native solutions that are cheaper than running
a whole VM.
* [**Azure App Service**](https://azure.microsoft.com/en-us/services/app-service/ "Azure App Service"): Allows for Hosting Website, Continuous Jobs,
and Docker images on a Windows base or Linux base machine.
* [**AWS Fargate**](https://aws.amazon.com/fargate/ "AWS Fargate"): Allows for hosting Docker images within Amazon Web Services
* [**Jelastic**](https://jelastic.com/docker/ "Jelastic"): Allows for hosting Docker images.
### Making your publishing life easier
Now that we have covered where you can possibly host your application, now lets cover how to make your life easier publishing it. Many different
source control solutions out there are free and also offer some type of CI/CD integration (paid and free). Below are some of the
solutions that we recommend:
* [**Azure Devops**](https://azure.microsoft.com/en-us/services/devops/?nav=min "Azure Devops"): Allows for GIT source control hosting along with integrated CI/CD
pipelines to auto compile and publish your applications. You can also use their CI/CD service if your code is hosted in a different source control environment like Github.
* [**Github**](https://github.com/ "GitHub") Allows for GIT source control hosting. From here you can leverage many different CI/CD options to compile and publish your
applications.
* [**Bitbucket**](https://bitbucket.org/ "Bitbucket"): Allows for GIT source control hosting along with integrated CI/CD pipelines to auto compile and publish your applications.
diff --git a/DisCatSharp.Docs/articles/application_commands/events.md b/DisCatSharp.Docs/articles/modules/application_commands/events.md
similarity index 72%
rename from DisCatSharp.Docs/articles/application_commands/events.md
rename to DisCatSharp.Docs/articles/modules/application_commands/events.md
index 3e434135f..a20fddc52 100644
--- a/DisCatSharp.Docs/articles/application_commands/events.md
+++ b/DisCatSharp.Docs/articles/modules/application_commands/events.md
@@ -1,94 +1,104 @@
---
-uid: application_commands_events
+uid: modules_application_commands_events
title: Application Commands Events
---
# Application Commands events
Sometimes we need to add a variety of actions and checks before and after executing a command.
We can do this in the commands itself, or we can use special events for this.
## Before execution
The simplest example in this case: checking if the command was executed within the guild.
Suppose we have a certain class with commands that must be executed ONLY in the guilds:
```cs
public class MyGuildCommands : ApplicationCommandsModule
{
[SlashCommand("mute", "Mute user.")]
public static async Task Mute(InteractionContext context)
{
}
[SlashCommand("kick", "Kick user.")]
public static async Task Kick(InteractionContext context)
{
}
[SlashCommand("ban", "Ban user.")]
public static async Task Ban(InteractionContext context)
{
}
}
```
In this case, the easiest way would be to override the method from [ApplicationCommandsModule](xref:DisCatSharp.ApplicationCommands.ApplicationCommandsModule).
```cs
public class MyGuildCommands : ApplicationCommandsModule
{
public override async Task BeforeSlashExecutionAsync(InteractionContext ctx)
{
if (ctx.Guild == null)
return false;
}
[SlashCommand("mute", "Mute user.")]
public static async Task Mute(InteractionContext context)
{
}
[SlashCommand("kick", "Kick user.")]
public static async Task Kick(InteractionContext context)
{
}
[SlashCommand("ban", "Ban user.")]
public static async Task Ban(InteractionContext context)
{
}
}
```
Now, before executing any of these commands, the `BeforeSlashExecutionAsync` method will be executed. You can do anything in it, for example, special logging.
If you return `true`, then the command method will be executed after that, otherwise the execution will end there.
## After execution
If you want to create actions after executing the command, then you need to do the same, but override a different method:
```cs
public override async Task AfterSlashExecutionAsync(InteractionContext ctx)
{
// some actions
}
```
## Context menus
You can also add similar actions for the context menus. But this time, you need to override the other methods:
```cs
public class MyGuildCommands : ApplicationCommandsModule
{
public override async Task BeforeContextMenuExecutionAsync(ContextMenuContext ctx)
{
if (ctx.Guild == null)
return false;
}
public override async Task AfterContextMenuExecutionAsync(ContextMenuContext ctx)
{
// some actions
}
}
```
+
+## Error handling
+
+If you want to handle errors for slash commands, subscribe to the [SlashCommandErrored](xref:DisCatSharp.ApplicationCommands.ApplicationCommandsExtension#DisCatSharp_ApplicationCommands_ApplicationCommandsExtension_SlashCommandErrored) event.
+
+A separate method exists for context menus as [ContextMenuErrored](xref:DisCatSharp.ApplicationCommands.ApplicationCommandsExtension#DisCatSharp_ApplicationCommands_ApplicationCommandsExtension_ContextMenuErrored) event.
+
+It contains a castable field `Exception` with the exception that was thrown during the execution of the command.
+
+As example it can be a type of [SlashExecutionChecksFailedException](xref:DisCatSharp.ApplicationCommands.Exceptions.SlashExecutionChecksFailedException) or [ContextMenuExecutionChecksFailedException](xref:DisCatSharp.ApplicationCommands.Exceptions.ContextMenuExecutionChecksFailedException) which contains a list of failed checks.
diff --git a/DisCatSharp.Docs/articles/application_commands/intro.md b/DisCatSharp.Docs/articles/modules/application_commands/intro.md
similarity index 97%
rename from DisCatSharp.Docs/articles/application_commands/intro.md
rename to DisCatSharp.Docs/articles/modules/application_commands/intro.md
index 286240fe7..a222f49e9 100644
--- a/DisCatSharp.Docs/articles/application_commands/intro.md
+++ b/DisCatSharp.Docs/articles/modules/application_commands/intro.md
@@ -1,133 +1,133 @@
---
-uid: application_commands_intro
+uid: modules_application_commands_intro
title: Application Commands Introduction
---
>[!NOTE]
> This article assumes you've recently read the article on *[writing your first bot](xref:basics_first_bot)*.
# Introduction to App Commands
Discord provides built-in commands called: *Application Commands*.
Be sure to install the `DisCatSharp.ApplicationCommands` package from NuGet before continuing.
At the moment it is possible to create such commands:
- Slash commands
- User context menu commands
- Message context menu commands
## Writing an Application Commands
### Creation of the first commands
>[!NOTE]
> In order for the bot to be able to create commands in the guild, it must be added to a guild with `applications.commands` scope.
Each command is a method with the attribute [SlashCommand](xref:DisCatSharp.ApplicationCommands.Attributes.SlashCommandAttribute) or [ContextMenu](xref:DisCatSharp.ApplicationCommands.Attributes.ContextMenuAttribute). They must be in classes that inherit from [ApplicationCommandsModule](xref:DisCatSharp.ApplicationCommands.ApplicationCommandsModule).
Also, the first argument to the method must be [InteractionContext](xref:DisCatSharp.ApplicationCommands.Context.InteractionContext) or [ContextMenuContext](xref:DisCatSharp.ApplicationCommands.Context.ContextMenuContext).
Simple slash command:
```cs
public class MyCommand : ApplicationCommandsModule
{
[SlashCommand("my_command", "This is description of the command.")]
public async Task MySlashCommand(InteractionContext context)
{
}
}
```
Simple context menu command:
```cs
public class MySecondCommand : ApplicationCommandsModule
{
[ContextMenu(ApplicationCommandType.User, "My Command")]
public async Task MyContextMenuCommand(ContextMenuContext context)
{
}
}
```
Now let's add some actions to the commands, for example, send a reply:
```cs
await context.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
{
Content = "Hello :3"
});
```
If the command will be executed for more than 3 seconds, we must response at the beginning of execution and edit it at the end.
```cs
await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource, new DiscordInteractionResponseBuilder());
await Task.Delay(5000); // Simulating a long command execution.
await ctx.EditResponseAsync(new DiscordWebhookBuilder()
{
Content = "Hello :3"
});
```
>[!NOTE]
-> Note that you can make your commands static, but then you cannot use [Dependency Injection](xref:commands_dependency_injection) in them.
+> Note that you can make your commands static, but then you cannot use [Dependency Injection](xref:modules_commandsnext_dependency_injection) in them.
### Registration of commands
After writing the commands, we must register them. For this we need a [DiscordClient](xref:DisCatSharp.DiscordClient).
```cs
var appCommands = client.UseApplicationCommands();
appCommands.RegisterGlobalCommands();
appCommands.RegisterGlobalCommands();
```
Simple, isn't it? You can register global and guild commands.
Global commands will be available on all guilds of which the bot is a member. Guild commands will only appear in a specific guild.
>[!NOTE]
>Global commands are updated within an hour, so it is recommended to use guild commands for testing and development.
To register guild commands, it is enough to specify the Id of the guild as the first argument of the registration method.
```cs
var appCommands = client.UseApplicationCommands();
appCommands.RegisterGuildCommands();
appCommands.RegisterGuildCommands();
```
## Command Groups
Sometimes we may need to combine slash commands into groups.
In this case, we need to wrap our class with commands in another class and add the [SlashCommandGroup](xref:DisCatSharp.ApplicationCommands.Attributes.SlashCommandGroupAttribute) attribute.
```cs
public class MyCommand : ApplicationCommandsModule
{
[SlashCommandGroup("my_command", "This is description of the command group.")]
public class MyCommandGroup : ApplicationCommandsModule
{
[SlashCommand("first", "This is description of the command.")]
public async Task MySlashCommand(InteractionContext context)
{
await context.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
{
Content = "This is first subcommand."
});
}
[SlashCommand("second", "This is description of the command.")]
public async Task MySecondCommand(InteractionContext context)
{
await context.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
{
Content = "This is second subcommand."
});
}
}
}
```
Commands will now be available via `/my_command first` and `/my_command second`.
Also, note that both classes must inherit [ApplicationCommandsModule](xref:DisCatSharp.ApplicationCommands.ApplicationCommandsModule).
diff --git a/DisCatSharp.Docs/articles/modules/application_commands/modals.md b/DisCatSharp.Docs/articles/modules/application_commands/modals.md
new file mode 100644
index 000000000..fb4aa699e
--- /dev/null
+++ b/DisCatSharp.Docs/articles/modules/application_commands/modals.md
@@ -0,0 +1,37 @@
+---
+uid: modules_application_commands_modals
+title: Modals
+---
+
+# Modals
+
+**The package `DisCatSharp.Interactivity` is required for this to work.**
+
+You probably heard about the modal feature in Discord. It's a new feature that allows you to create a popup window that can be used to ask for information from the user. This is a great way to create a more interactive user experience.
+
+The code below shows an example application command on how this could look.
+
+```cs
+using DisCatSharp.Interactivity;
+using DisCatSharp.Interactivity.Enums;
+using DisCatSharp.Interactivity.Extensions;
+```
+
+```cs
+[SlashCommand("modals", "A modal!")]
+public async Task SendModalAsync(InteractionContext ctx)
+{
+ DiscordInteractionModalBuilder builder = new DiscordInteractionModalBuilder();
+ builder.WithCustomId("modal_test");
+ builder.WithTitle("Modal Test");
+ builder.AddTextComponent(new DiscordTextComponent(TextComponentStyle.Paragraph, label: "Some input", required: false)));
+
+ await ctx.CreateModalResponseAsync(builder);
+ var res = await ctx.Client.GetInteractivity().WaitForModalAsync(builder.CustomId, TimeSpan.FromMinutes(1));
+
+ if (res.TimedOut)
+ return;
+
+ await res.Result.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordWebhookBuilder().WithContent(res.Result.Interaction.Data.Components?.First()?.Value ?? "Nothing was submitted."));
+}
+```
diff --git a/DisCatSharp.Docs/articles/application_commands/options.md b/DisCatSharp.Docs/articles/modules/application_commands/options.md
similarity index 99%
rename from DisCatSharp.Docs/articles/application_commands/options.md
rename to DisCatSharp.Docs/articles/modules/application_commands/options.md
index de23d8c9d..65465a7d0 100644
--- a/DisCatSharp.Docs/articles/application_commands/options.md
+++ b/DisCatSharp.Docs/articles/modules/application_commands/options.md
@@ -1,251 +1,251 @@
---
-uid: application_commands_options
+uid: modules_application_commands_options
title: Application Commands Options
---
# Slash Commands options
For slash commands, you can create options. They allow users to submit additional information to commands.
Command options can be of the following types:
- string
- int
- long
- double
- bool
- [DiscordUser](xref:DisCatSharp.Entities.DiscordUser)
- [DiscordRole](xref:DisCatSharp.Entities.DiscordRole)
- [DiscordChannel](xref:DisCatSharp.Entities.DiscordChannel)
- [DiscordAttachment](xref:DisCatSharp.Entities.DiscordAttachment)
- mentionable (ulong)
- Enum
## Basic usage
>[!NOTE]
>Options can only be added in the slash commands. Context menus do not support this!
All of options must contain the [Option](xref:DisCatSharp.ApplicationCommands.Attributes.OptionAttribute) attribute.
They should be after [InteractionContext](xref:DisCatSharp.ApplicationCommands.Context.InteractionContext).
```cs
public class MyCommand : ApplicationCommandsModule
{
[SlashCommand("my_command", "This is description of the command.")]
public static async Task MySlashCommand(InteractionContext context, [Option("argument", "This is description of the option.")] string firstParam)
{
await context.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
{
Content = firstParam
});
}
}
```
## Choices
Sometimes, we need to allow users to choose from several pre-created options.
We can of course add a string or long parameter and let users guess the options, but why when we can make things more convenient?
We have 3 ways to make choices:
- Enums
- [Choice Attribute](xref:DisCatSharp.ApplicationCommands.Attributes.ChoiceAttribute)
- [Choice Providers](xref:DisCatSharp.ApplicationCommands.Attributes.IChoiceProvider)
### Enums
This is the easiest option. We just need to specify the required Enum as a command parameter.
```cs
public class MyCommand : ApplicationCommandsModule
{
[SlashCommand("my_command", "This is description of the command.")]
public static async Task MySlashCommand(InteractionContext context, [Option("enum_param", "Description")] MyEnum enumParameter)
{
}
}
public enum MyEnum
{
FirstOption,
SecondOption
}
```
In this case, the user will be shown this as options: `FirstOption` and `SecondOption`.
Therefore, if you want to define different names for options without changing the Enum, you can add a special attribute:
```cs
public enum MyEnum
{
[ChoiceName("First option")]
FirstOption,
[ChoiceName("Second option")]
SecondOption
}
```
### Choice Attribute
With this way, you can get rid of unnecessary conversions within the command.
To do this, you need to add one or more [Choice Attributes](xref:DisCatSharp.ApplicationCommands.Attributes.ChoiceAttribute) before the [Option](xref:DisCatSharp.ApplicationCommands.Attributes.OptionAttribute) attribute
```cs
[SlashCommand("my_command", "This is description of the command.")]
public static async Task MySlashCommand(InteractionContext context, [Choice("First option", 1)] [Choice("Second option", 2)] [Option("option", "Description")] long firstParam)
{
}
```
As the first parameter, [Choice](xref:DisCatSharp.ApplicationCommands.Attributes.ChoiceAttribute) takes a name that will be visible to the user, and the second - a value that will be passed to the command.
You can also use strings.
### Choice Provider
Perhaps the most difficult way. It consists in writing a method that will generate a list of options when registering commands.
This way we don't have to list all of them in the code when there are many of them.
To create your own provider, you need to create a class that inherits [IChoiceProvider](xref:DisCatSharp.ApplicationCommands.Attributes.IChoiceProvider) and contains the `Provider()` method.
```cs
public class MyChoiceProvider : IChoiceProvider
{
public Task> Provider()
{
}
}
```
As seen above, the method should return a list of [DiscordApplicationCommandOptionChoice](xref:DisCatSharp.Entities.DiscordApplicationCommandOptionChoice).
Now we need to create a list and add items to it:
```cs
var options = new List
{
new DiscordApplicationCommandOptionChoice("First option", 1),
new DiscordApplicationCommandOptionChoice("Second option", 2)
};
return Task.FromResult(options.AsEnumerable());
```
Of course you can generate this list as you like. The main thing is that the method should return this list.
Now let's add our new provider to the command.
```cs
[SlashCommand("my_command", "This is description of the command.")]
public static async Task MySlashCommand(InteractionContext context, [ChoiceProvider(typeof(MyChoiceProvider))] [Option("option", "Description")] long option)
{
}
```
All the code that we got:
```cs
public class MyCommand : ApplicationCommandsModule
{
[SlashCommand("my_command", "This is description of the command.")]
public static async Task MySlashCommand(InteractionContext context, [ChoiceProvider(typeof(MyChoiceProvider))] [Option("option", "Description")] long option)
{
}
}
public class MyChoiceProvider : IChoiceProvider
{
public Task> Provider()
{
var options = new List
{
new DiscordApplicationCommandOptionChoice("First option", 1),
new DiscordApplicationCommandOptionChoice("Second option", 2)
};
return Task.FromResult(options.AsEnumerable());
}
}
```
That's all, for a better example for [ChoiceProvider](xref:DisCatSharp.ApplicationCommands.Attributes.IChoiceProvider) refer to the examples.
## Autocomplete
Autocomplete works in the same way as ChoiceProvider, with one difference:
the method that creates the list of choices is triggered not once when the commands are registered, but whenever the user types a command.
It is advisable to use this method exactly when you have a list that will be updated while the bot is running.
In other cases, when the choices will not change, it is advisable to use the previous methods.
Creating an autocomplete is similar to creating a ChoiceProvider with a few changes:
```cs
public class MyAutocompleteProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext ctx)
{
var options = new List
{
new DiscordApplicationCommandAutocompleteChoice("First option", 1),
new DiscordApplicationCommandAutocompleteChoice("Second option", 2)
};
return Task.FromResult(options.AsEnumerable());
}
}
```
The changes are that instead of [IChoiceProvider](xref:DisCatSharp.ApplicationCommands.Attributes.IChoiceProvider), the class inherits [IAutocompleteProvider](xref:DisCatSharp.ApplicationCommands.Attributes.IAutocompleteProvider), and the Provider method should return a list with [DiscordApplicationCommandAutocompleteChoice](xref:DisCatSharp.Entities.DiscordApplicationCommandAutocompleteChoice).
Now we add it to the command:
```cs
[SlashCommand("my_command", "This is description of the command.")]
public static async Task MySlashCommand(InteractionContext context, [Autocomplete(typeof(MyAutocompleteProvider))] [Option("option", "Description", true)] long option)
{
}
```
Note that we have not only replaced [ChoiceProvider](xref:DisCatSharp.ApplicationCommands.Attributes.ChoiceProviderAttribute) with [Autocomplete](xref:DisCatSharp.ApplicationCommands.Attributes.AutocompleteAttribute), but also added `true` to [Option](xref:DisCatSharp.ApplicationCommands.Attributes.OptionAttribute).
## Channel types
Sometimes we may need to give users the ability to select only a certain type of channels, for example, only text, or voice channels.
This can be done by adding the [ChannelTypes](xref:DisCatSharp.ApplicationCommands.Attributes.ChannelTypesAttribute) attribute to the option with the [DiscordChannel](xref:DisCatSharp.Entities.DiscordChannel) type.
```cs
[SlashCommand("my_command", "This is description of the command.")]
public static async Task MySlashCommand(InteractionContext context, [Option("channel", "You can select only text channels."), ChannelTypes(ChannelType.Text)] DiscordChannel channel)
{
}
```
This will make it possible to select only text channels.
## MinimumValue / MaximumValue Attribute
Sometimes we may need to give users the ability to select only a certain range of values.
This can be done by adding the [MinimumValue](xref:DisCatSharp.ApplicationCommands.Attributes.MinimumValueAttribute) and [MaximumValue](xref:DisCatSharp.ApplicationCommands.Attributes.MaximumValueAttribute) attribute to the option.
It can be used only for the types `double`, `int` and `long` tho.
```cs
[SlashCommand("my_command", "This is description of the command.")]
public static async Task MySlashCommand(InteractionContext context, [Option("number", "You can select only a certain range."), MinimumValue(50), MaximumValue(100)] int numbers)
{
}
```
## MinimumLength / MaximumLength Attribute
Sometimes we may need to limit the user to a certain string length.
This can be done by adding the [MinimumLength](xref:DisCatSharp.ApplicationCommands.Attributes.MinimumLengthAttribute) and [MaximumLength](xref:DisCatSharp.ApplicationCommands.Attributes.MaximumLengthAttribute) attribute to the option.
It can be used only for the type `string`.
```cs
[SlashCommand("my_command", "This is description of the command.")]
public static async Task MySlashCommand(InteractionContext context, [Option("text", "You can only send text with a length between 10 and 50 characters."), MinimumLength(10), MaximumLength(50)] string text)
{
}
```
diff --git a/DisCatSharp.Docs/articles/application_commands/paginated_modals.md b/DisCatSharp.Docs/articles/modules/application_commands/paginated_modals.md
similarity index 98%
rename from DisCatSharp.Docs/articles/application_commands/paginated_modals.md
rename to DisCatSharp.Docs/articles/modules/application_commands/paginated_modals.md
index c719aff6e..1246e575c 100644
--- a/DisCatSharp.Docs/articles/application_commands/paginated_modals.md
+++ b/DisCatSharp.Docs/articles/modules/application_commands/paginated_modals.md
@@ -1,52 +1,52 @@
---
-uid: paginated_modals
+uid: modules_application_commands_paginated_modals
title: Paginated Modals
---
# Paginated Modals
**The package `DisCatSharp.Interactivity` is required for this to work.**
You may need multi-step modals to collect a variety of information from a user. We implemented an easy way of doing this with paginated modals.
You simply construct all your modals, call `DiscordInteraction.CreatePaginatedModalResponseAsync` and you're good to go. After the user submitted all modals, you'll get back a `PaginatedModalResponse` which has a `TimedOut` bool, the `DiscordInteraction` that was used to submit the last modal and a `IReadOnlyDictionary` with the component custom ids as key.
The code below shows an example application command on how this could look.
```cs
using DisCatSharp.Interactivity;
using DisCatSharp.Interactivity.Enums;
using DisCatSharp.Interactivity.Extensions;
```
```cs
[SlashCommand("paginated-modals", "Paginated modals!")]
public async Task PaginatedModals(InteractionContext ctx)
{
_ = Task.Run(async () =>
{
var responses = await ctx.Interaction.CreatePaginatedModalResponseAsync(
new List()
{
new ModalPage(new DiscordInteractionModalBuilder().WithTitle("First Title")
.AddModalComponents(new DiscordTextComponent(TextComponentStyle.Small, "title", "Title", "Name", 0, 250, false))),
new ModalPage(new DiscordInteractionModalBuilder().WithTitle("Second Title")
.AddModalComponents(new DiscordTextComponent(TextComponentStyle.Small, "title1", "Next Modal", "Some value here"))
.AddModalComponents(new DiscordTextComponent(TextComponentStyle.Paragraph, "description1", "Some bigger thing here", required: false))),
new ModalPage(new DiscordInteractionModalBuilder().WithTitle("Third Title")
.AddModalComponents(new DiscordTextComponent(TextComponentStyle.Small, "title2", "Title2", "Even more here", 0, 250, false))
.AddModalComponents(new DiscordTextComponent(TextComponentStyle.Paragraph, "description2", "and stuff here", required: false))),
});
// If the user didn't submit all modals, TimedOut will be true. We return the command as there is nothing to handle.
if (responses.TimedOut)
return;
// We simply throw all response into the Console, you can do whatever with this.
foreach (var b in responses.Responses)
Console.WriteLine(b.ToString());
// We use EditOriginalResponseAsync here because CreatePaginatedModalResponseAsync responds to the last modal with a thinking state.
await responses.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent("Success"));
});
}
```
diff --git a/DisCatSharp.Docs/articles/application_commands/translations/reference.md b/DisCatSharp.Docs/articles/modules/application_commands/translations/reference.md
similarity index 90%
rename from DisCatSharp.Docs/articles/application_commands/translations/reference.md
rename to DisCatSharp.Docs/articles/modules/application_commands/translations/reference.md
index c729d1932..2fbe085d1 100644
--- a/DisCatSharp.Docs/articles/application_commands/translations/reference.md
+++ b/DisCatSharp.Docs/articles/modules/application_commands/translations/reference.md
@@ -1,110 +1,114 @@
---
-uid: application_commands_translations_reference
+uid: modules_application_commands_translations_reference
title: Translation Reference
---
# Translation Reference
> [!NOTE]
> DisCatSharp uses [JSON](https://www.json.org) to inject the translations of [Application Commands](https://discord.com/developers/docs/interactions/application-commands).
## Command Object
| Key | Value | Description |
| ------------------------ | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| name | string | name of the application command |
+| description? | string | description of the application command |
| type | int | [type](#application-command-type) of application command, used to map command types |
| name_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command name |
| description_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command description, only valid for slash commands |
| options | array of [Option Objects](#option-object) | array of option objects containing translations |
### Application Command Type
| Type | Value |
| ---------------------------- | ----- |
| Slash Command | 1 |
| User Context Menu Command | 2 |
| Message Context Menu Command | 3 |
## Command Group Object
| Key | Value | Description |
| ------------------------ | --------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| name | string | name of the application command group |
+| description? | string | description of the application command group |
| name_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command group name |
| description_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command group description |
| commands | array of [Command Objects](#command-object) | array of command objects containing translations |
| groups | array of [Sub Command Group Objects](#sub-command-group-object) | array of sub command group objects containing translations |
## Sub Command Group Object
| Key | Value | Description |
| ------------------------ | --------------------------------------------- | -------------------------------------------------------------------------------------- |
| name | string | name of the application command sub group |
+| description? | string | description of the application command group |
| name_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command sub group name |
| description_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command sub group description |
| commands | array of [Command Objects](#command-object) | array of command objects containing translations |
## Option Object
| Key | Value | Description |
| ------------------------ | ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| name | string | name of the application command option |
+| description? | string | description of the application command group |
| name_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command option name |
| description_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command option description |
| choices | array of [Option Choice Objects](#option-choice-object) | array of option choice objects containing translations |
## Option Choice Object
| Key | Value | Description |
| ------------------------ | --------------------------------------------- | ----------------------------------------------------------------------------------- |
| name | string | name of the application command option choice |
| name_translations | array of [Translation KVPs](#translation-kvp) | array of translation key-value-pairs for the application command option choice name |
## Translation KVP
A translation object is a key-value-pair of `"locale": "value"`.
### Example Translation Array:
```json
{
"en-US": "Hello",
"de": "Hallo"
}
```
## Valid Locales
| Locale | Language |
| ------ | --------------------- |
| da | Danish |
| de | German |
| en-GB | English, UK |
| en-US | English, US |
| es-ES | Spanish |
| fr | French |
| hr | Croatian |
| it | Italian |
| lt | Lithuanian |
| hu | Hungarian |
| nl | Dutch |
| no | Norwegian |
| pl | Polish |
| pt-BR | Portuguese, Brazilian |
| ro | Romanian, Romania |
| fi | Finnish |
| sv-SE | Swedish |
| vi | Vietnamese |
| tr | Turkish |
| cs | Czech |
| el | Greek |
| bg | Bulgarian |
| ru | Russian |
| uk | Ukrainian |
| hi | Hindi |
| th | Thai |
| zh-CN | Chinese, China |
| ja | Japanese |
| zh-TW | Chinese, Taiwan |
| ko | Korean |
diff --git a/DisCatSharp.Docs/articles/application_commands/translations/using.md b/DisCatSharp.Docs/articles/modules/application_commands/translations/using.md
similarity index 87%
rename from DisCatSharp.Docs/articles/application_commands/translations/using.md
rename to DisCatSharp.Docs/articles/modules/application_commands/translations/using.md
index dba6a0c9e..9783f5c01 100644
--- a/DisCatSharp.Docs/articles/application_commands/translations/using.md
+++ b/DisCatSharp.Docs/articles/modules/application_commands/translations/using.md
@@ -1,197 +1,201 @@
---
-uid: application_commands_translations_using
+uid: modules_application_commands_translations_using
title: Using Translations
---
# Using Translations
## Why Do We Outsource Translation In External JSON Files
Pretty simple: It's common to have translations external stored.
This makes it easier to modify them, while keeping the code itself clean.
## Adding Translations
Translations are added the same way like permissions are added to Application Commands:
```cs
const string TRANSLATION_PATH = "translations/";
Client.GetApplicationCommands().RegisterGuildCommands(1215484634894646844, translations =>
{
string json = File.ReadAllText(TRANSLATION_PATH + "my_command.json");
translations.AddTranslation(json);
});
Client.GetApplicationCommands().RegisterGuildCommands(1215484634894646844, translations =>
{
string json = File.ReadAllText(TRANSLATION_PATH + "my_simple_command.json");
translations.AddTranslation(json);
});
```
> [!WARNING]
> If you add a translation to a class, you have to supply translations for every command in this class. Otherwise it will fail.
## Creating The Translation JSON
We split the translation in two categories.
One for slash command groups and one for normal slash commands and context menu commands.
The `name` key in the JSON will be mapped to the command / option / choice names internally.
### Translation For Slash Command Groups
Imagine, your class look like the following example:
```cs
public class MyCommand : ApplicationCommandsModule
{
[SlashCommandGroup("my_command", "This is description of the command group.")]
public class MyCommandGroup : ApplicationCommandsModule
{
[SlashCommand("first", "This is description of the command.")]
public async Task MySlashCommand(InteractionContext context)
{
await context.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
{
Content = "This is first subcommand."
});
}
[SlashCommand("second", "This is description of the command.")]
public async Task MySecondCommand(InteractionContext context, [Option("value", "Some string value.")] string value)
{
await context.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
{
Content = "This is second subcommand. The value was " + value
});
}
}
}
```
-The translation json is a object of [Command Group Objects](xref:application_commands_translations_reference#command-group-object)
+The translation json is a object of [Command Group Objects](xref:modules_application_commands_translations_reference#command-group-object)
A correct translation json for english and german would look like that:
```json
[
{
"name": "my_command",
"name_translations": {
"en-US": "my_command",
"de": "mein_befehl"
},
"description_translations": {
"en-US": "This is description of the command group.",
"de": "Das ist die description der Befehl Gruppe."
},
"commands": [
{
"name": "first",
"type": 1, // Type 1 for slash command
"name_translations": {
"en-US": "first",
"de": "erste"
},
"description_translations": {
"en-US": "This is description of the command.",
"de": "Das ist die Beschreibung des Befehls."
}
},
{
"name": "second",
"type": 1, // Type 1 for slash command
"name_translations": {
"en-US": "second",
"de": "zweite"
},
"description_translations": {
"en-US": "This is description of the command.",
"de": "Das ist die Beschreibung des Befehls."
},
"options": [
{
"name": "value",
"name_translations": {
"en-US": "value",
"de": "wert"
},
"description_translations": {
"en-US": "Some string value.",
"de": "Ein string Wert."
}
}
]
}
]
}
]
```
### Translation For Slash Commands & Context Menu Commands
Now imagine, that your class look like this example:
```cs
public class MySimpleCommands : ApplicationCommandsModule
{
[SlashCommand("my_command", "This is description of the command.")]
public async Task MySlashCommand(InteractionContext context)
{
}
[ContextMenu(ApplicationCommandType.User, "My Command")]
public async Task MyContextMenuCommand(ContextMenuContext context)
{
}
}
```
-The slash command is a simple [Command Object](xref:application_commands_translations_reference#command-object).
+The slash command is a simple [Command Object](xref:modules_application_commands_translations_reference#command-object).
Same goes for the context menu command, but note that it can't have a description.
-Slash Commands has the [type](xref:application_commands_translations_reference#application-command-type) `1` and context menu commands the [type](xref:application_commands_translations_reference#application-command-type) `2` or `3`.
+Slash Commands has the [type](xref:modules_application_commands_translations_reference#application-command-type) `1` and context menu commands the [type](xref:modules_application_commands_translations_reference#application-command-type) `2` or `3`.
We use this to determine, where the translation belongs to.
+Please note that the description field is optional. We suggest setting it for slash commands if you want to use our translation generator, which we're building right now.
+Context menu commands can't have a description, so omit it.
+
A correct json for this example would look like that:
```json
[
{
"name":"my_command",
+ "description": "This is description of the command.",
"type": 1, // Type 1 for slash command
"name_translations":{
"en-US":"my_command",
"de":"mein_befehl"
},
"description_translations":{
"en-US":"This is description of the command.",
"de":"Das ist die Beschreibung des Befehls."
}
},
{
"name":"My Command",
"type": 2, // Type 2 for user context menu command
"name_translations":{
"en-US":"My Command",
"de":"Mein Befehl"
}
}
]
```
## Available Locales
Discord has a limited choice of locales, in particular, the ones you can select in the client.
-To see the available locales, visit [this](xref:application_commands_translations_reference#valid-locales) page.
+To see the available locales, visit [this](xref:modules_application_commands_translations_reference#valid-locales) page.
## Can We Get The User And Guild Locale?
Yes, you can!
Discord sends the user on all [interaction types](xref:DisCatSharp.InteractionType), except `Ping`.
We introduced two new properties `Locale` and `GuildLocale` on [InteractionContext](xref:DisCatSharp.ApplicationCommands.Context.InteractionContext), [ContextMenuContext](xref:DisCatSharp.ApplicationCommands.Context.ContextMenuContext), [AutoCompleteContext](xref:DisCatSharp.ApplicationCommands.Context.AutocompleteContext) and [DiscordInteraction](xref:DisCatSharp.Entities.DiscordInteraction).
`Locale` is the locale of the user and always represented.
`GuildLocale` is only represented, when the interaction is **not** in a DM.
Furthermore we cache known user locales on the [DiscordUser](xref:DisCatSharp.Entities.DiscordUser#DisCatSharp_Entities_DiscordUser_Locale) object.
diff --git a/DisCatSharp.Docs/articles/audio/lavalink/configuration.md b/DisCatSharp.Docs/articles/modules/audio/lavalink/configuration.md
similarity index 98%
rename from DisCatSharp.Docs/articles/audio/lavalink/configuration.md
rename to DisCatSharp.Docs/articles/modules/audio/lavalink/configuration.md
index 437daaf65..fe976aebe 100644
--- a/DisCatSharp.Docs/articles/audio/lavalink/configuration.md
+++ b/DisCatSharp.Docs/articles/modules/audio/lavalink/configuration.md
@@ -1,112 +1,112 @@
---
-uid: audio_lavalink_configuration
+uid: modules_audio_lavalink_configuration
title: Lavalink Configuration
---
# Setting up DisCatSharp.Lavalink
## Configuring Your Client
To begin using DisCatSharp's Lavalink client, you will need to add the `DisCatSharp.Lavalink` nuget package. Once installed, simply add these namespaces at the top of your bot file:
```csharp
using DisCatSharp.Net;
using DisCatSharp.Lavalink;
```
After that, we will need to create a configuration for our extension to use. This is where the special values from the server configuration are used.
```csharp
var endpoint = new ConnectionEndpoint
{
Hostname = "127.0.0.1", // From your server configuration.
Port = 2333 // From your server configuration
};
var lavalinkConfig = new LavalinkConfiguration
{
Password = "youshallnotpass", // From your server configuration.
RestEndpoint = endpoint,
SocketEndpoint = endpoint
};
```
Finally, initialize the extension.
```csharp
var lavalink = Discord.UseLavalink();
```
## Connecting with Lavalink
We are now ready to connect to the server. Call the Lavalink extension's connect method and pass the configuration. Make sure to call this **after** your Discord client connects. This can be called either directly after your client's connect method or in your client's ready event.
```csharp
LavalinkNode = await Lavalink.ConnectAsync(lavalinkConfig);
```
Your main bot file should now look like this:
```csharp
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using DisCatSharp;
using DisCatSharp.Net;
using DisCatSharp.Lavalink;
namespace MyFirstMusicBot
{
class Program
{
public static DiscordClient Discord;
static void Main(string[] args)
{
MainAsync(args).ConfigureAwait(false).GetAwaiter().GetResult();
}
static async Task MainAsync(string[] args)
{
Discord = new DiscordClient(new DiscordConfiguration
{
Token = "",
TokenType = TokenType.Bot,
MinimumLogLevel = LogLevel.Debug
});
var endpoint = new ConnectionEndpoint
{
Hostname = "127.0.0.1", // From your server configuration.
Port = 2333 // From your server configuration
};
var lavalinkConfig = new LavalinkConfiguration
{
Password = "youshallnotpass", // From your server configuration.
RestEndpoint = endpoint,
SocketEndpoint = endpoint
};
var lavalink = Discord.UseLavalink();
await Discord.ConnectAsync();
await lavalink.ConnectAsync(lavalinkConfig); // Make sure this is after Discord.ConnectAsync().
await Task.Delay(-1);
}
}
}
```
We are now ready to start the bot. If everything is configured properly, you should see a Lavalink connection appear in your DisCatSharp console:
```
[2020-10-10 17:56:07 -04:00] [403 /LavalinkConn] [Debug] Connection to Lavalink node established
```
And a client connection appear in your Lavalink console:
```
INFO 5180 --- [ XNIO-1 task-1] io.undertow.servlet : Initializing Spring DispatcherServlet 'dispatcherServlet'
INFO 5180 --- [ XNIO-1 task-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
INFO 5180 --- [ XNIO-1 task-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 8 ms
INFO 5180 --- [ XNIO-1 task-1] l.server.io.HandshakeInterceptorImpl : Incoming connection from /0:0:0:0:0:0:0:1:58238
INFO 5180 --- [ XNIO-1 task-1] lavalink.server.io.SocketServer : Connection successfully established from /0:0:0:0:0:0:0:1:58238
```
We are now ready to set up some music commands!
diff --git a/DisCatSharp.Docs/articles/audio/lavalink/music_commands.md b/DisCatSharp.Docs/articles/modules/audio/lavalink/music_commands.md
similarity index 98%
rename from DisCatSharp.Docs/articles/audio/lavalink/music_commands.md
rename to DisCatSharp.Docs/articles/modules/audio/lavalink/music_commands.md
index 782af98d4..add84cb2e 100644
--- a/DisCatSharp.Docs/articles/audio/lavalink/music_commands.md
+++ b/DisCatSharp.Docs/articles/modules/audio/lavalink/music_commands.md
@@ -1,338 +1,338 @@
---
-uid: audio_lavalink_music_commands
+uid: modules_audio_lavalink_music_commands
title: Lavalink Music Commands
---
# Adding Music Commands
-This article assumes that you know how to use CommandsNext. If you do not, you should learn [here](xref:commands_intro) before continuing with this guide.
+This article assumes that you know how to use CommandsNext. If you do not, you should learn [here](xref:modules_commandsnext_intro) before continuing with this guide.
## Prerequisites
Before we start we will need to make sure CommandsNext is configured. For this we can make a simple configuration and command class:
```csharp
using DisCatSharp.CommandsNext;
namespace MyFirstMusicBot
{
public class MyLavalinkCommands : BaseCommandModule
{
}
}
```
And be sure to register it in your program file:
```csharp
CommandsNext = Discord.UseCommandsNext(new CommandsNextConfiguration
{
StringPrefixes = new string[] { ";;" }
});
CommandsNext.RegisterCommands();
```
## Adding join and leave commands
Your bot, and Lavalink, will need to connect to a voice channel to play music. Let's create the base for these commands:
```csharp
[Command]
public async Task Join(CommandContext ctx, DiscordChannel channel)
{
}
[Command]
public async Task Leave(CommandContext ctx, DiscordChannel channel)
{
}
```
In order to connect to a voice channel, we'll need to do a few things.
1. Get our node connection. You can either use linq or `GetIdealNodeConnection()`
2. Check if the channel is a voice channel, and tell the user if not.
3. Connect the node to the channel.
And for the leave command:
1. Get the node connection, using the same process.
2. Check if the channel is a voice channel, and tell the user if not.
3. Get our existing connection.
4. Check if the connection exists, and tell the user if not.
5. Disconnect from the channel.
`GetIdealNodeConnection()` will return the least affected node through load balancing, which is useful for larger bots. It can also filter nodes based on an optional voice region to use the closest nodes available. Since we only have one connection we can use linq's `.First()` method on the extensions connected nodes to get what we need.
So far, your command class should look something like this:
```csharp
using System.Threading.Tasks;
using DisCatSharp;
using DisCatSharp.Entities;
using DisCatSharp.CommandsNext;
using DisCatSharp.CommandsNext.Attributes;
namespace MyFirstMusicBot
{
public class MyLavalinkCommands : BaseCommandModule
{
[Command]
public async Task Join(CommandContext ctx, DiscordChannel channel)
{
var lava = ctx.Client.GetLavalink();
if (!lava.ConnectedNodes.Any())
{
await ctx.RespondAsync("The Lavalink connection is not established");
return;
}
var node = lava.ConnectedNodes.Values.First();
if (channel.Type != ChannelType.Voice)
{
await ctx.RespondAsync("Not a valid voice channel.");
return;
}
await node.ConnectAsync(channel);
await ctx.RespondAsync($"Joined {channel.Name}!");
}
[Command]
public async Task Leave(CommandContext ctx, DiscordChannel channel)
{
var lava = ctx.Client.GetLavalink();
if (!lava.ConnectedNodes.Any())
{
await ctx.RespondAsync("The Lavalink connection is not established");
return;
}
var node = lava.ConnectedNodes.Values.First();
if (channel.Type != ChannelType.Voice)
{
await ctx.RespondAsync("Not a valid voice channel.");
return;
}
var conn = node.GetGuildConnection(channel.Guild);
if (conn == null)
{
await ctx.RespondAsync("Lavalink is not connected.");
return;
}
await conn.DisconnectAsync();
await ctx.RespondAsync($"Left {channel.Name}!");
}
}
}
```
## Adding player commands
Now that we can join a voice channel, we can make our bot play music! Let's now create the base for a play command:
```csharp
[Command]
public async Task Play(CommandContext ctx, [RemainingText] string search)
{
}
```
One of Lavalink's best features is its ability to search for tracks from a variety of media sources, such as YouTube, SoundCloud, Twitch, and more. This is what makes bots like Rythm, Fredboat, and Groovy popular. The search is used in a REST request to get the track data, which is then sent through the WebSocket connection to play the track in the voice channel. That is what we will be doing in this command.
Lavalink can also play tracks directly from a media url, in which case the play command can look like this:
```csharp
[Command]
public async Task Play(CommandContext ctx, Uri url)
{
}
```
Like before, we will need to get our node and guild connection and have the appropriate checks. Since it wouldn't make sense to have the channel as a parameter, we will instead get it from the member's voice state:
```csharp
//Important to check the voice state itself first,
//as it may throw a NullReferenceException if they don't have a voice state.
if (ctx.Member.VoiceState == null || ctx.Member.VoiceState.Channel == null)
{
await ctx.RespondAsync("You are not in a voice channel.");
return;
}
var lava = ctx.Client.GetLavalink();
var node = lava.ConnectedNodes.Values.First();
var conn = node.GetGuildConnection(ctx.Member.VoiceState.Guild);
if (conn == null)
{
await ctx.RespondAsync("Lavalink is not connected.");
return;
}
```
Next, we will get the track details by calling `node.Rest.GetTracksAsync()`. There are a variety of overloads for this:
1. `GetTracksAsync(LavalinkSearchType.Youtube, search)` will search YouTube for your search string.
2. `GetTracksAsync(LavalinkSearchType.SoundCloud, search)` will search SoundCloud for your search string.
3. `GetTracksAsync(Uri)` will use the direct url to obtain the track. This is mainly used for the other media sources.
For this guide we will be searching YouTube. Let's pass in our search string and store the result in a variable:
```csharp
//We don't need to specify the search type here
//since it is YouTube by default.
var loadResult = await node.Rest.GetTracksAsync(search);
```
The load result will contain an enum called `LoadResultType`, which will inform us if Lavalink was able to retrieve the track data. We can use this as a check:
```csharp
//If something went wrong on Lavalink's end
if (loadResult.LoadResultType == LavalinkLoadResultType.LoadFailed
//or it just couldn't find anything.
|| loadResult.LoadResultType == LavalinkLoadResultType.NoMatches)
{
await ctx.RespondAsync($"Track search failed for {search}.");
return;
}
```
Lavalink will return the track data from your search in a collection called `loadResult.Tracks`, similar to using the search bar in YouTube or SoundCloud directly. The first track is typically the most accurate one, so that is what we will use:
```csharp
var track = loadResult.Tracks.First();
```
And finally, we can play the track:
```csharp
await conn.PlayAsync(track);
await ctx.RespondAsync($"Now playing {track.Title}!");
```
Your play command should look like this:
```csharp
[Command]
public async Task Play(CommandContext ctx, [RemainingText] string search)
{
if (ctx.Member.VoiceState == null || ctx.Member.VoiceState.Channel == null)
{
await ctx.RespondAsync("You are not in a voice channel.");
return;
}
var lava = ctx.Client.GetLavalink();
var node = lava.ConnectedNodes.Values.First();
var conn = node.GetGuildConnection(ctx.Member.VoiceState.Guild);
if (conn == null)
{
await ctx.RespondAsync("Lavalink is not connected.");
return;
}
var loadResult = await node.Rest.GetTracksAsync(search);
if (loadResult.LoadResultType == LavalinkLoadResultType.LoadFailed
|| loadResult.LoadResultType == LavalinkLoadResultType.NoMatches)
{
await ctx.RespondAsync($"Track search failed for {search}.");
return;
}
var track = loadResult.Tracks.First();
await conn.PlayAsync(track);
await ctx.RespondAsync($"Now playing {track.Title}!");
}
```
Being able to pause the player is also useful. For this we can use most of the base from the play command:
```csharp
[Command]
public async Task Pause(CommandContext ctx)
{
if (ctx.Member.VoiceState == null || ctx.Member.VoiceState.Channel == null)
{
await ctx.RespondAsync("You are not in a voice channel.");
return;
}
var lava = ctx.Client.GetLavalink();
var node = lava.ConnectedNodes.Values.First();
var conn = node.GetGuildConnection(ctx.Member.VoiceState.Guild);
if (conn == null)
{
await ctx.RespondAsync("Lavalink is not connected.");
return;
}
}
```
For this command we will also want to check the player state to determine if we should send a pause command. We can do so by checking `conn.CurrentState.CurrentTrack`:
```csharp
if (conn.CurrentState.CurrentTrack == null)
{
await ctx.RespondAsync("There are no tracks loaded.");
return;
}
```
And finally, we can call pause:
```csharp
await conn.PauseAsync();
```
The finished command should look like so:
```csharp
[Command]
public async Task Pause(CommandContext ctx)
{
if (ctx.Member.VoiceState == null || ctx.Member.VoiceState.Channel == null)
{
await ctx.RespondAsync("You are not in a voice channel.");
return;
}
var lava = ctx.Client.GetLavalink();
var node = lava.ConnectedNodes.Values.First();
var conn = node.GetGuildConnection(ctx.Member.VoiceState.Guild);
if (conn == null)
{
await ctx.RespondAsync("Lavalink is not connected.");
return;
}
if (conn.CurrentState.CurrentTrack == null)
{
await ctx.RespondAsync("There are no tracks loaded.");
return;
}
await conn.PauseAsync();
}
```
Of course, there are other commands Lavalink has to offer. Check out [the docs](https://docs.dcs.aitsys.dev/api/DisCatSharp.Lavalink.LavalinkGuildConnection.html#methods) to view the commands you can use while playing tracks.
diff --git a/DisCatSharp.Docs/articles/audio/lavalink/setup.md b/DisCatSharp.Docs/articles/modules/audio/lavalink/setup.md
similarity index 95%
rename from DisCatSharp.Docs/articles/audio/lavalink/setup.md
rename to DisCatSharp.Docs/articles/modules/audio/lavalink/setup.md
index cff66251b..393601327 100644
--- a/DisCatSharp.Docs/articles/audio/lavalink/setup.md
+++ b/DisCatSharp.Docs/articles/modules/audio/lavalink/setup.md
@@ -1,93 +1,93 @@
---
-uid: audio_lavalink_setup
+uid: modules_audio_lavalink_setup
title: Lavalink Setup
---
# Lavalink - the newer, better way to do music
-[Lavalink](https://github.com/freyacodes/Lavalink) is a standalone program, written in Java. It's a
-lightweight solution for playing music from sources such as YouTube or
-Soundcloud. Unlike raw voice solutions, such as VoiceNext, Lavalink can handle
+[Lavalink](https://github.com/freyacodes/Lavalink) is a standalone program, written in Java. It's a
+lightweight solution for playing music from sources such as YouTube or
+Soundcloud. Unlike raw voice solutions, such as VoiceNext, Lavalink can handle
hundreds of concurrent streams, and supports sharding.
## Configuring Java
In order to run Lavalink, you must have Java 13 or greater installed. Certain Java versions may not be functional with Lavalink, so it is best to check the [requirements](https://github.com/freyacodes/Lavalink#requirements) before downloading.
The latest releases can be found [here](https://www.oracle.com/technetwork/java/javase/downloads/index.html).
Make sure the location of the newest JRE's bin folder is added to your system variable's path. This will make the `java` command run from the latest runtime. You can verify that you have the right version by entering `java -version` in your command prompt or terminal.
-## Downloading Lavalink
+## Downloading Lavalink
Next, head over to the [releases](https://github.com/freyacodes/Lavalink/releases) tab on the Lavalink GitHub page and download the Jar file from the latest version. Alternatively, stable builds with the latest changes can be found on their [CI Server](https://ci.fredboat.com/viewLog.html?buildId=lastSuccessful&buildTypeId=Lavalink_Build&tab=artifacts&guest=1).
The program will not be ready to run yet, as you will need to create a configuration file first. To do so, create a new YAML file called `application.yml`, and use the [example file](https://github.com/freyacodes/Lavalink/blob/master/LavalinkServer/application.yml.example), or copy this text:
```yaml
server: # REST and WS server
port: 2333
address: 127.0.0.1
spring:
main:
banner-mode: log
lavalink:
server:
password: "youshallnotpass"
sources:
youtube: true
bandcamp: true
soundcloud: true
twitch: true
vimeo: true
mixer: true
http: true
local: false
bufferDurationMs: 400
youtubePlaylistLoadLimit: 6 # Number of pages at 100 each
youtubeSearchEnabled: true
soundcloudSearchEnabled: true
gc-warnings: true
metrics:
prometheus:
enabled: false
endpoint: /metrics
sentry:
dsn: ""
# tags:
# some_key: some_value
# another_key: another_value
logging:
file:
max-history: 30
max-size: 1GB
path: ./logs/
level:
root: INFO
lavalink: INFO
```
YAML is whitespace-sensitive. Make sure you are using a text editor which properly handles this.
There are a few values to keep in mind.
-`host` is the IP of the Lavalink host. This will be `0.0.0.0` by default, but it should be changed as it is a security risk. For this guide, set this to `127.0.0.1` as we will be running Lavalink locally.
+`host` is the IP of the Lavalink host. This will be `0.0.0.0` by default, but it should be changed as it is a security risk. For this guide, set this to `127.0.0.1` as we will be running Lavalink locally.
`port` is the allowed port for the Lavalink connection. `2333` is the default port, and is what will be used for this guide.
`password` is the password that you will need to specify when connecting. This can be anything as long as it is a valid YAML string. Keep it as `youshallnotpass` for this guide.
When you are finished configuring this, save the file in the same directory as your Lavalink executable.
Keep note of your `port`, `address`, and `password` values, as you will need them later for connecting.
## Starting Lavalink
Open your command prompt or terminal and navigate to the directory containing Lavalink. Once there, type `java -jar Lavalink.jar`. You should start seeing log output from Lavalink.
-If everything is configured properly, you should see this appear somewhere in the log output without any errors:
+If everything is configured properly, you should see this appear somewhere in the log output without any errors:
```
[ main] lavalink.server.Launcher : Started Launcher in 5.769 seconds (JVM running for 6.758)
```
If it does, congratulations. We are now ready to interact with it using DisCatSharp.
diff --git a/DisCatSharp.Docs/articles/audio/voicenext/prerequisites.md b/DisCatSharp.Docs/articles/modules/audio/voicenext/prerequisites.md
similarity index 83%
rename from DisCatSharp.Docs/articles/audio/voicenext/prerequisites.md
rename to DisCatSharp.Docs/articles/modules/audio/voicenext/prerequisites.md
index 692ded601..483210fa6 100644
--- a/DisCatSharp.Docs/articles/audio/voicenext/prerequisites.md
+++ b/DisCatSharp.Docs/articles/modules/audio/voicenext/prerequisites.md
@@ -1,37 +1,42 @@
---
-uid: voicenext_prerequisites
+uid: modules_audio_voicenext_prerequisites
title: VoiceNext Prerequisites
---
+# VoiceNext Prerequisites
+
+> [!NOTE]
+ > We highly suggest using the [DisCatSharp.Lavalink](xref:modules_audio_lavalink_configuration) package for audio playback. It is much easier to use and has a lot of features that VoiceNext does not have.
+
## Required Libraries
VoiceNext depends on the [libsodium](https://github.com/jedisct1/libsodium) and [Opus](https://opus-codec.org/) libraries to decrypt and process audio packets.
Both *must* be available on your development and host machines otherwise VoiceNext will *not* work.
### Windows
When installing VoiceNext though NuGet, an additional package containing the native Windows binaries will automatically be included with **no additional steps required**.
However, if you are using DisCatSharp from source or without a NuGet package manager, you must manually [download](xref:natives) the binaries and place them at the root of your working directory where your application is located.
### MacOS
Native libraries for Apple's macOS can be installed using the [Homebrew](https://brew.sh) package manager:
```console
$ brew install opus libsodium
```
### Linux
#### Debian and Derivatives
Opus package naming is consistent across Debian, Ubuntu, and Linux Mint.
```bash
sudo apt-get install libopus0 libopus-dev
```
Package naming for *libsodium* will vary depending on your distro and version:
Distributions|Terminal Command
:---:|:---:
Ubuntu 18.04+, Debian 10+|`sudo apt-get install libsodium23 libsodium-dev`
Linux Mint, Ubuntu 16.04, Debian 9 |`sudo apt-get install libsodium18 libsodium-dev`
Debian 8|`sudo apt-get install libsodium13 libsodium-dev`
diff --git a/DisCatSharp.Docs/articles/audio/voicenext/receive.md b/DisCatSharp.Docs/articles/modules/audio/voicenext/receive.md
similarity index 98%
rename from DisCatSharp.Docs/articles/audio/voicenext/receive.md
rename to DisCatSharp.Docs/articles/modules/audio/voicenext/receive.md
index aa78c36cf..b07b5e118 100644
--- a/DisCatSharp.Docs/articles/audio/voicenext/receive.md
+++ b/DisCatSharp.Docs/articles/modules/audio/voicenext/receive.md
@@ -1,101 +1,101 @@
---
-uid: voicenext_receive
+uid: modules_audio_voicenext_receive
title: Receiving
---
## Receiving with VoiceNext
### Enable Receiver
Receiving incoming audio is disabled by default to save on bandwidth, as most users will never make use of incoming data.
This can be changed by providing a configuration object to `DiscordClient#UseVoiceNext()`.
```cs
var discord = new DiscordClient();
discord.UseVoiceNext(new VoiceNextConfiguration()
{
EnableIncoming = true
});
```
### Establish Connection
The voice channel join process is the exact same as when transmitting.
```cs
DiscordChannel channel;
VoiceNextConnection connection = await channel.ConnectAsync();
```
### Write Event Handler
We'll be able to receive incoming audio from the `VoiceReceived` event fired by `VoiceNextConnection`.
```cs
connection.VoiceReceived += ReceiveHandler;
```
-Writing the logic for this event handler will depend on your overall goal.
+Writing the logic for this event handler will depend on your overall goal.
The event arguments will contain a PCM audio packet for you to make use of.
You can convert each packet to another format, concatenate them all together, feed them into an external program, or process the packets any way that'll suit your needs.
When a user is speaking, `VoiceReceived` should fire once every twenty milliseconds and its packet will contain around twenty milliseconds worth of audio; this can vary due to differences in client settings.
To help keep track of the torrent of packets for each user, you can use user IDs in combination the synchronization value (SSRC) sent by Discord to determine the source of each packet.
This short-and-simple example will use [ffmpeg](https://ffmpeg.org/about.html) to convert each packet to a *wav* file.
```cs
private async Task ReceiveHandler(VoiceNextConnection _, VoiceReceiveEventArgs args)
{
var name = DateTimeOffset.Now.ToUnixTimeMilliseconds();
var ffmpeg = Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $@"-ac 2 -f s16le -ar 48000 -i pipe:0 -ac 2 -ar 44100 {name}.wav",
RedirectStandardInput = true
});
await ffmpeg.StandardInput.BaseStream.WriteAsync(args.PcmData);
}
```
That's really all there is to it. Connect to a voice channel, hook an event, process the data as you see fit.
![Wav Files](/images/voicenext_receive_01.png)
## Example Commands
```cs
[Command("start")]
public async Task StartCommand(CommandContext ctx, DiscordChannel channel = null)
{
channel ??= ctx.Member.VoiceState?.Channel;
var connection = await channel.ConnectAsync();
Directory.CreateDirectory("Output");
connection.VoiceReceived += VoiceReceiveHandler;
}
[Command("stop")]
public Task StopCommand(CommandContext ctx)
{
var vnext = ctx.Client.GetVoiceNext();
var connection = vnext.GetConnection(ctx.Guild);
connection.VoiceReceived -= VoiceReceiveHandler;
connection.Dispose();
return Task.CompletedTask;
}
private async Task VoiceReceiveHandler(VoiceNextConnection connection, VoiceReceiveEventArgs args)
{
var fileName = DateTimeOffset.Now.ToUnixTimeMilliseconds();
var ffmpeg = Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $@"-ac 2 -f s16le -ar 48000 -i pipe:0 -ac 2 -ar 44100 Output/{fileName}.wav",
RedirectStandardInput = true
});
await ffmpeg.StandardInput.BaseStream.WriteAsync(args.PcmData);
ffmpeg.Dispose();
}
-```
\ No newline at end of file
+```
diff --git a/DisCatSharp.Docs/articles/audio/voicenext/transmit.md b/DisCatSharp.Docs/articles/modules/audio/voicenext/transmit.md
similarity index 99%
rename from DisCatSharp.Docs/articles/audio/voicenext/transmit.md
rename to DisCatSharp.Docs/articles/modules/audio/voicenext/transmit.md
index 7041ef69b..4216f5c73 100644
--- a/DisCatSharp.Docs/articles/audio/voicenext/transmit.md
+++ b/DisCatSharp.Docs/articles/modules/audio/voicenext/transmit.md
@@ -1,118 +1,118 @@
---
-uid: voicenext_transmit
+uid: modules_audio_voicenext_transmit
title: Transmitting
---
## Transmitting with VoiceNext
### Enable VoiceNext
Install the `DisCatSharp.VoiceNext` package from NuGet.
![NuGet Package Manager](/images/voicenext_transmit_01.png)
Then use the `UseVoiceNext` extension method on your instance of `DiscordClient`.
```cs
var discord = new DiscordClient();
discord.UseVoiceNext();
```
### Connect
Joining a voice channel is *very* easy; simply use the `ConnectAsync` extension method on `DiscordChannel`.
```cs
DiscordChannel channel;
VoiceNextConnection connection = await channel.ConnectAsync();
```
### Transmit
Discord requires that we send Opus encoded stereo PCM audio data at a sample rate of 48,000 Hz.
You'll need to convert your audio source to PCM S16LE using your preferred program for media conversion, then read
that data into a `Stream` object or an array of `byte` to be used with VoiceNext. Opus encoding of the PCM data will
be done automatically by VoiceNext before sending it to Discord.
This example will use [ffmpeg](https://ffmpeg.org/about.html) to convert an MP3 file to a PCM stream.
```cs
var filePath = "funiculi_funicula.mp3";
var ffmpeg = Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $@"-i ""{filePath}"" -ac 2 -f s16le -ar 48000 pipe:1",
RedirectStandardOutput = true,
UseShellExecute = false
});
Stream pcm = ffmpeg.StandardOutput.BaseStream;
```
Now that our audio is the correct format, we'll need to get a *transmit sink* for the channel we're connected to.
You can think of the transmit stream as our direct interface with a voice channel; any data written to one will be
processed by VoiceNext, queued, and sent to Discord which will then be output to the connected voice channel.
```cs
VoiceTransmitSink transmit = connection.GetTransmitSink();
```
Once we have a transmit sink, we can 'play' our audio by copying our PCM data to the transmit sink buffer.
```cs
await pcm.CopyToAsync(transmit);
```
`Stream#CopyToAsync()` will copy PCM data from the input stream to the output sink, up to the sink's configured
capacity, at which point it will wait until it can copy more. This means that the call will hold the task's execution,
until such time that the entire input stream has been consumed, and enqueued in the sink.
This operation cannot be cancelled. If you'd like to have finer control of the playback, you should instead consider
using `Stream#ReadAsync()` and `VoiceTransmitSink#WriteAsync()` to manually copy small portions of PCM data to the
transmit sink.
### Disconnect
Similar to joining, leaving a voice channel is rather straightforward.
```cs
var vnext = discord.GetVoiceNext();
var connection = vnext.GetConnection();
connection.Disconnect();
```
## Example Commands
```cs
[Command("join")]
public async Task JoinCommand(CommandContext ctx, DiscordChannel channel = null)
{
channel ??= ctx.Member.VoiceState?.Channel;
await channel.ConnectAsync();
}
[Command("play")]
public async Task PlayCommand(CommandContext ctx, string path)
{
var vnext = ctx.Client.GetVoiceNext();
var connection = vnext.GetConnection(ctx.Guild);
var transmit = connection.GetTransmitSink();
var pcm = ConvertAudioToPcm(path);
await pcm.CopyToAsync(transmit);
await pcm.DisposeAsync();
}
[Command("leave")]
public async Task LeaveCommand(CommandContext ctx)
{
var vnext = ctx.Client.GetVoiceNext();
var connection = vnext.GetConnection(ctx.Guild);
connection.Disconnect();
}
private Stream ConvertAudioToPcm(string filePath)
{
var ffmpeg = Process.Start(new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $@"-i ""{filePath}"" -ac 2 -f s16le -ar 48000 pipe:1",
RedirectStandardOutput = true,
UseShellExecute = false
});
return ffmpeg.StandardOutput.BaseStream;
}
```
diff --git a/DisCatSharp.Docs/articles/commands/argument_converters.md b/DisCatSharp.Docs/articles/modules/commandsnext/argument_converters.md
similarity index 95%
rename from DisCatSharp.Docs/articles/commands/argument_converters.md
rename to DisCatSharp.Docs/articles/modules/commandsnext/argument_converters.md
index c2e7ed760..e88a91866 100644
--- a/DisCatSharp.Docs/articles/commands/argument_converters.md
+++ b/DisCatSharp.Docs/articles/modules/commandsnext/argument_converters.md
@@ -1,60 +1,60 @@
---
-uid: commands_argument_converters
+uid: modules_commandsnext_argument_converters
title: Argument Converter
---
## Custom Argument Converter
Writing your own argument converter will enable you to convert custom types and replace the functionality of existing converters.
Like many things in DisCatSharp, doing this is straightforward and simple.
First, create a new class which implements `IArgumentConverter` and its method `ConvertAsync`.
Our example will be a boolean converter, so we'll also pass `bool` as the type parameter for `IArgumentConverter`.
```cs
public class CustomArgumentConverter : IArgumentConverter
{
public Task> ConvertAsync(string value, CommandContext ctx)
{
if (bool.TryParse(value, out var boolean))
{
return Task.FromResult(Optional.FromValue(boolean));
- }
+ }
switch (value.ToLower())
{
case "yes":
case "y":
case "t":
return Task.FromResult(Optional.FromValue(true));
case "no":
case "n":
case "f":
return Task.FromResult(Optional.FromValue(false));
default:
return Task.FromResult(Optional.FromNoValue());
- }
- }
+ }
+ }
}
```
Then register the argument converter with CommandContext.
```cs
var discord = new DiscordClient();
var commands = discord.UseCommandsNext();
commands.RegisterConverter(new CustomArgumentConverter());
```
Once the argument converter is written and registered, we'll be able to use it:
```cs
[Command("boolean")]
public async Task BooleanCommand(CommandContext ctx, bool boolean)
{
await ctx.RespondAsync($"Converted to {boolean}");
}
```
![true](/images/commands_argument_converters_01.png)
diff --git a/DisCatSharp.Docs/articles/commands/command_attributes.md b/DisCatSharp.Docs/articles/modules/commandsnext/command_attributes.md
similarity index 95%
rename from DisCatSharp.Docs/articles/commands/command_attributes.md
rename to DisCatSharp.Docs/articles/modules/commandsnext/command_attributes.md
index 9600cdcff..901b77e4e 100644
--- a/DisCatSharp.Docs/articles/commands/command_attributes.md
+++ b/DisCatSharp.Docs/articles/modules/commandsnext/command_attributes.md
@@ -1,103 +1,101 @@
---
-uid: commands_command_attributes
+uid: modules_commandsnext_command_attributes
title: Command Attributes
---
## Built-In Attributes
CommandsNext has a variety of built-in attributes to enhance your commands and provide some access control.
The majority of these attributes can be applied to your command methods and command groups.
- @DisCatSharp.CommandsNext.Attributes.AliasesAttribute
- @DisCatSharp.CommandsNext.Attributes.CooldownAttribute
- @DisCatSharp.CommandsNext.Attributes.DescriptionAttribute
- @DisCatSharp.CommandsNext.Attributes.DontInjectAttribute
- @DisCatSharp.CommandsNext.Attributes.HiddenAttribute
- @DisCatSharp.CommandsNext.Attributes.ModuleLifespanAttribute
- @DisCatSharp.CommandsNext.Attributes.PriorityAttribute
- @DisCatSharp.CommandsNext.Attributes.RemainingTextAttribute
- @DisCatSharp.CommandsNext.Attributes.RequireBotPermissionsAttribute
- @DisCatSharp.CommandsNext.Attributes.RequireCommunityAttribute
- @DisCatSharp.CommandsNext.Attributes.RequireDirectMessageAttribute
-- @DisCatSharp.CommandsNext.Attributes.RequireDiscordCertifiedModeratorAttribute
-- @DisCatSharp.CommandsNext.Attributes.RequireDiscordEmployeeAttribute
- @DisCatSharp.CommandsNext.Attributes.RequireGuildAttribute
- @DisCatSharp.CommandsNext.Attributes.RequireGuildOwnerAttribute
- @DisCatSharp.CommandsNext.Attributes.RequireMemberVerificationGateAttribute
- @DisCatSharp.CommandsNext.Attributes.RequireNsfwAttribute
- @DisCatSharp.CommandsNext.Attributes.RequireOwnerAttribute
- @DisCatSharp.CommandsNext.Attributes.RequireOwnerOrIdAttribute
- @DisCatSharp.CommandsNext.Attributes.RequirePermissionsAttribute
- @DisCatSharp.CommandsNext.Attributes.RequirePrefixesAttribute
- @DisCatSharp.CommandsNext.Attributes.RequireRolesAttribute
- @DisCatSharp.CommandsNext.Attributes.RequireUserPermissionsAttribute
- @DisCatSharp.CommandsNext.Attributes.RequireWelcomeScreenAttribute
## Custom Attributes
If the above attributes don't meet your needs, CommandsNext also gives you the option of writing your own!
Simply create a new class which inherits from `CheckBaseAttribute` and implement the required method.
Our example below will only allow a command to be ran during a specified year.
```cs
public class RequireYearAttribute : CheckBaseAttribute
{
public int AllowedYear { get; private set; }
public RequireYearAttribute(int year)
{
AllowedYear = year;
}
public override Task ExecuteCheckAsync(CommandContext ctx, bool help)
{
return Task.FromResult(AllowedYear == DateTime.Now.Year);
}
}
```
You'll also need to apply the `AttributeUsage` attribute to your attribute.
For our example attribute, we'll set it to only be usable once on methods.
```cs
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class RequireYearAttribute : CheckBaseAttribute
{
// ...
}
```
You can provide feedback to the user using the `CommandsNextExtension#CommandErrored` event.
```cs
private async Task MainAsync()
{
var discord = new DiscordClient();
var commands = discord.UseCommandsNext();
commands.CommandErrored += CmdErroredHandler;
}
private async Task CmdErroredHandler(CommandsNextExtension _, CommandErrorEventArgs e)
{
var failedChecks = ((ChecksFailedException)e.Exception).FailedChecks;
foreach (var failedCheck in failedChecks)
{
if (failedCheck is RequireYearAttribute)
{
var yearAttribute = (RequireYearAttribute)failedCheck;
await e.Context.RespondAsync($"Only usable during year {yearAttribute.AllowedYear}.");
}
}
}
```
Once you've got all of that completed, you'll be able to use it on a command!
```cs
[Command("generic"), RequireYear(2030)]
public async Task GenericCommand(CommandContext ctx, string generic)
{
await ctx.RespondAsync("Generic response.");
}
```
![Generic Image](/images/commands_command_attributes_01.png)
diff --git a/DisCatSharp.Docs/articles/commands/command_handler.md b/DisCatSharp.Docs/articles/modules/commandsnext/command_handler.md
similarity index 98%
rename from DisCatSharp.Docs/articles/commands/command_handler.md
rename to DisCatSharp.Docs/articles/modules/commandsnext/command_handler.md
index 0ab01d9f3..84f2dc1d6 100644
--- a/DisCatSharp.Docs/articles/commands/command_handler.md
+++ b/DisCatSharp.Docs/articles/modules/commandsnext/command_handler.md
@@ -1,92 +1,92 @@
---
-uid: commands_command_handler
+uid: modules_commandsnext_command_handler
title: Custom Command Handler
---
## Custom Command Handler
> [!IMPORTANT]
> Writing your own handler logic should only be done if *you know what you're doing*.
> You will be responsible for command execution and preventing deadlocks.
-
+
### Disable Default Handler
To begin, we'll need to disable the default command handler provided by CommandsNext.
This is done by setting the `UseDefaultCommandHandler` configuration property to `false`.
```cs
var discord = new DiscordClient();
var commands = discord.UseCommandsNext(new CommandsNextConfiguration()
{
UseDefaultCommandHandler = false
});
```
### Create Event Handler
We'll then write a new handler for the `MessageCreated` event fired from `DiscordClient`.
```cs
discord.MessageCreated += CommandHandler;
// ...
private Task CommandHandler(DiscordClient client, MessageCreateEventArgs e)
{
// See below ...
}
```
This event handler will be our command handler, and you'll need to write the logic for it.
### Handle Commands
Start by parsing the message content for a prefix and command string
```cs
var cnext = client.GetCommandsNext();
var msg = e.Message;
// Check if message has valid prefix.
var cmdStart = msg.GetStringPrefixLength("!");
if (cmdStart == -1) return;
// Retrieve prefix.
var prefix = msg.Content.Substring(0, cmdStart);
// Retrieve full command string.
var cmdString = msg.Content.Substring(cmdStart);
```
Then provide the command string to `CommandsNextExtension#FindCommand`
```cs
var command = cnext.FindCommand(cmdString, out var args);
```
Create a command context using our message and prefix, along with the command and its arguments
```cs
var ctx = cnext.CreateContext(msg, prefix, command, args);
```
And pass the context to `CommandsNextExtension#ExecuteCommandAsync` to execute the command.
```cs
_ = Task.Run(async () => await cnext.ExecuteCommandAsync(ctx));
// Wrapped in Task.Run() to prevent deadlocks.
```
### Finished Product
Altogether, your implementation should function similarly to the following:
```cs
private Task CommandHandler(DiscordClient client, MessageCreateEventArgs e)
{
var cnext = client.GetCommandsNext();
var msg = e.Message;
var cmdStart = msg.GetStringPrefixLength("!");
if (cmdStart == -1) return Task.CompletedTask;
var prefix = msg.Content.Substring(0, cmdStart);
var cmdString = msg.Content.Substring(cmdStart);
var command = cnext.FindCommand(cmdString, out var args);
if (command == null) return Task.CompletedTask;
var ctx = cnext.CreateContext(msg, prefix, command, args);
Task.Run(async () => await cnext.ExecuteCommandAsync(ctx));
-
+
return Task.CompletedTask;
}
-```
\ No newline at end of file
+```
diff --git a/DisCatSharp.Docs/articles/commands/dependency_injection.md b/DisCatSharp.Docs/articles/modules/commandsnext/dependency_injection.md
similarity index 92%
rename from DisCatSharp.Docs/articles/commands/dependency_injection.md
rename to DisCatSharp.Docs/articles/modules/commandsnext/dependency_injection.md
index 994063654..483d83fdc 100644
--- a/DisCatSharp.Docs/articles/commands/dependency_injection.md
+++ b/DisCatSharp.Docs/articles/modules/commandsnext/dependency_injection.md
@@ -1,98 +1,98 @@
---
-uid: commands_dependency_injection
+uid: modules_commandsnext_dependency_injection
title: Dependency Injection
---
## Dependency Injection
As you begin to write more complex commands, you'll find that you need a way to get data in and out of them.
Although you *could* use `static` fields to accomplish this, the preferred solution would be *dependency injection*.
This would involve placing all required object instances and types (referred to as *services*) in a container, then providing that container to CommandsNext.
Each time a command module is instantiated, CommandsNext will then attempt to populate constructor parameters, `public` properties, and `public` fields exposed by the module with instances of objects from the service container.
We'll go through a simple example of this process to help you understand better.
### Create a Service Provider
To begin, we'll need to create a service provider; this will act as the container for the services you need for your commands.
Create a new variable just before you register CommandsNext with your `DiscordClient` and assign it a new instance of `ServiceCollection`.
```cs
-var discord = new DiscordClient();
+var discord = new DiscordClient();
var services = new ServiceCollection(); // Right here!
var commands = discord.UseCommandsNext();
```
We'll use `.AddSingleton` to add type `Random` to the collection, then chain that call with the `.BuildServiceProvider()` extension method.
The resulting type will be `ServiceProvider`.
```cs
var services = new ServiceCollection()
.AddSingleton()
.BuildServiceProvider();
```
Then we'll need to provide CommandsNext with our services.
```cs
var commands = discord.UseCommandsNext(new CommandsNextConfiguration()
{
Services = services
});
```
### Using Your Services
Now that we have our services set up, we're able to use them in commands.
-We'll be tweaking our [random number command](xref:commands_intro#argument-converters) to demonstrate.
+We'll be tweaking our [random number command](xref:modules_commandsnext_intro#argument-converters) to demonstrate.
Add a new property to the command module named *Rng*. Make sure it has a `public` setter.
```cs
public class MyFirstModule : BaseCommandModule
{
public Random Rng { private get; set; } // Implied public setter.
// ...
}
```
Modify the *random* command to use our property.
```cs
[Command("random")]
public async Task RandomCommand(CommandContext ctx, int min, int max)
{
await ctx.RespondAsync($"Your number is: {Rng.Next(min, max)}");
}
```
Then we can give it a try!
-![Command Execution](/images/commands_dependency_injection_01.png)
+![Command Execution](/images/commands_intro_05.png)
CommandsNext has automatically injected our singleton `Random` instance into the `Rng` property when our command module was instantiated.
Now, for any command that needs `Random`, we can simply declare one as a property, field, or in the module constructor and CommandsNext will take care of the rest.
Ain't that neat?
## Lifespans
### Modules
By default, all command modules have a singleton lifespan; this means each command module is instantiated once for the lifetime of the CommandsNext instance.
However, if the reuse of a module instance is undesired, you also have the option to change the lifespan of a module to *transient* using the `ModulesLifespan` attribute.
```cs
[ModuleLifespan(ModuleLifespan.Transient)]
public class MyFirstModule : BaseCommandModule
{
// ...
}
```
Transient command modules are instantiated each time one of its containing commands is executed.
### Services
In addition to the `.AddSingleton()` extension method, you're also able to use the `.AddScoped()` and `.AddTransient()` extension methods to add services to the collection.
The extension method chosen will affect when and how often the service is instantiated.
Scoped and transient services should only be used in transient command modules, as singleton modules will always have their services injected once.
Lifespan|Instantiated
:---:|:---
Singleton|One time when added to the collection.
Scoped|Once for each command module.
-Transient|Each time its requested.
\ No newline at end of file
+Transient|Each time its requested.
diff --git a/DisCatSharp.Docs/articles/commands/help_formatter.md b/DisCatSharp.Docs/articles/modules/commandsnext/help_formatter.md
similarity index 92%
rename from DisCatSharp.Docs/articles/commands/help_formatter.md
rename to DisCatSharp.Docs/articles/modules/commandsnext/help_formatter.md
index e0b0a4a4a..8853f5a5e 100644
--- a/DisCatSharp.Docs/articles/commands/help_formatter.md
+++ b/DisCatSharp.Docs/articles/modules/commandsnext/help_formatter.md
@@ -1,83 +1,83 @@
---
-uid: commands_help_formatter
+uid: modules_commandsnext_help_formatter
title: Help Formatter
---
## Custom Help Formatter
The built-in help command provided by CommandsNext is generated with a *help formatter*.
This simple mechanism is given a command and its subcommands then returns a formatted help message.
If you're not happy with the default help formatter, you're able to write your own and customize the output to your liking.
Simply inherit from `BaseHelpFormatter` and provide an implementation for each of the required methods.
```cs
public class CustomHelpFormatter : BaseHelpFormatter
{
// protected DiscordEmbedBuilder _embed;
// protected StringBuilder _strBuilder;
public CustomHelpFormatter(CommandContext ctx) : base(ctx)
{
// _embed = new DiscordEmbedBuilder();
// _strBuilder = new StringBuilder();
-
+
// Help formatters do support dependency injection.
- // Any required services can be specified by declaring constructor parameters.
+ // Any required services can be specified by declaring constructor parameters.
// Other required initialization here ...
}
public override BaseHelpFormatter WithCommand(Command command)
{
- // _embed.AddField(command.Name, command.Description);
+ // _embed.AddField(command.Name, command.Description);
// _strBuilder.AppendLine($"{command.Name} - {command.Description}");
return this;
}
public override BaseHelpFormatter WithSubcommands(IEnumerable cmds)
{
foreach (var cmd in cmds)
{
- // _embed.AddField(cmd.Name, cmd.Description);
+ // _embed.AddField(cmd.Name, cmd.Description);
// _strBuilder.AppendLine($"{cmd.Name} - {cmd.Description}");
}
return this;
}
public override CommandHelpMessage Build()
{
// return new CommandHelpMessage(embed: _embed);
// return new CommandHelpMessage(content: _strBuilder.ToString());
}
}
```
Alternatively, if you're only wanting to make a few small tweaks to the default help, you can write a simple help formatter which inherits from `DefaultHelpFormatter` and modify the inherited `EmbedBuilder` property.
```cs
public class CustomHelpFormatter : DefaultHelpFormatter
{
public CustomHelpFormatter(CommandContext ctx) : base(ctx) { }
public override CommandHelpMessage Build()
{
EmbedBuilder.Color = DiscordColor.SpringGreen;
return base.Build();
}
}
```
Your final step is to register your help formatter with CommandsNext.
```cs
var discord = new DiscordClient();
var commands = discord.UseCommandsNext();
commands.SetHelpFormatter();
```
That's all there is to it.
![Fresh New Look](/images/commands_help_formatter_01.png)
diff --git a/DisCatSharp.Docs/articles/commands/intro.md b/DisCatSharp.Docs/articles/modules/commandsnext/intro.md
similarity index 97%
rename from DisCatSharp.Docs/articles/commands/intro.md
rename to DisCatSharp.Docs/articles/modules/commandsnext/intro.md
index 4d8cf77e7..0628921b8 100644
--- a/DisCatSharp.Docs/articles/commands/intro.md
+++ b/DisCatSharp.Docs/articles/modules/commandsnext/intro.md
@@ -1,330 +1,330 @@
---
-uid: commands_intro
+uid: modules_commandsnext_intro
title: CommandsNext Introduction
---
>[!NOTE]
> This article assumes you've recently read the article on *[writing your first bot](xref:basics_first_bot)*.
# Introduction to CommandsNext
This article will introduce you to some basic concepts of our native command framework: *CommandsNext*.
Be sure to install the `DisCatSharp.CommandsNext` package from NuGet before continuing.
![CommandsNext NuGet Package](/images/commands_intro_01.png)
## Writing a Basic Command
### Create a Command Module
A command module is simply a class which acts as a container for your command methods. Instead of registering individual commands,
you'd register a single command module which contains multiple commands. There's no limit to the amount of modules you can have,
and no limit to the amount of commands each module can contain. For example: you could have a module for moderation commands and
a separate module for image commands. This will help you keep your commands organized and reduce the clutter in your project.
Our first demonstration will be simple, consisting of one command module with a simple command.
We'll start by creating a new folder named `Commands` which contains a new class named `MyFirstModule`.
![Solution Explorer](/images/commands_intro_02.png)
Give this new class `public` access and have it inherit from `BaseCommandModule`.
```cs
public class MyFirstModule : BaseCommandModule
{
}
```
### Create a Command Method
Within our new module, create a method named `GreetCommand` marked as `async` with a `Task` return type.
The first parameter of your method *must* be of type `CommandContext`, as required by CommandsNext.
```cs
public async Task GreetCommand(CommandContext ctx)
{
}
```
In the body of our new method, we'll use `CommandContext#RespondAsync` to send a simple message.
```cs
await ctx.RespondAsync("Greetings! Thank you for executing me!");
```
Finally, mark your command method with the `Command` attribute so CommandsNext will know to treat our method as a command method.
This attribute takes a single parameter: the name of the command.
We'll name our command *greet* to match the name of the method.
```cs
[Command("greet")]
public async Task GreetCommand(CommandContext ctx)
{
await ctx.RespondAsync("Greetings! Thank you for executing me!");
}
```
Your command module should now resemble this:
```cs
using System.Threading.Tasks;
using DisCatSharp.CommandsNext;
using DisCatSharp.CommandsNext.Attributes;
public class MyFirstModule : BaseCommandModule
{
[Command("greet")]
public async Task GreetCommand(CommandContext ctx)
{
await ctx.RespondAsync("Greetings! Thank you for executing me!");
}
}
```
### Cleanup and Configuration
Before we can run our new command, we'll need modify our main method.
Start by removing the event handler we created [previously](xref:basics_first_bot#spicing-up-your-bot).
```cs
var discord = new DiscordClient();
discord.MessageCreated += async (s, e) => // REMOVE
{ // ALL
if (e.Message.Content.ToLower().StartsWith("ping")) // OF
await e.Message.RespondAsync("pong!"); // THESE
}; // LINES
await discord.ConnectAsync();
```
Next, call the `UseCommandsNext` extension method on your `DiscordClient` instance and pass it a new `CommandsNextConfiguration` instance.
Assign the resulting `CommandsNextExtension` instance to a new variable named *commands*. This important step will enable CommandsNext for your Discord client.
```cs
var discord = new DiscordClient();
var commands = discord.UseCommandsNext(new CommandsNextConfiguration());
```
Create an object initializer for `CommandsNextConfiguration` and assign the `StringPrefixes` property a new `string` array containing your desired prefixes.
Our example below will only define a single prefix: `!`.
```cs
new CommandsNextConfiguration()
{
StringPrefixes = new[] { "!" }
}
```
Now we'll register our command module.
Call the `RegisterCommands` method on our `CommandsNextExtension` instance and provide it with your command module.
```cs
var discord = new DiscordClient();
var commands = discord.UseCommandsNext();
commands.RegisterCommands();
await discord.ConnectAsync();
```
Alternatively, you can pass in your assembly to register commands from all modules in your program.
```cs
commands.RegisterCommands(Assembly.GetExecutingAssembly());
```
Your main method should look similar to the following:
```cs
internal static async Task MainAsync()
{
var discord = new DiscordClient(new DiscordConfiguration());
var commands = discord.UseCommandsNext(new CommandsNextConfiguration()
{
StringPrefixes = new[] { "!" }
});
commands.RegisterCommands();
await discord.ConnectAsync();
await Task.Delay(-1);
}
```
### Running Your Command
It's now the moment of truth; all your blood, sweat, and tears have lead to this moment.
Hit `F5` on your keyboard to compile and run your bot, then execute your command in any channel that your bot account has access to.
![Congratulations, You've Won!](/images/commands_intro_03.png)
[That was easy](https://www.youtube.com/watch?v=GsQXadrmhws).
## Taking User Input
### Command Arguments
Now that we have a basic command down, let's spice it up a bit by defining *arguments* to accept user input.
Defining an argument is simple; just add additional parameters to your signature of your command method.
CommandsNext will automatically parse user input and populate the parameters of your command method with those arguments.
To demonstrate, we'll modify our *greet* command to greet a user with a given name.
Head back to `MyFirstModule` and add a parameter of type `string` to the `GreetCommand` method.
```cs
[Command("greet")]
public async Task GreetCommand(CommandContext ctx, string name)
```
CommandsNext will now interpret this as a command named *greet* that takes one argument.
Next, replace our original response message with an [interpolated string](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/interpolated)
which uses our new parameter.
```cs
public async Task GreetCommand(CommandContext ctx, string name)
{
await ctx.RespondAsync($"Greetings, {name}! You're pretty neat!");
}
```
That's all there is to it. Smack `F5` and test it out in a channel your bot account has access to.
![Greet Part 2: Electric Boogaloo](/images/commands_intro_04.png)
Now, you may have noticed that providing more than one word simply does not work.
For example, `!greet Luke Smith` will result in no response from your bot.
This fails because a valid [overload](#command-overloads) could not be found for your command.
CommandsNext will split arguments by whitespace. This means `Luke Smith` is counted as two separate arguments; `Luke` and `Smith`.
In addition to this, CommandsNext will attempt to find and execute an overload of your command that has the *same number* of provided arguments.
Together, this means that any additional arguments will prevent CommandsNext from finding a valid overload to execute.
The simplest way to get around this would be to wrap your input with double quotes.
CommandsNext will parse this as one argument, allowing your command to be executed.
```
!greet "Luke Smith"
```
If you would prefer not to use quotes, you can use the `RemainingText` attribute on your parameter.
This attribute will instruct CommandsNext to parse all remaining arguments into that parameter.
```cs
public async Task GreetCommand(CommandContext ctx, [RemainingText] string name)
```
Alternatively, you can use the `params` keyword to have all remaining arguments parsed into an array.
```cs
public async Task GreetCommand(CommandContext ctx, params string[] names)
```
A more obvious solution is to add additional parameters to the method signature of your command method.
```cs
public async Task GreetCommand(CommandContext ctx, string firstName, string lastName)
```
Each of these has their own caveats; it'll be up to you to choose the best solution for your commands.
### Argument Converters
CommandsNext can convert arguments, which are natively `string`, to the type specified by a command method parameter.
This functionality is powered by *argument converters*, and it'll help to eliminate the boilerplate code needed to parse and convert `string` arguments.
CommandsNext has built-in argument converters for the following types:
Category|Types
:---:|:---
Discord|`DiscordGuild`, `DiscordChannel`, `DiscordMember`, `DiscordUser`, `DiscordRole`, `DiscordMessage`, `DiscordEmoji`, `DiscordColor`
Integral|`byte`, `short`, `int`, `long`, `sbyte`, `ushort`, `uint`, `ulong`
Floating-Point|`float`, `double`, `decimal`
Date|`DateTime`, `DateTimeOffset`, `TimeSpan`
Character|`string`, `char`
Boolean|`bool`
-You're also able to create and provide your own [custom argument converters](xref:commands_argument_converters), if desired.
+You're also able to create and provide your own [custom argument converters](xref:modules_commandsnext_argument_converters), if desired.
Let's do a quick demonstration of the built-in converters.
Create a new command method above our `GreetCommand` method named `RandomCommand` and have it take two integer arguments.
As the method name suggests, this command will be named *random*.
```cs
[Command("random")]
public async Task RandomCommand(CommandContext ctx, int min, int max)
{
}
```
Make a variable with a new instance of `Random`.
```cs
var random = new Random();
```
Finally, we'll respond with a random number within the range provided by the user.
```cs
await ctx.RespondAsync($"Your number is: {random.Next(min, max)}");
```
Run your bot once more with `F5` and give this a try in a text channel.
![Discord Channel](/images/commands_intro_05.png)
CommandsNext converted the two arguments from `string` into `int` and passed them to the parameters of our command,
removing the need to manually parse and convert the arguments yourself.
We'll do one more to drive the point home. Head back to our old `GreetCommand` method, remove our
`name` parameter, and replace it with a new parameter of type `DiscordMember` named `member`.
```cs
public async Task GreetCommand(CommandContext ctx, DiscordMember member)
```
Then modify the response to mention the provided member with the `Mention` property on `DiscordMember`.
```cs
public async Task GreetCommand(CommandContext ctx, DiscordMember member)
{
await ctx.RespondAsync($"Greetings, {member.Mention}! Enjoy the mention!");
}
```
Go ahead and give that a test run.
![According to all known laws of aviation,](/images/commands_intro_06.png)
![there is no way a bee should be able to fly.](/images/commands_intro_07.png)
![Its wings are too small to get its fat little body off the ground.](/images/commands_intro_08.png)
The argument converter for `DiscordMember` is able to parse mentions, usernames, nicknames, and user IDs then look for a matching member within the guild the command was executed from.
Ain't that neat?
## Command Overloads
Command method overloading allows you to create multiple argument configurations for a single command.
```cs
[Command("foo")]
public Task FooCommand(CommandContext ctx, string bar, int baz) { }
[Command("foo")]
public Task FooCommand(CommandContext ctx, DiscordUser bar) { }
```
Executing `!foo green 5` will run the first method, and `!foo @SecondUser` will run the second method.
Additionally, all check attributes are shared between overloads.
```cs
[Command("foo"), Aliases("bar", "baz")]
[RequireGuild, RequireBotPermissions(Permissions.AttachFiles)]
public Task FooCommand(CommandContext ctx, int bar, int baz, string qux = "agony") { }
[Command("foo")]
public Task FooCommand(CommandContext ctx, DiscordChannel bar, TimeSpan baz) { }
```
The additional attributes and checks applied to the first method will also be applied to the second method.
## Further Reading
Now that you've gotten an understanding of CommandsNext, it'd be a good idea check out the following:
-* [Command Attributes](xref:commands_command_attributes)
-* [Help Formatter](xref:commands_help_formatter)
-* [Dependency Injection](xref:commands_dependency_injection)
+* [Command Attributes](xref:modules_commandsnext_command_attributes)
+* [Help Formatter](xref:modules_commandsnext_help_formatter)
+* [Dependency Injection](xref:modules_commandsnext_dependency_injection)
diff --git a/DisCatSharp.Docs/articles/interactivity.md b/DisCatSharp.Docs/articles/modules/interactivity.md
similarity index 97%
rename from DisCatSharp.Docs/articles/interactivity.md
rename to DisCatSharp.Docs/articles/modules/interactivity.md
index 3535eb45e..b207f7da9 100644
--- a/DisCatSharp.Docs/articles/interactivity.md
+++ b/DisCatSharp.Docs/articles/modules/interactivity.md
@@ -1,120 +1,120 @@
---
-uid: interactivity
+uid: modules_interactivity
title: Interactivity Introduction
---
# Introduction to Interactivity
Interactivity will enable you to write commands which the user can interact with through reactions and messages.
The goal of this article is to introduce you to the general flow of this extension.
Make sure to install the `DisCatSharp.Interactivity` package from NuGet before continuing.
![Interactivity NuGet](/images/interactivity_01.png)
## Enabling Interactivity
Interactivity can be registered using the `DiscordClient#UseInteractivity()` extension method.
Optionally, you can also provide an instance of `InteractivityConfiguration` to modify default behaviors.
```cs
var discord = new DiscordClient();
discord.UseInteractivity(new InteractivityConfiguration()
{
PollBehaviour = PollBehaviour.KeepEmojis,
Timeout = TimeSpan.FromSeconds(30)
});
```
## Using Interactivity
There are two ways available to use interactivity:
* Extension methods available for `DiscordChannel`, `DiscordMessage`, `DiscordClient` and `DiscordInteraction`.
* [Instance methods](xref:DisCatSharp.Interactivity.InteractivityExtension#methods) available from `InteractivityExtension`.
We'll have a quick look at a few common interactivity methods along with an example of use for each.
The first (and arguably most useful) extension method is `SendPaginatedMessageAsync` for `DiscordChannel`.
This method displays a collection of *'pages'* which are selected one-at-a-time by the user through reaction buttons.
Each button click will move the page view in one direction or the other until the timeout is reached.
You'll need to create a collection of pages before you can invoke this method.
This can be done easily using the `GeneratePagesInEmbed` and `GeneratePagesInContent` instance methods from `InteractivityExtension`.
Alternatively, for pre-generated content, you can create and add individual instances of `Page` to a collection.
This example will use the `GeneratePagesInEmbed` method to generate the pages.
```cs
public async Task PaginationCommand(CommandContext ctx)
{
var reallyLongString = "Lorem ipsum dolor sit amet, consectetur adipiscing ..."
var interactivity = ctx.Client.GetInteractivity();
var pages = interactivity.GeneratePagesInEmbed(reallyLongString);
await ctx.Channel.SendPaginatedMessageAsync(ctx.Member, pages);
}
```
![Pagination Pages](/images/interactivity_02.png)
Next we'll look at the `WaitForReactionAsync` extension method for `DiscordMessage`.
This method waits for a reaction from a specific user and returns the emoji that was used.
An overload of this method also enables you to wait for a *specific* reaction, as shown in the example below.
```cs
public async Task ReactionCommand(CommandContext ctx, DiscordMember member)
{
var emoji = DiscordEmoji.FromName(ctx.Client, ":ok_hand:");
var message = await ctx.RespondAsync($"{member.Mention}, react with {emoji}.");
var result = await message.WaitForReactionAsync(member, emoji);
if (!result.TimedOut) await ctx.RespondAsync("Thank you!");
}
```
![Thank You!](/images/interactivity_03.png)
Another reaction extension method for `DiscordMessage` is `CollectReactionsAsync`.
As the name implies, this method collects all reactions on a message until the timeout is reached.
```cs
public async Task CollectionCommand(CommandContext ctx)
{
var message = await ctx.RespondAsync("React here!");
var reactions = await message.CollectReactionsAsync();
var strBuilder = new StringBuilder();
foreach (var reaction in reactions)
{
strBuilder.AppendLine($"{reaction.Emoji}: {reaction.Total}");
}
await ctx.RespondAsync(strBuilder.ToString());
}
```
![Reaction Count](/images/interactivity_04.png)
The final one we'll take a look at is the `GetNextMessageAsync` extension method for `DiscordMessage`.
This method will return the next message sent from the author of the original message.
Our example here will use its alternate overload which accepts an additional predicate.
```cs
public async Task ActionCommand(CommandContext ctx)
{
- await ctx.RespondAsync("Respond with *confirm* to continue.");
+ await ctx.RespondAsync("Respond with `confirm` to continue.");
var result = await ctx.Message.GetNextMessageAsync(m =>
{
return m.Content.ToLower() == "confirm";
});
if (!result.TimedOut) await ctx.RespondAsync("Action confirmed.");
}
```
![Confirmed](/images/interactivity_05.png)
diff --git a/DisCatSharp.Docs/articles/toc.yml b/DisCatSharp.Docs/articles/toc.yml
index 039405123..b8d534846 100644
--- a/DisCatSharp.Docs/articles/toc.yml
+++ b/DisCatSharp.Docs/articles/toc.yml
@@ -1,113 +1,119 @@
- name: Preamble
href: preamble.md
- name: Important Changes
items:
+ - name: Version 10.1.0
+ href: important_changes/10_1_0.md
- name: Version 10.0.0
href: important_changes/10_0_0.md
- name: Version 9.9.0
href: important_changes/9_9_0.md
- name: Version 9.8.5
href: important_changes/9_8_5.md
- name: Version 9.8.4
href: important_changes/9_8_4.md
- name: Version 9.8.3
href: important_changes/9_8_3.md
- name: Version 9.8.2
href: important_changes/9_8_2.md
- name: The Basics
items:
- name: Creating a Bot Account
href: basics/bot_account.md
- name: Writing Your First Bot
href: basics/first_bot.md
- name: Bot as Hosted Service
href: basics/web_app.md
- name: Project Templates
href: basics/templates.md
- name: Beyond Basics
items:
- name: Events
href: beyond_basics/events.md
- name: Logging
href: beyond_basics/logging/default.md
items:
- name: The Default Logger
href: beyond_basics/logging/default.md
- name: Third Party Loggers
href: beyond_basics/logging/third_party.md
- name: Dependency Injection Loggers
href: beyond_basics/logging/di.md
- name: Intents
href: beyond_basics/intents.md
- name: Sharding
href: beyond_basics/sharding.md
- name: Message Builder
href: beyond_basics/messagebuilder.md
- name: Components
items:
- name: Buttons
href: beyond_basics/components/buttons.md
- name: Select Menu
href: beyond_basics/components/select_menus.md
- name: Workarounds
href: beyond_basics/workarounds.md
-- name: Application Commands
+- name: Modules
items:
- - name: Introduction
- href: application_commands/intro.md
- - name: Options
- href: application_commands/options.md
- - name: Events
- href: application_commands/events.md
- - name: Translations
+ - name: Application Commands
items:
+ - name: Introduction
+ href: modules/application_commands/intro.md
+ - name: Options
+ href: modules/application_commands/options.md
+ - name: Events
+ href: modules/application_commands/events.md
+ - name: Translations
+ items:
- name: Using Translations
- href: application_commands/translations/using.md
+ href: modules/application_commands/translations/using.md
- name: Translation Reference
- href: application_commands/translations/reference.md
- - name: Paginated Modals
- href: application_commands/paginated_modals.md
-- name: Commands
- items:
- - name: Introduction
- href: commands/intro.md
- - name: Command Attributes
- href: commands/command_attributes.md
- - name: Dependency Injection
- href: commands/dependency_injection.md
- - name: Customization
- items:
- - name: Help Formatter
- href: commands/help_formatter.md
- - name: Argument Converters
- href: commands/argument_converters.md
- - name: Command Handler
- href: commands/command_handler.md
-- name: Audio
- items:
- - name: Lavalink
+ href: modules/application_commands/translations/reference.md
+ - name: Modals
+ href: modules/application_commands/modals.md
+ - name: Paginated Modals
+ href: modules/application_commands/paginated_modals.md
+ - name: CommandsNext
items:
- - name: Setup
- href: audio/lavalink/setup.md
- - name: Configuration
- href: audio/lavalink/configuration.md
- - name: Music Commands
- href: audio/lavalink/music_commands.md
- - name: VoiceNext
+ - name: Introduction
+ href: modules/commandsnext/intro.md
+ - name: Command Attributes
+ href: modules/commandsnext/command_attributes.md
+ - name: Dependency Injection
+ href: modules/commandsnext/dependency_injection.md
+ - name: Customization
+ items:
+ - name: Help Formatter
+ href: modules/commandsnext/help_formatter.md
+ - name: Argument Converters
+ href: modules/commandsnext/argument_converters.md
+ - name: Command Handler
+ href: modules/commandsnext/command_handler.md
+ - name: Audio
items:
- - name: Prerequisites
- href: audio/voicenext/prerequisites.md
- - name: Transmitting
- href: audio/voicenext/transmit.md
- - name: Receiving
- href: audio/voicenext/receive.md
-- name: Interactivity
- href: interactivity.md
-- name: Hosting
- href: hosting.md
+ - name: Lavalink
+ items:
+ - name: Setup
+ href: modules/audio/lavalink/setup.md
+ - name: Configuration
+ href: modules/audio/lavalink/configuration.md
+ - name: Music Commands
+ href: modules/audio/lavalink/music_commands.md
+ - name: VoiceNext
+ items:
+ - name: Prerequisites
+ href: modules/audio/voicenext/prerequisites.md
+ - name: Transmitting
+ href: modules/audio/voicenext/transmit.md
+ - name: Receiving
+ href: modules/audio/voicenext/receive.md
+ - name: Interactivity
+ href: modules/interactivity.md
- name: Miscellaneous
items:
- name: Nightly Builds
href: misc/nightly_builds.md
- name: Reporting Issues
href: misc/reporting_issues.md
+ - name: Hosting
+ href: misc/hosting.md
diff --git a/DisCatSharp.Docs/faq.md b/DisCatSharp.Docs/faq.md
index a9dec19b5..c1f237a9f 100644
--- a/DisCatSharp.Docs/faq.md
+++ b/DisCatSharp.Docs/faq.md
@@ -1,84 +1,84 @@
---
uid: faq
title: Frequently Asked Questions
---
# Frequently Asked Questions
### Code I copied from an article isn't compiling or working as expected. Why?
*Please use the code snippets as a reference; don't blindly copy-paste code!*
The snippets of code in the articles are meant to serve as examples to help you understand how to use a part of the library.
Although most will compile and work at the time of writing, changes to the library over time can make some snippets obsolete.
Many issues can be resolved with Intellisense by searching for similarly named methods and verifying method parameters.
### I'm targeting Mono and have exceptions, crashes, or other problems.
As mentioned in the [preamble](xref:preamble), the Mono runtime is inherently unstable and has numerous flaws.
Because of this we do not support Mono in any way, nor will we support any other projects which use it.
Instead, we recommend using either the latest LTS release or most recent stable version of [.NET Core](https://dotnet.microsoft.com/download).
### Connecting to a voice channel with VoiceNext will either hang or throw an exception.
To troubleshoot, please ensure that:
* You are using the latest version of DisCatSharp.
* You have properly enabled VoiceNext with your instance of @DisCatSharp.DiscordClient.
* You are *not* using VoiceNext in an event handler.
-* You have [opus and libsodium](xref:voicenext_prerequisites) available in your target environment.
+* You have [opus and libsodium](xref:modules_audio_voicenext_prerequisites) available in your target environment.
### Why am I getting *heartbeat skipped* message in my console?
There are two possible reasons:
#### Connection issue between your bot application and Discord.
Check your internet connection and ensure that the machine your bot is hosted on has a stable internet connection.
If your local network has no issues, the problem could be with either Discord or Cloudflare. In which case, it's out of your control.
#### Complex, long-running code in an event handler.
Any event handlers that have the potential to run for more than a few seconds could cause a deadlock, and cause several heartbeats to be skipped.
Please take a look at our short article on [handling exceptions](xref:beyond_basics_events) to learn how to avoid this.
### Why am I getting a 4XX error and how can I fix it?
HTTP Error Code|Cause|Resolution
:---:|:---|:---
`401`|Invalid token.|Verify your token and make sure no errors were made. The client secret found on the 'general information' tab of your application page *is not* your token.
`403`|Not enough permissions.|Verify permissions and ensure your bot account has a role higher than the target user. Administrator permissions *do not* bypass the role hierarchy.
`404`|Requested object not found.|This usually means the entity does not exist. You should reattempt then inform your user.
### I cannot modify a specific user or role. Why is this?
In order to modify a user, the highest role of your bot account must be higher than the target user.
Changing the properties of a role requires that your bot account have a role higher than that role.
### Does CommandsNext support dependency injection?
-It does! Please take a look at our [article](xref:commands_dependency_injection) on the subject.
+It does! Please take a look at our [article](xref:modules_commandsnext_dependency_injection) on the subject.
### Can I use a user token?
Automating a user account is against Discord's [Terms of Service](https://dis.gd/terms) and is not supported by DisCatSharp.
### How can I set a custom status?
If you mean a *true* custom status like this:
![help](/images/faq_01.png)
No, you cannot. Discord does not allow bots to use custom statuses.
However, if you meant an activity like this:
![Bot Presence](/images/faq_02.png)
You can use either of the following
* The overload for [ConnectAsync](xref:DisCatSharp.DiscordClient#DisCatSharp_DiscordClient_ConnectAsync_DisCatSharp_Entities_DiscordActivity_System_Nullable_DisCatSharp_Entities_UserStatus__System_Nullable_DateTimeOffset__)([DiscordActivity](xref:DisCatSharp.Entities.DiscordActivity), System.Nullable{UserStatus}, System.Nullable{DateTimeOffset}) which accepts a [DiscordActivity](xref:DisCatSharp.Entities.DiscordActivity).
* [UpdateStatusAsync](xref:DisCatSharp.DiscordClient#DisCatSharp_DiscordClient_UpdateStatusAsync_DisCatSharp_Entities_DiscordActivity_System_Nullable_DisCatSharp_Entities_UserStatus__System_Nullable_DateTimeOffset__)([DiscordActivity](xref:DisCatSharp.Entities.DiscordActivity), System.Nullable{UserStatus}, System.Nullable{DateTimeOffset}) OR [UpdateStatusAsync](xref:DisCatSharp.DiscordShardedClient#DisCatSharp_DiscordShardedClient_UpdateStatusAsync_DisCatSharp_Entities_DiscordActivity_System_Nullable_DisCatSharp_Entities_UserStatus__System_Nullable_DateTimeOffset__)(DiscordActivity, System.Nullable{[UserStatus](xref:DisCatSharp.Entities.UserStatus)}, System.Nullable{DateTimeOffset}) (for the sharded client) at any point after `Ready` has been fired.
### Am I able to retrieve a @DisCatSharp.Entities.DiscordRole by name?
Yes. Use LINQ on the `Roles` property of your instance of [DiscordGuild](xref:DisCatSharp.Entities.DiscordGuild) and compare against the `Name` of each [DiscordRole](xref:DisCatSharp.Entities.DiscordRole).
### Why are you using Newtonsoft.Json when System.Text.Json is available
Yes `System.Text.Json` is available to use but it still doesn't stand up to what we currently need which is why we still use Newtonsoft.Json.
Maybe in time we can switch to your favorite Json Deserializer but for right now we will be using Newtonsoft.Json for the foreseeable future.
### Why the hell are my events not firing?
This is because in the Discord V8+ API, they require @DisCatSharp.DiscordIntents to be enabled on [DiscordConfiguration](xref:DisCatSharp.DiscordConfiguration) and the
Discord Application Portal. We have an [article](xref:beyond_basics_intents) that covers all that has to be done to set this up.
diff --git a/DisCatSharp.Docs/images/_beyond_basics_logging_user_05.png b/DisCatSharp.Docs/images/_beyond_basics_logging_user_05.png
deleted file mode 100644
index 683a37feb..000000000
Binary files a/DisCatSharp.Docs/images/_beyond_basics_logging_user_05.png and /dev/null differ
diff --git a/DisCatSharp.Docs/images/_beyond_basics_logging_user_06.png b/DisCatSharp.Docs/images/_beyond_basics_logging_user_06.png
deleted file mode 100644
index 7ddcf4bcc..000000000
Binary files a/DisCatSharp.Docs/images/_beyond_basics_logging_user_06.png and /dev/null differ
diff --git a/DisCatSharp.Docs/images/advanced_topics_buttons_01.png b/DisCatSharp.Docs/images/advanced_topics_buttons_01.png
index 48c14b2ec..68a440142 100644
Binary files a/DisCatSharp.Docs/images/advanced_topics_buttons_01.png and b/DisCatSharp.Docs/images/advanced_topics_buttons_01.png differ
diff --git a/DisCatSharp.Docs/images/advanced_topics_buttons_02.png b/DisCatSharp.Docs/images/advanced_topics_buttons_02.png
index 02e3ee3eb..084f6e023 100644
Binary files a/DisCatSharp.Docs/images/advanced_topics_buttons_02.png and b/DisCatSharp.Docs/images/advanced_topics_buttons_02.png differ
diff --git a/DisCatSharp.Docs/images/basics_bot_account_01.png b/DisCatSharp.Docs/images/basics_bot_account_01.png
index 4bc918bdb..5596901d2 100644
Binary files a/DisCatSharp.Docs/images/basics_bot_account_01.png and b/DisCatSharp.Docs/images/basics_bot_account_01.png differ
diff --git a/DisCatSharp.Docs/images/basics_bot_account_02.png b/DisCatSharp.Docs/images/basics_bot_account_02.png
index a301f725a..436ded4c3 100644
Binary files a/DisCatSharp.Docs/images/basics_bot_account_02.png and b/DisCatSharp.Docs/images/basics_bot_account_02.png differ
diff --git a/DisCatSharp.Docs/images/basics_bot_account_03.png b/DisCatSharp.Docs/images/basics_bot_account_03.png
index 04f818fc7..af85b27cc 100644
Binary files a/DisCatSharp.Docs/images/basics_bot_account_03.png and b/DisCatSharp.Docs/images/basics_bot_account_03.png differ
diff --git a/DisCatSharp.Docs/images/basics_bot_account_04.png b/DisCatSharp.Docs/images/basics_bot_account_04.png
index 994674570..77f7a356b 100644
Binary files a/DisCatSharp.Docs/images/basics_bot_account_04.png and b/DisCatSharp.Docs/images/basics_bot_account_04.png differ
diff --git a/DisCatSharp.Docs/images/basics_bot_account_05.png b/DisCatSharp.Docs/images/basics_bot_account_05.png
index c802b4cd1..52c002845 100644
Binary files a/DisCatSharp.Docs/images/basics_bot_account_05.png and b/DisCatSharp.Docs/images/basics_bot_account_05.png differ
diff --git a/DisCatSharp.Docs/images/basics_bot_account_06.png b/DisCatSharp.Docs/images/basics_bot_account_06.png
index e5fbf3cf4..370f3126e 100644
Binary files a/DisCatSharp.Docs/images/basics_bot_account_06.png and b/DisCatSharp.Docs/images/basics_bot_account_06.png differ
diff --git a/DisCatSharp.Docs/images/basics_bot_account_07.png b/DisCatSharp.Docs/images/basics_bot_account_07.png
index 3048be982..ca6b3fed6 100644
Binary files a/DisCatSharp.Docs/images/basics_bot_account_07.png and b/DisCatSharp.Docs/images/basics_bot_account_07.png differ
diff --git a/DisCatSharp.Docs/images/basics_bot_account_08.png b/DisCatSharp.Docs/images/basics_bot_account_08.png
index d86d27443..b46e28e52 100644
Binary files a/DisCatSharp.Docs/images/basics_bot_account_08.png and b/DisCatSharp.Docs/images/basics_bot_account_08.png differ
diff --git a/DisCatSharp.Docs/images/basics_bot_account_09.png b/DisCatSharp.Docs/images/basics_bot_account_09.png
index 3ef21b80a..ea2a174ac 100644
Binary files a/DisCatSharp.Docs/images/basics_bot_account_09.png and b/DisCatSharp.Docs/images/basics_bot_account_09.png differ
diff --git a/DisCatSharp.Docs/images/basics_bot_account_10.png b/DisCatSharp.Docs/images/basics_bot_account_10.png
new file mode 100644
index 000000000..db62792b5
Binary files /dev/null and b/DisCatSharp.Docs/images/basics_bot_account_10.png differ
diff --git a/DisCatSharp.Docs/images/basics_first_bot_01.png b/DisCatSharp.Docs/images/basics_first_bot_01.png
index 1585521dd..6daf95d21 100644
Binary files a/DisCatSharp.Docs/images/basics_first_bot_01.png and b/DisCatSharp.Docs/images/basics_first_bot_01.png differ
diff --git a/DisCatSharp.Docs/images/basics_first_bot_02.png b/DisCatSharp.Docs/images/basics_first_bot_02.png
index a17cc518b..a50d12b99 100644
Binary files a/DisCatSharp.Docs/images/basics_first_bot_02.png and b/DisCatSharp.Docs/images/basics_first_bot_02.png differ
diff --git a/DisCatSharp.Docs/images/basics_first_bot_03.png b/DisCatSharp.Docs/images/basics_first_bot_03.png
index a0ae365ac..c8f144092 100644
Binary files a/DisCatSharp.Docs/images/basics_first_bot_03.png and b/DisCatSharp.Docs/images/basics_first_bot_03.png differ
diff --git a/DisCatSharp.Docs/images/basics_first_bot_04.png b/DisCatSharp.Docs/images/basics_first_bot_04.png
index 908055fe8..c806c6d83 100644
Binary files a/DisCatSharp.Docs/images/basics_first_bot_04.png and b/DisCatSharp.Docs/images/basics_first_bot_04.png differ
diff --git a/DisCatSharp.Docs/images/basics_first_bot_05.png b/DisCatSharp.Docs/images/basics_first_bot_05.png
index 4fa997e66..e5291b7a6 100644
Binary files a/DisCatSharp.Docs/images/basics_first_bot_05.png and b/DisCatSharp.Docs/images/basics_first_bot_05.png differ
diff --git a/DisCatSharp.Docs/images/basics_first_bot_06.png b/DisCatSharp.Docs/images/basics_first_bot_06.png
index 49403b7b3..718404c27 100644
Binary files a/DisCatSharp.Docs/images/basics_first_bot_06.png and b/DisCatSharp.Docs/images/basics_first_bot_06.png differ
diff --git a/DisCatSharp.Docs/images/basics_first_bot_07.png b/DisCatSharp.Docs/images/basics_first_bot_07.png
index 1dc2f5810..7d820be0d 100644
Binary files a/DisCatSharp.Docs/images/basics_first_bot_07.png and b/DisCatSharp.Docs/images/basics_first_bot_07.png differ
diff --git a/DisCatSharp.Docs/images/basics_first_bot_08.png b/DisCatSharp.Docs/images/basics_first_bot_08.png
index d431e8ae9..f6b8fc736 100644
Binary files a/DisCatSharp.Docs/images/basics_first_bot_08.png and b/DisCatSharp.Docs/images/basics_first_bot_08.png differ
diff --git a/DisCatSharp.Docs/images/basics_first_bot_09.png b/DisCatSharp.Docs/images/basics_first_bot_09.png
index eb54d7fe2..caac19d9d 100644
Binary files a/DisCatSharp.Docs/images/basics_first_bot_09.png and b/DisCatSharp.Docs/images/basics_first_bot_09.png differ
diff --git a/DisCatSharp.Docs/images/basics_first_bot_10.png b/DisCatSharp.Docs/images/basics_first_bot_10.png
index a80e1732e..2ebb7c446 100644
Binary files a/DisCatSharp.Docs/images/basics_first_bot_10.png and b/DisCatSharp.Docs/images/basics_first_bot_10.png differ
diff --git a/DisCatSharp.Docs/images/basics_first_bot_11.png b/DisCatSharp.Docs/images/basics_first_bot_11.png
index 73a9bbaab..ddbaa9db9 100644
Binary files a/DisCatSharp.Docs/images/basics_first_bot_11.png and b/DisCatSharp.Docs/images/basics_first_bot_11.png differ
diff --git a/DisCatSharp.Docs/images/basics_first_bot_13.png b/DisCatSharp.Docs/images/basics_first_bot_13.png
index 68333989c..77e0f27fd 100644
Binary files a/DisCatSharp.Docs/images/basics_first_bot_13.png and b/DisCatSharp.Docs/images/basics_first_bot_13.png differ
diff --git a/DisCatSharp.Docs/images/beyond_basics_logging_user_01.png b/DisCatSharp.Docs/images/beyond_basics_logging_user_01.png
deleted file mode 100644
index b92abca84..000000000
Binary files a/DisCatSharp.Docs/images/beyond_basics_logging_user_01.png and /dev/null differ
diff --git a/DisCatSharp.Docs/images/beyond_basics_logging_user_02.png b/DisCatSharp.Docs/images/beyond_basics_logging_user_02.png
deleted file mode 100644
index 5204e1caa..000000000
Binary files a/DisCatSharp.Docs/images/beyond_basics_logging_user_02.png and /dev/null differ
diff --git a/DisCatSharp.Docs/images/beyond_basics_logging_user_03.png b/DisCatSharp.Docs/images/beyond_basics_logging_user_03.png
deleted file mode 100644
index 18104b9b1..000000000
Binary files a/DisCatSharp.Docs/images/beyond_basics_logging_user_03.png and /dev/null differ
diff --git a/DisCatSharp.Docs/images/beyond_basics_logging_user_04.png b/DisCatSharp.Docs/images/beyond_basics_logging_user_04.png
deleted file mode 100644
index 43f2bdc4d..000000000
Binary files a/DisCatSharp.Docs/images/beyond_basics_logging_user_04.png and /dev/null differ
diff --git a/DisCatSharp.Docs/images/commands_argument_converters_01.png b/DisCatSharp.Docs/images/commands_argument_converters_01.png
index 17c224e90..7e2468d05 100644
Binary files a/DisCatSharp.Docs/images/commands_argument_converters_01.png and b/DisCatSharp.Docs/images/commands_argument_converters_01.png differ
diff --git a/DisCatSharp.Docs/images/commands_command_attributes_01.png b/DisCatSharp.Docs/images/commands_command_attributes_01.png
index a5551aef1..7a66e5b14 100644
Binary files a/DisCatSharp.Docs/images/commands_command_attributes_01.png and b/DisCatSharp.Docs/images/commands_command_attributes_01.png differ
diff --git a/DisCatSharp.Docs/images/commands_dependency_injection_01.png b/DisCatSharp.Docs/images/commands_dependency_injection_01.png
deleted file mode 100644
index 08e1c7326..000000000
Binary files a/DisCatSharp.Docs/images/commands_dependency_injection_01.png and /dev/null differ
diff --git a/DisCatSharp.Docs/images/commands_help_formatter_01.png b/DisCatSharp.Docs/images/commands_help_formatter_01.png
index 93ad412d3..3a06eee61 100644
Binary files a/DisCatSharp.Docs/images/commands_help_formatter_01.png and b/DisCatSharp.Docs/images/commands_help_formatter_01.png differ
diff --git a/DisCatSharp.Docs/images/commands_help_formatter_02.png b/DisCatSharp.Docs/images/commands_help_formatter_02.png
deleted file mode 100644
index 64c544afe..000000000
Binary files a/DisCatSharp.Docs/images/commands_help_formatter_02.png and /dev/null differ
diff --git a/DisCatSharp.Docs/images/commands_intro_01.png b/DisCatSharp.Docs/images/commands_intro_01.png
index 7c71b1b6a..5ceb6dfe2 100644
Binary files a/DisCatSharp.Docs/images/commands_intro_01.png and b/DisCatSharp.Docs/images/commands_intro_01.png differ
diff --git a/DisCatSharp.Docs/images/commands_intro_03.png b/DisCatSharp.Docs/images/commands_intro_03.png
index 50e9ca3ff..acd1ba27a 100644
Binary files a/DisCatSharp.Docs/images/commands_intro_03.png and b/DisCatSharp.Docs/images/commands_intro_03.png differ
diff --git a/DisCatSharp.Docs/images/commands_intro_04.png b/DisCatSharp.Docs/images/commands_intro_04.png
index d9e1dc7f9..7571e673c 100644
Binary files a/DisCatSharp.Docs/images/commands_intro_04.png and b/DisCatSharp.Docs/images/commands_intro_04.png differ
diff --git a/DisCatSharp.Docs/images/commands_intro_05.png b/DisCatSharp.Docs/images/commands_intro_05.png
index cf537a8e5..20dda5317 100644
Binary files a/DisCatSharp.Docs/images/commands_intro_05.png and b/DisCatSharp.Docs/images/commands_intro_05.png differ
diff --git a/DisCatSharp.Docs/images/commands_intro_06.png b/DisCatSharp.Docs/images/commands_intro_06.png
index dc09489ab..8cade104b 100644
Binary files a/DisCatSharp.Docs/images/commands_intro_06.png and b/DisCatSharp.Docs/images/commands_intro_06.png differ
diff --git a/DisCatSharp.Docs/images/commands_intro_07.png b/DisCatSharp.Docs/images/commands_intro_07.png
index ff48bc04c..593fe85a0 100644
Binary files a/DisCatSharp.Docs/images/commands_intro_07.png and b/DisCatSharp.Docs/images/commands_intro_07.png differ
diff --git a/DisCatSharp.Docs/images/commands_intro_08.png b/DisCatSharp.Docs/images/commands_intro_08.png
index 34271b117..88051458b 100644
Binary files a/DisCatSharp.Docs/images/commands_intro_08.png and b/DisCatSharp.Docs/images/commands_intro_08.png differ
diff --git a/DisCatSharp.Docs/images/interactivity_01.png b/DisCatSharp.Docs/images/interactivity_01.png
index c37dccff0..0ecbe341e 100644
Binary files a/DisCatSharp.Docs/images/interactivity_01.png and b/DisCatSharp.Docs/images/interactivity_01.png differ
diff --git a/DisCatSharp.Docs/images/interactivity_02.png b/DisCatSharp.Docs/images/interactivity_02.png
index 707b5efa7..f27282537 100644
Binary files a/DisCatSharp.Docs/images/interactivity_02.png and b/DisCatSharp.Docs/images/interactivity_02.png differ
diff --git a/DisCatSharp.Docs/images/interactivity_03.png b/DisCatSharp.Docs/images/interactivity_03.png
index c293193c0..f4bef6148 100644
Binary files a/DisCatSharp.Docs/images/interactivity_03.png and b/DisCatSharp.Docs/images/interactivity_03.png differ
diff --git a/DisCatSharp.Docs/images/interactivity_04.png b/DisCatSharp.Docs/images/interactivity_04.png
index c113d1019..dda454ec6 100644
Binary files a/DisCatSharp.Docs/images/interactivity_04.png and b/DisCatSharp.Docs/images/interactivity_04.png differ
diff --git a/DisCatSharp.Docs/images/interactivity_05.png b/DisCatSharp.Docs/images/interactivity_05.png
index a3b5c9795..93921e846 100644
Binary files a/DisCatSharp.Docs/images/interactivity_05.png and b/DisCatSharp.Docs/images/interactivity_05.png differ
diff --git a/DisCatSharp.Docs/images/voicenext_transmit_01.png b/DisCatSharp.Docs/images/voicenext_transmit_01.png
index 8db7e369d..11429803c 100644
Binary files a/DisCatSharp.Docs/images/voicenext_transmit_01.png and b/DisCatSharp.Docs/images/voicenext_transmit_01.png differ
diff --git a/DisCatSharp.Docs/index.md b/DisCatSharp.Docs/index.md
index ee37e095c..c0917edad 100644
--- a/DisCatSharp.Docs/index.md
+++ b/DisCatSharp.Docs/index.md
@@ -1,18 +1,46 @@
---
uid: index
-title: DisCatSharp 10.0.0 Documentation
+title: DisCatSharp 10.1.0 Documentation
---
-DisCatSharp 10.0.0 Documentation
+# DisCatSharp ![Stable](https://img.shields.io/nuget/v/DisCatSharp?color=%23ebb34b&label=Stable&style=flat-square&logo=nuget) ![Nightly](https://img.shields.io/nuget/vpre/DisCatSharp?color=%23ff1493&label=Nightly&style=flat-square&logo=nuget)
+
![DisCatSharp Logo](/logobig.png "DisCatSharp Documentation")
-## DisCatSharp 10.0.0 Documentation
-[DisCatSharp](https://github.com/Aiko-IT-Systems/DisCatSharp) (DCS) is an unofficial .NET wrapper for the [Discord API](https://discordapp.com/developers/docs/intro "Discord API") based off [DSharpPlus](https://github.com/DSharpPlus/DSharpPlus).
-The library has been rewritten to fit quality and API standards as well as target wider range of .NET implementations. Furthermore this lib will includes many new features of Discord and is pretty fast with keeping up with Discords API.
+## DisCatSharp 10.1.0 Documentation
+[DisCatSharp](https://github.com/Aiko-IT-Systems/DisCatSharp) (DCS) is an unofficial .NET wrapper for the [Discord API](https://discord.com/developers/docs/intro "Discord API") based off DSharpPlus.
+The library has been rewritten to fit quality and API standards. Furthermore this lib includes many new features of Discord and is pretty fast with keeping up with Discords API.
+
+## Why DisCatSharp?
+
+If you:
+- want a library where you get kind and efficient help
+- would like to have and use the most recent features of the Discord API
+- are ready to build great things
+
+Then this is the right place for you!
## Getting Started
New users probably want to take a look into the [articles](xref:preamble) for quick start guides, tutorials, and examples of use.
Once you've gotten through the articles, head over to the [API Documentation](/api/index.html) for all classes and methods provided by this library.
## Source and Contributors
DisCatSharp is licensed under MIT License, as detailed in the [license](https://github.com/Aiko-IT-Systems/DisCatSharp/blob/main/LICENSE.md) found in the repository.
The repository containing the source code for this library can be found [here](https://github.com/Aiko-IT-Systems/DisCatSharp). Contributions are welcomed.
+
+### Sponsors [![Sponsors](https://img.shields.io/github/sponsors/Aiko-IT-Systems?label=&style=flat-square&logo=github)](https://github.com/sponsors/Aiko-IT-Systems) / [![Sponsors](https://img.shields.io/github/sponsors/Lulalaby?label=&style=flat-square&logo=github)](https://github.com/sponsors/Lulalaby)
+
+- [Deividas Kazakevicius](https://github.com/DeividasKaza)
+- [Will](https://github.com/villChurch)
+
+### Thanks
+
+Big thanks goes to the following people who helped us without being part of the core team ♥️
+- [Auros Nexus](https://github.com/Auros)
+- [Lunar Starstrum](https://github.com/OoLunar)
+- [Geferon](https://github.com/geferon)
+- [Alice](https://github.com/QuantuChi)
+- [Will](https://github.com/villChurch)
+
+### Special Thanks
+
+The special thanks goes to Nagisa. Make sure to check out her [Instagram](https://www.instagram.com/nagisaarts_/) ♥️♥️
diff --git a/DisCatSharp.targets b/DisCatSharp.targets
index 205873bca..901664964 100644
--- a/DisCatSharp.targets
+++ b/DisCatSharp.targets
@@ -1,28 +1,28 @@
- AITSYS, aiko, J_M_Lutra , Xorog, Badger2-3, AITSYS Contributors
+ AITSYS, DisCatSharp, DisCatSharp Contributors
AITSYS
False
https://github.com/Aiko-IT-Systems/DisCatSharp
https://github.com/Aiko-IT-Systems/DisCatSharp
Git
logobig.png
LICENSE.md
True
True
True
diff --git a/DisCatSharp/Entities/Application/DiscordApplicationCommandLocalization.cs b/DisCatSharp/Entities/Application/DiscordApplicationCommandLocalization.cs
index d315327d8..f713af913 100644
--- a/DisCatSharp/Entities/Application/DiscordApplicationCommandLocalization.cs
+++ b/DisCatSharp/Entities/Application/DiscordApplicationCommandLocalization.cs
@@ -1,105 +1,105 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// 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.
using System;
using System.Collections.Generic;
namespace DisCatSharp.Entities;
///
/// Represents a application command localization.
///
public sealed class DiscordApplicationCommandLocalization
{
///
/// Gets the localization dict.
///
public Dictionary Localizations { get; internal set; }
///
- /// Gets valid [locales](xref:application_commands_translations_reference#valid-locales) for Discord.
+ /// Gets valid [locales](xref:modules_application_commands_translations_reference#valid-locales) for Discord.
///
internal List ValidLocales = new() { "ru", "fi", "hr", "de", "hu", "sv-SE", "cs", "fr", "it", "en-GB", "pt-BR", "ja", "tr", "en-US", "es-ES", "uk", "hi", "th", "el", "no", "ro", "ko", "zh-TW", "vi", "zh-CN", "pl", "bg", "da", "nl", "lt" };
///
/// Adds a localization.
///
- /// The [locale](xref:application_commands_translations_reference#valid-locales) to add.
+ /// The [locale](xref:modules_application_commands_translations_reference#valid-locales) to add.
/// The translation to add.
public void AddLocalization(string locale, string value)
{
if (this.Validate(locale))
{
this.Localizations.Add(locale, value);
}
else
{
throw new NotSupportedException($"The provided locale \"{locale}\" is not valid for Discord.\n" +
$"Valid locales: {string.Join(", ", this.ValidLocales)}");
}
}
///
/// Removes a localization.
///
- /// The [locale](xref:application_commands_translations_reference#valid-locales) to remove.
+ /// The [locale](xref:modules_application_commands_translations_reference#valid-locales) to remove.
public void RemoveLocalization(string locale)
=> this.Localizations.Remove(locale);
///
/// Initializes a new instance of .
///
public DiscordApplicationCommandLocalization() { }
///
/// Initializes a new instance of .
///
/// Localizations.
public DiscordApplicationCommandLocalization(Dictionary localizations)
{
if (localizations != null)
{
foreach (var locale in localizations.Keys)
{
if (!this.Validate(locale))
throw new NotSupportedException($"The provided locale \"{locale}\" is not valid for Discord.\n" +
$"Valid locales: {string.Join(", ", this.ValidLocales)}");
}
}
this.Localizations = localizations;
}
///
/// Gets the KVPs.
///
///
public Dictionary GetKeyValuePairs()
=> this.Localizations;
///
- /// Whether the [locale](xref:application_commands_translations_reference#valid-locales) to be added is valid for Discord.
+ /// Whether the [locale](xref:modules_application_commands_translations_reference#valid-locales) to be added is valid for Discord.
///
- /// [Locale](xref:application_commands_translations_reference#valid-locales) string.
+ /// [Locale](xref:modules_application_commands_translations_reference#valid-locales) string.
public bool Validate(string lang)
=> this.ValidLocales.Contains(lang);
}
diff --git a/README.md b/README.md
index fd117f75d..975de982a 100644
--- a/README.md
+++ b/README.md
@@ -1,128 +1,133 @@
# DisCatSharp ![Stable](https://img.shields.io/nuget/v/DisCatSharp?color=%23ebb34b&label=Stable&style=flat-square&logo=nuget) ![Nightly](https://img.shields.io/nuget/vpre/DisCatSharp?color=%23ff1493&label=Nightly&style=flat-square&logo=nuget)
## A Discord Bot Library written in C# for .NET
![DisCatSharp Logo](https://github.com/Aiko-IT-Systems/DisCatSharp/blob/main/DisCatSharp.Logos/android-chrome-192x192.png?raw=true)
----
[![Build](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/dotnet.yml/badge.svg)](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/dotnet.yml)
[![Documentation](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/docs.yml/badge.svg)](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/docs.yml)
[![Typos](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/typos.yml/badge.svg)](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/typos.yml)
[![CodeQL](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/codeql.yml/badge.svg)](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/codeql.yml)
-![Wakatime](https://wakatime.com/badge/github/Aiko-IT-Systems/DisCatSharp.svg)
+
[![AppVeyor](https://img.shields.io/appveyor/build/AITSYS/DisCatSharp?label=Appveyor&logo=appveyor&style=flat-square)](https://ci.appveyor.com/project/AITSYS/discatsharp)
[![GitHub last commit](https://img.shields.io/github/last-commit/Aiko-IT-Systems/DisCatSharp?label=Last%20Commit&style=flat-square&logo=github)](https://aitsys.dev/source/DisCatSharp/history/)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/w/Aiko-IT-Systems/DisCatSharp?label=Commit%20Activity&style=flat-square&logo=github)](https://github.com/Aiko-IT-Systems/DisCatSharp/commits/main)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/Aiko-IT-Systems/DisCatSharp?label=PRs&style=flat-square&logo=github&logo=gitub)](https://github.com/Aiko-IT-Systems/DisCatSharp/pulls)
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/Aiko-IT-Systems/DisCatSharp?label=Size&style=flat-square&logo=github)
----
+# News
-## **NEW!**
+## New in DisCatSharp 10.1.0
- Support for application commands (guild and global) in shards
- Translation template export based on your own code.
+## Upcoming
+- We're building a translation generator tool for the template export function introduced in 10.1.0.
+
+# About
+
## Why DisCatSharp?
If you:
- want a library where you get kind and efficient help
- would like to have and use the most recent features of the Discord API
- are ready to build great things
Then this is the right place for you!
## Installing
-You can install the library from following source: [NuGet](https://www.nuget.org/profiles/DisCatSharp).
+You can install the library from the following sources:
+- [NuGet](https://www.nuget.org/profiles/DisCatSharp)
+- [GitHub](https://github.com/orgs/Aiko-IT-Systems/packages?tab=packages&q=DisCatSharp)
## Documentation
-
-
-The documentation of the nightly versions is available at [docs.dcs.aitsys.dev](https://docs.discatsharp.tech).
+The documentation of the nightly versions is available at [docs.discatsharp.tech](https://docs.discatsharp.tech).
-Fallback docs are available at [docs-alt.dcs.aitsys.dev](https://docs-alt.dcs.aitsys.dev).
+Alternative hosts for our docs are:
+- Backup Host [docs-alt.dcs.aitsys.dev](https://docs-alt.dcs.aitsys.dev)
+- Cloudflare [fallback-docs.dcs.aitsys.dev](https://fallback-docs.dcs.aitsys.dev) or [discatsharp-docs.pages.dev](https://discatsharp-docs.pages.dev)
-##### Note: DocFx is broken right now, waiting for fix from upstream. So the documentation is outdated.
## Bugs or Feature requests?
Either join our official support guild at https://discord.gg/Uk7sggRBTm
Or write us an mail at [bugs@aitsys.dev](mailto:bugs@aitsys.dev).
-All requests are tracked at [aitsys.dev](https://aitsys.dev).
+All requests are tracked at [aitsys.dev](https://aitsys.dev/project/view/1/).
## Tutorials
* [Howto](https://docs.discatsharp.tech/articles/basics/bot_account.html)
* [Examples](https://examples.dcs.aitsys.dev)
## Snippets
[Snippets for Visual Studio](https://github.com/Aiko-IT-Systems/DisCatSharp.Snippets)
----
## NuGet Packages
### Main
| Package | Stable | Nightly |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| DisCatSharp | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) |
| DisCatSharp.ApplicationCommands | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.ApplicationCommands.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.ApplicationCommands.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) |
| DisCatSharp.CommandsNext | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.CommandsNext.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.CommandsNext.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) |
| DisCatSharp.Interactivity | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Interactivity.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.Interactivity.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) |
### Voice
| Package | Stable | Nightly |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| DisCatSharp.Lavalink | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Lavalink.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.Lavalink.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) |
| DisCatSharp.VoiceNext | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.VoiceNext.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.VoiceNext.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) |
| DisCatSharp.VoiceNext.Natives | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.VoiceNext.Natives.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.VoiceNext.Natives.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) |
### Hosting
| Package | Stable | Nightly |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| DisCatSharp.Configuration | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Configuration.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.Configuration.svg?label=&logo=nuget&color=%23ff1493&style=flat-square) |
| DisCatSharp.Hosting | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Hosting.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.Hosting.svg?label=&logo=nuget&color=%23ff1493&style=flat-square) |
| DisCatSharp.Hosting.DependencyInjection | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Hosting.DependencyInjection.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.Hosting.DependencyInjection.svg?label=&logo=nuget&color=%23ff1493&style=flat-square) |
### Templates
| Package | Stable | Nightly |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| DisCatSharp.ProjectTemplates | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.ProjectTemplates.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.ProjectTemplates.svg?label=&logo=nuget&color=%23ff1493&style=flat-square) |
### Development / Commons
| Package | Stable | Nightly |
| ------------------ | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| DisCatSharp.Common | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Common.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.Common.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) |
----
## Sponsors [![Sponsors](https://img.shields.io/github/sponsors/Aiko-IT-Systems?label=&style=flat-square&logo=github)](https://github.com/sponsors/Aiko-IT-Systems) / [![Sponsors](https://img.shields.io/github/sponsors/Lulalaby?label=&style=flat-square&logo=github)](https://github.com/sponsors/Lulalaby)
- [Deividas Kazakevicius](https://github.com/DeividasKaza)
- [Will](https://github.com/villChurch)
## Thanks
-Big thanks goes to the following people who helped us ♥️
+Big thanks goes to the following people who helped us without being part of the core team ♥️
- [Auros Nexus](https://github.com/Auros)
- [Lunar Starstrum](https://github.com/OoLunar)
-- [Johannes](https://github.com/JMLutra)
- [Geferon](https://github.com/geferon)
- [Alice](https://github.com/QuantuChi)
- [Will](https://github.com/villChurch)
-- [Mira](https://github.com/TheXorog)
## Special Thanks
-The special thanks goes to Nagisa. Make sure to check out her [instagram](https://www.instagram.com/nagisaarts_/) ♥️♥️
+The special thanks goes to Nagisa. Make sure to check out her [Instagram](https://www.instagram.com/nagisaarts_/) ♥️♥️
diff --git a/Version.targets b/Version.targets
index 4b678d829..ab30b2b86 100644
--- a/Version.targets
+++ b/Version.targets
@@ -1,21 +1,21 @@
- 10.0.1
+ 10.1.0
$(VersionPrefix)-$(VersionSuffix)-$(BuildNumber)
$(VersionPrefix).$(BuildNumber)
$(VersionPrefix).$(BuildNumber)
$(VersionPrefix)-$(VersionSuffix)
$(VersionPrefix).0
$(VersionPrefix).0
$(VersionPrefix)
$(VersionPrefix).0
$(VersionPrefix).0
diff --git a/appveyor.yml b/appveyor.yml
index 83ec84cba..1b7fc7492 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,140 +1,140 @@
-
branches:
only:
- main
- version: 10.0.1-nightly-{build}
+ version: 10.1.0-nightly-{build}
pull_requests:
do_not_increment_build_number: true
skip_tags: true
max_jobs: 1
image: Visual Studio 2022
clone_depth: 1
build_script:
- ps: |-
# Version number
$BUILD_NUMBER = [int]$Env:APPVEYOR_BUILD_NUMBER
$BUILD_SUFFIX = "nightly"
# Branch
$BRANCH = "$Env:APPVEYOR_REPO_BRANCH"
$Env:DOCFX_SOURCE_BRANCH_NAME = "$BRANCH"
# Output directory
$Env:ARTIFACT_DIR = ".\artifacts"
$dir = New-Item -type directory $env:ARTIFACT_DIR
$dir = $dir.FullName
# Verbosity
Write-Host "Build: $BUILD_NUMBER / Branch: $BRANCH"
Write-Host "Artifacts will be placed in: $dir"
# Check if this is a PR
if (-not $Env:APPVEYOR_PULL_REQUEST_NUMBER)
{
Write-Host "Commencing complete build"
& .\rebuild-all.ps1 -ArtifactLocation "$dir" -Configuration "Release" -VersionSuffix "$BUILD_SUFFIX" -BuildNumber $BUILD_NUMBER
& Remove-Item "$dir\*.symbols.nupkg"
}
else
{
Write-Host "Building from PR ($Env:APPVEYOR_PULL_REQUEST_NUMBER)"
& .\rebuild-all.ps1 -ArtifactLocation "$dir" -Configuration "Release" -VersionSuffix "$BUILD_SUFFIX" -BuildNumber $BUILD_NUMBER
& Remove-Item "$dir\*.symbols.nupkg"
}
artifacts:
- path: artifacts\*.snupkg
- path: artifacts\*.nupkg
- path: artifacts\dcs-docs.tar.xz
deploy:
- provider: NuGet
server:
api_key:
secure: RjohrwOJE0RgzgIfe71AH42hRr7aZnEz5So/EKRI1FM7LQ/SLrEwSTDsNEQWMQik
skip_symbols: false
- provider: GitHub
auth_token:
secure: oMF8sv9mhVjO7pBctQOwlmfd5aHQ4hvMoVCz77bgO9+1zBQSelPHxk0bCVfXNCCp
prerelease: true
force_update: true
- provider: NuGet
server: https://nuget.pkg.github.com/Aiko-IT-Systems/index.json
username: lulalaby
api_key:
secure: SBGo8KrGJ7t5wwMNHKD0WSzrQ+PLJbqXE3FtDH2yGkSrQewO+kzmwp/xGk5a84He
skip_symbols: true
on_success:
- ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1
- ps: ./send.ps1 success $env:WEBHOOK_URL
on_failure:
- ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1
- ps: ./send.ps1 failure $env:WEBHOOK_URL
# Releases
-
branches:
only:
- /release/
version: 10.0.0
pull_requests:
do_not_increment_build_number: true
skip_tags: true
max_jobs: 1
image: Visual Studio 2022
clone_depth: 1
build_script:
- ps: |-
# Version number
$BUILD_NUMBER = [int]$Env:APPVEYOR_BUILD_NUMBER
# Branch
$BRANCH = "$Env:APPVEYOR_REPO_BRANCH"
$Env:DOCFX_SOURCE_BRANCH_NAME = "$BRANCH"
# Output directory
$Env:ARTIFACT_DIR = ".\artifacts"
$dir = New-Item -type directory $env:ARTIFACT_DIR
$dir = $dir.FullName
# Verbosity
Write-Host "Build: $BUILD_NUMBER / Branch: $BRANCH"
Write-Host "Artifacts will be placed in: $dir"
# Check if this is a PR
if (-not $Env:APPVEYOR_PULL_REQUEST_NUMBER)
{
Write-Host "Commencing complete build"
& .\rebuild-all.ps1 -ArtifactLocation "$dir" -Configuration "Release" -VersionSuffix "$BUILD_SUFFIX" -BuildNumber $BUILD_NUMBER
& Remove-Item "$dir\*.symbols.nupkg"
}
else
{
Write-Host "Building from PR ($Env:APPVEYOR_PULL_REQUEST_NUMBER)"
& .\rebuild-all.ps1 -ArtifactLocation "$dir" -Configuration "Release" -VersionSuffix "$BUILD_SUFFIX" -BuildNumber $BUILD_NUMBER
& Remove-Item "$dir\*.symbols.nupkg"
}
artifacts:
- path: artifacts\*.snupkg
- path: artifacts\*.nupkg
- path: artifacts\dcs-docs.tar.xz
deploy:
- provider: NuGet
server:
api_key:
secure: hFr7dmC41bUWKRLZTu5SpkkN8Xv0b69VdkDL/wcsMkI4RmkVW6GKWi1iUMrtz0IT
skip_symbols: false
- provider: GitHub
auth_token:
secure: oMF8sv9mhVjO7pBctQOwlmfd5aHQ4hvMoVCz77bgO9+1zBQSelPHxk0bCVfXNCCp
prerelease: false
force_update: true
- provider: NuGet
server: https://nuget.pkg.github.com/Aiko-IT-Systems/index.json
username: lulalaby
api_key:
secure: SBGo8KrGJ7t5wwMNHKD0WSzrQ+PLJbqXE3FtDH2yGkSrQewO+kzmwp/xGk5a84He
skip_symbols: true
on_success:
- ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1
- ps: ./send.ps1 success $env:WEBHOOK_URL
on_failure:
- ps: Invoke-RestMethod https://raw.githubusercontent.com/DiscordHooks/appveyor-discord-webhook/master/send.ps1 -o send.ps1
- ps: ./send.ps1 failure $env:WEBHOOK_URL