diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 56e3a7d18..f4fc24738 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -1,54 +1,55 @@
name: "DisCatSharp Docs"
on:
push:
- branches: [ main ]
+ branches: [ main ]
+ workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
path: DisCatSharp
- uses: actions/checkout@v2
with:
repository: Aiko-IT-Systems/DisCatSharp.Docs
path: DisCatSharp.Docs
token: ${{ secrets.DOCS_TOKEN }}
- name: Get SSH
uses: webfactory/ssh-agent@v0.5.3
with:
ssh-private-key: ${{ secrets.AITSYS_SSH }}
- name: Build Docs
working-directory: ./DisCatSharp
shell: pwsh
run: |
./rebuild-docs.ps1 -DocsPath "./DisCatSharp.Docs" -Output ".." -PackageName "dcs-docs"
- name: Purge old docs
working-directory: ./DisCatSharp.Docs
run: |
shopt -s extglob
rm -rf !(.git|.gitignore)
- name: Extract new docs
run: |
tar -xf dcs-docs.tar.xz -C ./DisCatSharp.Docs
- name: Commit and push changes
uses: EndBug/add-and-commit@master
with:
cwd: ./DisCatSharp.Docs
default_author: github_actions
author_name: DisCatSharp
author_email: discatsharp@aitsys.dev
message: 'Docs update for commit ${{ github.repository }} (${{ github.sha }})'
pull: 'NO-PULL'
- name: Publish to Prod
run: |
ssh -o StrictHostKeyChecking=no -T root@80.153.182.68 -f 'cd /var/www/dcs/docs && git pull -f'
diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsConfiguration.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsConfiguration.cs
index 6ab119fdb..ad6ebe3a9 100644
--- a/DisCatSharp.ApplicationCommands/ApplicationCommandsConfiguration.cs
+++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsConfiguration.cs
@@ -1,46 +1,50 @@
// 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 Microsoft.Extensions.DependencyInjection;
namespace DisCatSharp.ApplicationCommands
{
///
/// A configuration for a
///
public class ApplicationCommandsConfiguration
{
///
/// Sets the service provider.
/// Objects in this provider are used when instantiating application command modules. This allows passing data around without resorting to static members.
/// Defaults to null.
///
public IServiceProvider ServiceProvider { internal get; set; } = new ServiceCollection().BuildServiceProvider(true);
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The service provider.
[ActivatorUtilitiesConstructor]
public ApplicationCommandsConfiguration(IServiceProvider provider)
{
this.ServiceProvider = provider;
}
}
}
diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
index 6110efa55..fd7dfad8c 100644
--- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
+++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
@@ -1,1368 +1,1368 @@
// 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.Reflection;
using System.Threading.Tasks;
using System.Collections.Generic;
using DisCatSharp.Entities;
using System.Linq;
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;
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 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 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.
+ /// 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.ApplicationCommands/Context/BaseContext.cs b/DisCatSharp.ApplicationCommands/Context/BaseContext.cs
index 75363284a..c4aea5d85 100644
--- a/DisCatSharp.ApplicationCommands/Context/BaseContext.cs
+++ b/DisCatSharp.ApplicationCommands/Context/BaseContext.cs
@@ -1,163 +1,170 @@
// 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.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using Microsoft.Extensions.DependencyInjection;
namespace DisCatSharp.ApplicationCommands
{
///
/// Respresents a base context for application command contexts.
///
public class BaseContext
{
///
/// Gets the interaction that was created.
///
public DiscordInteraction Interaction { get; internal set; }
///
/// Gets the client for this interaction.
///
public DiscordClient Client { get; internal set; }
///
/// Gets the guild this interaction was executed in.
///
public DiscordGuild Guild { get; internal set; }
///
/// Gets the channel this interaction was executed in.
///
public DiscordChannel Channel { get; internal set; }
///
/// Gets the user which executed this interaction.
///
public DiscordUser User { get; internal set; }
///
/// Gets the member which executed this interaction, or null if the command is in a DM.
///
public DiscordMember Member
=> this.User is DiscordMember member ? member : null;
///
/// Gets the application command module this interaction was created in.
///
public ApplicationCommandsExtension ApplicationCommandsExtension { get; internal set; }
///
/// Gets the token for this interaction.
///
public string Token { get; internal set; }
///
/// Gets the id for this interaction.
///
public ulong InteractionId { get; internal set; }
///
/// Gets the name of the command.
///
public string CommandName { get; internal set; }
///
/// Gets the type of this interaction.
///
public ApplicationCommandType Type { get; internal set;}
///
/// Gets the service provider.
/// This allows passing data around without resorting to static members.
/// Defaults to null.
///
public IServiceProvider Services { get; internal set; } = new ServiceCollection().BuildServiceProvider(true);
///
/// Creates a response to this interaction.
/// You must create a response within 3 seconds of this interaction being executed; if the command has the potential to take more than 3 seconds, create a at the start, and edit the response later.
///
/// The type of the response.
/// The data to be sent, if any.
///
public Task CreateResponseAsync(InteractionResponseType type, DiscordInteractionResponseBuilder builder = null)
=> this.Interaction.CreateResponseAsync(type, builder);
+ ///
+ /// Creates a modal response to this interaction.
+ ///
+ /// The data to send.
+ public Task CreateModalResponseAsync(DiscordInteractionModalBuilder builder) =>
+ this.Interaction.CreateInteractionModalResponseAsync(builder);
+
///
/// Edits the interaction response.
///
/// The data to edit the response with.
///
public Task EditResponseAsync(DiscordWebhookBuilder builder)
=> this.Interaction.EditOriginalResponseAsync(builder);
///
/// Deletes the interaction response.
///
///
public Task DeleteResponseAsync()
=> this.Interaction.DeleteOriginalResponseAsync();
///
/// Creates a follow up message to the interaction.
///
/// The message to be sent, in the form of a webhook.
/// The created message.
public Task FollowUpAsync(DiscordFollowupMessageBuilder builder)
=> this.Interaction.CreateFollowupMessageAsync(builder);
///
/// Edits a followup message.
///
/// The id of the followup message to edit.
/// The webhook builder.
///
public Task EditFollowupAsync(ulong followupMessageId, DiscordWebhookBuilder builder)
=> this.Interaction.EditFollowupMessageAsync(followupMessageId, builder);
///
/// Deletes a followup message.
///
/// The id of the followup message to delete.
///
public Task DeleteFollowupAsync(ulong followupMessageId)
=> this.Interaction.DeleteFollowupMessageAsync(followupMessageId);
///
/// Gets the followup message.
///
/// The followup message id.
public Task GetFollowupMessageAsync(ulong followupMessageId)
=> this.Interaction.GetFollowupMessageAsync(followupMessageId);
///
/// Gets the original interaction response.
///
/// The original interaction response.
public Task GetOriginalResponseAsync()
=> this.Interaction.GetOriginalResponseAsync();
}
}
diff --git a/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj b/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj
index ca8e6cccd..5b9feb4e3 100644
--- a/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj
+++ b/DisCatSharp.ApplicationCommands/DisCatSharp.ApplicationCommands.csproj
@@ -1,39 +1,40 @@
DisCatSharp.ApplicationCommands
DisCatSharp.ApplicationCommands
Library
netstandard2.0
DisCatSharp.ApplicationCommands
ApplicationCommands for DisCatSharp
discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, slash, slashcommands, contextmenu
LICENSE.md
True
-
+
+
diff --git a/DisCatSharp.ApplicationCommands/ExtensionMethods.cs b/DisCatSharp.ApplicationCommands/ExtensionMethods.cs
index 6c3164b74..fa76989e0 100644
--- a/DisCatSharp.ApplicationCommands/ExtensionMethods.cs
+++ b/DisCatSharp.ApplicationCommands/ExtensionMethods.cs
@@ -1,134 +1,134 @@
// 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.Globalization;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace DisCatSharp.ApplicationCommands
{
///
/// Defines various extension methods for application commands.
///
public static class ExtensionMethods
{
///
/// Enables application commands on this .
///
/// Client to enable application commands for.
/// Configuration to use.
/// Created .
public static ApplicationCommandsExtension UseApplicationCommands(this DiscordClient client,
ApplicationCommandsConfiguration config = null)
{
if (client.GetExtension() != null)
throw new InvalidOperationException("Application commands are already enabled for that client.");
var scomm = new ApplicationCommandsExtension(config);
client.AddExtension(scomm);
return scomm;
}
///
/// Gets the application commands module for this client.
///
/// Client to get application commands for.
/// The module, or null if not activated.
public static ApplicationCommandsExtension GetApplicationCommands(this DiscordClient client)
=> client.GetExtension();
///
/// Enables application commands on this .
///
/// Client to enable application commands on.
/// Configuration to use.
/// A dictionary of created with the key being the shard id.
public static async Task> UseApplicationCommandsAsync(this DiscordShardedClient client, ApplicationCommandsConfiguration config = null)
{
var modules = new Dictionary();
await (Task)client.GetType().GetMethod("InitializeShardsAsync", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(client, null);
foreach (var shard in client.ShardClients.Values)
{
var scomm = shard.GetApplicationCommands();
if (scomm == null)
scomm = shard.UseApplicationCommands(config);
modules[shard.ShardId] = scomm;
}
return modules;
}
///
/// Registers a commands class.
///
/// The command class to register.
/// The modules to register it on.
/// The guild id to register it on. If you want global commands, leave it null.
public static void RegisterCommands(this IReadOnlyDictionary modules, ulong? guildId = null) where T : ApplicationCommandsModule
{
foreach (var module in modules.Values)
module.RegisterCommands(guildId);
}
///
/// Registers a command class.
///
/// The modules to register it on.
- /// The of the command class to register.
+ /// The of the command class to register.
/// The guild id to register it on. If you want global commands, leave it null.
public static void RegisterCommands(this IReadOnlyDictionary modules, Type type, ulong? guildId = null)
{
foreach (var module in modules.Values)
module.RegisterCommands(type, guildId);
}
///
/// Gets the name from the for this enum value.
///
/// The name.
public static string GetName(this T e) where T : IConvertible
{
if (e is Enum)
{
var type = e.GetType();
var values = Enum.GetValues(type);
foreach (int val in values)
{
if (val == e.ToInt32(CultureInfo.InvariantCulture))
{
var memInfo = type.GetMember(type.GetEnumName(val));
return memInfo[0]
.GetCustomAttributes(typeof(ChoiceNameAttribute), false)
.FirstOrDefault() is ChoiceNameAttribute nameAttribute ? nameAttribute.Name : type.GetEnumName(val);
}
}
}
return null;
}
}
}
diff --git a/DisCatSharp.CommandsNext/Attributes/RequireDiscordCertifiedModeratorAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireCertifiedModeratorAttribute.cs
similarity index 91%
rename from DisCatSharp.CommandsNext/Attributes/RequireDiscordCertifiedModeratorAttribute.cs
rename to DisCatSharp.CommandsNext/Attributes/RequireCertifiedModeratorAttribute.cs
index 6e69d25dc..59a306f45 100644
--- a/DisCatSharp.CommandsNext/Attributes/RequireDiscordCertifiedModeratorAttribute.cs
+++ b/DisCatSharp.CommandsNext/Attributes/RequireCertifiedModeratorAttribute.cs
@@ -1,42 +1,42 @@
// 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.Linq;
using System.Threading.Tasks;
namespace DisCatSharp.CommandsNext.Attributes
{
///
/// Defines that usage of this command is restricted to discord certified moderators.
///
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
- public sealed class RequireDiscordCertifiedModeratorAttribute : CheckBaseAttribute
+ public sealed class RequireCertifiedModeratorAttribute : CheckBaseAttribute
{
///
/// Executes the a check.
///
/// The command context.
/// If true, help - returns true.
- public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => ctx.User.Flags.HasValue ? Task.FromResult(ctx.User.Flags.Value.HasFlag(UserFlags.DiscordCertifiedModerator)) : Task.FromResult(false);
+ public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => ctx.User.Flags.HasValue ? Task.FromResult(ctx.User.Flags.Value.HasFlag(UserFlags.CertifiedModerator)) : Task.FromResult(false);
}
}
diff --git a/DisCatSharp.CommandsNext/Attributes/RequireDiscordEmployeeAttribute.cs b/DisCatSharp.CommandsNext/Attributes/RequireStaffAttribute.cs
similarity index 91%
rename from DisCatSharp.CommandsNext/Attributes/RequireDiscordEmployeeAttribute.cs
rename to DisCatSharp.CommandsNext/Attributes/RequireStaffAttribute.cs
index 5cffdfccb..b7bdb4b35 100644
--- a/DisCatSharp.CommandsNext/Attributes/RequireDiscordEmployeeAttribute.cs
+++ b/DisCatSharp.CommandsNext/Attributes/RequireStaffAttribute.cs
@@ -1,42 +1,42 @@
// 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.Linq;
using System.Threading.Tasks;
namespace DisCatSharp.CommandsNext.Attributes
{
///
/// Defines that usage of this command is restricted to discord employees.
///
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
- public sealed class RequireDiscordEmployeeAttribute : CheckBaseAttribute
+ public sealed class RequireStaffAttribute : CheckBaseAttribute
{
///
/// Executes the a check.
///
/// The command context.
/// If true, help - returns true.
- public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => ctx.User.Flags.HasValue ? Task.FromResult(ctx.User.Flags.Value.HasFlag(UserFlags.DiscordEmployee)) : Task.FromResult(false);
+ public override Task ExecuteCheckAsync(CommandContext ctx, bool help) => ctx.User.Flags.HasValue ? Task.FromResult(ctx.User.Flags.Value.HasFlag(UserFlags.Staff)) : Task.FromResult(false);
}
}
diff --git a/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs b/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs
index 1ba977af9..623c545c2 100644
--- a/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs
+++ b/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs
@@ -1,156 +1,160 @@
// 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.Threading.Tasks;
using DisCatSharp.CommandsNext.Attributes;
using DisCatSharp.CommandsNext.Converters;
using DisCatSharp.Entities;
using Microsoft.Extensions.DependencyInjection;
namespace DisCatSharp.CommandsNext
{
///
/// Represents a delegate for a function that takes a message, and returns the position of the start of command invocation in the message. It has to return -1 if prefix is not present.
///
/// It is recommended that helper methods and
/// be used internally for checking. Their output can be passed through.
///
///
/// Message to check for prefix.
/// Position of the command invocation or -1 if not present.
public delegate Task PrefixResolverDelegate(DiscordMessage msg);
///
/// Represents a configuration for .
///
public sealed class CommandsNextConfiguration
{
///
/// Sets the string prefixes used for commands.
/// Defaults to no value (disabled).
///
public IEnumerable StringPrefixes { internal get; set; }
///
/// Sets the custom prefix resolver used for commands.
/// Defaults to none (disabled).
///
public PrefixResolverDelegate PrefixResolver { internal get; set; } = null;
///
/// Sets whether to allow mentioning the bot to be used as command prefix.
/// Defaults to true.
///
public bool EnableMentionPrefix { internal get; set; } = true;
///
/// Sets whether strings should be matched in a case-sensitive manner.
/// This switch affects the behaviour of default prefix resolver, command searching, and argument conversion.
/// Defaults to false.
///
public bool CaseSensitive { internal get; set; } = false;
///
/// Sets whether to enable default help command.
/// Disabling this will allow you to make your own help command.
///
- /// Modifying default help can be achieved via custom help formatters (see and for more details).
+ /// Modifying default help can be achieved via custom help formatters (see and for more details).
/// It is recommended to use help formatter instead of disabling help.
///
/// Defaults to true.
///
public bool EnableDefaultHelp { internal get; set; } = true;
///
/// Controls whether the default help will be sent via DMs or not.
/// Enabling this will make the bot respond with help via direct messages.
/// Defaults to false.
///
public bool DmHelp { internal get; set; } = false;
///
/// Sets the default pre-execution checks for the built-in help command.
/// Only applicable if default help is enabled.
/// Defaults to null.
///
public IEnumerable DefaultHelpChecks { internal get; set; } = null;
///
/// Sets whether commands sent via direct messages should be processed.
/// Defaults to true.
///
public bool EnableDms { internal get; set; } = true;
///
/// Sets the service provider for this CommandsNext instance.
/// Objects in this provider are used when instantiating command modules. This allows passing data around without resorting to static members.
/// Defaults to null.
///
public IServiceProvider ServiceProvider { internal get; set; } = new ServiceCollection().BuildServiceProvider(true);
///
/// Gets whether any extra arguments passed to commands should be ignored or not. If this is set to false, extra arguments will throw, otherwise they will be ignored.
/// Defaults to false.
///
public bool IgnoreExtraArguments { internal get; set; } = false;
///
/// Gets or sets whether to automatically enable handling commands.
/// If this is set to false, you will need to manually handle each incoming message and pass it to CommandsNext.
/// Defaults to true.
///
public bool UseDefaultCommandHandler { internal get; set; } = true;
///
/// Creates a new instance of .
///
public CommandsNextConfiguration() { }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The service provider.
[ActivatorUtilitiesConstructor]
public CommandsNextConfiguration(IServiceProvider provider)
{
this.ServiceProvider = provider;
}
///
/// Creates a new instance of , copying the properties of another configuration.
///
/// Configuration the properties of which are to be copied.
public CommandsNextConfiguration(CommandsNextConfiguration other)
{
this.CaseSensitive = other.CaseSensitive;
this.PrefixResolver = other.PrefixResolver;
this.DefaultHelpChecks = other.DefaultHelpChecks;
this.EnableDefaultHelp = other.EnableDefaultHelp;
this.EnableDms = other.EnableDms;
this.EnableMentionPrefix = other.EnableMentionPrefix;
this.IgnoreExtraArguments = other.IgnoreExtraArguments;
this.UseDefaultCommandHandler = other.UseDefaultCommandHandler;
this.ServiceProvider = other.ServiceProvider;
this.StringPrefixes = other.StringPrefixes?.ToArray();
this.DmHelp = other.DmHelp;
}
}
}
diff --git a/DisCatSharp.CommandsNext/CommandsNextExtension.cs b/DisCatSharp.CommandsNext/CommandsNextExtension.cs
index e8079de3a..dc519b047 100644
--- a/DisCatSharp.CommandsNext/CommandsNextExtension.cs
+++ b/DisCatSharp.CommandsNext/CommandsNextExtension.cs
@@ -1,1083 +1,1083 @@
// 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.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using DisCatSharp.CommandsNext.Attributes;
using DisCatSharp.CommandsNext.Builders;
using DisCatSharp.CommandsNext.Converters;
using DisCatSharp.CommandsNext.Entities;
using DisCatSharp.CommandsNext.Exceptions;
using DisCatSharp.Entities;
using DisCatSharp.EventArgs;
using DisCatSharp.Common.Utilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace DisCatSharp.CommandsNext
{
///
/// This is the class which handles command registration, management, and execution.
///
public class CommandsNextExtension : BaseExtension
{
///
/// Gets the config.
///
private CommandsNextConfiguration Config { get; }
///
/// Gets the help formatter.
///
private HelpFormatterFactory HelpFormatter { get; }
///
/// Gets the convert generic.
///
private MethodInfo ConvertGeneric { get; }
///
/// Gets the user friendly type names.
///
private Dictionary UserFriendlyTypeNames { get; }
///
/// Gets the argument converters.
///
internal Dictionary ArgumentConverters { get; }
///
/// Gets the service provider this CommandsNext module was configured with.
///
public IServiceProvider Services
=> this.Config.ServiceProvider;
///
/// Initializes a new instance of the class.
///
/// The cfg.
internal CommandsNextExtension(CommandsNextConfiguration cfg)
{
this.Config = new CommandsNextConfiguration(cfg);
this.TopLevelCommands = new Dictionary();
this._registeredCommandsLazy = new Lazy>(() => new ReadOnlyDictionary(this.TopLevelCommands));
this.HelpFormatter = new HelpFormatterFactory();
this.HelpFormatter.SetFormatterType();
this.ArgumentConverters = new Dictionary
{
[typeof(string)] = new StringConverter(),
[typeof(bool)] = new BoolConverter(),
[typeof(sbyte)] = new Int8Converter(),
[typeof(byte)] = new Uint8Converter(),
[typeof(short)] = new Int16Converter(),
[typeof(ushort)] = new Uint16Converter(),
[typeof(int)] = new Int32Converter(),
[typeof(uint)] = new Uint32Converter(),
[typeof(long)] = new Int64Converter(),
[typeof(ulong)] = new Uint64Converter(),
[typeof(float)] = new Float32Converter(),
[typeof(double)] = new Float64Converter(),
[typeof(decimal)] = new Float128Converter(),
[typeof(DateTime)] = new DateTimeConverter(),
[typeof(DateTimeOffset)] = new DateTimeOffsetConverter(),
[typeof(TimeSpan)] = new TimeSpanConverter(),
[typeof(Uri)] = new UriConverter(),
[typeof(DiscordUser)] = new DiscordUserConverter(),
[typeof(DiscordMember)] = new DiscordMemberConverter(),
[typeof(DiscordRole)] = new DiscordRoleConverter(),
[typeof(DiscordChannel)] = new DiscordChannelConverter(),
[typeof(DiscordGuild)] = new DiscordGuildConverter(),
[typeof(DiscordMessage)] = new DiscordMessageConverter(),
[typeof(DiscordEmoji)] = new DiscordEmojiConverter(),
[typeof(DiscordThreadChannel)] = new DiscordThreadChannelConverter(),
[typeof(DiscordInvite)] = new DiscordInviteConverter(),
[typeof(DiscordColor)] = new DiscordColorConverter()
};
this.UserFriendlyTypeNames = new Dictionary()
{
[typeof(string)] = "string",
[typeof(bool)] = "boolean",
[typeof(sbyte)] = "signed byte",
[typeof(byte)] = "byte",
[typeof(short)] = "short",
[typeof(ushort)] = "unsigned short",
[typeof(int)] = "int",
[typeof(uint)] = "unsigned int",
[typeof(long)] = "long",
[typeof(ulong)] = "unsigned long",
[typeof(float)] = "float",
[typeof(double)] = "double",
[typeof(decimal)] = "decimal",
[typeof(DateTime)] = "date and time",
[typeof(DateTimeOffset)] = "date and time",
[typeof(TimeSpan)] = "time span",
[typeof(Uri)] = "URL",
[typeof(DiscordUser)] = "user",
[typeof(DiscordMember)] = "member",
[typeof(DiscordRole)] = "role",
[typeof(DiscordChannel)] = "channel",
[typeof(DiscordGuild)] = "guild",
[typeof(DiscordMessage)] = "message",
[typeof(DiscordEmoji)] = "emoji",
[typeof(DiscordThreadChannel)] = "thread",
[typeof(DiscordInvite)] = "invite",
[typeof(DiscordColor)] = "color"
};
var ncvt = typeof(NullableConverter<>);
var nt = typeof(Nullable<>);
var cvts = this.ArgumentConverters.Keys.ToArray();
foreach (var xt in cvts)
{
var xti = xt.GetTypeInfo();
if (!xti.IsValueType)
continue;
var xcvt = ncvt.MakeGenericType(xt);
var xnt = nt.MakeGenericType(xt);
if (this.ArgumentConverters.ContainsKey(xcvt))
continue;
var xcv = Activator.CreateInstance(xcvt) as IArgumentConverter;
this.ArgumentConverters[xnt] = xcv;
this.UserFriendlyTypeNames[xnt] = this.UserFriendlyTypeNames[xt];
}
var t = typeof(CommandsNextExtension);
var ms = t.GetTypeInfo().DeclaredMethods;
var m = ms.FirstOrDefault(xm => xm.Name == "ConvertArgument" && xm.ContainsGenericParameters && !xm.IsStatic && xm.IsPublic);
this.ConvertGeneric = m;
}
///
/// Sets the help formatter to use with the default help command.
///
/// Type of the formatter to use.
public void SetHelpFormatter() where T : BaseHelpFormatter => this.HelpFormatter.SetFormatterType();
#region DiscordClient Registration
///
/// DO NOT USE THIS MANUALLY.
///
/// DO NOT USE THIS MANUALLY.
- ///
+ ///
protected internal override void Setup(DiscordClient client)
{
if (this.Client != null)
throw new InvalidOperationException("What did I tell you?");
this.Client = client;
this._executed = new AsyncEvent("COMMAND_EXECUTED", TimeSpan.Zero, this.Client.EventErrorHandler);
this._error = new AsyncEvent("COMMAND_ERRORED", TimeSpan.Zero, this.Client.EventErrorHandler);
if (this.Config.UseDefaultCommandHandler)
this.Client.MessageCreated += this.HandleCommandsAsync;
else
this.Client.Logger.LogWarning(CommandsNextEvents.Misc, "Not attaching default command handler - if this is intentional, you can ignore this message");
if (this.Config.EnableDefaultHelp)
{
this.RegisterCommands(typeof(DefaultHelpModule), null, null, out var tcmds);
if (this.Config.DefaultHelpChecks != null)
{
var checks = this.Config.DefaultHelpChecks.ToArray();
for (var i = 0; i < tcmds.Count; i++)
tcmds[i].WithExecutionChecks(checks);
}
if (tcmds != null)
foreach (var xc in tcmds)
this.AddToCommandDictionary(xc.Build(null));
}
}
#endregion
#region Command Handling
///
/// Handles the commands async.
///
/// The sender.
/// The e.
/// A Task.
private async Task HandleCommandsAsync(DiscordClient sender, MessageCreateEventArgs e)
{
if (e.Author.IsBot) // bad bot
return;
if (!this.Config.EnableDms && e.Channel.IsPrivate)
return;
var mpos = -1;
if (this.Config.EnableMentionPrefix)
mpos = e.Message.GetMentionPrefixLength(this.Client.CurrentUser);
if (this.Config.StringPrefixes?.Any() == true)
foreach (var pfix in this.Config.StringPrefixes)
if (mpos == -1 && !string.IsNullOrWhiteSpace(pfix))
mpos = e.Message.GetStringPrefixLength(pfix, this.Config.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
if (mpos == -1 && this.Config.PrefixResolver != null)
mpos = await this.Config.PrefixResolver(e.Message).ConfigureAwait(false);
if (mpos == -1)
return;
var pfx = e.Message.Content.Substring(0, mpos);
var cnt = e.Message.Content.Substring(mpos);
var __ = 0;
var fname = cnt.ExtractNextArgument(ref __);
var cmd = this.FindCommand(cnt, out var args);
var ctx = this.CreateContext(e.Message, pfx, cmd, args);
if (cmd == null)
{
await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = new CommandNotFoundException(fname) }).ConfigureAwait(false);
return;
}
_ = Task.Run(async () => await this.ExecuteCommandAsync(ctx).ConfigureAwait(false));
}
///
/// Finds a specified command by its qualified name, then separates arguments.
///
/// Qualified name of the command, optionally with arguments.
/// Separated arguments.
/// Found command or null if none was found.
public Command FindCommand(string commandString, out string rawArguments)
{
rawArguments = null;
var ignoreCase = !this.Config.CaseSensitive;
var pos = 0;
var next = commandString.ExtractNextArgument(ref pos);
if (next == null)
return null;
if (!this.RegisteredCommands.TryGetValue(next, out var cmd))
{
if (!ignoreCase)
return null;
next = next.ToLowerInvariant();
var cmdKvp = this.RegisteredCommands.FirstOrDefault(x => x.Key.ToLowerInvariant() == next);
if (cmdKvp.Value == null)
return null;
cmd = cmdKvp.Value;
}
- if (!(cmd is CommandGroup))
+ if (cmd is not CommandGroup)
{
rawArguments = commandString.Substring(pos).Trim();
return cmd;
}
while (cmd is CommandGroup)
{
var cm2 = cmd as CommandGroup;
var oldPos = pos;
next = commandString.ExtractNextArgument(ref pos);
if (next == null)
break;
if (ignoreCase)
{
next = next.ToLowerInvariant();
cmd = cm2.Children.FirstOrDefault(x => x.Name.ToLowerInvariant() == next || x.Aliases?.Any(xx => xx.ToLowerInvariant() == next) == true);
}
else
{
cmd = cm2.Children.FirstOrDefault(x => x.Name == next || x.Aliases?.Contains(next) == true);
}
if (cmd == null)
{
cmd = cm2;
pos = oldPos;
break;
}
}
rawArguments = commandString.Substring(pos).Trim();
return cmd;
}
///
/// Creates a command execution context from specified arguments.
///
/// Message to use for context.
/// Command prefix, used to execute commands.
/// Command to execute.
/// Raw arguments to pass to command.
/// Created command execution context.
public CommandContext CreateContext(DiscordMessage msg, string prefix, Command cmd, string rawArguments = null)
{
var ctx = new CommandContext
{
Client = this.Client,
Command = cmd,
Message = msg,
Config = this.Config,
RawArgumentString = rawArguments ?? "",
Prefix = prefix,
CommandsNext = this,
Services = this.Services
};
if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null))
{
var scope = ctx.Services.CreateScope();
ctx.ServiceScopeContext = new CommandContext.ServiceContext(ctx.Services, scope);
ctx.Services = scope.ServiceProvider;
}
return ctx;
}
///
/// Executes specified command from given context.
///
/// Context to execute command from.
///
public async Task ExecuteCommandAsync(CommandContext ctx)
{
try
{
var cmd = ctx.Command;
await this.RunAllChecksAsync(cmd, ctx).ConfigureAwait(false);
var res = await cmd.ExecuteAsync(ctx).ConfigureAwait(false);
if (res.IsSuccessful)
await this._executed.InvokeAsync(this, new CommandExecutionEventArgs(this.Client.ServiceProvider) { Context = res.Context }).ConfigureAwait(false);
else
await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = res.Context, Exception = res.Exception }).ConfigureAwait(false);
}
catch (Exception ex)
{
await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = ex }).ConfigureAwait(false);
}
finally
{
if (ctx.ServiceScopeContext.IsInitialized)
ctx.ServiceScopeContext.Dispose();
}
}
///
/// Runs the all checks async.
///
/// The cmd.
/// The ctx.
/// A Task.
private async Task RunAllChecksAsync(Command cmd, CommandContext ctx)
{
if (cmd.Parent != null)
await this.RunAllChecksAsync(cmd.Parent, ctx).ConfigureAwait(false);
var fchecks = await cmd.RunChecksAsync(ctx, false).ConfigureAwait(false);
if (fchecks.Any())
throw new ChecksFailedException(cmd, ctx, fchecks);
}
#endregion
#region Command Registration
///
/// Gets a dictionary of registered top-level commands.
///
public IReadOnlyDictionary RegisteredCommands
=> this._registeredCommandsLazy.Value;
///
/// Gets or sets the top level commands.
///
private Dictionary TopLevelCommands { get; set; }
private readonly Lazy> _registeredCommandsLazy;
///
/// Registers all commands from a given assembly. The command classes need to be public to be considered for registration.
///
/// Assembly to register commands from.
public void RegisterCommands(Assembly assembly)
{
var types = assembly.ExportedTypes.Where(xt =>
{
var xti = xt.GetTypeInfo();
return xti.IsModuleCandidateType() && !xti.IsNested;
});
foreach (var xt in types)
this.RegisterCommands(xt);
}
///
/// Registers all commands from a given command class.
///
/// Class which holds commands to register.
public void RegisterCommands() where T : BaseCommandModule
{
var t = typeof(T);
this.RegisterCommands(t);
}
///
/// Registers all commands from a given command class.
///
/// Type of the class which holds commands to register.
public void RegisterCommands(Type t)
{
if (t == null)
throw new ArgumentNullException(nameof(t), "Type cannot be null.");
if (!t.IsModuleCandidateType())
throw new ArgumentNullException(nameof(t), "Type must be a class, which cannot be abstract or static.");
this.RegisterCommands(t, null, null, out var tempCommands);
if (tempCommands != null)
foreach (var command in tempCommands)
this.AddToCommandDictionary(command.Build(null));
}
///
/// Registers the commands.
///
/// The type.
/// The current parent.
/// The inherited checks.
/// The found commands.
private void RegisterCommands(Type t, CommandGroupBuilder currentParent, IEnumerable inheritedChecks, out List foundCommands)
{
var ti = t.GetTypeInfo();
var lifespan = ti.GetCustomAttribute();
var moduleLifespan = lifespan != null ? lifespan.Lifespan : ModuleLifespan.Singleton;
var module = new CommandModuleBuilder()
.WithType(t)
.WithLifespan(moduleLifespan)
.Build(this.Services);
// restrict parent lifespan to more or equally restrictive
if (currentParent?.Module is TransientCommandModule && moduleLifespan != ModuleLifespan.Transient)
throw new InvalidOperationException("In a transient module, child modules can only be transient.");
// check if we are anything
var groupBuilder = new CommandGroupBuilder(module);
var isModule = false;
var moduleAttributes = ti.GetCustomAttributes();
var moduleHidden = false;
var moduleChecks = new List();
foreach (var xa in moduleAttributes)
{
switch (xa)
{
case GroupAttribute g:
isModule = true;
var moduleName = g.Name;
if (moduleName == null)
{
moduleName = ti.Name;
if (moduleName.EndsWith("Group") && moduleName != "Group")
moduleName = moduleName.Substring(0, moduleName.Length - 5);
else if (moduleName.EndsWith("Module") && moduleName != "Module")
moduleName = moduleName.Substring(0, moduleName.Length - 6);
else if (moduleName.EndsWith("Commands") && moduleName != "Commands")
moduleName = moduleName.Substring(0, moduleName.Length - 8);
}
if (!this.Config.CaseSensitive)
moduleName = moduleName.ToLowerInvariant();
groupBuilder.WithName(moduleName);
if (inheritedChecks != null)
foreach (var chk in inheritedChecks)
groupBuilder.WithExecutionCheck(chk);
foreach (var mi in ti.DeclaredMethods.Where(x => x.IsCommandCandidate(out _) && x.GetCustomAttribute() != null))
groupBuilder.WithOverload(new CommandOverloadBuilder(mi));
break;
case AliasesAttribute a:
foreach (var xalias in a.Aliases)
groupBuilder.WithAlias(this.Config.CaseSensitive ? xalias : xalias.ToLowerInvariant());
break;
case HiddenAttribute h:
groupBuilder.WithHiddenStatus(true);
moduleHidden = true;
break;
case DescriptionAttribute d:
groupBuilder.WithDescription(d.Description);
break;
case CheckBaseAttribute c:
moduleChecks.Add(c);
groupBuilder.WithExecutionCheck(c);
break;
default:
groupBuilder.WithCustomAttribute(xa);
break;
}
}
if (!isModule)
{
groupBuilder = null;
if (inheritedChecks != null)
moduleChecks.AddRange(inheritedChecks);
}
// candidate methods
var methods = ti.DeclaredMethods;
var commands = new List();
var commandBuilders = new Dictionary();
foreach (var m in methods)
{
if (!m.IsCommandCandidate(out _))
continue;
var attrs = m.GetCustomAttributes();
if (attrs.FirstOrDefault(xa => xa is CommandAttribute) is not CommandAttribute cattr)
continue;
var commandName = cattr.Name;
if (commandName == null)
{
commandName = m.Name;
if (commandName.EndsWith("Async") && commandName != "Async")
commandName = commandName.Substring(0, commandName.Length - 5);
}
if (!this.Config.CaseSensitive)
commandName = commandName.ToLowerInvariant();
if (!commandBuilders.TryGetValue(commandName, out var commandBuilder))
{
commandBuilders.Add(commandName, commandBuilder = new CommandBuilder(module).WithName(commandName));
if (!isModule)
if (currentParent != null)
currentParent.WithChild(commandBuilder);
else
commands.Add(commandBuilder);
else
groupBuilder.WithChild(commandBuilder);
}
commandBuilder.WithOverload(new CommandOverloadBuilder(m));
if (!isModule && moduleChecks.Any())
foreach (var chk in moduleChecks)
commandBuilder.WithExecutionCheck(chk);
foreach (var xa in attrs)
{
switch (xa)
{
case AliasesAttribute a:
foreach (var xalias in a.Aliases)
commandBuilder.WithAlias(this.Config.CaseSensitive ? xalias : xalias.ToLowerInvariant());
break;
case CheckBaseAttribute p:
commandBuilder.WithExecutionCheck(p);
break;
case DescriptionAttribute d:
commandBuilder.WithDescription(d.Description);
break;
case HiddenAttribute h:
commandBuilder.WithHiddenStatus(true);
break;
default:
commandBuilder.WithCustomAttribute(xa);
break;
}
}
if (!isModule && moduleHidden)
commandBuilder.WithHiddenStatus(true);
}
// candidate types
var types = ti.DeclaredNestedTypes
.Where(xt => xt.IsModuleCandidateType() && xt.DeclaredConstructors.Any(xc => xc.IsPublic));
foreach (var type in types)
{
this.RegisterCommands(type.AsType(),
groupBuilder,
!isModule ? moduleChecks : null,
out var tempCommands);
if (isModule)
foreach (var chk in moduleChecks)
groupBuilder.WithExecutionCheck(chk);
if (isModule && tempCommands != null)
foreach (var xtcmd in tempCommands)
groupBuilder.WithChild(xtcmd);
else if (tempCommands != null)
commands.AddRange(tempCommands);
}
if (isModule && currentParent == null)
commands.Add(groupBuilder);
else if (isModule)
currentParent.WithChild(groupBuilder);
foundCommands = commands;
}
///
/// Builds and registers all supplied commands.
///
/// Commands to build and register.
public void RegisterCommands(params CommandBuilder[] cmds)
{
foreach (var cmd in cmds)
this.AddToCommandDictionary(cmd.Build(null));
}
///
/// Unregisters specified commands from CommandsNext.
///
/// Commands to unregister.
public void UnregisterCommands(params Command[] cmds)
{
if (cmds.Any(x => x.Parent != null))
throw new InvalidOperationException("Cannot unregister nested commands.");
var keys = this.RegisteredCommands.Where(x => cmds.Contains(x.Value)).Select(x => x.Key).ToList();
foreach (var key in keys)
this.TopLevelCommands.Remove(key);
}
///
/// Adds the to command dictionary.
///
/// The cmd.
private void AddToCommandDictionary(Command cmd)
{
if (cmd.Parent != null)
return;
if (this.TopLevelCommands.ContainsKey(cmd.Name) || (cmd.Aliases != null && cmd.Aliases.Any(xs => this.TopLevelCommands.ContainsKey(xs))))
throw new DuplicateCommandException(cmd.QualifiedName);
this.TopLevelCommands[cmd.Name] = cmd;
if (cmd.Aliases != null)
foreach (var xs in cmd.Aliases)
this.TopLevelCommands[xs] = cmd;
}
#endregion
#region Default Help
///
/// Represents the default help module.
///
[ModuleLifespan(ModuleLifespan.Transient)]
public class DefaultHelpModule : BaseCommandModule
{
///
/// Defaults the help async.
///
/// The ctx.
/// The command.
/// A Task.
[Command("help"), Description("Displays command help.")]
public async Task DefaultHelpAsync(CommandContext ctx, [Description("Command to provide help for.")] params string[] command)
{
var topLevel = ctx.CommandsNext.TopLevelCommands.Values.Distinct();
var helpBuilder = ctx.CommandsNext.HelpFormatter.Create(ctx);
if (command != null && command.Any())
{
Command cmd = null;
var searchIn = topLevel;
foreach (var c in command)
{
if (searchIn == null)
{
cmd = null;
break;
}
cmd = ctx.Config.CaseSensitive
? searchIn.FirstOrDefault(xc => xc.Name == c || (xc.Aliases != null && xc.Aliases.Contains(c)))
: searchIn.FirstOrDefault(xc => xc.Name.ToLowerInvariant() == c.ToLowerInvariant() || (xc.Aliases != null && xc.Aliases.Select(xs => xs.ToLowerInvariant()).Contains(c.ToLowerInvariant())));
if (cmd == null)
break;
var failedChecks = await cmd.RunChecksAsync(ctx, true).ConfigureAwait(false);
if (failedChecks.Any())
throw new ChecksFailedException(cmd, ctx, failedChecks);
searchIn = cmd is CommandGroup ? (cmd as CommandGroup).Children : null;
}
if (cmd == null)
throw new CommandNotFoundException(string.Join(" ", command));
helpBuilder.WithCommand(cmd);
if (cmd is CommandGroup group)
{
var commandsToSearch = group.Children.Where(xc => !xc.IsHidden);
var eligibleCommands = new List();
foreach (var candidateCommand in commandsToSearch)
{
if (candidateCommand.ExecutionChecks == null || !candidateCommand.ExecutionChecks.Any())
{
eligibleCommands.Add(candidateCommand);
continue;
}
var candidateFailedChecks = await candidateCommand.RunChecksAsync(ctx, true).ConfigureAwait(false);
if (!candidateFailedChecks.Any())
eligibleCommands.Add(candidateCommand);
}
if (eligibleCommands.Any())
helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name));
}
}
else
{
var commandsToSearch = topLevel.Where(xc => !xc.IsHidden);
var eligibleCommands = new List();
foreach (var sc in commandsToSearch)
{
if (sc.ExecutionChecks == null || !sc.ExecutionChecks.Any())
{
eligibleCommands.Add(sc);
continue;
}
var candidateFailedChecks = await sc.RunChecksAsync(ctx, true).ConfigureAwait(false);
if (!candidateFailedChecks.Any())
eligibleCommands.Add(sc);
}
if (eligibleCommands.Any())
helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name));
}
var helpMessage = helpBuilder.Build();
var builder = new DiscordMessageBuilder().WithContent(helpMessage.Content).WithEmbed(helpMessage.Embed);
if (!ctx.Config.DmHelp || ctx.Channel is DiscordDmChannel || ctx.Guild == null)
await ctx.RespondAsync(builder).ConfigureAwait(false);
else
await ctx.Member.SendMessageAsync(builder).ConfigureAwait(false);
}
}
#endregion
#region Sudo
///
/// Creates a fake command context to execute commands with.
///
/// The user or member to use as message author.
/// The channel the message is supposed to appear from.
/// Contents of the message.
/// Command prefix, used to execute commands.
/// Command to execute.
/// Raw arguments to pass to command.
/// Created fake context.
public CommandContext CreateFakeContext(DiscordUser actor, DiscordChannel channel, string messageContents, string prefix, Command cmd, string rawArguments = null)
{
var epoch = new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero);
var now = DateTimeOffset.UtcNow;
var timeSpan = (ulong)(now - epoch).TotalMilliseconds;
// create fake message
var msg = new DiscordMessage
{
Discord = this.Client,
Author = actor,
ChannelId = channel.Id,
Content = messageContents,
Id = timeSpan << 22,
Pinned = false,
MentionEveryone = messageContents.Contains("@everyone"),
IsTTS = false,
_attachments = new List(),
_embeds = new List(),
TimestampRaw = now.ToString("yyyy-MM-ddTHH:mm:sszzz"),
_reactions = new List()
};
var mentionedUsers = new List();
var mentionedRoles = msg.Channel.Guild != null ? new List() : null;
var mentionedChannels = msg.Channel.Guild != null ? new List() : null;
if (!string.IsNullOrWhiteSpace(msg.Content))
{
if (msg.Channel.Guild != null)
{
mentionedUsers = Utilities.GetUserMentions(msg).Select(xid => msg.Channel.Guild._members.TryGetValue(xid, out var member) ? member : null).Cast().ToList();
mentionedRoles = Utilities.GetRoleMentions(msg).Select(xid => msg.Channel.Guild.GetRole(xid)).ToList();
mentionedChannels = Utilities.GetChannelMentions(msg).Select(xid => msg.Channel.Guild.GetChannel(xid)).ToList();
}
else
{
mentionedUsers = Utilities.GetUserMentions(msg).Select(this.Client.GetCachedOrEmptyUserInternal).ToList();
}
}
msg._mentionedUsers = mentionedUsers;
msg._mentionedRoles = mentionedRoles;
msg._mentionedChannels = mentionedChannels;
var ctx = new CommandContext
{
Client = this.Client,
Command = cmd,
Message = msg,
Config = this.Config,
RawArgumentString = rawArguments ?? "",
Prefix = prefix,
CommandsNext = this,
Services = this.Services
};
if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null))
{
var scope = ctx.Services.CreateScope();
ctx.ServiceScopeContext = new CommandContext.ServiceContext(ctx.Services, scope);
ctx.Services = scope.ServiceProvider;
}
return ctx;
}
#endregion
#region Type Conversion
///
/// Converts a string to specified type.
///
/// Type to convert to.
/// Value to convert.
/// Context in which to convert to.
/// Converted object.
#pragma warning disable IDE1006 // Naming Styles
public async Task ConvertArgument(string value, CommandContext ctx)
#pragma warning restore IDE1006 // Naming Styles
{
var t = typeof(T);
if (!this.ArgumentConverters.ContainsKey(t))
throw new ArgumentException("There is no converter specified for given type.", nameof(T));
if (this.ArgumentConverters[t] is not IArgumentConverter cv)
throw new ArgumentException("Invalid converter registered for this type.", nameof(T));
var cvr = await cv.ConvertAsync(value, ctx).ConfigureAwait(false);
return !cvr.HasValue ? throw new ArgumentException("Could not convert specified value to given type.", nameof(value)) : cvr.Value;
}
///
/// Converts a string to specified type.
///
/// Value to convert.
/// Context in which to convert to.
/// Type to convert to.
/// Converted object.
#pragma warning disable IDE1006 // Naming Styles
public async Task ConvertArgument(string value, CommandContext ctx, Type type)
#pragma warning restore IDE1006 // Naming Styles
{
var m = this.ConvertGeneric.MakeGenericMethod(type);
try
{
return await (m.Invoke(this, new object[] { value, ctx }) as Task).ConfigureAwait(false);
}
catch (TargetInvocationException ex)
{
throw ex.InnerException;
}
}
///
/// Registers an argument converter for specified type.
///
/// Type for which to register the converter.
/// Converter to register.
public void RegisterConverter(IArgumentConverter converter)
{
if (converter == null)
throw new ArgumentNullException(nameof(converter), "Converter cannot be null.");
var t = typeof(T);
var ti = t.GetTypeInfo();
this.ArgumentConverters[t] = converter;
if (!ti.IsValueType)
return;
var nullableConverterType = typeof(NullableConverter<>).MakeGenericType(t);
var nullableType = typeof(Nullable<>).MakeGenericType(t);
if (this.ArgumentConverters.ContainsKey(nullableType))
return;
var nullableConverter = Activator.CreateInstance(nullableConverterType) as IArgumentConverter;
this.ArgumentConverters[nullableType] = nullableConverter;
}
///
/// Unregisters an argument converter for specified type.
///
/// Type for which to unregister the converter.
public void UnregisterConverter()
{
var t = typeof(T);
var ti = t.GetTypeInfo();
if (this.ArgumentConverters.ContainsKey(t))
this.ArgumentConverters.Remove(t);
if (this.UserFriendlyTypeNames.ContainsKey(t))
this.UserFriendlyTypeNames.Remove(t);
if (!ti.IsValueType)
return;
var nullableType = typeof(Nullable<>).MakeGenericType(t);
if (!this.ArgumentConverters.ContainsKey(nullableType))
return;
this.ArgumentConverters.Remove(nullableType);
this.UserFriendlyTypeNames.Remove(nullableType);
}
///
/// Registers a user-friendly type name.
///
/// Type to register the name for.
/// Name to register.
public void RegisterUserFriendlyTypeName(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentNullException(nameof(value), "Name cannot be null or empty.");
var t = typeof(T);
var ti = t.GetTypeInfo();
if (!this.ArgumentConverters.ContainsKey(t))
throw new InvalidOperationException("Cannot register a friendly name for a type which has no associated converter.");
this.UserFriendlyTypeNames[t] = value;
if (!ti.IsValueType)
return;
var nullableType = typeof(Nullable<>).MakeGenericType(t);
this.UserFriendlyTypeNames[nullableType] = value;
}
///
/// Converts a type into user-friendly type name.
///
/// Type to convert.
/// User-friendly type name.
public string GetUserFriendlyTypeName(Type t)
{
if (this.UserFriendlyTypeNames.ContainsKey(t))
return this.UserFriendlyTypeNames[t];
var ti = t.GetTypeInfo();
if (ti.IsGenericTypeDefinition && t.GetGenericTypeDefinition() == typeof(Nullable<>))
{
var tn = ti.GenericTypeArguments[0];
return this.UserFriendlyTypeNames.ContainsKey(tn) ? this.UserFriendlyTypeNames[tn] : tn.Name;
}
return t.Name;
}
#endregion
#region Helpers
///
/// Gets the configuration-specific string comparer. This returns or ,
/// depending on whether is set to or .
///
/// A string comparer.
internal IEqualityComparer GetStringComparer()
=> this.Config.CaseSensitive
? StringComparer.Ordinal
: StringComparer.OrdinalIgnoreCase;
#endregion
#region Events
///
/// Triggered whenever a command executes successfully.
///
public event AsyncEventHandler CommandExecuted
{
add { this._executed.Register(value); }
remove { this._executed.Unregister(value); }
}
private AsyncEvent _executed;
///
/// Triggered whenever a command throws an exception during execution.
///
public event AsyncEventHandler CommandErrored
{
add { this._error.Register(value); }
remove { this._error.Unregister(value); }
}
private AsyncEvent _error;
///
/// Ons the command executed.
///
/// The e.
/// A Task.
private Task OnCommandExecuted(CommandExecutionEventArgs e)
=> this._executed.InvokeAsync(this, e);
///
/// Ons the command errored.
///
/// The e.
/// A Task.
private Task OnCommandErrored(CommandErrorEventArgs e)
=> this._error.InvokeAsync(this, e);
#endregion
}
}
diff --git a/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj b/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj
index 0e0eae41e..cc520bb74 100644
--- a/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj
+++ b/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj
@@ -1,39 +1,43 @@
DisCatSharp.CommandsNext
DisCatSharp.CommandsNext
Library
netstandard2.0
DisCatSharp.CommandsNext
CommandNext extension for DisCatSharp.
discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, commands, commandsnext
LICENSE.md
-
+
True
+
+
+
+
diff --git a/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs b/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs
index 9c8cc3159..b3f04c0c4 100644
--- a/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs
+++ b/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs
@@ -1,133 +1,133 @@
// 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.Globalization;
namespace DisCatSharp.Common.Serialization
{
///
- /// Defines the format for string-serialized and objects.
+ /// Defines the format for string-serialized and objects.
///
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class DateTimeFormatAttribute : SerializationAttribute
{
///
/// Gets the ISO 8601 format string of "yyyy-MM-ddTHH:mm:ss.fffzzz".
///
public const string FormatISO8601 = "yyyy-MM-ddTHH:mm:ss.fffzzz";
///
/// Gets the RFC 1123 format string of "R".
///
public const string FormatRFC1123 = "R";
///
/// Gets the general long format.
///
public const string FormatLong = "G";
///
/// Gets the general short format.
///
public const string FormatShort = "g";
///
/// Gets the custom datetime format string to use.
///
public string Format { get; }
///
/// Gets the predefined datetime format kind.
///
public DateTimeFormatKind Kind { get; }
///
/// Specifies a predefined format to use.
///
/// Predefined format kind to use.
public DateTimeFormatAttribute(DateTimeFormatKind kind)
{
if (kind < 0 || kind > DateTimeFormatKind.InvariantLocaleShort)
throw new ArgumentOutOfRangeException(nameof(kind), "Specified format kind is not legal or supported.");
this.Kind = kind;
this.Format = null;
}
///
/// Specifies a custom format to use.
/// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings for more details.
///
/// Custom format string to use.
public DateTimeFormatAttribute(string format)
{
if (string.IsNullOrWhiteSpace(format))
throw new ArgumentNullException(nameof(format), "Specified format cannot be null or empty.");
this.Kind = DateTimeFormatKind.Custom;
this.Format = format;
}
}
///
- /// Defines which built-in format to use for for and serialization.
+ /// Defines which built-in format to use for for and serialization.
/// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings and https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings for more details.
///
public enum DateTimeFormatKind : int
{
///
/// Specifies ISO 8601 format, which is equivalent to .NET format string of "yyyy-MM-ddTHH:mm:ss.fffzzz".
///
ISO8601 = 0,
///
/// Specifies RFC 1123 format, which is equivalent to .NET format string of "R".
///
RFC1123 = 1,
///
- /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons.
+ /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons.
///
CurrentLocaleLong = 2,
///
- /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons.
+ /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons.
///
CurrentLocaleShort = 3,
///
- /// Specifies a format defined by , with a format string of "G".
+ /// Specifies a format defined by , with a format string of "G".
///
InvariantLocaleLong = 4,
///
- /// Specifies a format defined by , with a format string of "g".
+ /// Specifies a format defined by , with a format string of "g".
///
InvariantLocaleShort = 5,
///
/// Specifies a custom format. This value is not usable directly.
///
Custom = int.MaxValue
}
}
diff --git a/DisCatSharp.Common/Attributes/TimeSpanAttributes.cs b/DisCatSharp.Common/Attributes/TimeSpanAttributes.cs
index cacfe8d67..c97653667 100644
--- a/DisCatSharp.Common/Attributes/TimeSpanAttributes.cs
+++ b/DisCatSharp.Common/Attributes/TimeSpanAttributes.cs
@@ -1,42 +1,42 @@
// 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;
namespace DisCatSharp.Common.Serialization
{
///
- /// Specifies that this will be serialized as a number of whole seconds.
+ /// Specifies that this will be serialized as a number of whole seconds.
/// This value will always be serialized as a number.
///
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class TimeSpanSecondsAttribute : SerializationAttribute
{ }
///
- /// Specifies that this will be serialized as a number of whole milliseconds.
+ /// Specifies that this will be serialized as a number of whole milliseconds.
/// This value will always be serialized as a number.
///
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class TimeSpanMillisecondsAttribute : SerializationAttribute
{ }
}
diff --git a/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs b/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs
index e0df68803..78eeb6fc5 100644
--- a/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs
+++ b/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs
@@ -1,133 +1,133 @@
// 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.Globalization;
namespace DisCatSharp.Common.Serialization
{
///
- /// Defines the format for string-serialized objects.
+ /// Defines the format for string-serialized objects.
///
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class TimeSpanFormatAttribute : SerializationAttribute
{
///
/// Gets the ISO 8601 format string of @"ddThh\:mm\:ss\.fff".
///
public const string FormatISO8601 = @"ddThh\:mm\:ss\.fff";
///
/// Gets the constant format.
///
public const string FormatConstant = "c";
///
/// Gets the general long format.
///
public const string FormatLong = "G";
///
/// Gets the general short format.
///
public const string FormatShort = "g";
///
/// Gets the custom datetime format string to use.
///
public string Format { get; }
///
/// Gets the predefined datetime format kind.
///
public TimeSpanFormatKind Kind { get; }
///
/// Specifies a predefined format to use.
///
/// Predefined format kind to use.
public TimeSpanFormatAttribute(TimeSpanFormatKind kind)
{
if (kind < 0 || kind > TimeSpanFormatKind.InvariantLocaleShort)
throw new ArgumentOutOfRangeException(nameof(kind), "Specified format kind is not legal or supported.");
this.Kind = kind;
this.Format = null;
}
///
/// Specifies a custom format to use.
/// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-timespan-format-strings for more details.
///
/// Custom format string to use.
public TimeSpanFormatAttribute(string format)
{
if (string.IsNullOrWhiteSpace(format))
throw new ArgumentNullException(nameof(format), "Specified format cannot be null or empty.");
this.Kind = TimeSpanFormatKind.Custom;
this.Format = format;
}
}
///
- /// Defines which built-in format to use for serialization.
+ /// Defines which built-in format to use for serialization.
/// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings and https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-timespan-format-strings for more details.
///
public enum TimeSpanFormatKind : int
{
///
/// Specifies ISO 8601-like time format, which is equivalent to .NET format string of @"ddThh\:mm\:ss\.fff".
///
ISO8601 = 0,
///
- /// Specifies a format defined by , with a format string of "c".
+ /// Specifies a format defined by , with a format string of "c".
///
InvariantConstant = 1,
///
- /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons.
+ /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons.
///
CurrentLocaleLong = 2,
///
- /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons.
+ /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons.
///
CurrentLocaleShort = 3,
///
- /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons.
+ /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons.
///
InvariantLocaleLong = 4,
///
- /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons.
+ /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons.
///
InvariantLocaleShort = 5,
///
/// Specifies a custom format. This value is not usable directly.
///
Custom = int.MaxValue
}
}
diff --git a/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs b/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs
index 758145715..aadb6bff6 100644
--- a/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs
+++ b/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs
@@ -1,42 +1,42 @@
-// This file is part of the DisCatSharp project.
+// 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;
namespace DisCatSharp.Common.Serialization
{
///
- /// Specifies that this or will be serialized as Unix timestamp seconds.
+ /// Specifies that this or will be serialized as Unix timestamp seconds.
/// This value will always be serialized as a number.
///
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class UnixSecondsAttribute : SerializationAttribute
{ }
///
- /// Specifies that this or will be serialized as Unix timestamp milliseconds.
+ /// Specifies that this or will be serialized as Unix timestamp milliseconds.
/// This value will always be serialized as a number.
///
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class UnixMillisecondsAttribute : SerializationAttribute
{ }
}
diff --git a/DisCatSharp.Common/DisCatSharp.Common.csproj b/DisCatSharp.Common/DisCatSharp.Common.csproj
index 7e175c5e1..2ba59dad9 100644
--- a/DisCatSharp.Common/DisCatSharp.Common.csproj
+++ b/DisCatSharp.Common/DisCatSharp.Common.csproj
@@ -1,45 +1,46 @@
DisCatSharp.Common
DisCatSharp.Common
9.0
True
True
True
Portable
Library
netstandard2.0
DisCatSharp.Common
Assortment of various common types and utilities for DisCatSharp's projects.
common utilities dotnet dotnet-core dotnetfx netfx netcore csharp
LICENSE.MD
False
-
+
+
-
+
True
diff --git a/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs b/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs
index a38c211d8..40cc56b0a 100644
--- a/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs
+++ b/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs
@@ -1,830 +1,830 @@
// 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;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace DisCatSharp.Common
{
///
- /// Represents collection of string keys and values, allowing the use of for dictionary operations.
+ /// Represents collection of string keys and values, allowing the use of for dictionary operations.
///
/// Type of items in this dictionary.
public sealed class CharSpanLookupDictionary :
IDictionary,
IReadOnlyDictionary,
IDictionary
{
///
/// Gets the collection of all keys present in this dictionary.
///
public IEnumerable Keys => this.GetKeysInternal();
///
/// Gets the keys.
///
ICollection IDictionary.Keys => this.GetKeysInternal();
///
/// Gets the keys.
///
ICollection IDictionary.Keys => this.GetKeysInternal();
///
/// Gets the collection of all values present in this dictionary.
///
public IEnumerable Values => this.GetValuesInternal();
///
/// Gets the values.
///
ICollection IDictionary.Values => this.GetValuesInternal();
///
/// Gets the values.
///
ICollection IDictionary.Values => this.GetValuesInternal();
///
/// Gets the total number of items in this dictionary.
///
public int Count { get; private set; } = 0;
///
/// Gets whether this dictionary is read-only.
///
public bool IsReadOnly => false;
///
/// Gets whether this dictionary has a fixed size.
///
public bool IsFixedSize => false;
///
/// Gets whether this dictionary is considered thread-safe.
///
public bool IsSynchronized => false;
///
/// Gets the object which allows synchronizing access to this dictionary.
///
public object SyncRoot { get; } = new object();
///
/// Gets or sets a value corresponding to given key in this dictionary.
///
/// Key to get or set the value for.
/// Value matching the supplied key, if applicable.
public TValue this[string key]
{
get
{
if (key == null)
throw new ArgumentNullException(nameof(key));
if (!this.TryRetrieveInternal(key.AsSpan(), out var value))
throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary.");
return value;
}
set
{
if (key == null)
throw new ArgumentNullException(nameof(key));
this.TryInsertInternal(key, value, true);
}
}
///
/// Gets or sets a value corresponding to given key in this dictionary.
///
/// Key to get or set the value for.
/// Value matching the supplied key, if applicable.
public TValue this[ReadOnlySpan key]
{
get
{
if (!this.TryRetrieveInternal(key, out var value))
throw new KeyNotFoundException($"The given key was not present in the dictionary.");
return value;
}
#if NETCOREAPP
set => this.TryInsertInternal(new string(key), value, true);
#else
set
{
unsafe
{
fixed (char* chars = &key.GetPinnableReference())
this.TryInsertInternal(new string(chars, 0, key.Length), value, true);
}
}
#endif
}
object IDictionary.this[object key]
{
get
{
if (!(key is string tkey))
throw new ArgumentException("Key needs to be an instance of a string.");
if (!this.TryRetrieveInternal(tkey.AsSpan(), out var value))
throw new KeyNotFoundException($"The given key '{tkey}' was not present in the dictionary.");
return value;
}
set
{
if (!(key is string tkey))
throw new ArgumentException("Key needs to be an instance of a string.");
if (!(value is TValue tvalue))
{
tvalue = default;
if (tvalue != null)
throw new ArgumentException($"Value needs to be an instance of {typeof(TValue)}.");
}
this.TryInsertInternal(tkey, tvalue, true);
}
}
///
/// Gets the internal buckets.
///
private Dictionary InternalBuckets { get; }
///
/// Creates a new, empty with string keys and items of type .
///
public CharSpanLookupDictionary()
{
this.InternalBuckets = new Dictionary();
}
///
/// Creates a new, empty with string keys and items of type and sets its initial capacity to specified value.
///
/// Initial capacity of the dictionary.
public CharSpanLookupDictionary(int initialCapacity)
{
this.InternalBuckets = new Dictionary(initialCapacity);
}
///
/// Creates a new with string keys and items of type and populates it with key-value pairs from supplied dictionary.
///
/// Dictionary containing items to populate this dictionary with.
public CharSpanLookupDictionary(IDictionary values)
: this(values.Count)
{
foreach (var (k, v) in values)
this.Add(k, v);
}
///
/// Creates a new with string keys and items of type and populates it with key-value pairs from supplied dictionary.
///
/// Dictionary containing items to populate this dictionary with.
public CharSpanLookupDictionary(IReadOnlyDictionary values)
: this(values.Count)
{
foreach (var (k, v) in values)
this.Add(k, v);
}
///
/// Creates a new with string keys and items of type and populates it with key-value pairs from supplied key-value collection.
///
/// Dictionary containing items to populate this dictionary with.
public CharSpanLookupDictionary(IEnumerable> values)
: this()
{
foreach (var (k, v) in values)
this.Add(k, v);
}
///
/// Inserts a specific key and corresponding value into this dictionary.
///
/// Key to insert.
/// Value corresponding to this key.
public void Add(string key, TValue value)
{
if (!this.TryInsertInternal(key, value, false))
throw new ArgumentException("Given key is already present in the dictionary.", nameof(key));
}
///
/// Inserts a specific key and corresponding value into this dictionary.
///
/// Key to insert.
/// Value corresponding to this key.
public void Add(ReadOnlySpan key, TValue value)
#if NETCOREAPP
{
if (!this.TryInsertInternal(new string(key), value, false))
throw new ArgumentException("Given key is already present in the dictionary.", nameof(key));
}
#else
{
unsafe
{
fixed (char* chars = &key.GetPinnableReference())
if (!this.TryInsertInternal(new string(chars, 0, key.Length), value, false))
throw new ArgumentException("Given key is already present in the dictionary.", nameof(key));
}
}
#endif
///
/// Attempts to insert a specific key and corresponding value into this dictionary.
///
/// Key to insert.
/// Value corresponding to this key.
/// Whether the operation was successful.
public bool TryAdd(string key, TValue value)
=> this.TryInsertInternal(key, value, false);
///
/// Attempts to insert a specific key and corresponding value into this dictionary.
///
/// Key to insert.
/// Value corresponding to this key.
/// Whether the operation was successful.
public bool TryAdd(ReadOnlySpan key, TValue value)
#if NETCOREAPP
=> this.TryInsertInternal(new string(key), value, false);
#else
{
unsafe
{
fixed (char* chars = &key.GetPinnableReference())
return this.TryInsertInternal(new string(chars, 0, key.Length), value, false);
}
}
#endif
///
/// Attempts to retrieve a value corresponding to the supplied key from this dictionary.
///
/// Key to retrieve the value for.
/// Retrieved value.
/// Whether the operation was successful.
public bool TryGetValue(string key, out TValue value)
{
if (key == null)
throw new ArgumentNullException(nameof(key));
return this.TryRetrieveInternal(key.AsSpan(), out value);
}
///
/// Attempts to retrieve a value corresponding to the supplied key from this dictionary.
///
/// Key to retrieve the value for.
/// Retrieved value.
/// Whether the operation was successful.
public bool TryGetValue(ReadOnlySpan key, out TValue value)
=> this.TryRetrieveInternal(key, out value);
///
/// Attempts to remove a value corresponding to the supplied key from this dictionary.
///
/// Key to remove the value for.
/// Removed value.
/// Whether the operation was successful.
public bool TryRemove(string key, out TValue value)
{
if (key == null)
throw new ArgumentNullException(nameof(key));
return this.TryRemoveInternal(key.AsSpan(), out value);
}
///
/// Attempts to remove a value corresponding to the supplied key from this dictionary.
///
/// Key to remove the value for.
/// Removed value.
/// Whether the operation was successful.
public bool TryRemove(ReadOnlySpan key, out TValue value)
=> this.TryRemoveInternal(key, out value);
///
/// Checks whether this dictionary contains the specified key.
///
/// Key to check for in this dictionary.
/// Whether the key was present in the dictionary.
public bool ContainsKey(string key)
=> this.ContainsKeyInternal(key.AsSpan());
///
/// Checks whether this dictionary contains the specified key.
///
/// Key to check for in this dictionary.
/// Whether the key was present in the dictionary.
public bool ContainsKey(ReadOnlySpan key)
=> this.ContainsKeyInternal(key);
///
/// Removes all items from this dictionary.
///
public void Clear()
{
this.InternalBuckets.Clear();
this.Count = 0;
}
///
/// Gets an enumerator over key-value pairs in this dictionary.
///
///
public IEnumerator> GetEnumerator()
=> new Enumerator(this);
///
/// Removes the.
///
/// The key.
/// A bool.
bool IDictionary.Remove(string key)
=> this.TryRemove(key.AsSpan(), out _);
///
/// Adds the.
///
/// The key.
/// The value.
void IDictionary.Add(object key, object value)
{
if (!(key is string tkey))
throw new ArgumentException("Key needs to be an instance of a string.");
if (!(value is TValue tvalue))
{
tvalue = default;
if (tvalue != null)
throw new ArgumentException($"Value needs to be an instance of {typeof(TValue)}.");
}
this.Add(tkey, tvalue);
}
///
/// Removes the.
///
/// The key.
void IDictionary.Remove(object key)
{
if (!(key is string tkey))
throw new ArgumentException("Key needs to be an instance of a string.");
this.TryRemove(tkey, out _);
}
///
/// Contains the.
///
/// The key.
/// A bool.
bool IDictionary.Contains(object key)
{
if (!(key is string tkey))
throw new ArgumentException("Key needs to be an instance of a string.");
return this.ContainsKey(tkey);
}
///
/// Gets the enumerator.
///
/// An IDictionaryEnumerator.
IDictionaryEnumerator IDictionary.GetEnumerator()
=> new Enumerator(this);
///
/// Adds the.
///
/// The item.
void ICollection>.Add(KeyValuePair item)
=> this.Add(item.Key, item.Value);
///
/// Removes the.
///
/// The item.
/// A bool.
bool ICollection>.Remove(KeyValuePair item)
=> this.TryRemove(item.Key, out _);
///
/// Contains the.
///
/// The item.
/// A bool.
bool ICollection>.Contains(KeyValuePair item)
=> this.TryGetValue(item.Key, out var value) && EqualityComparer.Default.Equals(value, item.Value);
///
/// Copies the to.
///
/// The array.
/// The array index.
void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex)
{
if (array.Length - arrayIndex < this.Count)
throw new ArgumentException("Target array is too small.", nameof(array));
var i = arrayIndex;
foreach (var (k, v) in this.InternalBuckets)
{
var kdv = v;
while (kdv != null)
{
array[i++] = new KeyValuePair(kdv.Key, kdv.Value);
kdv = kdv.Next;
}
}
}
///
/// Copies the to.
///
/// The array.
/// The array index.
void ICollection.CopyTo(Array array, int arrayIndex)
{
if (array is KeyValuePair[] tarray)
{
(this as ICollection>).CopyTo(tarray, arrayIndex);
return;
}
- if (!(array is object[]))
+ if (array is not object[])
throw new ArgumentException($"Array needs to be an instance of {typeof(TValue[])} or object[].");
var i = arrayIndex;
foreach (var (k, v) in this.InternalBuckets)
{
var kdv = v;
while (kdv != null)
{
array.SetValue(new KeyValuePair(kdv.Key, kdv.Value), i++);
kdv = kdv.Next;
}
}
}
///
/// Gets the enumerator.
///
/// An IEnumerator.
IEnumerator IEnumerable.GetEnumerator()
=> this.GetEnumerator();
///
/// Tries the insert internal.
///
/// The key.
/// The value.
/// If true, replace.
/// A bool.
private bool TryInsertInternal(string key, TValue value, bool replace)
{
if (key == null)
throw new ArgumentNullException(nameof(key), "Key cannot be null.");
var hash = key.CalculateKnuthHash();
if (!this.InternalBuckets.ContainsKey(hash))
{
this.InternalBuckets.Add(hash, new KeyedValue(key, hash, value));
this.Count++;
return true;
}
var kdv = this.InternalBuckets[hash];
var kdvLast = kdv;
while (kdv != null)
{
if (kdv.Key == key)
{
if (!replace)
return false;
kdv.Value = value;
return true;
}
kdvLast = kdv;
kdv = kdv.Next;
}
kdvLast.Next = new KeyedValue(key, hash, value);
this.Count++;
return true;
}
///
/// Tries the retrieve internal.
///
/// The key.
/// The value.
/// A bool.
private bool TryRetrieveInternal(ReadOnlySpan key, out TValue value)
{
value = default;
var hash = key.CalculateKnuthHash();
if (!this.InternalBuckets.TryGetValue(hash, out var kdv))
return false;
while (kdv != null)
{
if (key.SequenceEqual(kdv.Key.AsSpan()))
{
value = kdv.Value;
return true;
}
}
return false;
}
///
/// Tries the remove internal.
///
/// The key.
/// The value.
/// A bool.
private bool TryRemoveInternal(ReadOnlySpan key, out TValue value)
{
value = default;
var hash = key.CalculateKnuthHash();
if (!this.InternalBuckets.TryGetValue(hash, out var kdv))
return false;
if (kdv.Next == null && key.SequenceEqual(kdv.Key.AsSpan()))
{
// Only bucket under this hash and key matches, pop the entire bucket
value = kdv.Value;
this.InternalBuckets.Remove(hash);
this.Count--;
return true;
}
else if (kdv.Next == null)
{
// Only bucket under this hash and key does not match, cannot remove
return false;
}
else if (key.SequenceEqual(kdv.Key.AsSpan()))
{
// First key in the bucket matches, pop it and set its child as current bucket
value = kdv.Value;
this.InternalBuckets[hash] = kdv.Next;
this.Count--;
return true;
}
var kdvLast = kdv;
kdv = kdv.Next;
while (kdv != null)
{
if (key.SequenceEqual(kdv.Key.AsSpan()))
{
// Key matched, remove this bucket from the chain
value = kdv.Value;
kdvLast.Next = kdv.Next;
this.Count--;
return true;
}
kdvLast = kdv;
kdv = kdv.Next;
}
return false;
}
///
/// Contains the key internal.
///
/// The key.
/// A bool.
private bool ContainsKeyInternal(ReadOnlySpan key)
{
var hash = key.CalculateKnuthHash();
if (!this.InternalBuckets.TryGetValue(hash, out var kdv))
return false;
while (kdv != null)
{
if (key.SequenceEqual(kdv.Key.AsSpan()))
return true;
kdv = kdv.Next;
}
return false;
}
///
/// Gets the keys internal.
///
/// An ImmutableArray.
private ImmutableArray GetKeysInternal()
{
var builder = ImmutableArray.CreateBuilder(this.Count);
foreach (var value in this.InternalBuckets.Values)
{
var kdv = value;
while (kdv != null)
{
builder.Add(kdv.Key);
kdv = kdv.Next;
}
}
return builder.MoveToImmutable();
}
///
/// Gets the values internal.
///
/// An ImmutableArray.
private ImmutableArray GetValuesInternal()
{
var builder = ImmutableArray.CreateBuilder(this.Count);
foreach (var value in this.InternalBuckets.Values)
{
var kdv = value;
while (kdv != null)
{
builder.Add(kdv.Value);
kdv = kdv.Next;
}
}
return builder.MoveToImmutable();
}
///
/// The keyed value.
///
private class KeyedValue
{
///
/// Gets the key hash.
///
public ulong KeyHash { get; }
///
/// Gets the key.
///
public string Key { get; }
///
/// Gets or sets the value.
///
public TValue Value { get; set; }
///
/// Gets or sets the next.
///
public KeyedValue Next { get; set; }
///
/// Initializes a new instance of the class.
///
/// The key.
/// The key hash.
/// The value.
public KeyedValue(string key, ulong keyHash, TValue value)
{
this.KeyHash = keyHash;
this.Key = key;
this.Value = value;
}
}
///
/// The enumerator.
///
private class Enumerator :
IEnumerator>,
IDictionaryEnumerator
{
///
/// Gets the current.
///
public KeyValuePair Current { get; private set; }
///
/// Gets the current.
///
object IEnumerator.Current => this.Current;
///
/// Gets the key.
///
object IDictionaryEnumerator.Key => this.Current.Key;
///
/// Gets the value.
///
object IDictionaryEnumerator.Value => this.Current.Value;
///
/// Gets the entry.
///
DictionaryEntry IDictionaryEnumerator.Entry => new DictionaryEntry(this.Current.Key, this.Current.Value);
///
/// Gets the internal dictionary.
///
private CharSpanLookupDictionary InternalDictionary { get; }
///
/// Gets the internal enumerator.
///
private IEnumerator> InternalEnumerator { get; }
///
/// Gets or sets the current value.
///
private KeyedValue CurrentValue { get; set; } = null;
///
/// Initializes a new instance of the class.
///
/// The sp dict.
public Enumerator(CharSpanLookupDictionary spDict)
{
this.InternalDictionary = spDict;
this.InternalEnumerator = this.InternalDictionary.InternalBuckets.GetEnumerator();
}
///
/// Moves the next.
///
/// A bool.
public bool MoveNext()
{
var kdv = this.CurrentValue;
if (kdv == null)
{
if (!this.InternalEnumerator.MoveNext())
return false;
kdv = this.InternalEnumerator.Current.Value;
this.Current = new KeyValuePair(kdv.Key, kdv.Value);
this.CurrentValue = kdv.Next;
return true;
}
this.Current = new KeyValuePair(kdv.Key, kdv.Value);
this.CurrentValue = kdv.Next;
return true;
}
///
/// Resets the.
///
public void Reset()
{
this.InternalEnumerator.Reset();
this.Current = default;
this.CurrentValue = null;
}
///
/// Disposes the.
///
public void Dispose()
{
this.Reset();
}
}
}
}
diff --git a/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs b/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs
index 775141380..8f955eb5d 100644
--- a/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs
+++ b/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs
@@ -1,417 +1,417 @@
// 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
namespace DisCatSharp.Common
{
///
- /// Represents collection of string keys and values, allowing the use of for dictionary operations.
+ /// Represents collection of string keys and values, allowing the use of for dictionary operations.
///
/// Type of items in this dictionary.
public sealed class CharSpanLookupReadOnlyDictionary : IReadOnlyDictionary
{
///