diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
index 0f706ba7d..02d04ff1a 100644
--- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
+++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
@@ -1,2067 +1,1794 @@
// 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.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using DisCatSharp.ApplicationCommands.Attributes;
using DisCatSharp.ApplicationCommands.EventArgs;
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;
///
/// Discord client.
///
internal static DiscordClient ClientInternal;
///
/// 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;
- ///
- /// Whether the permission failed to register.
- ///
- private static bool s_permError { get; set; } = false;
-
///
/// Gets whether check through all guilds is enabled.
///
internal static bool CheckAllGuilds { get; set; }
internal static bool ManOr { get; set; }
///
/// 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;
}
///
/// 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;
ClientInternal = client;
this._slashError = new AsyncEvent("SLASHCOMMAND_ERRORED", TimeSpan.Zero, null);
this._slashExecuted = new AsyncEvent("SLASHCOMMAND_EXECUTED", TimeSpan.Zero, null);
this._contextMenuErrored = new AsyncEvent("CONTEXTMENU_ERRORED", TimeSpan.Zero, null);
this._contextMenuExecuted = new AsyncEvent("CONTEXTMENU_EXECUTED", TimeSpan.Zero, null);
this._applicationCommandsModuleReady = new AsyncEvent("APPLICATION_COMMANDS_MODULE_READY", TimeSpan.Zero, null);
this._applicationCommandsModuleStartupFinished = new AsyncEvent("APPLICATION_COMMANDS_MODULE_STARTUP_FINISHED", TimeSpan.Zero, null);
this._globalApplicationCommandsRegistered = new AsyncEvent("GLOBAL_COMMANDS_REGISTERED", TimeSpan.Zero, null);
this._guildApplicationCommandsRegistered = new AsyncEvent("GUILD_COMMANDS_REGISTERED", TimeSpan.Zero, null);
this.Client.GuildDownloadCompleted += async (c, e) => await this.UpdateAsync();
this.Client.InteractionCreated += this.CatchInteractionsOnStartup;
this.Client.ContextMenuInteractionCreated += this.CatchContextMenuInteractionsOnStartup;
}
private async Task CatchInteractionsOnStartup(DiscordClient sender, InteractionCreateEventArgs e)
=> await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Application commands are unavailable for now."));
private async Task CatchContextMenuInteractionsOnStartup(DiscordClient sender, ContextMenuInteractionCreateEventArgs e)
=> 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."));
private void FinishedRegistration()
{
this.Client.InteractionCreated -= this.CatchInteractionsOnStartup;
this.Client.ContextMenuInteractionCreated -= this.CatchContextMenuInteractionsOnStartup;
this.Client.InteractionCreated += this.InteractionHandler;
this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler;
}
///
/// Registers a command class.
///
/// The command class to register.
public void RegisterGlobalCommands() where T : ApplicationCommandsModule
{
if (this.Client.ShardId == 0)
- this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), true)));
+ this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T))));
}
-
///
/// Registers a command class.
///
/// The of the command class to register.
public void RegisterGlobalCommands(Type type)
{
if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type))
throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type));
//If sharding, only register for shard 0
if (this.Client.ShardId == 0)
- this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, true)));
+ this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type)));
}
///
/// Cleans all guild application commands.
/// You normally don't need to execute it.
///
public 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.
///
public async Task CleanGlobalCommandsAsync()
=> await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty());
///
/// Registers a command class with permission and translation setup.
///
/// The command class to register.
/// The guild id to register it on.
- /// A callback to setup permissions with.
/// A callback to setup translations with.
- public void RegisterGuildCommands(ulong guildId, Action permissionSetup = null, Action translationSetup = null) where T : ApplicationCommandsModule
+ public void RegisterGuildCommands(ulong guildId, Action translationSetup = null) where T : ApplicationCommandsModule
{
if (this.Client.ShardId == 0)
- this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup, translationSetup)));
+ this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup)));
}
///
/// Registers a command class with permission and translation setup.
///
/// The of the command class to register.
/// The guild id to register it on.
- /// A callback to setup permissions with.
/// A callback to setup translations with.
- public void RegisterGuildCommands(Type type, ulong guildId, Action permissionSetup = null, Action translationSetup = null)
+ 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));
//If sharding, only register for shard 0
if (this.Client.ShardId == 0)
- this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type, permissionSetup, translationSetup)));
+ this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type, translationSetup)));
}
///
/// Registers a command class with permission setup but without a guild id.
///
/// The command class to register.
- /// A callback to setup permissions with.
/// A callback to setup translations with.
- public void RegisterGlobalCommands(Action globalGuildPermissionSetup = null, Action translationSetup = null) where T : ApplicationCommandsModule
+ public void RegisterGlobalCommands(Action translationSetup = null) where T : ApplicationCommandsModule
{
if (this.Client.ShardId == 0)
- this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), true, globalGuildPermissionSetup, translationSetup)));
+ this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup)));
}
///
/// Registers a command class with permission setup but without a guild id.
///
/// The of the command class to register.
- /// A callback to setup permissions with.
/// A callback to setup translations with.
- public void RegisterGlobalCommands(Type type, Action globalGuildPermissionSetup = null, Action translationSetup = null)
+ 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));
//If sharding, only register for shard 0
if (this.Client.ShardId == 0)
- this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, true, globalGuildPermissionSetup, translationSetup)));
+ 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()
{
//Only update for shard 0
if (this.Client.ShardId == 0)
{
GlobalDiscordCommands = new();
GuildDiscordCommands = new();
var commandsPending = this._updateList.Select(x => x.Key).Distinct();
s_expectedCount = commandsPending.Count();
this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Expected Count: {s_expectedCount}");
List failedGuilds = new();
IEnumerable globalCommands = null;
globalCommands = await this.Client.GetGlobalApplicationCommandsAsync() ?? null;
var guilds = CheckAllGuilds ? this.Client.Guilds?.Keys : this._updateList.Select(x => x.Key)?.Distinct().Where(x => x != null)?.Select(x => x.Value);
foreach (var guild in guilds)
{
IEnumerable commands = null;
var unauthorized = false;
try
{
commands = await this.Client.GetGuildApplicationCommandsAsync(guild) ?? null;
}
catch (UnauthorizedException)
{
unauthorized = true;
}
finally
{
if (!unauthorized && commands != null && commands.Any())
GuildDiscordCommands.Add(guild, commands.ToList());
else if (!unauthorized)
GuildDiscordCommands.Add(guild, null);
else
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))
{
this._updateList.Add(new KeyValuePair
- (null, new ApplicationCommandsModuleConfiguration(typeof(DefaultHelpModule), true)));
+ (null, new ApplicationCommandsModuleConfiguration(typeof(DefaultHelpModule))));
commandsPending = this._updateList.Select(x => x.Key).Distinct();
}
if (globalCommands != null && globalCommands.Any())
GlobalDiscordCommands.AddRange(globalCommands);
foreach (var key in commandsPending.ToList())
{
this.Client.Logger.LogInformation(key.HasValue ? $"Registering commands in guild {key.Value}" : "Registering global commands.");
this.RegisterCommands(this._updateList.Where(x => x.Key == key).Select(x => x.Value), key);
}
this._missingScopeGuildIds = failedGuilds;
await this._applicationCommandsModuleReady.InvokeAsync(this, new ApplicationCommandsModuleReadyEventArgs(Configuration?.ServiceProvider)
{
Handled = true,
GuildsWithoutScope = failedGuilds
});
}
}
///
/// Method for registering commands for a target from modules.
///
/// The types.
- /// The optional guild id.
- private void RegisterCommands(IEnumerable types, ulong? guildid)
+ /// The optional guild id.
+ private void RegisterCommands(IEnumerable types, ulong? guildId)
{
//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>();
_ = Task.Run(async () =>
{
//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
if (module.GetCustomAttribute() != null)
{
classes.Add(module);
}
else
{
//Otherwise add the nested groups
classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList();
}
List groupTranslations = null;
if (!string.IsNullOrEmpty(ctx.Translations))
{
groupTranslations = JsonConvert.DeserializeObject>(ctx.Translations);
}
- var slashGroupsTulpe = NestedCommandWorker.ParseSlashGroupsAsync(type, classes, guildid, groupTranslations).Result;
+ var slashGroupsTulpe = NestedCommandWorker.ParseSlashGroupsAsync(type, classes, guildId, groupTranslations).Result;
if (slashGroupsTulpe.applicationCommands != null && slashGroupsTulpe.applicationCommands.Any())
updateList.AddRange(slashGroupsTulpe.applicationCommands);
if (slashGroupsTulpe.commandTypeSources != null && slashGroupsTulpe.commandTypeSources.Any())
commandTypeSources.AddRange(slashGroupsTulpe.commandTypeSources);
if (slashGroupsTulpe.singletonModules != null && slashGroupsTulpe.singletonModules.Any())
s_singletonModules.AddRange(slashGroupsTulpe.singletonModules);
if (slashGroupsTulpe.groupCommands != null && slashGroupsTulpe.groupCommands.Any())
groupCommands.AddRange(slashGroupsTulpe.groupCommands);
if (slashGroupsTulpe.subGroupCommands != null && slashGroupsTulpe.subGroupCommands.Any())
subGroupCommands.AddRange(slashGroupsTulpe.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 = CommandWorker.ParseBasicSlashCommandsAsync(type, methods, guildid, commandTranslations).Result;
+ var slashCommands = CommandWorker.ParseBasicSlashCommandsAsync(type, methods, guildId, commandTranslations).Result;
if (slashCommands.applicationCommands != null && slashCommands.applicationCommands.Any())
updateList.AddRange(slashCommands.applicationCommands);
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 = CommandWorker.ParseContextMenuCommands(type, contextMethods, commandTranslations).Result;
if (contextCommands.applicationCommands != null && contextCommands.applicationCommands.Any())
updateList.AddRange(contextCommands.applicationCommands);
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: {brex.JsonMessage}");
else
this.Client.Logger.LogCritical(ex, $"There was an error parsing the application commands");
s_errored = true;
}
}
if (!s_errored)
{
try
{
List commands = new();
try
{
- if (guildid == null)
+ if (guildId == null)
{
if (updateList != null && updateList.Any())
{
var regCommands = RegistrationWorker.RegisterGlobalCommandsAsync(updateList).Result;
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 {cmd.Id}. Please clean up manually");
}
}
}
}
else
{
if (updateList != null && updateList.Any())
{
- var regCommands = RegistrationWorker.RegisterGuilldCommandsAsync(guildid.Value, updateList).Result;
+ var regCommands = RegistrationWorker.RegisterGuilldCommandsAsync(guildId.Value, updateList).Result;
var actualCommands = regCommands.Distinct().ToList();
commands.AddRange(actualCommands);
- GuildCommandsInternal.Add(guildid.Value, actualCommands);
- if (this.Client.Guilds.TryGetValue(guildid.Value, out var guild))
+ GuildCommandsInternal.Add(guildId.Value, actualCommands);
+ if (this.Client.Guilds.TryGetValue(guildId.Value, out var guild))
{
guild.InternalRegisteredApplicationCommands = new();
guild.InternalRegisteredApplicationCommands.AddRange(actualCommands);
}
}
else
{
- foreach (var cmd in GuildDiscordCommands[guildid.Value])
+ foreach (var cmd in GuildDiscordCommands[guildId.Value])
{
try
{
- await this.Client.DeleteGuildApplicationCommandAsync(guildid.Value, cmd.Id);
+ await this.Client.DeleteGuildApplicationCommandAsync(guildId.Value, cmd.Id);
}
catch (NotFoundException)
{
- this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Could not delete guild command {cmd.Id} in guild {guildid.Value}. Please clean up manually");
+ this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Could not delete guild command {cmd.Id} in guild {guildId.Value}. Please clean up manually");
}
}
}
}
}
catch (UnauthorizedException ex)
{
- this.Client.Logger.LogError($"Could not register application commands for guild {guildid}.\nError: {ex.JsonMessage}");
+ this.Client.Logger.LogError($"Could not register application commands for guild {guildId}.\nError: {ex.JsonMessage}");
return;
}
-
- // TODO: Rework permission and minimize put/post/patch calls
-
//Creates a guild command if a guild id is specified, otherwise global
//Checks against the ids and adds them to the command method lists
- List overwrites = new();
- List< KeyValuePair> guildOverwrites = new();
-
foreach (var command in commands)
{
if (commandMethods.GetFirstValueWhere(x => x.Name == command.Name, out var com))
- {
com.CommandId = command.Id;
-
- var source = commandTypeSources.FirstOrDefault(f => f.Key == com.Method.DeclaringType);
-
- if(guildid != null)
- {
- var (success, commandId, permissions) = PermissionWorker.ResolvePermissions(types, command.Id, com.Name, source.Value, source.Key);
-
- if (success)
- {
- overwrites.Add(new DiscordGuildApplicationCommandPermission()
- {
- Id = commandId.Value,
- Permissions = permissions
- });
- }
- }
- else
- {
- var (success, commandId, permissions) = PermissionWorker.ResolveGlobalPermissions(types, command.Id, com.Name, source.Value, source.Key);
-
- if (success)
- {
- var perms = permissions.Select(x => x.Key).Distinct();
- foreach(var guild in perms)
- {
- guildOverwrites.Add(new KeyValuePair(guild, new DiscordGuildApplicationCommandPermission()
- {
- Id = commandId.Value,
- Permissions = permissions.Where(x => x.Key == guild).Select(x => x.Value).ToList(),
- GuildId = guild
- }));
- }
- }
- }
- }
else if (groupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var groupCom))
- {
groupCom.CommandId = command.Id;
- foreach (var gCom in groupCom.Methods)
- {
- var source = commandTypeSources.FirstOrDefault(f => f.Key == gCom.Value.DeclaringType);
-
- if (guildid != null)
- {
- var (success, commandId, permissions) = PermissionWorker.ResolvePermissions(types, groupCom.CommandId, gCom.Key, source.Key, source.Value);
-
- if (success)
- overwrites.Add(new DiscordGuildApplicationCommandPermission()
- {
- Id = commandId.Value,
- Permissions = permissions
- });
- }
- else
- {
- var (success, commandId, permissions) = PermissionWorker.ResolveGlobalPermissions(types, groupCom.CommandId, gCom.Key, source.Key, source.Value);
-
- if (success)
- {
- var perms = permissions.Select(x => x.Key).Distinct();
- foreach (var guild in perms)
- {
- guildOverwrites.Add(new KeyValuePair(guild, new DiscordGuildApplicationCommandPermission()
- {
- Id = commandId.Value,
- Permissions = permissions.Where(x => x.Key == guild).Select(x => x.Value).ToList(),
- GuildId = guild
- }));
- }
- }
- }
- }
- }
else if (subGroupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var subCom))
- {
subCom.CommandId = command.Id;
-
- foreach (var groupComs in subCom.SubCommands)
- {
- foreach (var gCom in groupComs.Methods)
- {
- var source = commandTypeSources.FirstOrDefault(f => f.Key == gCom.Value.DeclaringType);
-
- if (guildid != null)
- {
- var (success, commandId, permissions) = PermissionWorker.ResolvePermissions(types, subCom.CommandId, gCom.Key, source.Key, source.Value);
-
- if (success)
- overwrites.Add(new DiscordGuildApplicationCommandPermission()
- {
- Id = commandId.Value,
- Permissions = permissions
- });
- }
- else
- {
- var (success, commandId, permissions) = PermissionWorker.ResolveGlobalPermissions(types, subCom.CommandId, gCom.Key, source.Key, source.Value);
-
- if (success)
- {
- var perms = permissions.Select(x => x.Key).Distinct();
- foreach (var guild in perms)
- {
- guildOverwrites.Add(new KeyValuePair(guild, new DiscordGuildApplicationCommandPermission()
- {
- Id = commandId.Value,
- Permissions = permissions.Where(x => x.Key == guild).Select(x => x.Value).ToList(),
- GuildId = guild
- }));
- }
- }
- }
- }
- }
- }
else if (contextMenuCommands.GetFirstValueWhere(x => x.Name == command.Name, out var cmCom))
- {
cmCom.CommandId = command.Id;
-
- var source = commandTypeSources.First(f => f.Key == cmCom.Method.DeclaringType);
-
- if (guildid != null)
- {
- var (success, commandId, permissions) = PermissionWorker.ResolvePermissions(types, command.Id, cmCom.Name, source.Value, source.Key);
-
- if (success)
- overwrites.Add(new DiscordGuildApplicationCommandPermission()
- {
- Id = commandId.Value,
- Permissions = permissions
- });
- }
- else
- {
- var (success, commandId, permissions) = PermissionWorker.ResolveGlobalPermissions(types, command.Id, cmCom.Name, source.Value, source.Key);
-
- if (success)
- {
- var perms = permissions.Select(x => x.Key).Distinct();
- foreach (var guild in perms)
- {
- guildOverwrites.Add(new KeyValuePair(guild, new DiscordGuildApplicationCommandPermission()
- {
- Id = commandId.Value,
- Permissions = permissions.Where(x => x.Key == guild).Select(x => x.Value).ToList(),
- GuildId = guild
- }));
- }
- }
- }
- }
- }
-
- if (guildid != null && overwrites != null && overwrites.Any())
- {
- if (overwrites.Any(x => x.Id == 0))
- {
- s_errored = true;
- s_permError = true;
- throw new ArgumentException("Overwrites has a value with command id 0. Seems like an error. Aborting.");
- }
-
- try
- {
- var perms = await PermissionWorker.BulkOverwriteCommandPermissionsAsync(guildid.Value, overwrites);
- if (this.Client.Guilds.TryGetValue(guildid.Value, out var guild))
- {
- guild.InternalGuildApplicationCommandPermissions = new();
- guild.InternalGuildApplicationCommandPermissions.AddRange(perms);
- }
- }
- catch (Exception ex)
- {
- if (ex is NotFoundException)
- this.Client.Logger.LogError($"[AC Perms] Command not found");
- else if (ex is BadRequestException)
- {
- s_permError = true;
- var exc = ex as BadRequestException;
- this.Client.Logger.LogError($"[AC Perms] Bad Request: {exc.JsonMessage}\nRestarting could help.\n" +
- $"{exc.WebResponse.Response}");
- }
- else
- this.Client.Logger.LogError($"[AC Perms] General exception: {ex.Message}\n{ex.StackTrace}\nRestarting could help.");
- }
- }
- else if (guildOverwrites != null && guildOverwrites.Any())
- {
- if (guildOverwrites.Select(x=> x.Value).Any(x => x.Id == 0))
- {
- s_errored = true;
- s_permError = true;
- throw new ArgumentException("Overwrites has a value with command id 0. Seems like an error. Aborting.");
- }
-
- try
- {
- var gOv = guildOverwrites.Select(x => x.Key).Distinct();
- foreach(var gid in gOv)
-{
- var perms = await PermissionWorker.BulkOverwriteCommandPermissionsAsync(gid, guildOverwrites.Where(x => x.Key == gid).Select(x => x.Value).ToList());
- if (this.Client.Guilds.TryGetValue(gid, out var guild))
- {
- guild.InternalGuildApplicationCommandPermissions = new();
- guild.InternalGuildApplicationCommandPermissions.AddRange(perms);
- }
- }
- }
- catch (Exception ex)
- {
- if (ex is NotFoundException)
- this.Client.Logger.LogError($"[AC Perms] Command not found");
- else if (ex is BadRequestException)
- {
- s_permError = true;
- var exc = ex as BadRequestException;
- this.Client.Logger.LogError($"[AC Perms] Bad Request: {exc.JsonMessage}\nRestarting could help.\n" +
- $"{exc.WebResponse.Response}");
- }
- else
- this.Client.Logger.LogError($"[AC Perms] General exception: {ex.Message}\n{ex.StackTrace}\nRestarting could help.");
- }
}
//Adds to the global lists finally
s_commandMethods.AddRange(commandMethods);
s_groupCommands.AddRange(groupCommands);
s_subGroupCommands.AddRange(subGroupCommands);
s_contextMenuCommands.AddRange(contextMenuCommands);
- s_registeredCommands.Add(new KeyValuePair>(guildid, commands.ToList()));
+ 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: {s_expectedCount}\nCurrent Count: {s_registrationCount}");
- if (guildid.HasValue)
+ 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
+ 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: {brex.JsonMessage}");
else
this.Client.Logger.LogCritical(ex, $"There was an general error registering application commands");
s_errored = true;
}
}
});
}
private async void CheckRegistrationStartup(bool man = false)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Checking counts...\n\nExpected Count: {s_expectedCount}\nCurrent Count: {s_registrationCount}");
- if ((s_registrationCount == s_expectedCount && !s_permError) || man)
+ if ((s_registrationCount == s_expectedCount) || man)
{
await this._applicationCommandsModuleStartupFinished.InvokeAsync(this, new ApplicationCommandsModuleStartupFinishedEventArgs(Configuration?.ServiceProvider)
{
Handled = true,
RegisteredGlobalCommands = GlobalCommandsInternal,
RegisteredGuildCommands = GuildCommandsInternal,
GuildsWithoutScope = this._missingScopeGuildIds
});
this.FinishedRegistration();
}
- else if(s_permError)
- {
- this.Client.Logger.LogWarning($"We had problems to register the permissions. Shutting down ...");
- await this.Client.DisconnectAsync();
- }
}
///
/// Interaction handler.
///
/// The client.
/// The event args.
private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e)
{
_ = 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
};
try
{
if (s_errored)
throw new InvalidOperationException("Slash 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())
throw new InvalidOperationException("A slash command was executed, but no command was registered for it.");
if (methods.Any())
{
var method = methods.First().Method;
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;
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;
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)
{
await this._slashError.InvokeAsync(this, new SlashCommandErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex });
}
}
else if (e.Interaction.Type == InteractionType.AutoComplete)
{
if (s_errored)
throw new InvalidOperationException("Slash 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())
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 = this.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
};
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
{
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
};
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
{
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
};
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)
{
_ = 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
};
try
{
if (s_errored)
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)
throw new InvalidOperationException("A context menu 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;
//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)
{
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)
{
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);
for (var i = 0; i < parameters.Count(); i++)
{
var parameter = parameters.ElementAt(i);
//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(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.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 if (e.Interaction.Data.Resolved.Attachments != null && e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment))
args.Add(attachment);
else
throw new ArgumentException("Error resolving mentionable option.");
}
else
throw new ArgumentException($"Error resolving interaction.");
}
}
return args;
}
///
/// Runs the preexecution 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(DiscordChannel)
? ApplicationCommandOptionType.Channel
: type == typeof(DiscordUser)
? ApplicationCommandOptionType.User
: type == typeof(DiscordRole)
? ApplicationCommandOptionType.Role
: type == typeof(SnowflakeObject)
? ApplicationCommandOptionType.Mentionable
: type == typeof(DiscordAttachment)
? ApplicationCommandOptionType.Attachment
: 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(ParameterInfo[] 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 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);
//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);
}
var channelTypes = parameter.GetCustomAttribute()?.ChannelTypes ?? null;
options.Add(new DiscordApplicationCommandOption(optionattribute.Name, optionattribute.Description, parametertype, !parameter.IsOptional, choices, null, channelTypes, optionattribute.Autocomplete, minimumValue, maximumValue));
}
return options;
}
///
/// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client.
/// Should only be run on the slash command extension linked to shard 0 if sharding.
/// 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_permError = false;
s_errored = false;
if (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 permission setup.
- ///
- public Action Permissions { get; }
-
- ///
- /// The global permission setup.
- ///
- public Action GlobalGuildPermissions { get; }
-
///
/// The translation setup.
///
public Action Translations { get; }
- ///
- /// Whether this config is global.
- ///
- internal bool IsGlobal = false;
-
- ///
- /// Creates a new command configuration.
- ///
- /// The type of the command module.
- /// The permission setup callback.
- /// The translation setup callback.
- public ApplicationCommandsModuleConfiguration(Type type, Action permissions = null, Action translations = null)
- {
- this.Type = type;
- this.Permissions = permissions;
- this.Translations = translations;
- this.GlobalGuildPermissions = null;
- }
-
///
/// Creates a new command configuration.
///
/// The type of the command module.
- /// Don't change that. Just set to true if you use global commands here.
- /// The global permission setup callback.
/// The translation setup callback.
- public ApplicationCommandsModuleConfiguration(Type type, bool global = true, Action globalGuildPermissions = null, Action translations = null)
+ public ApplicationCommandsModuleConfiguration(Type type, Action translations = null)
{
this.Type = type;
- this.Permissions = null;
this.Translations = translations;
- this.GlobalGuildPermissions = globalGuildPermissions;
- this.IsGlobal = global;
}
}
///
/// 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)
{
var globalCommandsTask = ctx.Client.GetGlobalApplicationCommandsAsync();
var guildCommandsTask= ctx.Client.GetGuildApplicationCommandsAsync(ctx.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
var applicationCommands = globalCommandsTask.Result.Concat(guildCommandsTask.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 for guild {ctx.Guild.Name}").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 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 = $"{Formatter.InlineCode(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(")`: ").Append(option.Description ?? "No description provided.").Append('\n');
sb.Append('\n');
discordEmbed.AddField("Arguments", sb.ToString().Trim(), false);
}
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 = $"{Formatter.InlineCode(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(")`: ").Append(option.Description ?? "No description provided.").Append('\n');
sb.Append('\n');
discordEmbed.AddField("Arguments", sb.ToString().Trim(), false);
}
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()
.WithContent($"No command called {commandName} in guild {ctx.Guild.Name}").AsEphemeral(true));
return;
}
var discordEmbed = new DiscordEmbedBuilder
{
Title = "Help",
Description = $"{Formatter.InlineCode(command.Name)}: {command.Description ?? "No description provided."}"
};
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(")`: ").Append(option.Description ?? "No description provided.").Append('\n');
sb.Append('\n');
discordEmbed.AddField("Arguments", sb.ToString().Trim(), false);
}
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true));
}
}
}
#endregion
}
diff --git a/DisCatSharp.ApplicationCommands/Context/ApplicationCommandsGlobalPermissionContext.cs b/DisCatSharp.ApplicationCommands/Context/ApplicationCommandsGlobalPermissionContext.cs
deleted file mode 100644
index 344bc7f89..000000000
--- a/DisCatSharp.ApplicationCommands/Context/ApplicationCommandsGlobalPermissionContext.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-// 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 DisCatSharp.Entities;
-
-namespace DisCatSharp.ApplicationCommands
-{
- ///
- /// The application commands permission context.
- ///
- public class ApplicationCommandsGlobalPermissionContext
- {
- ///
- /// Gets the type.
- ///
- public Type Type { get; }
-
- ///
- /// Gets the name.
- ///
- public string Name { get; }
-
- ///
- /// Gets the permissions.
- ///
- public List> GuildPermissions = new();
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The type.
- /// The name.
- internal ApplicationCommandsGlobalPermissionContext(Type type, string name)
- {
- this.Type = type;
- this.Name = name;
- }
-
- ///
- /// Adds a user to the permission system.
- ///
- /// The id of the guild to apply this permission to.
- /// The Id of the user to give this permission.
- /// The permission for the application command. If set to true, they can use the command. If set to false, they can't use the command.
- public void AddUser(ulong guildId, ulong userId, bool permission)
- => this.GuildPermissions.Add(new KeyValuePair(guildId, new DiscordApplicationCommandPermission(userId, ApplicationCommandPermissionType.User, permission)));
-
- ///
- /// Adds a user to the permission system.
- ///
- /// The id of the guild to apply this permission to.
- /// The Id of the role to give this permission.
- /// The permission for the application command. If set to true, they can use the command. If set to false, they can't use the command.
- public void AddRole(ulong guildId, ulong roleId, bool permission)
- => this.GuildPermissions.Add(new KeyValuePair(guildId, new DiscordApplicationCommandPermission(roleId, ApplicationCommandPermissionType.Role, permission)));
-
- ///
- /// Adds a channel to the permission system.
- ///
- /// The id of the guild to apply this permission to.
- /// The Id of the channel to give this permission.
- /// The permission for the application command. If set to true, they can use the command. If set to false, they can't use the command.
- public void AddChannel(ulong guildId, ulong channelId, bool permission)
- => this.GuildPermissions.Add(new KeyValuePair(guildId, new DiscordApplicationCommandPermission(channelId, ApplicationCommandPermissionType.Channel, permission)));
- }
-}
diff --git a/DisCatSharp.ApplicationCommands/Context/ApplicationCommandsPermissionContext.cs b/DisCatSharp.ApplicationCommands/Context/ApplicationCommandsPermissionContext.cs
deleted file mode 100644
index 84e610c53..000000000
--- a/DisCatSharp.ApplicationCommands/Context/ApplicationCommandsPermissionContext.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-// 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 DisCatSharp.Entities;
-
-namespace DisCatSharp.ApplicationCommands
-{
- ///
- /// The application commands permission context.
- ///
- public class ApplicationCommandsPermissionContext
- {
- ///
- /// Gets the type.
- ///
- public Type Type { get; }
-
- ///
- /// Gets the name.
- ///
- public string Name { get; }
-
- ///
- /// Gets the permissions.
- ///
- public IReadOnlyCollection Permissions => this._permissions;
- private readonly List _permissions = new();
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The type.
- /// The name.
- internal ApplicationCommandsPermissionContext(Type type, string name)
- {
- this.Type = type;
- this.Name = name;
- }
-
- ///
- /// Adds a user to the permission system.
- ///
- /// The Id of the user to give this permission.
- /// The permission for the application command. If set to true, they can use the command. If set to false, they can't use the command.
- public void AddUser(ulong userId, bool permission)
- => this._permissions.Add(new DiscordApplicationCommandPermission(userId, ApplicationCommandPermissionType.User, permission));
-
- ///
- /// Adds a user to the permission system.
- ///
- /// The Id of the role to give this permission.
- /// The permission for the application command. If set to true, they can use the command. If set to false, they can't use the command.
- public void AddRole(ulong roleId, bool permission)
- => this._permissions.Add(new DiscordApplicationCommandPermission(roleId, ApplicationCommandPermissionType.Role, permission));
-
- ///
- /// Adds a channel to the permission system.
- ///
- /// The Id of the channel to give this permission.
- /// The permission for the application command. If set to true, they can use the command. If set to false, they can't use the command.
- public void AddChannel(ulong channelId, bool permission)
- => this._permissions.Add(new DiscordApplicationCommandPermission(channelId, ApplicationCommandPermissionType.Channel, permission));
- }
-}
diff --git a/DisCatSharp.ApplicationCommands/Workers/PermissionWorker.cs b/DisCatSharp.ApplicationCommands/Workers/PermissionWorker.cs
deleted file mode 100644
index 58877ad3a..000000000
--- a/DisCatSharp.ApplicationCommands/Workers/PermissionWorker.cs
+++ /dev/null
@@ -1,124 +0,0 @@
-// 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.Linq;
-using System.Threading.Tasks;
-
-using DisCatSharp.Entities;
-
-using Microsoft.Extensions.Logging;
-
-namespace DisCatSharp.ApplicationCommands
-{
- ///
- /// Represents a .
- ///
- internal class PermissionWorker
- {
- ///
- /// Updates the application command permissions.
- ///
- /// The types.
- /// The optional guild id.
- /// The command id.
- /// The command name.
- /// The declaring command type.
- /// The root command type.
- internal static async Task UpdateCommandPermissionAsync(IEnumerable types, ulong? guildid, ulong commandId, string commandName, Type commandDeclaringType, Type commandRootType)
- {
- if (!guildid.HasValue)
- {
- ApplicationCommandsExtension.ClientInternal.Logger.LogTrace("You can't set global permissions till yet. See https://discord.com/developers/docs/interactions/application-commands#permissions");
- return;
- }
-
- var ctx = new ApplicationCommandsPermissionContext(commandDeclaringType, commandName);
- var conf = types.First(t => t.Type == commandRootType);
- conf.Permissions?.Invoke(ctx);
-
- if (ctx.Permissions.Count == 0)
- return;
-
- ApplicationCommandsExtension.ClientInternal.Logger.Log(ApplicationCommandsExtension.ApplicationCommandsLogLevel, $"[AC Perms] Command {commandName} permission update on {guildid.Value}");
-
- await ApplicationCommandsExtension.ClientInternal.OverwriteGuildApplicationCommandPermissionsAsync(guildid.Value, commandId, ctx.Permissions);
- }
-
- ///
- /// Gets the permissions.
- ///
- /// The types.
- /// The command id.
- /// The command name.
- /// The declaring command type.
- /// The root command type.
- /// Permissions on success.
- internal static (
- bool success,
- ulong? commandId,
- IReadOnlyList permissions
- ) ResolvePermissions(IEnumerable types, ulong commandId, string commandName, Type commandDeclaringType, Type commandRootType)
- {
- var ctx = new ApplicationCommandsPermissionContext(commandDeclaringType, commandName);
- var conf = types.First(t => t.Type == commandRootType);
- conf.Permissions?.Invoke(ctx);
-
- return ctx.Permissions.Count == 0 || commandId == 0
- ? (false, null, null)
- : (true, commandId, ctx.Permissions.ToList());
- }
-
- ///
- /// Gets the global permissions.
- ///
- /// The types.
- /// The command id.
- /// The command name.
- /// The declaring command type.
- /// The root command type.
- /// Permissions on success.
- internal static (
- bool success,
- ulong? commandId,
- IReadOnlyList> permissions
- ) ResolveGlobalPermissions(IEnumerable types, ulong commandId, string commandName, Type commandDeclaringType, Type commandRootType)
- {
- var ctx = new ApplicationCommandsGlobalPermissionContext(commandDeclaringType, commandName);
- var conf = types.First(t => t.Type == commandRootType);
- conf.GlobalGuildPermissions?.Invoke(ctx);
-
- return ctx.GuildPermissions.Count == 0 || commandId == 0
- ? (false, null, null)
- : (true, commandId, ctx.GuildPermissions);
- }
-
- ///
- /// Updates the permissions.
- ///
- /// The guild id.
- /// The permission overwrites.
- internal static async Task> BulkOverwriteCommandPermissionsAsync(ulong guildId, IEnumerable permissionOverwrites)
- => await ApplicationCommandsExtension.ClientInternal.BulkOverwriteGuildApplicationCommandsAsync(guildId, permissionOverwrites);
- }
-}
diff --git a/DisCatSharp/Entities/Guild/DiscordGuild.cs b/DisCatSharp/Entities/Guild/DiscordGuild.cs
index 627b7e0c6..e1f938b1d 100644
--- a/DisCatSharp/Entities/Guild/DiscordGuild.cs
+++ b/DisCatSharp/Entities/Guild/DiscordGuild.cs
@@ -1,3577 +1,3567 @@
// 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.Tasks;
using DisCatSharp.Enums;
using DisCatSharp.Net;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Models;
using DisCatSharp.Net.Serialization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DisCatSharp.Entities
{
///
/// Represents a Discord guild.
///
public class DiscordGuild : SnowflakeObject, IEquatable
{
///
/// Gets the guild's name.
///
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; internal set; }
///
/// Gets the guild icon's hash.
///
[JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)]
public string IconHash { get; internal set; }
///
/// Gets the guild icon's url.
///
[JsonIgnore]
public string IconUrl
=> !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.{(this.IconHash.StartsWith("a_") ? "gif" : "png")}?size=1024" : null;
///
/// Gets the guild splash's hash.
///
[JsonProperty("splash", NullValueHandling = NullValueHandling.Ignore)]
public string SplashHash { get; internal set; }
///
/// Gets the guild splash's url.
///
[JsonIgnore]
public string SplashUrl
=> !string.IsNullOrWhiteSpace(this.SplashHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.SplashHash}.png?size=1024" : null;
///
/// Gets the guild discovery splash's hash.
///
[JsonProperty("discovery_splash", NullValueHandling = NullValueHandling.Ignore)]
public string DiscoverySplashHash { get; internal set; }
///
/// Gets the guild discovery splash's url.
///
[JsonIgnore]
public string DiscoverySplashUrl
=> !string.IsNullOrWhiteSpace(this.DiscoverySplashHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.GUILD_DISCOVERY_SPLASHES}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.DiscoverySplashHash}.png?size=1024" : null;
///
/// Gets the preferred locale of this guild.
/// This is used for server discovery, interactions and notices from Discord. Defaults to en-US.
///
[JsonProperty("preferred_locale", NullValueHandling = NullValueHandling.Ignore)]
public string PreferredLocale { get; internal set; }
///
/// Gets the ID of the guild's owner.
///
[JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong OwnerId { get; internal set; }
///
/// Gets the guild's owner.
///
[JsonIgnore]
public DiscordMember Owner
=> this.Members.TryGetValue(this.OwnerId, out var owner)
? owner
: this.Discord.ApiClient.GetGuildMemberAsync(this.Id, this.OwnerId).ConfigureAwait(false).GetAwaiter().GetResult();
///
/// Gets permissions for the user in the guild (does not include channel overrides)
///
[JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)]
public Permissions? Permissions { get; set; }
///
/// Gets the guild's voice region ID.
///
[JsonProperty("region", NullValueHandling = NullValueHandling.Ignore)]
internal string VoiceRegionId { get; set; }
///
/// Gets the guild's voice region.
///
[JsonIgnore]
public DiscordVoiceRegion VoiceRegion
=> this.Discord.VoiceRegions[this.VoiceRegionId];
///
/// Gets the guild's AFK voice channel ID.
///
[JsonProperty("afk_channel_id", NullValueHandling = NullValueHandling.Ignore)]
internal ulong AfkChannelId { get; set; }
///
/// Gets the guild's AFK voice channel.
///
[JsonIgnore]
public DiscordChannel AfkChannel
=> this.GetChannel(this.AfkChannelId);
///
/// List of .
/// Null if DisCatSharp.ApplicationCommands is not used or no guild commands are registered.
///
[JsonIgnore]
public ReadOnlyCollection RegisteredApplicationCommands
=> new(this.InternalRegisteredApplicationCommands);
[JsonIgnore]
internal List InternalRegisteredApplicationCommands { get; set; } = null;
- ///
- /// List of .
- /// Null if DisCatSharp.ApplicationCommands is not used or no guild commands or permissions are registered.
- ///
- [JsonIgnore]
- public ReadOnlyCollection GuildApplicationCommandPermissions
- => new(this.InternalGuildApplicationCommandPermissions);
- [JsonIgnore]
- internal List InternalGuildApplicationCommandPermissions { get; set; } = null;
-
///
/// Gets the guild's AFK timeout.
///
[JsonProperty("afk_timeout", NullValueHandling = NullValueHandling.Ignore)]
public int AfkTimeout { get; internal set; }
///
/// Gets the guild's verification level.
///
[JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)]
public VerificationLevel VerificationLevel { get; internal set; }
///
/// Gets the guild's default notification settings.
///
[JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)]
public DefaultMessageNotifications DefaultMessageNotifications { get; internal set; }
///
/// Gets the guild's explicit content filter settings.
///
[JsonProperty("explicit_content_filter")]
public ExplicitContentFilter ExplicitContentFilter { get; internal set; }
///
/// Gets the guild's nsfw level.
///
[JsonProperty("nsfw_level")]
public NsfwLevel NsfwLevel { get; internal set; }
///
/// Gets the system channel id.
///
[JsonProperty("system_channel_id", NullValueHandling = NullValueHandling.Include)]
internal ulong? SystemChannelId { get; set; }
///
/// Gets the channel where system messages (such as boost and welcome messages) are sent.
///
[JsonIgnore]
public DiscordChannel SystemChannel => this.SystemChannelId.HasValue
? this.GetChannel(this.SystemChannelId.Value)
: null;
///
/// Gets the settings for this guild's system channel.
///
[JsonProperty("system_channel_flags")]
public SystemChannelFlags SystemChannelFlags { get; internal set; }
///
/// Gets whether this guild's widget is enabled.
///
[JsonProperty("widget_enabled", NullValueHandling = NullValueHandling.Ignore)]
public bool? WidgetEnabled { get; internal set; }
///
/// Gets the widget channel id.
///
[JsonProperty("widget_channel_id", NullValueHandling = NullValueHandling.Ignore)]
internal ulong? WidgetChannelId { get; set; }
///
/// Gets the widget channel for this guild.
///
[JsonIgnore]
public DiscordChannel WidgetChannel => this.WidgetChannelId.HasValue
? this.GetChannel(this.WidgetChannelId.Value)
: null;
///
/// Gets the rules channel id.
///
[JsonProperty("rules_channel_id")]
internal ulong? RulesChannelId { get; set; }
///
/// Gets the rules channel for this guild.
/// This is only available if the guild is considered "discoverable".
///
[JsonIgnore]
public DiscordChannel RulesChannel => this.RulesChannelId.HasValue
? this.GetChannel(this.RulesChannelId.Value)
: null;
///
/// Gets the public updates channel id.
///
[JsonProperty("public_updates_channel_id")]
internal ulong? PublicUpdatesChannelId { get; set; }
///
/// Gets the public updates channel (where admins and moderators receive messages from Discord) for this guild.
/// This is only available if the guild is considered "discoverable".
///
[JsonIgnore]
public DiscordChannel PublicUpdatesChannel => this.PublicUpdatesChannelId.HasValue
? this.GetChannel(this.PublicUpdatesChannelId.Value)
: null;
///
/// Gets the application id of this guild if it is bot created.
///
[JsonProperty("application_id")]
public ulong? ApplicationId { get; internal set; }
///
/// Gets a collection of this guild's roles.
///
[JsonIgnore]
public IReadOnlyDictionary Roles => new ReadOnlyConcurrentDictionary(this.RolesInternal);
[JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary RolesInternal;
///
/// Gets a collection of this guild's stickers.
///
[JsonIgnore]
public IReadOnlyDictionary Stickers => new ReadOnlyConcurrentDictionary(this.StickersInternal);
[JsonProperty("stickers", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary StickersInternal;
///
/// Gets a collection of this guild's emojis.
///
[JsonIgnore]
public IReadOnlyDictionary Emojis => new ReadOnlyConcurrentDictionary(this.EmojisInternal);
[JsonProperty("emojis", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary EmojisInternal;
///
/// Gets a collection of this guild's features.
///
[JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyList RawFeatures { get; internal set; }
///
/// Gets the guild's features.
///
[JsonIgnore]
public GuildFeatures Features => new(this);
///
/// Gets the required multi-factor authentication level for this guild.
///
[JsonProperty("mfa_level", NullValueHandling = NullValueHandling.Ignore)]
public MfaLevel MfaLevel { get; internal set; }
///
/// Gets this guild's join date.
///
[JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)]
public DateTimeOffset JoinedAt { get; internal set; }
///
/// Gets whether this guild is considered to be a large guild.
///
[JsonProperty("large", NullValueHandling = NullValueHandling.Ignore)]
public bool IsLarge { get; internal set; }
///
/// Gets whether this guild is unavailable.
///
[JsonProperty("unavailable", NullValueHandling = NullValueHandling.Ignore)]
public bool IsUnavailable { get; internal set; }
///
/// Gets the total number of members in this guild.
///
[JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)]
public int MemberCount { get; internal set; }
///
/// Gets the maximum amount of members allowed for this guild.
///
[JsonProperty("max_members")]
public int? MaxMembers { get; internal set; }
///
/// Gets the maximum amount of presences allowed for this guild.
///
[JsonProperty("max_presences")]
public int? MaxPresences { get; internal set; }
///
/// Gets the approximate number of members in this guild, when using and having withCounts set to true.
///
[JsonProperty("approximate_member_count", NullValueHandling = NullValueHandling.Ignore)]
public int? ApproximateMemberCount { get; internal set; }
///
/// Gets the approximate number of presences in this guild, when using and having withCounts set to true.
///
[JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)]
public int? ApproximatePresenceCount { get; internal set; }
///
/// Gets the maximum amount of users allowed per video channel.
///
[JsonProperty("max_video_channel_users", NullValueHandling = NullValueHandling.Ignore)]
public int? MaxVideoChannelUsers { get; internal set; }
///
/// Gets a dictionary of all the voice states for this guilds. The key for this dictionary is the ID of the user
/// the voice state corresponds to.
///
[JsonIgnore]
public IReadOnlyDictionary VoiceStates => new ReadOnlyConcurrentDictionary(this.VoiceStatesInternal);
[JsonProperty("voice_states", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary VoiceStatesInternal;
///
/// Gets a dictionary of all the members that belong to this guild. The dictionary's key is the member ID.
///
[JsonIgnore]
public IReadOnlyDictionary Members => new ReadOnlyConcurrentDictionary(this.MembersInternal);
[JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary MembersInternal;
///
/// Gets a dictionary of all the channels associated with this guild. The dictionary's key is the channel ID.
///
[JsonIgnore]
public IReadOnlyDictionary Channels => new ReadOnlyConcurrentDictionary(this.ChannelsInternal);
[JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary ChannelsInternal;
internal ConcurrentDictionary Invites;
///
/// Gets a dictionary of all the active threads associated with this guild the user has permission to view. The dictionary's key is the channel ID.
///
[JsonIgnore]
public IReadOnlyDictionary Threads { get; internal set; }
[JsonProperty("threads", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary ThreadsInternal = new();
///
/// Gets a dictionary of all active stage instances. The dictionary's key is the stage ID.
///
[JsonIgnore]
public IReadOnlyDictionary StageInstances { get; internal set; }
[JsonProperty("stage_instances", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary StageInstancesInternal = new();
///
/// Gets a dictionary of all scheduled events.
///
[JsonIgnore]
public IReadOnlyDictionary ScheduledEvents { get; internal set; }
[JsonProperty("guild_scheduled_events", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary ScheduledEventsInternal = new();
///
/// Gets the guild member for current user.
///
[JsonIgnore]
public DiscordMember CurrentMember
=> this._currentMemberLazy.Value;
[JsonIgnore]
private readonly Lazy _currentMemberLazy;
///
/// Gets the @everyone role for this guild.
///
[JsonIgnore]
public DiscordRole EveryoneRole
=> this.GetRole(this.Id);
[JsonIgnore]
internal bool IsOwnerInternal;
///
/// Gets whether the current user is the guild's owner.
///
[JsonProperty("owner", NullValueHandling = NullValueHandling.Ignore)]
public bool IsOwner
{
get => this.IsOwnerInternal || this.OwnerId == this.Discord.CurrentUser.Id;
internal set => this.IsOwnerInternal = value;
}
///
/// Gets the vanity URL code for this guild, when applicable.
///
[JsonProperty("vanity_url_code")]
public string VanityUrlCode { get; internal set; }
///
/// Gets the guild description, when applicable.
///
[JsonProperty("description")]
public string Description { get; internal set; }
///
/// Gets this guild's banner hash, when applicable.
///
[JsonProperty("banner")]
public string BannerHash { get; internal set; }
///
/// Gets this guild's banner in url form.
///
[JsonIgnore]
public string BannerUrl
=> !string.IsNullOrWhiteSpace(this.BannerHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}" : null;
///
/// Whether this guild has the community feature enabled.
///
[JsonIgnore]
public bool IsCommunity => this.Features.HasCommunityEnabled;
///
/// Whether this guild has enabled the welcome screen.
///
[JsonIgnore]
public bool HasWelcomeScreen => this.Features.HasWelcomeScreenEnabled;
///
/// Whether this guild has enabled membership screening.
///
[JsonIgnore]
public bool HasMemberVerificationGate => this.Features.HasMembershipScreeningEnabled;
///
/// Gets this guild's premium tier (Nitro boosting).
///
[JsonProperty("premium_tier")]
public PremiumTier PremiumTier { get; internal set; }
///
/// Gets the amount of members that boosted this guild.
///
[JsonProperty("premium_subscription_count", NullValueHandling = NullValueHandling.Ignore)]
public int? PremiumSubscriptionCount { get; internal set; }
///
/// Whether the premium progress bar is enabled.
///
[JsonProperty("premium_progress_bar_enabled", NullValueHandling = NullValueHandling.Ignore)]
public bool PremiumProgressBarEnabled { get; internal set; }
///
/// Gets whether this guild is designated as NSFW.
///
[JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)]
public bool IsNsfw { get; internal set; }
///
/// Gets this guild's hub type, if applicable.
///
[JsonProperty("hub_type", NullValueHandling = NullValueHandling.Ignore)]
public HubType HubType { get; internal set; }
///
/// Gets a dictionary of all by position ordered channels associated with this guild. The dictionary's key is the channel ID.
///
[JsonIgnore]
public IReadOnlyDictionary OrderedChannels => new ReadOnlyDictionary(this.InternalSortChannels());
///
/// Sorts the channels.
///
private Dictionary InternalSortChannels()
{
Dictionary keyValuePairs = new();
var ochannels = this.GetOrderedChannels();
foreach (var ochan in ochannels)
{
if (ochan.Key != 0)
keyValuePairs.Add(ochan.Key, this.GetChannel(ochan.Key));
foreach (var chan in ochan.Value)
keyValuePairs.Add(chan.Id, chan);
}
return keyValuePairs;
}
///
/// Gets an ordered list out of the channel cache.
/// Returns a Dictionary where the key is an ulong and can be mapped to s.
/// Ignore the 0 key here, because that indicates that this is the "has no category" list.
/// Each value contains a ordered list of text/news and voice/stage channels as .
///
/// A ordered list of categories with its channels
public Dictionary> GetOrderedChannels()
{
IReadOnlyList rawChannels = this.ChannelsInternal.Values.ToList();
Dictionary> orderedChannels = new();
orderedChannels.Add(0, new List());
foreach (var channel in rawChannels.Where(c => c.Type == ChannelType.Category).OrderBy(c => c.Position))
{
orderedChannels.Add(channel.Id, new List());
}
foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position))
{
orderedChannels[channel.ParentId.Value].Add(channel);
}
foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position))
{
orderedChannels[channel.ParentId.Value].Add(channel);
}
foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position))
{
orderedChannels[0].Add(channel);
}
foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position))
{
orderedChannels[0].Add(channel);
}
return orderedChannels;
}
///
/// Gets an ordered list.
/// Returns a Dictionary where the key is an ulong and can be mapped to s.
/// Ignore the 0 key here, because that indicates that this is the "has no category" list.
/// Each value contains a ordered list of text/news and voice/stage channels as .
///
/// A ordered list of categories with its channels
public async Task>> GetOrderedChannelsAsync()
{
var rawChannels = await this.Discord.ApiClient.GetGuildChannelsAsync(this.Id);
Dictionary> orderedChannels = new();
orderedChannels.Add(0, new List());
foreach (var channel in rawChannels.Where(c => c.Type == ChannelType.Category).OrderBy(c => c.Position))
{
orderedChannels.Add(channel.Id, new List());
}
foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position))
{
orderedChannels[channel.ParentId.Value].Add(channel);
}
foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position))
{
orderedChannels[channel.ParentId.Value].Add(channel);
}
foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position))
{
orderedChannels[0].Add(channel);
}
foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position))
{
orderedChannels[0].Add(channel);
}
return orderedChannels;
}
///
/// Whether it is synced.
///
[JsonIgnore]
internal bool IsSynced { get; set; }
///
/// Initializes a new instance of the class.
///
internal DiscordGuild()
{
this._currentMemberLazy = new Lazy(() => this.MembersInternal != null && this.MembersInternal.TryGetValue(this.Discord.CurrentUser.Id, out var member) ? member : null);
this.Invites = new ConcurrentDictionary();
this.Threads = new ReadOnlyConcurrentDictionary(this.ThreadsInternal);
this.StageInstances = new ReadOnlyConcurrentDictionary(this.StageInstancesInternal);
this.ScheduledEvents = new ReadOnlyConcurrentDictionary(this.ScheduledEventsInternal);
}
#region Guild Methods
///
/// Searches the current guild for members who's display name start with the specified name.
///
/// The name to search for.
/// The maximum amount of members to return. Max 1000. Defaults to 1.
/// The members found, if any.
public Task> SearchMembersAsync(string name, int? limit = 1)
=> this.Discord.ApiClient.SearchMembersAsync(this.Id, name, limit);
///
/// Adds a new member to this guild
///
/// User to add
/// User's access token (OAuth2)
/// new nickname
/// new roles
/// whether this user has to be muted
/// whether this user has to be deafened
/// Thrown when the client does not have the permission.
/// Thrown when the or is not found.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task AddMemberAsync(DiscordUser user, string accessToken, string nickname = null, IEnumerable roles = null,
bool muted = false, bool deaf = false)
=> this.Discord.ApiClient.AddGuildMemberAsync(this.Id, user.Id, accessToken, nickname, roles, muted, deaf);
///
/// Deletes this guild. Requires the caller to be the owner of the guild.
///
/// Thrown when the client is not the owner of the guild.
/// Thrown when Discord is unable to process the request.
public Task DeleteAsync()
=> this.Discord.ApiClient.DeleteGuildAsync(this.Id);
///
/// Modifies this guild.
///
/// Action to perform on this guild..
/// The modified guild object.
/// Thrown when the client does not have the permission.
/// Thrown when the guild does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ModifyAsync(Action action)
{
var mdl = new GuildEditModel();
action(mdl);
var afkChannelId = Optional.FromNoValue();
if (mdl.AfkChannel.HasValue && mdl.AfkChannel.Value.Type != ChannelType.Voice && mdl.AfkChannel.Value != null)
throw new ArgumentException("AFK channel needs to be a voice channel.");
else if (mdl.AfkChannel.HasValue && mdl.AfkChannel.Value != null)
afkChannelId = mdl.AfkChannel.Value.Id;
else if (mdl.AfkChannel.HasValue)
afkChannelId = null;
var rulesChannelId = Optional.FromNoValue();
if (mdl.RulesChannel.HasValue && mdl.RulesChannel.Value != null && mdl.RulesChannel.Value.Type != ChannelType.Text && mdl.RulesChannel.Value.Type != ChannelType.News)
throw new ArgumentException("Rules channel needs to be a text channel.");
else if (mdl.RulesChannel.HasValue && mdl.RulesChannel.Value != null)
rulesChannelId = mdl.RulesChannel.Value.Id;
else if (mdl.RulesChannel.HasValue)
rulesChannelId = null;
var publicUpdatesChannelId = Optional.FromNoValue();
if (mdl.PublicUpdatesChannel.HasValue && mdl.PublicUpdatesChannel.Value != null && mdl.PublicUpdatesChannel.Value.Type != ChannelType.Text && mdl.PublicUpdatesChannel.Value.Type != ChannelType.News)
throw new ArgumentException("Public updates channel needs to be a text channel.");
else if (mdl.PublicUpdatesChannel.HasValue && mdl.PublicUpdatesChannel.Value != null)
publicUpdatesChannelId = mdl.PublicUpdatesChannel.Value.Id;
else if (mdl.PublicUpdatesChannel.HasValue)
publicUpdatesChannelId = null;
var systemChannelId = Optional.FromNoValue();
if (mdl.SystemChannel.HasValue && mdl.SystemChannel.Value != null && mdl.SystemChannel.Value.Type != ChannelType.Text && mdl.SystemChannel.Value.Type != ChannelType.News)
throw new ArgumentException("Public updates channel needs to be a text channel.");
else if (mdl.SystemChannel.HasValue && mdl.SystemChannel.Value != null)
systemChannelId = mdl.SystemChannel.Value.Id;
else if (mdl.SystemChannel.HasValue)
systemChannelId = null;
var iconb64 = Optional.FromNoValue();
if (mdl.Icon.HasValue && mdl.Icon.Value != null)
using (var imgtool = new ImageTool(mdl.Icon.Value))
iconb64 = imgtool.GetBase64();
else if (mdl.Icon.HasValue)
iconb64 = null;
var splashb64 = Optional.FromNoValue();
if (mdl.Splash.HasValue && mdl.Splash.Value != null)
using (var imgtool = new ImageTool(mdl.Splash.Value))
splashb64 = imgtool.GetBase64();
else if (mdl.Splash.HasValue)
splashb64 = null;
var bannerb64 = Optional.FromNoValue();
if (mdl.Banner.HasValue && mdl.Banner.Value != null)
using (var imgtool = new ImageTool(mdl.Banner.Value))
bannerb64 = imgtool.GetBase64();
else if (mdl.Banner.HasValue)
bannerb64 = null;
var discoverySplash64 = Optional.FromNoValue();
if (mdl.DiscoverySplash.HasValue && mdl.DiscoverySplash.Value != null)
using (var imgtool = new ImageTool(mdl.DiscoverySplash.Value))
discoverySplash64 = imgtool.GetBase64();
else if (mdl.DiscoverySplash.HasValue)
discoverySplash64 = null;
var description = Optional.FromNoValue();
if (mdl.Description.HasValue && mdl.Description.Value != null)
description = mdl.Description;
else if (mdl.Description.HasValue)
description = null;
return await this.Discord.ApiClient.ModifyGuildAsync(this.Id, mdl.Name,
mdl.VerificationLevel, mdl.DefaultMessageNotifications, mdl.MfaLevel, mdl.ExplicitContentFilter,
afkChannelId, mdl.AfkTimeout, iconb64, mdl.Owner.IfPresent(e => e.Id), splashb64,
systemChannelId, mdl.SystemChannelFlags, publicUpdatesChannelId, rulesChannelId,
description, bannerb64, discoverySplash64, mdl.PreferredLocale, mdl.PremiumProgressBarEnabled, mdl.AuditLogReason).ConfigureAwait(false);
}
///
/// Modifies the community settings async.
/// This sets if not highest and .
///
/// If true, enable .
/// The rules channel.
/// The public updates channel.
/// The preferred locale. Defaults to en-US.
/// The description.
/// The default message notifications. Defaults to
/// The auditlog reason.
/// Thrown when the client does not have the permission.
/// Thrown when the guild does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ModifyCommunitySettingsAsync(bool enabled, DiscordChannel rulesChannel = null, DiscordChannel publicUpdatesChannel = null, string preferredLocale = "en-US", string description = null, DefaultMessageNotifications defaultMessageNotifications = DefaultMessageNotifications.MentionsOnly, string reason = null)
{
var verificationLevel = this.VerificationLevel;
if (this.VerificationLevel != VerificationLevel.Highest)
{
verificationLevel = VerificationLevel.High;
}
var explicitContentFilter = ExplicitContentFilter.AllMembers;
var rulesChannelId = Optional.FromNoValue();
if (rulesChannel != null && rulesChannel.Type != ChannelType.Text && rulesChannel.Type != ChannelType.News)
throw new ArgumentException("Rules channel needs to be a text channel.");
else if (rulesChannel != null)
rulesChannelId = rulesChannel.Id;
else if (rulesChannel == null)
rulesChannelId = null;
var publicUpdatesChannelId = Optional.FromNoValue();
if (publicUpdatesChannel != null && publicUpdatesChannel.Type != ChannelType.Text && publicUpdatesChannel.Type != ChannelType.News)
throw new ArgumentException("Public updates channel needs to be a text channel.");
else if (publicUpdatesChannel != null)
publicUpdatesChannelId = publicUpdatesChannel.Id;
else if (publicUpdatesChannel == null)
publicUpdatesChannelId = null;
List features = new();
var rfeatures = this.RawFeatures.ToList();
if (this.RawFeatures.Contains("COMMUNITY") && enabled)
{
features = rfeatures;
}
else if (!this.RawFeatures.Contains("COMMUNITY") && enabled)
{
rfeatures.Add("COMMUNITY");
features = rfeatures;
}
else if (this.RawFeatures.Contains("COMMUNITY") && !enabled)
{
rfeatures.Remove("COMMUNITY");
features = rfeatures;
}
else if (!this.RawFeatures.Contains("COMMUNITY") && !enabled)
{
features = rfeatures;
}
return await this.Discord.ApiClient.ModifyGuildCommunitySettingsAsync(this.Id, features, rulesChannelId, publicUpdatesChannelId, preferredLocale, description, defaultMessageNotifications, explicitContentFilter, verificationLevel, reason).ConfigureAwait(false);
}
///
/// Timeout a specified member in this guild.
///
/// Member to timeout.
/// The datetime offset to time out the user. Up to 28 days.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task TimeoutAsync(ulong memberId, DateTimeOffset until, string reason = null)
=> until.Subtract(DateTimeOffset.UtcNow).Days > 28
? throw new ArgumentException("Timeout can not be longer than 28 days")
: this.Discord.ApiClient.ModifyTimeoutAsync(this.Id, memberId, until, reason);
///
/// Timeout a specified member in this guild.
///
/// Member to timeout.
/// The timespan to time out the user. Up to 28 days.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task TimeoutAsync(ulong memberId, TimeSpan until, string reason = null)
=> this.TimeoutAsync(memberId, DateTimeOffset.UtcNow + until, reason);
///
/// Timeout a specified member in this guild.
///
/// Member to timeout.
/// The datetime to time out the user. Up to 28 days.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task TimeoutAsync(ulong memberId, DateTime until, string reason = null)
=> this.TimeoutAsync(memberId, until.ToUniversalTime() - DateTime.UtcNow, reason);
///
/// Removes the timeout from a specified member in this guild.
///
/// Member to remove the timeout from.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RemoveTimeoutAsync(ulong memberId, string reason = null)
=> this.Discord.ApiClient.ModifyTimeoutAsync(this.Id, memberId, null, reason);
///
/// Bans a specified member from this guild.
///
/// Member to ban.
/// How many days to remove messages from.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task BanMemberAsync(DiscordMember member, int deleteMessageDays = 0, string reason = null)
=> this.Discord.ApiClient.CreateGuildBanAsync(this.Id, member.Id, deleteMessageDays, reason);
///
/// Bans a specified user by ID. This doesn't require the user to be in this guild.
///
/// ID of the user to ban.
/// How many days to remove messages from.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task BanMemberAsync(ulong userId, int deleteMessageDays = 0, string reason = null)
=> this.Discord.ApiClient.CreateGuildBanAsync(this.Id, userId, deleteMessageDays, reason);
///
/// Unbans a user from this guild.
///
/// User to unban.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the user does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task UnbanMemberAsync(DiscordUser user, string reason = null)
=> this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, user.Id, reason);
///
/// Unbans a user by ID.
///
/// ID of the user to unban.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the user does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task UnbanMemberAsync(ulong userId, string reason = null)
=> this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, userId, reason);
///
/// Leaves this guild.
///
/// Thrown when Discord is unable to process the request.
public Task LeaveAsync()
=> this.Discord.ApiClient.LeaveGuildAsync(this.Id);
///
/// Gets the bans for this guild.
///
/// Collection of bans in this guild.
/// Thrown when the client does not have the permission.
/// Thrown when Discord is unable to process the request.
public Task> GetBansAsync()
=> this.Discord.ApiClient.GetGuildBansAsync(this.Id);
///
/// Gets a ban for a specific user.
///
/// The Id of the user to get the ban for.
/// Thrown when the specified user is not banned.
/// The requested ban object.
public Task GetBanAsync(ulong userId)
=> this.Discord.ApiClient.GetGuildBanAsync(this.Id, userId);
///
/// Gets a ban for a specific user.
///
/// The user to get the ban for.
/// Thrown when the specified user is not banned.
/// The requested ban object.
public Task GetBanAsync(DiscordUser user)
=> this.GetBanAsync(user.Id);
#region Sheduled Events
///