diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
index fd7dfad8c..b56abce69 100644
--- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
+++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
@@ -1,1368 +1,1366 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 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.Threading.Tasks;
-using System.Collections.Generic;
+using DisCatSharp.ApplicationCommands.Attributes;
+using DisCatSharp.ApplicationCommands.EventArgs;
+using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
-using System.Linq;
+using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
-using Microsoft.Extensions.Logging;
-using DisCatSharp.Common.Utilities;
-using Microsoft.Extensions.DependencyInjection;
-using DisCatSharp.ApplicationCommands.EventArgs;
using DisCatSharp.Exceptions;
-using DisCatSharp.Enums;
-using DisCatSharp.ApplicationCommands.Attributes;
-using System.Text.RegularExpressions;
-using DisCatSharp.Common;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
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 _commandMethods { get; set; } = new List();
///
/// List of groups.
///
private static List _groupCommands { get; set; } = new List();
///
/// List of groups with subgroups.
///
private static List _subGroupCommands { get; set; } = new List();
///
/// List of context menus.
///
private static List _contextMenuCommands { get; set; } = new List();
///
/// Singleton modules.
///
private static List _singletonModules { get; set; } = new List();
///
/// List of modules to register.
///
private List> _updateList { get; set; } = new List>();
///
/// Configuration for Discord.
///
private readonly ApplicationCommandsConfiguration _configuration;
///
/// Set to true if anything fails when registering.
///
private static bool _errored { get; set; } = false;
///
/// Gets a list of registered commands. The key is the guild id (null if global).
///
public IReadOnlyList>> RegisteredCommands
=> _registeredCommands;
private static List>> _registeredCommands = new();
///
/// Initializes a new instance of the class.
///
/// The configuration.
internal ApplicationCommandsExtension(ApplicationCommandsConfiguration configuration)
{
this._configuration = configuration;
}
///
/// Runs setup. DO NOT RUN THIS MANUALLY. DO NOT DO ANYTHING WITH THIS.
///
/// The client to setup on.
protected internal override void Setup(DiscordClient client)
{
if (this.Client != null)
throw new InvalidOperationException("What did I tell you?");
this.Client = client;
this._slashError = new AsyncEvent("SLASHCOMMAND_ERRORED", TimeSpan.Zero, null);
this._slashExecuted = new AsyncEvent("SLASHCOMMAND_EXECUTED", TimeSpan.Zero, null);
this._contextMenuErrored = new AsyncEvent("CONTEXTMENU_ERRORED", TimeSpan.Zero, null);
this._contextMenuExecuted = new AsyncEvent("CONTEXTMENU_EXECUTED", TimeSpan.Zero, null);
this.Client.Ready += this.Update;
this.Client.InteractionCreated += this.InteractionHandler;
this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler;
}
///
/// Registers a command class.
///
/// The command class to register.
/// The guild id to register it on. If you want global commands, leave it null.
public void RegisterCommands(ulong? guildId = null) where T : ApplicationCommandsModule
{
if (this.Client.ShardId == 0)
this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T))));
}
///
/// Registers a command class.
///
/// The of the command class to register.
/// The guild id to register it on. If you want global commands, leave it null.
public void RegisterCommands(Type type, ulong? guildId = 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)));
}
///
/// Registers a command class with permission setup.
///
/// The command class to register.
/// The guild id to register it on.
/// A callback to setup permissions with.
public void RegisterCommands(ulong guildId, Action permissionSetup = null) where T : ApplicationCommandsModule
{
if (this.Client.ShardId == 0)
this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup)));
}
///
/// Registers a command class with permission setup.
///
/// The of the command class to register.
/// The guild id to register it on.
/// A callback to setup permissions with.
public void RegisterCommands(Type type, ulong guildId, Action permissionSetup = 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)));
}
/*
///
/// Registers a command class with permission setup but without a guild id.
///
/// The command class to register.
/// A callback to setup permissions with.
public void RegisterCommands(Action permissionSetup = null) where T : ApplicationCommandsModule
{
if (this.Client.ShardId == 0)
this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup)));
}
///
/// 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.
public void RegisterCommands(Type type, Action permissionSetup = 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, permissionSetup)));
}
*/
///
/// To be run on ready.
///
/// The client.
/// The ready event args.
internal Task Update(DiscordClient client, ReadyEventArgs e)
=> this.Update();
///
/// Actual method for registering, used for RegisterCommands and on Ready.
///
internal Task Update()
{
//Only update for shard 0
if (this.Client.ShardId == 0)
{
//Groups commands by guild id or global
foreach (var key in this._updateList.Select(x => x.Key).Distinct())
{
this.RegisterCommands(this._updateList.Where(x => x.Key == key).Select(x => x.Value), key);
}
}
return Task.CompletedTask;
}
///
/// Method for registering commands for a target from modules.
///
/// The types.
/// 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();
//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();
}
//Handles groups
foreach (var subclassinfo in classes)
{
//Gets the attribute and methods in the group
var groupAttribute = subclassinfo.GetCustomAttribute();
var submethods = subclassinfo.DeclaredMethods.Where(x => x.GetCustomAttribute() != null);
var subclasses = subclassinfo.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null);
if (subclasses.Any() && submethods.Any())
{
throw new ArgumentException("Slash command groups cannot have both subcommands and subgroups!");
}
//Initializes the command
var payload = new DiscordApplicationCommand(groupAttribute.Name, groupAttribute.Description, default_permission: groupAttribute.DefaultPermission);
commandTypeSources.Add(new KeyValuePair(type, type));
var commandmethods = new List>();
//Handles commands in the group
foreach (var submethod in submethods)
{
var commandAttribute = submethod.GetCustomAttribute();
//Gets the paramaters and accounts for InteractionContext
var parameters = submethod.GetParameters();
if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext)))
throw new ArgumentException($"The first argument must be an InteractionContext!");
parameters = parameters.Skip(1).ToArray();
var options = await this.ParseParameters(parameters, guildid);
//Creates the subcommand and adds it to the main command
var subpayload = new DiscordApplicationCommandOption(commandAttribute.Name, commandAttribute.Description, ApplicationCommandOptionType.SubCommand, null, null, options);
payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, payload.DefaultPermission);
commandTypeSources.Add(new KeyValuePair(subclassinfo, type));
//Adds it to the method lists
commandmethods.Add(new KeyValuePair(commandAttribute.Name, submethod));
groupCommands.Add(new GroupCommand { Name = groupAttribute.Name, Methods = commandmethods });
}
var command = new SubGroupCommand { Name = groupAttribute.Name };
//Handles subgroups
foreach (var subclass in subclasses)
{
var subGroupAttribute = subclass.GetCustomAttribute();
//I couldn't think of more creative naming
var subsubmethods = subclass.DeclaredMethods.Where(x => x.GetCustomAttribute() != null);
var options = new List();
var currentMethods = new List>();
//Similar to the one for regular groups
foreach (var subsubmethod in subsubmethods)
{
var suboptions = new List();
var commatt = subsubmethod.GetCustomAttribute();
var parameters = subsubmethod.GetParameters();
if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext)))
throw new ArgumentException($"The first argument must be an InteractionContext!");
parameters = parameters.Skip(1).ToArray();
suboptions = suboptions.Concat(await this.ParseParameters(parameters, guildid)).ToList();
var subsubpayload = new DiscordApplicationCommandOption(commatt.Name, commatt.Description, ApplicationCommandOptionType.SubCommand, null, null, suboptions);
options.Add(subsubpayload);
commandmethods.Add(new KeyValuePair(commatt.Name, subsubmethod));
currentMethods.Add(new KeyValuePair(commatt.Name, subsubmethod));
}
//Adds the group to the command and method lists
var subpayload = new DiscordApplicationCommandOption(subGroupAttribute.Name, subGroupAttribute.Description, ApplicationCommandOptionType.SubCommandGroup, null, null, options);
command.SubCommands.Add(new GroupCommand { Name = subGroupAttribute.Name, Methods = currentMethods });
payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, payload.DefaultPermission);
commandTypeSources.Add(new KeyValuePair(subclass, type));
//Accounts for lifespans for the sub group
if (subclass.GetCustomAttribute() != null)
{
if (subclass.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton)
{
_singletonModules.Add(this.CreateInstance(subclass, this._configuration?.ServiceProvider));
}
}
}
if (command.SubCommands.Any()) subGroupCommands.Add(command);
updateList.Add(payload);
//Accounts for lifespans
if (subclassinfo.GetCustomAttribute() != null)
{
if (subclassinfo.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton)
{
_singletonModules.Add(this.CreateInstance(subclassinfo, this._configuration?.ServiceProvider));
}
}
}
//Handles methods and context menus, only if the module isn't a group itself
if (module.GetCustomAttribute() == null)
{
//Slash commands (again, similar to the one for groups)
var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null);
foreach (var method in methods)
{
var commandattribute = method.GetCustomAttribute();
var parameters = method.GetParameters();
if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(InteractionContext)))
throw new ArgumentException($"The first argument must be an InteractionContext!");
parameters = parameters.Skip(1).ToArray();
var options = await this.ParseParameters(parameters, guildid);
commandMethods.Add(new CommandMethod { Method = method, Name = commandattribute.Name });
var payload = new DiscordApplicationCommand(commandattribute.Name, commandattribute.Description, options, commandattribute.DefaultPermission);
updateList.Add(payload);
commandTypeSources.Add(new KeyValuePair(type, type));
}
//Context Menus
var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null);
foreach (var contextMethod in contextMethods)
{
var contextAttribute = contextMethod.GetCustomAttribute();
var command = new DiscordApplicationCommand(contextAttribute.Name, null, type: contextAttribute.Type, default_permission: contextAttribute.DefaultPermission);
var parameters = contextMethod.GetParameters();
if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(ContextMenuContext)))
throw new ArgumentException($"The first argument must be a ContextMenuContext!");
if (parameters.Length > 1)
throw new ArgumentException($"A context menu cannot have parameters!");
contextMenuCommands.Add(new ContextMenuCommand { Method = contextMethod, Name = contextAttribute.Name });
updateList.Add(command);
commandTypeSources.Add(new KeyValuePair(type, type));
}
//Accounts for lifespans
if (module.GetCustomAttribute() != null)
{
if (module.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton)
{
_singletonModules.Add(this.CreateInstance(module, this._configuration?.ServiceProvider));
}
}
}
}
catch (Exception ex)
{
//This isn't really much more descriptive but I added a separate case for it anyway
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 registering application commands");
_errored = true;
}
}
if (!_errored)
{
try
{
async Task UpdateCommandPermission(ulong commandId, string commandName, Type commandDeclaringType, Type commandRootType)
{
if (guildid == null)
{
//throw new NotImplementedException("You can't set global permissions till yet. See https://discord.com/developers/docs/interactions/application-commands#permissions");
}
else
{
var ctx = new ApplicationCommandsPermissionContext(commandDeclaringType, commandName);
var conf = types.First(t => t.Type == commandRootType);
conf.Setup?.Invoke(ctx);
if (ctx.Permissions.Count == 0)
return;
await this.Client.OverwriteGuildApplicationCommandPermissionsAsync(guildid.Value, commandId, ctx.Permissions);
}
}
async Task UpdateCommandPermissionGroup(GroupCommand groupCommand)
{
foreach (var com in groupCommand.Methods)
{
var source = commandTypeSources.FirstOrDefault(f => f.Key == com.Value.DeclaringType);
await UpdateCommandPermission(groupCommand.CommandId, com.Key, source.Key, source.Value);
}
}
var commands = guildid == null
? await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(updateList)
: (IEnumerable)await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guildid.Value, updateList);
//Creates a guild command if a guild id is specified, otherwise global
//Checks against the ids and adds them to the command method lists
foreach (var command in commands)
{
if (commandMethods.Any(x => x.Name == command.Name))
{
var com = commandMethods.First(x => x.Name == command.Name);
com.CommandId = command.Id;
var source = commandTypeSources.FirstOrDefault(f => f.Key == com.Method.DeclaringType);
await UpdateCommandPermission(command.Id, com.Name, source.Value, source.Key);
}
else if (groupCommands.Any(x => x.Name == command.Name))
{
var com = groupCommands.First(x => x.Name == command.Name);
com.CommandId = command.Id;
await UpdateCommandPermissionGroup(com);
}
else if (subGroupCommands.Any(x => x.Name == command.Name))
{
var com = subGroupCommands.First(x => x.Name == command.Name);
com.CommandId = command.Id;
foreach (var groupComs in com.SubCommands)
await UpdateCommandPermissionGroup(groupComs);
}
else if (contextMenuCommands.Any(x => x.Name == command.Name))
{
var com = contextMenuCommands.First(x => x.Name == command.Name);
com.CommandId = command.Id;
var source = commandTypeSources.First(f => f.Key == com.Method.DeclaringType);
await UpdateCommandPermission(command.Id, com.Name, source.Value, source.Key);
}
}
//Adds to the global lists finally
_commandMethods.AddRange(commandMethods);
_groupCommands.AddRange(groupCommands);
_subGroupCommands.AddRange(subGroupCommands);
_contextMenuCommands.AddRange(contextMenuCommands);
_registeredCommands.Add(new KeyValuePair>(guildid, commands.ToList()));
foreach (var command in commandMethods)
{
var app = types.First(t => t.Type == command.Method.DeclaringType);
}
}
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 registering application commands");
_errored = true;
}
}
});
}
///
/// 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 = this._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(),
Type = ApplicationCommandType.ChatInput
};
try
{
if (_errored)
throw new InvalidOperationException("Slash commands failed to register properly on startup.");
var methods = _commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id);
var groups = _groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
var subgroups = _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 (_errored)
throw new InvalidOperationException("Slash commands failed to register properly on startup.");
var methods = _commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id);
var groups = _groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
var subgroups = _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 = this._configuration?.ServiceProvider,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
Channel = e.Interaction.Channel,
User = e.Interaction.User,
Options = e.Interaction.Data.Options.ToList(),
FocusedOption = focusedOption
};
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 = this._configuration?.ServiceProvider,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
Channel = e.Interaction.Channel,
User = e.Interaction.User,
Options = command.Options.ToList(),
FocusedOption = focusedOption
};
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 method = methods.First().Method;
var group = subgroups.First().SubCommands.First(x => x.Name == command.Name);
var focusedOption = command.Options.First(x => x.Name == group.Name).Options.First(o => o.Focused);
this.Client.Logger.LogDebug("SUBGROUP::" + focusedOption.Name + ": " + focusedOption.RawValue);
var option = group.Methods.First(p => p.Value.GetCustomAttribute().Name == focusedOption.Name).Value;
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 = this._configuration?.Services,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
Channel = e.Interaction.Channel,
User = e.Interaction.User,
Options = command.Options.First(x => x.Name == group.Name).Options.ToList(),
FocusedOption = focusedOption
};
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 = this._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
};
try
{
if (_errored)
throw new InvalidOperationException("Context menus failed to register properly on startup.");
//Gets the method for the command
var method = _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(this._configuration?.ServiceProvider.CreateScope().ServiceProvider, method.DeclaringType) : this.CreateInstance(method.DeclaringType, this._configuration?.ServiceProvider.CreateScope().ServiceProvider);
break;
case ApplicationCommandModuleLifespan.Transient:
//Accounts for static methods and adds DI
classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(this._configuration?.ServiceProvider, method.DeclaringType) : this.CreateInstance(method.DeclaringType, this._configuration?.ServiceProvider);
break;
//If singleton, gets it from the singleton list
case ApplicationCommandModuleLifespan.Singleton:
classInstance = _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 copied over from CommandsNext
///
/// The type.
/// The services.
internal 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());
//Checks the type and casts/references resolved and adds the value to the list
//This can probably reference the slash command's type property that didn't exist when I wrote this and it could use a cleaner switch instead, but if it works it works
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(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
throw new ArgumentException("Error resolving mentionable option.");
}
else
throw new ArgumentException($"Error resolving interaction.");
}
}
return args;
}
///
/// Runs the preexecution checks.
///
/// The method info.
/// The basecontext.
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.
///
private 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 ApplicationCommandOptionType GetParameterType(Type type)
{
var parametertype = type == typeof(string)
? ApplicationCommandOptionType.String
: type == typeof(long) || type == typeof(long?)
? 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, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, SnowflakeObject, DiscordAttachment or an Enum.");
return parametertype;
}
///
/// Gets the choice attributes from parameter.
///
/// The choice attributes.
private List GetChoiceAttributesFromParameter(IEnumerable choiceattributes)
{
return !choiceattributes.Any()
? null
: choiceattributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList();
}
///
/// Parses the parameters.
///
/// The parameters.
/// The guild id.
/// A Task.
private async Task> ParseParameters(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 = this.GetParameterType(type);
//Handles choices
//From attributes
var choices = this.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 this.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()
{
_commandMethods.Clear();
_groupCommands.Clear();
_subGroupCommands.Clear();
_registeredCommands.Clear();
_contextMenuCommands.Clear();
await this.Update();
}
///
/// 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 Setup { get; }
///
/// Creates a new command configuration.
///
/// The type of the command module.
/// The permission setup callback.
public ApplicationCommandsModuleConfiguration(Type type, Action setup = null)
{
this.Type = type;
this.Setup = setup;
}
}
///
/// 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 List();
}
///
/// 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; }
}
}
diff --git a/DisCatSharp/Entities/Guild/DiscordAuditLogObjects.cs b/DisCatSharp/Entities/Guild/DiscordAuditLogObjects.cs
index 379095e4f..b4b9eaabc 100644
--- a/DisCatSharp/Entities/Guild/DiscordAuditLogObjects.cs
+++ b/DisCatSharp/Entities/Guild/DiscordAuditLogObjects.cs
@@ -1,1062 +1,1068 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
+using System;
using System.Collections.Generic;
namespace DisCatSharp.Entities
{
///
/// Represents an audit log entry.
///
public abstract class DiscordAuditLogEntry : SnowflakeObject
{
///
/// Gets the entry's action type.
///
public AuditLogActionType ActionType { get; internal set; }
///
/// Gets the user responsible for the action.
///
public DiscordUser UserResponsible { get; internal set; }
///
/// Gets the reason defined in the action.
///
public string Reason { get; internal set; }
///
/// Gets the category under which the action falls.
///
public AuditLogActionCategory ActionCategory { get; internal set; }
}
///
/// Represents a description of how a property changed.
///
/// Type of the changed property.
public sealed class PropertyChange
{
///
/// The property's value before it was changed.
///
public T Before { get; internal set; }
///
/// The property's value after it was changed.
///
public T After { get; internal set; }
}
///
/// Represents a audit log guild entry.
///
public sealed class DiscordAuditLogGuildEntry : DiscordAuditLogEntry
{
///
/// Gets the affected guild.
///
public DiscordGuild Target { get; internal set; }
///
/// Gets the description of guild name's change.
///
public PropertyChange NameChange { get; internal set; }
///
/// Gets the description of owner's change.
///
public PropertyChange OwnerChange { get; internal set; }
///
/// Gets the description of icon's change.
///
public PropertyChange IconChange { get; internal set; }
///
/// Gets the description of verification level's change.
///
public PropertyChange VerificationLevelChange { get; internal set; }
///
/// Gets the description of afk channel's change.
///
public PropertyChange AfkChannelChange { get; internal set; }
///
/// Gets the description of widget channel's change.
///
public PropertyChange EmbedChannelChange { get; internal set; }
///
/// Gets the description of notification settings' change.
///
public PropertyChange NotificationSettingsChange { get; internal set; }
///
/// Gets the description of system message channel's change.
///
public PropertyChange SystemChannelChange { get; internal set; }
///
/// Gets the description of explicit content filter settings' change.
///
public PropertyChange ExplicitContentFilterChange { get; internal set; }
///
/// Gets the description of guild's mfa level change.
///
public PropertyChange MfaLevelChange { get; internal set; }
///
/// Gets the description of invite splash's change.
///
public PropertyChange SplashChange { get; internal set; }
///
/// Gets the description of the guild's region change.
///
public PropertyChange RegionChange { get; internal set; }
///
/// Gets the description of the guild's premium progress bar enabled state.
///
public PropertyChange PremiumProgressBarChange { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogGuildEntry() { }
}
///
/// Represents a audit log channel entry.
///
public sealed class DiscordAuditLogChannelEntry : DiscordAuditLogEntry
{
///
/// Gets the affected channel.
///
public DiscordChannel Target { get; internal set; }
///
/// Gets the description of channel's name change.
///
public PropertyChange NameChange { get; internal set; }
///
/// Gets the description of channel's type change.
///
public PropertyChange TypeChange { get; internal set; }
///
/// Gets the description of channel's nsfw flag change.
///
public PropertyChange NsfwChange { get; internal set; }
///
/// Gets the description of channel's bitrate change.
///
public PropertyChange BitrateChange { get; internal set; }
///
/// Gets the description of channel permission overwrites' change.
///
public PropertyChange> OverwriteChange { get; internal set; }
///
/// Gets the description of channel's topic change.
///
public PropertyChange TopicChange { get; internal set; }
///
/// Gets the description of channel's slow mode timeout change.
///
public PropertyChange PerUserRateLimitChange { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogChannelEntry() { }
}
///
/// Represents a audit log overwrite entry.
///
public sealed class DiscordAuditLogOverwriteEntry : DiscordAuditLogEntry
{
///
/// Gets the affected overwrite.
///
public DiscordOverwrite Target { get; internal set; }
///
/// Gets the channel for which the overwrite was changed.
///
public DiscordChannel Channel { get; internal set; }
///
/// Gets the description of overwrite's allow value change.
///
public PropertyChange AllowChange { get; internal set; }
///
/// Gets the description of overwrite's deny value change.
///
public PropertyChange DenyChange { get; internal set; }
///
/// Gets the description of overwrite's type change.
///
public PropertyChange TypeChange { get; internal set; }
///
/// Gets the description of overwrite's target id change.
///
public PropertyChange TargetIdChange { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogOverwriteEntry() { }
}
///
/// Represents a audit log kick entry.
///
public sealed class DiscordAuditLogKickEntry : DiscordAuditLogEntry
{
///
/// Gets the kicked member.
///
public DiscordMember Target { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogKickEntry() { }
}
///
/// Represents a audit log prune entry.
///
public sealed class DiscordAuditLogPruneEntry : DiscordAuditLogEntry
{
///
/// Gets the number inactivity days after which members were pruned.
///
public int Days { get; internal set; }
///
/// Gets the number of members pruned.
///
public int Toll { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogPruneEntry() { }
}
///
/// Represents a audit log ban entry.
///
public sealed class DiscordAuditLogBanEntry : DiscordAuditLogEntry
{
///
/// Gets the banned member.
///
public DiscordMember Target { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogBanEntry() { }
}
///
/// Represents a audit log member update entry.
///
public sealed class DiscordAuditLogMemberUpdateEntry : DiscordAuditLogEntry
{
///
/// Gets the affected member.
///
public DiscordMember Target { get; internal set; }
///
/// Gets the description of member's nickname change.
///
public PropertyChange NicknameChange { get; internal set; }
///
/// Gets the roles that were removed from the member.
///
public IReadOnlyList RemovedRoles { get; internal set; }
///
/// Gets the roles that were added to the member.
///
public IReadOnlyList AddedRoles { get; internal set; }
///
/// Gets the description of member's mute status change.
///
public PropertyChange MuteChange { get; internal set; }
///
/// Gets the description of member's deaf status change.
///
public PropertyChange DeafenChange { get; internal set; }
+ ///
+ /// Get's the timeout change.
+ ///
+ public PropertyChange CommunicationDisabledUntilChange { get; internal set; }
+
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogMemberUpdateEntry() { }
}
///
/// Represents a audit log role update entry.
///
public sealed class DiscordAuditLogRoleUpdateEntry : DiscordAuditLogEntry
{
///
/// Gets the affected role.
///
public DiscordRole Target { get; internal set; }
///
/// Gets the description of role's name change.
///
public PropertyChange NameChange { get; internal set; }
///
/// Gets the description of role's color change.
///
public PropertyChange ColorChange { get; internal set; }
///
/// Gets the description of role's permission set change.
///
public PropertyChange PermissionChange { get; internal set; }
///
/// Gets the description of the role's position change.
///
public PropertyChange PositionChange { get; internal set; }
///
/// Gets the description of the role's mentionability change.
///
public PropertyChange MentionableChange { get; internal set; }
///
/// Gets the description of the role's hoist status change.
///
public PropertyChange HoistChange { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogRoleUpdateEntry() { }
}
///
/// Represents a audit log invite entry.
///
public sealed class DiscordAuditLogInviteEntry : DiscordAuditLogEntry
{
///
/// Gets the affected invite.
///
public DiscordInvite Target { get; internal set; }
///
/// Gets the description of invite's max age change.
///
public PropertyChange MaxAgeChange { get; internal set; }
///
/// Gets the description of invite's code change.
///
public PropertyChange CodeChange { get; internal set; }
///
/// Gets the description of invite's temporariness change.
///
public PropertyChange TemporaryChange { get; internal set; }
///
/// Gets the description of invite's inviting member change.
///
public PropertyChange InviterChange { get; internal set; }
///
/// Gets the description of invite's target channel change.
///
public PropertyChange ChannelChange { get; internal set; }
///
/// Gets the description of invite's use count change.
///
public PropertyChange UsesChange { get; internal set; }
///
/// Gets the description of invite's max use count change.
///
public PropertyChange MaxUsesChange { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogInviteEntry() { }
}
///
/// Represents a audit log webhook entry.
///
public sealed class DiscordAuditLogWebhookEntry : DiscordAuditLogEntry
{
///
/// Gets the affected webhook.
///
public DiscordWebhook Target { get; internal set; }
///
/// Gets the description of webhook's name change.
///
public PropertyChange NameChange { get; internal set; }
///
/// Gets the description of webhook's target channel change.
///
public PropertyChange ChannelChange { get; internal set; }
///
/// Gets the description of webhook's type change.
///
public PropertyChange TypeChange { get; internal set; }
///
/// Gets the description of webhook's avatar change.
///
public PropertyChange AvatarHashChange { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogWebhookEntry() { }
}
///
/// Represents a audit log emoji entry.
///
public sealed class DiscordAuditLogEmojiEntry : DiscordAuditLogEntry
{
///
/// Gets the affected emoji.
///
public DiscordEmoji Target { get; internal set; }
///
/// Gets the description of emoji's name change.
///
public PropertyChange NameChange { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogEmojiEntry() { }
}
///
/// Represents a audit log sticker entry.
///
public sealed class DiscordAuditLogStickerEntry : DiscordAuditLogEntry
{
///
/// Gets the affected sticker.
///
public DiscordSticker Target { get; internal set; }
///
/// Gets the description of sticker's name change.
///
public PropertyChange NameChange { get; internal set; }
///
/// Gets the description of sticker's description change.
///
public PropertyChange DescriptionChange { get; internal set; }
///
/// Gets the description of sticker's tags change.
///
public PropertyChange TagsChange { get; internal set; }
///
/// Gets the description of sticker's tags change.
///
public PropertyChange AssetChange { get; internal set; }
///
/// Gets the description of sticker's guild id change.
///
public PropertyChange GuildIdChange { get; internal set; }
///
/// Gets the description of sticker's availability change.
///
public PropertyChange AvailabilityChange { get; internal set; }
///
/// Gets the description of sticker's id change.
///
public PropertyChange IdChange { get; internal set; }
///
/// Gets the description of sticker's type change.
///
public PropertyChange TypeChange { get; internal set; }
///
/// Gets the description of sticker's format change.
///
public PropertyChange FormatChange { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogStickerEntry() { }
}
///
/// Represents a audit log message entry.
///
public sealed class DiscordAuditLogMessageEntry : DiscordAuditLogEntry
{
///
/// Gets the affected message. Note that more often than not, this will only have ID specified.
///
public DiscordMessage Target { get; internal set; }
///
/// Gets the channel in which the action occurred.
///
public DiscordChannel Channel { get; internal set; }
///
/// Gets the number of messages that were affected.
///
public int? MessageCount { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogMessageEntry() { }
}
///
/// Represents a audit log message pin entry.
///
public sealed class DiscordAuditLogMessagePinEntry : DiscordAuditLogEntry
{
///
/// Gets the affected message's user.
///
public DiscordUser Target { get; internal set; }
///
/// Gets the channel the message is in.
///
public DiscordChannel Channel { get; internal set; }
///
/// Gets the message the pin action was for.
///
public DiscordMessage Message { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogMessagePinEntry() { }
}
///
/// Represents a audit log bot add entry.
///
public sealed class DiscordAuditLogBotAddEntry : DiscordAuditLogEntry
{
///
/// Gets the bot that has been added to the guild.
///
public DiscordUser TargetBot { get; internal set; }
}
///
/// Represents a audit log member move entry.
///
public sealed class DiscordAuditLogMemberMoveEntry : DiscordAuditLogEntry
{
///
/// Gets the channel the members were moved in.
///
public DiscordChannel Channel { get; internal set; }
///
/// Gets the amount of users that were moved out from the voice channel.
///
public int UserCount { get; internal set; }
}
///
/// Represents a audit log member disconnect entry.
///
public sealed class DiscordAuditLogMemberDisconnectEntry : DiscordAuditLogEntry
{
///
/// Gets the amount of users that were disconnected from the voice channel.
///
public int UserCount { get; internal set; }
}
///
/// Represents a audit log integration entry.
///
public sealed class DiscordAuditLogIntegrationEntry : DiscordAuditLogEntry
{
///
/// Gets the description of emoticons' change.
///
public PropertyChange EnableEmoticons { get; internal set; }
///
/// Gets the description of expire grace period's change.
///
public PropertyChange ExpireGracePeriod { get; internal set; }
///
/// Gets the description of expire behavior change.
///
public PropertyChange ExpireBehavior { get; internal set; }
}
///
/// Represents a audit log stage entry.
///
public sealed class DiscordAuditLogStageEntry : DiscordAuditLogEntry
{
///
/// Gets the affected stage instance
///
public DiscordStageInstance Target { get; internal set; }
///
/// Gets the description of stage instance's topic change.
///
public PropertyChange TopicChange { get; internal set; }
///
/// Gets the description of stage instance's privacy level change.
///
public PropertyChange PrivacyLevelChange { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogStageEntry() { }
}
///
/// Represents a audit log event entry.
///
public sealed class DiscordAuditLogGuildScheduledEventEntry : DiscordAuditLogEntry
{
///
/// Gets the affected thread
///
public DiscordScheduledEvent Target { get; internal set; }
///
/// Gets the channel change.
///
public PropertyChange ChannelIdChange { get; internal set; }
///
/// Gets the description change.
///
public PropertyChange DescriptionChange { get; internal set; }
/* Will be added https://github.com/discord/discord-api-docs/pull/3586#issuecomment-969137241
public PropertyChange<> ScheduledStartTimeChange { get; internal set; }
public PropertyChange<> ScheduledEndTimeChange { get; internal set; }
*/
///
/// Gets the location change.
///
public PropertyChange LocationChange { get; internal set; }
///
/// Gets the privacy level change.
///
public PropertyChange PrivacyLevelChange { get; internal set; }
///
/// Gets the status change.
///
public PropertyChange StatusChange { get; internal set; }
///
/// Gets the entity type change.
///
public PropertyChange EntityTypeChange { get; internal set; }
/*///
/// Gets the sku ids change.
///
public PropertyChange> SkuIdsChange { get; internal set; }*/
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogGuildScheduledEventEntry() { }
}
///
/// Represents a audit log thread entry.
///
public sealed class DiscordAuditLogThreadEntry : DiscordAuditLogEntry
{
///
/// Gets the affected thread
///
public DiscordThreadChannel Target { get; internal set; }
///
/// Gets the name of the thread.
///
public PropertyChange NameChange { get; internal set; }
///
/// Gets the type of the thread.
///
public PropertyChange TypeChange { get; internal set; }
///
/// Gets the archived state of the thread.
///
public PropertyChange ArchivedChange { get; internal set; }
///
/// Gets the locked state of the thread.
///
public PropertyChange LockedChange { get; internal set; }
///
/// Gets the new auto archive duration of the thread.
///
public PropertyChange AutoArchiveDurationChange { get; internal set; }
///
/// Gets the new ratelimit of the thread.
///
public PropertyChange PerUserRateLimitChange { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordAuditLogThreadEntry() { }
}
///
/// Indicates audit log action category.
///
public enum AuditLogActionCategory
{
///
/// Indicates that this action resulted in creation or addition of an object.
///
Create,
///
/// Indicates that this action resulted in update of an object.
///
Update,
///
/// Indicates that this action resulted in deletion or removal of an object.
///
Delete,
///
/// Indicates that this action resulted in something else than creation, addition, update, deleteion, or removal of an object.
///
Other
}
// below is taken from
// https://github.com/Rapptz/discord.py/blob/rewrite/discord/enums.py#L125
///
/// Represents type of the action that was taken in given audit log event.
///
public enum AuditLogActionType : int
{
///
/// Indicates that the guild was updated.
///
GuildUpdate = 1,
///
/// Indicates that the channel was created.
///
ChannelCreate = 10,
///
/// Indicates that the channel was updated.
///
ChannelUpdate = 11,
///
/// Indicates that the channel was deleted.
///
ChannelDelete = 12,
///
/// Indicates that the channel permission overwrite was created.
///
OverwriteCreate = 13,
///
/// Indicates that the channel permission overwrite was updated.
///
OverwriteUpdate = 14,
///
/// Indicates that the channel permission overwrite was deleted.
///
OverwriteDelete = 15,
///
/// Indicates that the user was kicked.
///
Kick = 20,
///
/// Indicates that users were pruned.
///
Prune = 21,
///
/// Indicates that the user was banned.
///
Ban = 22,
///
/// Indicates that the user was unbanned.
///
Unban = 23,
///
/// Indicates that the member was updated.
///
MemberUpdate = 24,
///
/// Indicates that the member's roles were updated.
///
MemberRoleUpdate = 25,
///
/// Indicates that the member has moved to another voice channel.
///
MemberMove = 26,
///
/// Indicates that the member has disconnected from a voice channel.
///
MemberDisconnect = 27,
///
/// Indicates that a bot was added to the guild.
///
BotAdd = 28,
///
/// Indicates that the role was created.
///
RoleCreate = 30,
///
/// Indicates that the role was updated.
///
RoleUpdate = 31,
///
/// Indicates that the role was deleted.
///
RoleDelete = 32,
///
/// Indicates that the invite was created.
///
InviteCreate = 40,
///
/// Indicates that the invite was updated.
///
InviteUpdate = 41,
///
/// Indicates that the invite was deleted.
///
InviteDelete = 42,
///
/// Indicates that the webhook was created.
///
WebhookCreate = 50,
///
/// Indicates that the webook was updated.
///
WebhookUpdate = 51,
///
/// Indicates that the webhook was deleted.
///
WebhookDelete = 52,
///
/// Indicates that an emoji was created.
///
EmojiCreate = 60,
///
/// Indicates that an emoji was updated.
///
EmojiUpdate = 61,
///
/// Indicates that an emoji was deleted.
///
EmojiDelete = 62,
///
/// Indicates that the message was deleted.
///
MessageDelete = 72,
///
/// Indicates that messages were bulk-deleted.
///
MessageBulkDelete = 73,
///
/// Indicates that a message was pinned.
///
MessagePin = 74,
///
/// Indicates that a message was unpinned.
///
MessageUnpin = 75,
///
/// Indicates that an integration was created.
///
IntegrationCreate = 80,
///
/// Indicates that an integration was updated.
///
IntegrationUpdate = 81,
///
/// Indicates that an integration was deleted.
///
IntegrationDelete = 82,
///
/// Indicates that an stage instance was created.
///
StageInstanceCreate = 83,
///
/// Indicates that an stage instance was updated.
///
StageInstanceUpdate = 84,
///
/// Indicates that an stage instance was deleted.
///
StageInstanceDelete = 85,
///
/// Indicates that an sticker was created.
///
StickerCreate = 90,
///
/// Indicates that an sticker was updated.
///
StickerUpdate = 91,
///
/// Indicates that an sticker was deleted.
///
StickerDelete = 92,
///
/// Indicates that an event was created.
///
GuildScheduledEventCreate = 100,
///
/// Indicates that an event was updated.
///
GuildScheduledEventUpdate = 101,
///
/// Indicates that an event was deleted.
///
GuildScheduledEventDelete = 102,
///
/// Indicates that an thread was created.
///
ThreadCreate = 110,
///
/// Indicates that an thread was updated.
///
ThreadUpdate = 111,
///
/// Indicates that an thread was deleted.
///
ThreadDelete = 112,
}
}
diff --git a/DisCatSharp/Entities/Guild/DiscordGuild.cs b/DisCatSharp/Entities/Guild/DiscordGuild.cs
index 168a816e6..16dcf2f6b 100644
--- a/DisCatSharp/Entities/Guild/DiscordGuild.cs
+++ b/DisCatSharp/Entities/Guild/DiscordGuild.cs
@@ -1,3483 +1,3490 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#pragma warning disable CS0618
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Exceptions;
using DisCatSharp.Net;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Models;
using DisCatSharp.Net.Serialization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
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 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; } = 0;
///
/// Gets the guild's AFK voice channel.
///
[JsonIgnore]
public DiscordChannel AfkChannel
=> this.GetChannel(this.AfkChannelId);
///
/// 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._roles);
[JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary _roles;
///
/// Gets a collection of this guild's stickers.
///
[JsonIgnore]
public IReadOnlyDictionary Stickers => new ReadOnlyConcurrentDictionary(this._stickers);
[JsonProperty("stickers", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary _stickers;
///
/// Gets a collection of this guild's emojis.
///
[JsonIgnore]
public IReadOnlyDictionary Emojis => new ReadOnlyConcurrentDictionary(this._emojis);
[JsonProperty("emojis", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary _emojis;
///
/// 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; }
#pragma warning disable CS1734
///
/// Gets the approximate number of members in this guild, when using and having 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 set to true.
///
[JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)]
public int? ApproximatePresenceCount { get; internal set; }
#pragma warning restore CS1734
///
/// 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._voiceStates);
[JsonProperty("voice_states", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary _voiceStates;
///
/// Gets a dictionary of all the members that belong to this guild. The dictionary's key is the member ID.
///
[JsonIgnore] // TODO overhead of => vs Lazy? it's a struct
public IReadOnlyDictionary Members => new ReadOnlyConcurrentDictionary(this._members);
[JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary _members;
///
/// 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._channels);
[JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))]
internal ConcurrentDictionary _channels;
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 _threads = 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 _stageInstances = 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 _scheduledEvents = new();
///
/// Gets the guild member for current user.
///
[JsonIgnore]
public DiscordMember CurrentMember
=> this._current_member_lazy.Value;
[JsonIgnore]
private readonly Lazy _current_member_lazy;
///
/// Gets the @everyone role for this guild.
///
[JsonIgnore]
public DiscordRole EveryoneRole
=> this.GetRole(this.Id);
[JsonIgnore]
internal bool _isOwner;
///
/// Gets whether the current user is the guild's owner.
///
[JsonProperty("owner", NullValueHandling = NullValueHandling.Ignore)]
public bool IsOwner
{
get => this._isOwner || this.OwnerId == this.Discord.CurrentUser.Id;
internal set => this._isOwner = 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 raw_channels = this._channels.Values.ToList();
Dictionary> ordered_channels = new();
ordered_channels.Add(0, new List());
foreach (var channel in raw_channels.Where(c => c.Type == ChannelType.Category).OrderBy(c => c.Position))
{
ordered_channels.Add(channel.Id, new List());
}
foreach (var channel in raw_channels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position))
{
ordered_channels[channel.ParentId.Value].Add(channel);
}
foreach (var channel in raw_channels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position))
{
ordered_channels[channel.ParentId.Value].Add(channel);
}
foreach (var channel in raw_channels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position))
{
ordered_channels[0].Add(channel);
}
foreach (var channel in raw_channels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position))
{
ordered_channels[0].Add(channel);
}
return ordered_channels;
}
///
/// 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 raw_channels = await this.Discord.ApiClient.GetGuildChannelsAsync(this.Id);
Dictionary> ordered_channels = new();
ordered_channels.Add(0, new List());
foreach (var channel in raw_channels.Where(c => c.Type == ChannelType.Category).OrderBy(c => c.Position))
{
ordered_channels.Add(channel.Id, new List());
}
foreach (var channel in raw_channels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position))
{
ordered_channels[channel.ParentId.Value].Add(channel);
}
foreach (var channel in raw_channels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position))
{
ordered_channels[channel.ParentId.Value].Add(channel);
}
foreach (var channel in raw_channels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position))
{
ordered_channels[0].Add(channel);
}
foreach (var channel in raw_channels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position))
{
ordered_channels[0].Add(channel);
}
return ordered_channels;
}
///
/// Whether it is synced.
///
[JsonIgnore]
internal bool IsSynced { get; set; }
///
/// Initializes a new instance of the class.
///
internal DiscordGuild()
{
this._current_member_lazy = new Lazy(() => (this._members != null && this._members.TryGetValue(this.Discord.CurrentUser.Id, out var member)) ? member : null);
this._invites = new ConcurrentDictionary();
this.Threads = new ReadOnlyConcurrentDictionary