diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index f88f816c3..ade981109 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -1,99 +1,105 @@
name: "Documentation"
on:
push:
branches: [ main ]
workflow_dispatch:
+
+env:
+ DOTNET_NOLOGO: true
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
+ DOTNET_CLI_TELEMETRY_OPTOUT: true
+
jobs:
build:
- runs-on: windows-latest
+ runs-on: ubuntu-latest
name: Build documentation
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
path: DisCatSharp
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
- dotnet-version: 6.0.202
+ dotnet-version: 6.0.400
- name: Restore packages
working-directory: DisCatSharp
run: dotnet restore
- name: Build library
working-directory: DisCatSharp
run: dotnet build -c Release
- name: Test library
working-directory: DisCatSharp
run: dotnet test -c Release
continue-on-error: true
- name: Build Docs
working-directory: DisCatSharp
shell: pwsh
run: ./rebuild-docs.ps1 -DocsPath "./DisCatSharp.Docs" -Output ".." -PackageName "dcs-docs"
- name: Upload packed docs
uses: actions/upload-artifact@v3
with:
name: dcs-docs.zip
path: dcs-docs.zip
retention-days: 30
documentation:
runs-on: windows-latest
name: Upload documentation
needs: build
steps:
- name: Checkout docs repository
uses: actions/checkout@v3
with:
repository: Aiko-IT-Systems/DisCatSharp.Docs
path: DisCatSharp.Docs
token: ${{ secrets.NYUW_TOKEN_GH }}
- name: Download packed docs
uses: actions/download-artifact@v3
with:
name: dcs-docs.zip
- name: Purge old docs
working-directory: DisCatSharp.Docs
shell: pwsh
run: Get-ChildItem -Exclude .git* | Remove-Item -Recurse -Force
- name: Extract new docs
shell: pwsh
run: Expand-Archive -Path dcs-docs.zip DisCatSharp.Docs/
- name: Delete packed docs
uses: geekyeggo/delete-artifact@v1
with:
name: dcs-docs.zip
- name: Commit and push changes
uses: EndBug/add-and-commit@main
with:
cwd: DisCatSharp.Docs
default_author: user_info
author_name: DisCatSharp
author_email: team@aitsys.dev
committer_name: NyuwBot
committer_email: nyuw@aitsys.dev
commit: --signoff
message: 'Docs update for commit ${{ github.repository }} (${{ github.sha }})'
publish-main:
runs-on: ubuntu-latest
name: Publish documentation on main server
needs: documentation
steps:
- name: Get SSH Agent
uses: webfactory/ssh-agent@v0.5.4
with:
ssh-private-key: ${{ secrets.AITSYS_SSH }}
- name: Publish on server
continue-on-error: true
run: ssh -o StrictHostKeyChecking=no -T root@80.153.182.68 -f 'cd /var/www/dcs.aitsys.dev/docs && git pull -f'
publish-backup:
runs-on: ubuntu-latest
name: Publish documentation on backup server
needs: documentation
steps:
- name: Get SSH Agent
uses: webfactory/ssh-agent@v0.5.4
with:
ssh-private-key: ${{ secrets.AITSYS_SSH }}
- name: Publish on server
continue-on-error: true
run: ssh -o StrictHostKeyChecking=no -T root@207.180.240.241 -f 'cd /var/www/dcsdocs && git pull -f'
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index ab4154169..b130084c8 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -1,26 +1,26 @@
name: "Build"
on:
push:
pull_request:
workflow_dispatch:
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- dotnet: [6.0.202]
+ dotnet: [6.0.400]
runs-on: ${{ matrix.os }}
name: Build library
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
dotnet-version: ${{ matrix.dotnet }}
- name: Restore dependencies
run: dotnet restore
- name: Build library
run: dotnet build -c Release
- name: Test library
run: dotnet test --verbosity normal -c Release
diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
index 56396797c..3df1cd78d 100644
--- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
+++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
@@ -1,2068 +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; }
+ internal bool StartupFinished { get; set; } = false;
///
/// Gets the service provider this module was configured with.
///
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.StartupFinished = false;
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.First();
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.First().Options);
await this.RunCommandAsync(context, method, args);
}
else if (subgroups.Any())
{
var command = e.Interaction.Data.Options.First();
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.First().Options.First().Options);
await this.RunCommandAsync(context, method, args);
}
await this._slashExecuted.InvokeAsync(this, new SlashCommandExecutedEventArgs(this.Client.ServiceProvider) { Context = context });
}
catch (Exception ex)
{
this.Client.Logger.LogError(@"{msg}", ex.Message);
this.Client.Logger.LogError(@"{stack}", ex.StackTrace);
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.First();
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.First();
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?))
args.Add((long?)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)
{
- await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
- .WithContent($"There are no slash commands").AsEphemeral(true));
+ 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()));
}
- await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
- new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true));
+ 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)
{
- await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
+ 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()));
}
- await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
+ 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.ApplicationCommands/global.json b/DisCatSharp.ApplicationCommands/global.json
index 90a51a911..4e0e89bbd 100644
--- a/DisCatSharp.ApplicationCommands/global.json
+++ b/DisCatSharp.ApplicationCommands/global.json
@@ -1,6 +1,6 @@
{
- "sdk": {
- "version": "6.0.202",
- "rollForward": "major"
- }
+ "sdk": {
+ "version": "6.0.400",
+ "rollForward": "latestMinor"
+ }
}
diff --git a/DisCatSharp.CommandsNext/global.json b/DisCatSharp.CommandsNext/global.json
index 90a51a911..4e0e89bbd 100644
--- a/DisCatSharp.CommandsNext/global.json
+++ b/DisCatSharp.CommandsNext/global.json
@@ -1,6 +1,6 @@
{
- "sdk": {
- "version": "6.0.202",
- "rollForward": "major"
- }
+ "sdk": {
+ "version": "6.0.400",
+ "rollForward": "latestMinor"
+ }
}
diff --git a/DisCatSharp.Common/global.json b/DisCatSharp.Common/global.json
index 90a51a911..4e0e89bbd 100644
--- a/DisCatSharp.Common/global.json
+++ b/DisCatSharp.Common/global.json
@@ -1,6 +1,6 @@
{
- "sdk": {
- "version": "6.0.202",
- "rollForward": "major"
- }
+ "sdk": {
+ "version": "6.0.400",
+ "rollForward": "latestMinor"
+ }
}
diff --git a/DisCatSharp.Configuration.Tests/global.json b/DisCatSharp.Configuration.Tests/global.json
index 90a51a911..4e0e89bbd 100644
--- a/DisCatSharp.Configuration.Tests/global.json
+++ b/DisCatSharp.Configuration.Tests/global.json
@@ -1,6 +1,6 @@
{
- "sdk": {
- "version": "6.0.202",
- "rollForward": "major"
- }
+ "sdk": {
+ "version": "6.0.400",
+ "rollForward": "latestMinor"
+ }
}
diff --git a/DisCatSharp.Configuration/global.json b/DisCatSharp.Configuration/global.json
index 90a51a911..4e0e89bbd 100644
--- a/DisCatSharp.Configuration/global.json
+++ b/DisCatSharp.Configuration/global.json
@@ -1,6 +1,6 @@
{
- "sdk": {
- "version": "6.0.202",
- "rollForward": "major"
- }
+ "sdk": {
+ "version": "6.0.400",
+ "rollForward": "latestMinor"
+ }
}
diff --git a/DisCatSharp.Docs/articles/application_commands/intro.md b/DisCatSharp.Docs/articles/application_commands/intro.md
index 605a61951..286240fe7 100644
--- a/DisCatSharp.Docs/articles/application_commands/intro.md
+++ b/DisCatSharp.Docs/articles/application_commands/intro.md
@@ -1,133 +1,133 @@
---
uid: 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.SlashCommandAttribute) or [ContextMenu](xref:DisCatSharp.ApplicationCommands.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.InteractionContext) or [ContextMenuContext](xref:DisCatSharp.ApplicationCommands.ContextMenuContext).
+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.
### 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.SlashCommandGroupAttribute) attribute.
+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/application_commands/options.md b/DisCatSharp.Docs/articles/application_commands/options.md
index 6fc3cc253..de23d8c9d 100644
--- a/DisCatSharp.Docs/articles/application_commands/options.md
+++ b/DisCatSharp.Docs/articles/application_commands/options.md
@@ -1,251 +1,251 @@
---
uid: 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.OptionAttribute) attribute.
-They should be after [InteractionContext](xref:DisCatSharp.ApplicationCommands.InteractionContext).
+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.ChoiceAttribute)
-- [Choice Providers](xref:DisCatSharp.ApplicationCommands.IChoiceProvider)
+- [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.ChoiceAttribute) before the [Option](xref:DisCatSharp.ApplicationCommands.OptionAttribute) attribute
+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.ChoiceAttribute) takes a name that will be visible to the user, and the second - a value that will be passed to the command.
+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.IChoiceProvider) and contains the `Provider()` method.
+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.IChoiceProvider) refer to the examples.
+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.IChoiceProvider), the class inherits [IAutocompleteProvider](xref:DisCatSharp.ApplicationCommands.Attributes.IAutocompleteProvider), and the Provider method should return a list with [DiscordApplicationCommandAutocompleteChoice](xref:DisCatSharp.Entities.DiscordApplicationCommandAutocompleteChoice).
+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.ChoiceProviderAttribute) with [Autocomplete](xref:DisCatSharp.ApplicationCommands.Attributes.AutocompleteAttribute), but also added `true` to [Option](xref:DisCatSharp.ApplicationCommands.OptionAttribute).
+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/translations/using.md b/DisCatSharp.Docs/articles/application_commands/translations/using.md
index 78f4577bb..dba6a0c9e 100644
--- a/DisCatSharp.Docs/articles/application_commands/translations/using.md
+++ b/DisCatSharp.Docs/articles/application_commands/translations/using.md
@@ -1,197 +1,197 @@
---
uid: 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)
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).
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`.
We use this to determine, where the translation belongs to.
A correct json for this example would look like that:
```json
[
{
"name":"my_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.
## 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.InteractionContext), [ContextMenuContext](xref:DisCatSharp.ApplicationCommands.ContextMenuContext), [AutoCompleteContext](xref:DisCatSharp.ApplicationCommands.AutocompleteContext) and [DiscordInteraction](xref:DisCatSharp.Entities.DiscordInteraction).
+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/important_changes/10_0_0.md b/DisCatSharp.Docs/articles/important_changes/10_0_0.md
index 3185e016f..ed1299ac3 100644
--- a/DisCatSharp.Docs/articles/important_changes/10_0_0.md
+++ b/DisCatSharp.Docs/articles/important_changes/10_0_0.md
@@ -1,25 +1,25 @@
---
uid: important_changes_10_0_0
title: Version 10.0.0
---
# Upgrade from **9.9.0** to **10.0.0**
## What is new in DisCatSharp?
- Advanced dependency injection system
- Support for API v10
- Message content intent
- Properly working application command localization
- Optimized lib code
- Pre-implementation of upcoming things
-- Support for [Channel Type](xref:DisCatSharp.ChannelType) `Forum` (WIP)
+- Support for [Channel Type](xref:DisCatSharp.Enums.ChannelType) `Forum` (WIP)
## What changed?
To get message content with API v10, you have to enable the message content intent in the developer portal AND specify the [DiscordIntent](xref:DisCatSharp.DiscordIntents) `MessageContent`.
Otherwise you won't receive message contents from guild messages where the bot isn't mentioned.
## Backwards Compatibility
You can always choose to use a previous API version.
I.e. if you want to use API V9, you can use `DiscordIntents.AllV9Less` to enable all intents that are valid for this version.
diff --git a/DisCatSharp.Docs/faq.md b/DisCatSharp.Docs/faq.md
index a39994696..a9dec19b5 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.
### 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.
### 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_DiscordActivity_System_Nullable_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_DiscordActivity_System_Nullable_UserStatus__System_Nullable_DateTimeOffset__)([DiscordActivity](xref:DisCatSharp.Entities.DiscordActivity), System.Nullable{UserStatus}, System.Nullable{DateTimeOffset}) OR [UpdateStatusAsync](xref:DisCatSharp.DiscordShardedClient#DisCatSharp_DiscordShardedClient_UpdateStatusAsync_DiscordActivity_System_Nullable_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.
+* 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.EventHandlers.Tests/global.json b/DisCatSharp.EventHandlers.Tests/global.json
index 90a51a911..4e0e89bbd 100644
--- a/DisCatSharp.EventHandlers.Tests/global.json
+++ b/DisCatSharp.EventHandlers.Tests/global.json
@@ -1,6 +1,6 @@
{
- "sdk": {
- "version": "6.0.202",
- "rollForward": "major"
- }
+ "sdk": {
+ "version": "6.0.400",
+ "rollForward": "latestMinor"
+ }
}
diff --git a/DisCatSharp.Hosting.DependencyInjection/global.json b/DisCatSharp.Hosting.DependencyInjection/global.json
index 90a51a911..4e0e89bbd 100644
--- a/DisCatSharp.Hosting.DependencyInjection/global.json
+++ b/DisCatSharp.Hosting.DependencyInjection/global.json
@@ -1,6 +1,6 @@
{
- "sdk": {
- "version": "6.0.202",
- "rollForward": "major"
- }
+ "sdk": {
+ "version": "6.0.400",
+ "rollForward": "latestMinor"
+ }
}
diff --git a/DisCatSharp.Hosting.Tests/global.json b/DisCatSharp.Hosting.Tests/global.json
index 90a51a911..4e0e89bbd 100644
--- a/DisCatSharp.Hosting.Tests/global.json
+++ b/DisCatSharp.Hosting.Tests/global.json
@@ -1,6 +1,6 @@
{
- "sdk": {
- "version": "6.0.202",
- "rollForward": "major"
- }
+ "sdk": {
+ "version": "6.0.400",
+ "rollForward": "latestMinor"
+ }
}
diff --git a/DisCatSharp.Hosting/global.json b/DisCatSharp.Hosting/global.json
index 90a51a911..4e0e89bbd 100644
--- a/DisCatSharp.Hosting/global.json
+++ b/DisCatSharp.Hosting/global.json
@@ -1,6 +1,6 @@
{
- "sdk": {
- "version": "6.0.202",
- "rollForward": "major"
- }
+ "sdk": {
+ "version": "6.0.400",
+ "rollForward": "latestMinor"
+ }
}
diff --git a/DisCatSharp.Interactivity/Extensions/InteractionExtensions.cs b/DisCatSharp.Interactivity/Extensions/InteractionExtensions.cs
index 73aa9e37b..a6de60d97 100644
--- a/DisCatSharp.Interactivity/Extensions/InteractionExtensions.cs
+++ b/DisCatSharp.Interactivity/Extensions/InteractionExtensions.cs
@@ -1,121 +1,120 @@
// 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.Threading;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Interactivity.Enums;
using DisCatSharp.Interactivity.EventHandling;
namespace DisCatSharp.Interactivity.Extensions;
///
/// The interaction extensions.
///
public static class InteractionExtensions
{
///
/// Sends a paginated message in response to an interaction.
///
/// Pass the interaction directly. Interactivity will ACK it.
///
///
/// The interaction to create a response to.
/// Whether the interaction was deferred.
/// Whether the response should be ephemeral.
/// The user to listen for button presses from.
/// The pages to paginate.
/// Optional: custom buttons
/// Pagination behaviour.
/// Deletion behaviour
/// A custom cancellation token that can be cancelled at any point.
public static Task SendPaginatedResponseAsync(this DiscordInteraction interaction, bool deferred, bool ephemeral, DiscordUser user, IEnumerable pages, PaginationButtons buttons = null, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
=> MessageExtensions.GetInteractivity(interaction.Message).SendPaginatedResponseAsync(interaction, deferred, ephemeral, user, pages, buttons, behaviour, deletion, token);
///
/// Sends multiple modals to the user with a prompt to open the next one.
///
/// The interaction to create a response to.
/// The modal pages.
/// A custom timeout. (Default: 15 minutes)
/// A read-only dictionary with the customid of the components as the key.
/// Is thrown when no modals are defined.
/// Is thrown when interactivity is not enabled for the client/shard.
public static async Task CreatePaginatedModalResponseAsync(this DiscordInteraction interaction, IReadOnlyList modals, TimeSpan? timeOutOverride = null)
{
if (modals is null || modals.Count == 0)
throw new ArgumentException("You have to set at least one page");
var client = (DiscordClient)interaction.Discord;
var interactivity = client.GetInteractivity() ?? throw new InvalidOperationException($"Interactivity is not enabled for this {(client.IsShard ? "shard" : "client")}.");
timeOutOverride ??= TimeSpan.FromMinutes(15);
Dictionary caughtResponses = new();
var previousInteraction = interaction;
foreach (var b in modals)
{
var modal = b.Modal.WithCustomId(Guid.NewGuid().ToString());
if (previousInteraction.Type is InteractionType.Ping or InteractionType.ModalSubmit)
{
await previousInteraction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, b.OpenMessage.AddComponents(b.OpenButton));
var originalResponse = await previousInteraction.GetOriginalResponseAsync();
var modalOpen = await interactivity.WaitForButtonAsync(originalResponse, new List { b.OpenButton }, timeOutOverride);
if (modalOpen.TimedOut)
{
_ = previousInteraction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent(b.OpenMessage.Content).AddComponents(b.OpenButton.Disable()));
return new PaginatedModalResponse { TimedOut = true };
}
await modalOpen.Result.Interaction.CreateInteractionModalResponseAsync(modal);
}
else
{
await previousInteraction.CreateInteractionModalResponseAsync(modal);
}
_ = previousInteraction.EditOriginalResponseAsync(new DiscordWebhookBuilder().WithContent(b.OpenMessage.Content).AddComponents(b.OpenButton.Disable()));
var modalResult = await interactivity.WaitForModalAsync(modal.CustomId, timeOutOverride);
if (modalResult.TimedOut)
return new PaginatedModalResponse { TimedOut = true };
- foreach (var row in modalResult.Result.Interaction.Data.Components)
- foreach (var submissions in row.Components)
+ foreach (var submissions in modalResult.Result.Interaction.Data.Components)
caughtResponses.Add(submissions.CustomId, submissions.Value);
previousInteraction = modalResult.Result.Interaction;
}
await previousInteraction.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral());
return new PaginatedModalResponse { TimedOut = false, Responses = caughtResponses, Interaction = previousInteraction };
}
}
diff --git a/DisCatSharp.Interactivity/global.json b/DisCatSharp.Interactivity/global.json
index 90a51a911..4e0e89bbd 100644
--- a/DisCatSharp.Interactivity/global.json
+++ b/DisCatSharp.Interactivity/global.json
@@ -1,6 +1,6 @@
{
- "sdk": {
- "version": "6.0.202",
- "rollForward": "major"
- }
+ "sdk": {
+ "version": "6.0.400",
+ "rollForward": "latestMinor"
+ }
}
diff --git a/DisCatSharp.Lavalink/global.json b/DisCatSharp.Lavalink/global.json
index 90a51a911..4e0e89bbd 100644
--- a/DisCatSharp.Lavalink/global.json
+++ b/DisCatSharp.Lavalink/global.json
@@ -1,6 +1,6 @@
{
- "sdk": {
- "version": "6.0.202",
- "rollForward": "major"
- }
+ "sdk": {
+ "version": "6.0.400",
+ "rollForward": "latestMinor"
+ }
}
diff --git a/DisCatSharp.Tests/global.json b/DisCatSharp.Tests/global.json
index 90a51a911..4e0e89bbd 100644
--- a/DisCatSharp.Tests/global.json
+++ b/DisCatSharp.Tests/global.json
@@ -1,6 +1,6 @@
{
- "sdk": {
- "version": "6.0.202",
- "rollForward": "major"
- }
+ "sdk": {
+ "version": "6.0.400",
+ "rollForward": "latestMinor"
+ }
}
diff --git a/DisCatSharp.VoiceNext.Natives/global.json b/DisCatSharp.VoiceNext.Natives/global.json
index 90a51a911..4e0e89bbd 100644
--- a/DisCatSharp.VoiceNext.Natives/global.json
+++ b/DisCatSharp.VoiceNext.Natives/global.json
@@ -1,6 +1,6 @@
{
- "sdk": {
- "version": "6.0.202",
- "rollForward": "major"
- }
+ "sdk": {
+ "version": "6.0.400",
+ "rollForward": "latestMinor"
+ }
}
diff --git a/DisCatSharp.VoiceNext/global.json b/DisCatSharp.VoiceNext/global.json
index 90a51a911..4e0e89bbd 100644
--- a/DisCatSharp.VoiceNext/global.json
+++ b/DisCatSharp.VoiceNext/global.json
@@ -1,6 +1,6 @@
{
- "sdk": {
- "version": "6.0.202",
- "rollForward": "major"
- }
+ "sdk": {
+ "version": "6.0.400",
+ "rollForward": "latestMinor"
+ }
}
diff --git a/DisCatSharp/Clients/BaseDiscordClient.cs b/DisCatSharp/Clients/BaseDiscordClient.cs
index 9b6e68718..d223c1b6e 100644
--- a/DisCatSharp/Clients/BaseDiscordClient.cs
+++ b/DisCatSharp/Clients/BaseDiscordClient.cs
@@ -1,320 +1,321 @@
// 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.
#pragma warning disable CS0618
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace DisCatSharp;
///
/// Represents a common base for various Discord Client implementations.
///
public abstract class BaseDiscordClient : IDisposable
{
///
/// Gets the api client.
///
internal protected DiscordApiClient ApiClient { get; }
///
/// Gets the configuration.
///
internal protected DiscordConfiguration Configuration { get; }
///
/// Gets the instance of the logger for this client.
///
public ILogger Logger { get; internal set; }
///
/// Gets the string representing the version of bot lib.
///
public string VersionString { get; }
///
/// Gets the bot library name.
///
public string BotLibrary { get; }
[Obsolete("Use GetLibraryDeveloperTeamAsync")]
public DisCatSharpTeam LibraryDeveloperTeamAsync
=> this.GetLibraryDevelopmentTeamAsync().Result;
///
/// Gets the current user.
///
public DiscordUser CurrentUser { get; internal set; }
///
/// Gets the current application.
///
public DiscordApplication CurrentApplication { get; internal set; }
///
/// Exposes a Http Client for custom operations.
///
public HttpClient RestClient { get; internal set; }
///
/// Gets the cached guilds for this client.
///
public abstract IReadOnlyDictionary Guilds { get; }
///
/// Gets the cached users for this client.
///
public ConcurrentDictionary UserCache { get; internal set; }
///
/// Gets the service provider.
/// This allows passing data around without resorting to static members.
/// Defaults to null.
///
internal IServiceProvider ServiceProvider { get; set; }
///
/// Gets the list of available voice regions. Note that this property will not contain VIP voice regions.
///
public IReadOnlyDictionary VoiceRegions
=> this.VoiceRegionsLazy.Value;
///
/// Gets the list of available voice regions. This property is meant as a way to modify .
///
protected internal ConcurrentDictionary InternalVoiceRegions { get; set; }
internal Lazy> VoiceRegionsLazy;
///
/// Initializes this Discord API client.
///
/// Configuration for this client.
protected BaseDiscordClient(DiscordConfiguration config)
{
this.Configuration = new DiscordConfiguration(config);
this.ServiceProvider = config.ServiceProvider;
if (this.ServiceProvider != null)
{
this.Configuration.LoggerFactory ??= config.ServiceProvider.GetService();
this.Logger = config.ServiceProvider.GetService>();
}
if (this.Configuration.LoggerFactory == null)
{
this.Configuration.LoggerFactory = new DefaultLoggerFactory();
this.Configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this));
}
this.Logger ??= this.Configuration.LoggerFactory.CreateLogger();
this.ApiClient = new DiscordApiClient(this);
this.UserCache = new ConcurrentDictionary();
this.InternalVoiceRegions = new ConcurrentDictionary();
this.VoiceRegionsLazy = new Lazy>(() => new ReadOnlyDictionary(this.InternalVoiceRegions));
this.RestClient = new();
this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent());
+ this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("X-Discord-Locale", this.Configuration.Locale);
var a = typeof(DiscordClient).GetTypeInfo().Assembly;
var iv = a.GetCustomAttribute();
if (iv != null)
{
this.VersionString = iv.InformationalVersion;
}
else
{
var v = a.GetName().Version;
var vs = v.ToString(3);
if (v.Revision > 0)
this.VersionString = $"{vs}, CI build {v.Revision}";
}
this.BotLibrary = "DisCatSharp";
}
///
/// Gets the current API application.
///
public async Task GetCurrentApplicationAsync()
{
var tapp = await this.ApiClient.GetCurrentApplicationInfoAsync().ConfigureAwait(false);
var app = new DiscordApplication
{
Discord = this,
Id = tapp.Id,
Name = tapp.Name,
Description = tapp.Description,
Summary = tapp.Summary,
IconHash = tapp.IconHash,
RpcOrigins = tapp.RpcOrigins != null ? new ReadOnlyCollection(tapp.RpcOrigins) : null,
Flags = tapp.Flags,
RequiresCodeGrant = tapp.BotRequiresCodeGrant,
IsPublic = tapp.IsPublicBot,
IsHook = tapp.IsHook,
Type = tapp.Type,
PrivacyPolicyUrl = tapp.PrivacyPolicyUrl,
TermsOfServiceUrl = tapp.TermsOfServiceUrl,
CustomInstallUrl = tapp.CustomInstallUrl,
InstallParams = tapp.InstallParams,
Tags = (tapp.Tags ?? Enumerable.Empty()).ToArray()
};
if (tapp.Team == null)
{
app.Owners = new ReadOnlyCollection(new[] { new DiscordUser(tapp.Owner) });
app.Team = null;
app.TeamName = null;
}
else
{
app.Team = new DiscordTeam(tapp.Team);
var members = tapp.Team.Members
.Select(x => new DiscordTeamMember(x) { TeamId = app.Team.Id, TeamName = app.Team.Name, User = new DiscordUser(x.User) })
.ToArray();
var owners = members
.Where(x => x.MembershipStatus == DiscordTeamMembershipStatus.Accepted)
.Select(x => x.User)
.ToArray();
app.Owners = new ReadOnlyCollection(owners);
app.Team.Owner = owners.FirstOrDefault(x => x.Id == tapp.Team.OwnerId);
app.Team.Members = new ReadOnlyCollection(members);
app.TeamName = app.Team.Name;
}
app.GuildId = tapp.GuildId.ValueOrDefault();
app.Slug = tapp.Slug.ValueOrDefault();
app.PrimarySkuId = tapp.PrimarySkuId.ValueOrDefault();
app.VerifyKey = tapp.VerifyKey.ValueOrDefault();
app.CoverImageHash = tapp.CoverImageHash.ValueOrDefault();
return app;
}
///
/// Gets a list of voice regions.
///
/// Thrown when Discord is unable to process the request.
public Task> ListVoiceRegionsAsync()
=> this.ApiClient.ListVoiceRegionsAsync();
///
/// Initializes this client. This method fetches information about current user, application, and voice regions.
///
public virtual async Task InitializeAsync()
{
if (this.CurrentUser == null)
{
this.CurrentUser = await this.ApiClient.GetCurrentUserAsync().ConfigureAwait(false);
this.UserCache.AddOrUpdate(this.CurrentUser.Id, this.CurrentUser, (id, xu) => this.CurrentUser);
}
if (this.Configuration.TokenType == TokenType.Bot && this.CurrentApplication == null)
this.CurrentApplication = await this.GetCurrentApplicationAsync().ConfigureAwait(false);
if (this.Configuration.TokenType != TokenType.Bearer && this.InternalVoiceRegions.IsEmpty)
{
var vrs = await this.ListVoiceRegionsAsync().ConfigureAwait(false);
foreach (var xvr in vrs)
this.InternalVoiceRegions.TryAdd(xvr.Id, xvr);
}
}
///
/// Gets the current gateway info for the provided token.
/// If no value is provided, the configuration value will be used instead.
///
/// A gateway info object.
public async Task GetGatewayInfoAsync(string token = null)
{
if (this.Configuration.TokenType != TokenType.Bot)
throw new InvalidOperationException("Only bot tokens can access this info.");
if (string.IsNullOrEmpty(this.Configuration.Token))
{
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Could not locate a valid token.");
this.Configuration.Token = token;
var res = await this.ApiClient.GetGatewayInfoAsync().ConfigureAwait(false);
this.Configuration.Token = null;
return res;
}
return await this.ApiClient.GetGatewayInfoAsync().ConfigureAwait(false);
}
///
/// Gets some information about the development team behind DisCatSharp.
/// Can be used for crediting etc.
/// Note: This call contacts servers managed by the DCS team, no information is collected.
/// The team, or null with errors being logged on failure.
///
public async Task GetLibraryDevelopmentTeamAsync()
=> await DisCatSharpTeam.Get(this.RestClient, this.Logger, this.ApiClient).ConfigureAwait(false);
///
/// Gets a cached user.
///
/// The user id.
internal DiscordUser GetCachedOrEmptyUserInternal(ulong userId)
{
this.TryGetCachedUserInternal(userId, out var user);
return user;
}
///
/// Tries the get a cached user.
///
/// The user id.
/// The user.
internal bool TryGetCachedUserInternal(ulong userId, out DiscordUser user)
{
if (this.UserCache.TryGetValue(userId, out user))
return true;
user = new DiscordUser { Id = userId, Discord = this };
return false;
}
///
/// Disposes this client.
///
public abstract void Dispose();
}
diff --git a/DisCatSharp/Clients/DiscordClient.cs b/DisCatSharp/Clients/DiscordClient.cs
index b3107076d..3b4d63efa 100644
--- a/DisCatSharp/Clients/DiscordClient.cs
+++ b/DisCatSharp/Clients/DiscordClient.cs
@@ -1,1327 +1,1327 @@
// 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.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Exceptions;
using DisCatSharp.Net;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Models;
using DisCatSharp.Net.Serialization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
namespace DisCatSharp;
///
/// A Discord API wrapper.
///
public sealed partial class DiscordClient : BaseDiscordClient
{
#region Internal Fields/Properties
internal bool IsShard = false;
///
/// Gets the message cache.
///
internal RingBuffer MessageCache { get; }
private List _extensions = new();
private StatusUpdate _status;
///
/// Gets the connection lock.
///
private readonly ManualResetEventSlim _connectionLock = new(true);
#endregion
#region Public Fields/Properties
///
/// Gets the gateway protocol version.
///
public int GatewayVersion { get; internal set; }
///
/// Gets the gateway session information for this client.
///
public GatewayInfo GatewayInfo { get; internal set; }
///
/// Gets the gateway URL.
///
public Uri GatewayUri { get; internal set; }
///
/// Gets the total number of shards the bot is connected to.
///
public int ShardCount => this.GatewayInfo != null
? this.GatewayInfo.ShardCount
: this.Configuration.ShardCount;
///
/// Gets the currently connected shard ID.
///
public int ShardId
=> this.Configuration.ShardId;
///
/// Gets the intents configured for this client.
///
public DiscordIntents Intents
=> this.Configuration.Intents;
///
/// Gets a dictionary of guilds that this client is in. The dictionary's key is the guild ID. Note that the
/// guild objects in this dictionary will not be filled in if the specific guilds aren't available (the
/// or events haven't been fired yet)
///
public override IReadOnlyDictionary Guilds { get; }
internal ConcurrentDictionary GuildsInternal = new();
///
/// Gets the websocket latency for this client.
///
public int Ping
=> Volatile.Read(ref this._ping);
private int _ping;
///
/// Gets the collection of presences held by this client.
///
public IReadOnlyDictionary Presences
=> this._presencesLazy.Value;
internal Dictionary PresencesInternal = new();
private Lazy> _presencesLazy;
///
/// Gets the collection of presences held by this client.
///
public IReadOnlyDictionary EmbeddedActivities
=> this._embeddedActivitiesLazy.Value;
internal Dictionary EmbeddedActivitiesInternal = new();
private Lazy> _embeddedActivitiesLazy;
#endregion
#region Constructor/Internal Setup
///
/// Initializes a new instance of .
///
/// Specifies configuration parameters.
public DiscordClient(DiscordConfiguration config)
: base(config)
{
if (this.Configuration.MessageCacheSize > 0)
{
var intents = this.Configuration.Intents;
this.MessageCache = intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages)
? new RingBuffer(this.Configuration.MessageCacheSize)
: null;
}
this.InternalSetup();
this.Guilds = new ReadOnlyConcurrentDictionary(this.GuildsInternal);
}
///
/// Internal setup of the Client.
///
internal void InternalSetup()
{
this._clientErrored = new AsyncEvent("CLIENT_ERRORED", EventExecutionLimit, this.Goof);
this._socketErrored = new AsyncEvent("SOCKET_ERRORED", EventExecutionLimit, this.Goof);
this._socketOpened = new AsyncEvent("SOCKET_OPENED", EventExecutionLimit, this.EventErrorHandler);
this._socketClosed = new AsyncEvent("SOCKET_CLOSED", EventExecutionLimit, this.EventErrorHandler);
this._ready = new AsyncEvent("READY", EventExecutionLimit, this.EventErrorHandler);
this._resumed = new AsyncEvent("RESUMED", EventExecutionLimit, this.EventErrorHandler);
this._channelCreated = new AsyncEvent("CHANNEL_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._channelUpdated = new AsyncEvent("CHANNEL_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._channelDeleted = new AsyncEvent("CHANNEL_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._dmChannelDeleted = new AsyncEvent("DM_CHANNEL_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._channelPinsUpdated = new AsyncEvent("CHANNEL_PINS_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildCreated = new AsyncEvent("GUILD_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._guildAvailable = new AsyncEvent("GUILD_AVAILABLE", EventExecutionLimit, this.EventErrorHandler);
this._guildUpdated = new AsyncEvent("GUILD_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildDeleted = new AsyncEvent("GUILD_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._guildUnavailable = new AsyncEvent("GUILD_UNAVAILABLE", EventExecutionLimit, this.EventErrorHandler);
this._guildDownloadCompletedEv = new AsyncEvent("GUILD_DOWNLOAD_COMPLETED", EventExecutionLimit, this.EventErrorHandler);
this._inviteCreated = new AsyncEvent("INVITE_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._inviteDeleted = new AsyncEvent("INVITE_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._messageCreated = new AsyncEvent("MESSAGE_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._presenceUpdated = new AsyncEvent("PRESENCE_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildBanAdded = new AsyncEvent("GUILD_BAN_ADD", EventExecutionLimit, this.EventErrorHandler);
this._guildBanRemoved = new AsyncEvent("GUILD_BAN_REMOVED", EventExecutionLimit, this.EventErrorHandler);
this._guildEmojisUpdated = new AsyncEvent("GUILD_EMOJI_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildStickersUpdated = new AsyncEvent("GUILD_STICKER_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildIntegrationsUpdated = new AsyncEvent("GUILD_INTEGRATIONS_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildMemberAdded = new AsyncEvent("GUILD_MEMBER_ADD", EventExecutionLimit, this.EventErrorHandler);
this._guildMemberRemoved = new AsyncEvent("GUILD_MEMBER_REMOVED", EventExecutionLimit, this.EventErrorHandler);
this._guildMemberUpdated = new AsyncEvent("GUILD_MEMBER_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildRoleCreated = new AsyncEvent("GUILD_ROLE_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._guildRoleUpdated = new AsyncEvent("GUILD_ROLE_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildRoleDeleted = new AsyncEvent("GUILD_ROLE_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._messageAcknowledged = new AsyncEvent("MESSAGE_ACKNOWLEDGED", EventExecutionLimit, this.EventErrorHandler);
this._messageUpdated = new AsyncEvent("MESSAGE_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._messageDeleted = new AsyncEvent("MESSAGE_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._messagesBulkDeleted = new AsyncEvent("MESSAGE_BULK_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._interactionCreated = new AsyncEvent("INTERACTION_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._componentInteractionCreated = new AsyncEvent("COMPONENT_INTERACTED", EventExecutionLimit, this.EventErrorHandler);
this._contextMenuInteractionCreated = new AsyncEvent("CONTEXT_MENU_INTERACTED", EventExecutionLimit, this.EventErrorHandler);
this._typingStarted = new AsyncEvent("TYPING_STARTED", EventExecutionLimit, this.EventErrorHandler);
this._userSettingsUpdated = new AsyncEvent("USER_SETTINGS_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._userUpdated = new AsyncEvent