diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml
index bf9c20a63..ee8e1430f 100644
--- a/.github/workflows/docs-preview.yml
+++ b/.github/workflows/docs-preview.yml
@@ -1,56 +1,53 @@
name: Docs Preview
on:
- pull_request:
-
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.Preview
path: DisCatSharp.Docs.Preview
token: ${{ secrets.DOCS_TOKEN }}
- name: Show Dir
run: dir
- name: Build Docs
working-directory: ./DisCatSharp
shell: pwsh
run: |
./rebuild-docs.ps1 -DocsPath "./DisCatSharp.Docs" -Output ".." -PackageName "dcs-docs-preview"
- name: Archive Docs
uses: actions/upload-artifact@v2
with:
name: preview-docs
path: dcs-docs-preview.tar.xz
- name: Purge old docs
working-directory: ./DisCatSharp.Docs.Preview
run: |
shopt -s extglob
rm -rf !(.git|.gitignore)
- name: Extract new docs
run: |
tar -xf dcs-docs-preview.tar.xz -C ./DisCatSharp.Docs.Preview
- name: Commit and push changes
uses: EndBug/add-and-commit@master
with:
cwd: ./DisCatSharp.Docs.Preview
default_author: github_actions
author_name: DisCatSharp
author_email: discatsharp@aitsys.dev
message: 'Preview docs (${{ github.sha }})'
- pull: 'NO-PULL'
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/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 4cd6d1040..d91360631 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -1,25 +1,30 @@
name: "DisCatSharp .NET"
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
+ workflow_dispatch:
jobs:
build:
-
- runs-on: ubuntu-latest
-
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ dotnet: [6.0.x]
+
+ runs-on: ${{ matrix.os }}
+
steps:
- uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
- dotnet-version: 5.0.x
+ dotnet-version: ${{ matrix.dotnet }}
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
diff --git a/BUILDING.md b/BUILDING.md
index 970e8a32d..1571f043c 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -1,74 +1,74 @@
# Building DisCatSharp
These are detailed instructions on how to build the DisCatSharp library under various environmnets.
It is recommended you have prior experience with multi-target .NET Core/Standard projects, as well as the `dotnet` CLI utility, and MSBuild.
## Requirements
In order to build the library, you will first need to install some software.
### Windows
On Windows, we only officially support Visual Studio 2019 16.10 or newer. Visual Studio Code and other IDEs might work, but are generally not supported or even guaranteed to work properly.
* **Windows 10** - while we support running the library on Windows 7 and above, we only support building on Windows 10 and better.
* [**Git for Windows**](https://git-scm.com/download/win) - required to clone the repository.
* [**Visual Studio 2019**](https://www.visualstudio.com/downloads/) - community edition or better. We do not support Visual Studio 2017 and older. Note that to build the library, you need Visual Studio 2019 version 16.10 or newer.
* **Workloads**:
* **.NET Framework Desktop** - required to build .NETFX (4.5, 4.6, and 4.7 targets)
* **.NET Core Cross-Platform Development** - required to build .NET Standard targets (1.1, 1.3, and 2.0) and the project overall.
* **Individual Components**:
* **.NET Framework 4.5 SDK** - required for .NETFX 4.5 target
* **.NET Framework 4.6 SDK** - required for .NETFX 4.6 target
* **.NET Framework 4.7 SDK** - required for .NETFX 4.7 target
* [**.NET Core SDK 3.1**](https://www.microsoft.com/net/download) - required to build the project.
* **Windows PowerShell** - required to run the build scripts. You need to make sure your script execution policy allows execution of unsigned scripts.
### GNU/Linux
On GNU/Linux, we support building via Visual Studio Code and .NET Core SDK. Other IDEs might work, but are not supported or guaranteed to work properly.
While these should apply to any modern distribution, we only test against Debian 10. Your mileage may vary.
When installing the below, make sure you install all the dependencies properly. We might ship a build environmnent as a docker container in the future.
* **Any modern GNU/Linux distribution** - like Debian 10.
* **Git** - to clone the repository.
* [**Visual Studio Code**](https://code.visualstudio.com/Download) - a recent version is required.
* **C# for Visual Studio Code (powered by OmniSharp)** - required for syntax highlighting and basic Intellisense
* [**.NET Core SDK 3.1**](https://www.microsoft.com/net/download) - required to build the project.
* [**Mono 5.x**](http://www.mono-project.com/download/#download-lin) - required to build the .NETFX 4.5, 4.6, and 4.7 targets, as well as to build the docs.
* [**PowerShell Core**](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-7.1) - required to execute the build scripts.
* **p7zip-full** - required to package docs.
## Instructions
Once you install all the necessary prerequisites, you can proceed to building. These instructions assume you have already cloned the repository.
### Windows
Building on Windows is relatively easy. There's 2 ways to build the project:
#### Building through Visual Studio
Building through Visual Studio yields just binaries you can use in your projects.
1. Open the solution in Visual Studio.
2. Set the configuration to Release.
3. Select Build > Build Solution to build the project.
-4. Select Build > Publish DSharpPlus to publish the binaries.
+4. Select Build > Publish DisCatSharp to publish the binaries.
#### Building with the build script
Building this way outputs NuGet packages, and a documentation package. Ensure you have an internet connection available, as the script will install programs necessary to build the documentation.
1. Open PowerShell and navigate to the directory which you cloned DisCatSharp to.
2. Execute `.\s_oneclick-rebuild-all.ps1 -configuration Release` and wait for the script to finish execution.
3. Once it's done, the artifacts will be available in *dcs-artifacts* directory, next to the directory to which the repository is cloned.
### GNU/Linux
When all necessary prerequisites are installed, you can proceed to building. There are technically 2 ways to build the library, though both of them perform the same steps, they are just invoked slightly differently.
#### Through Visual Studio Code
1. Open Visual Studio Code and open the folder to which you cloned DisCatSharp as your workspace.
2. Select Build > Run Task...
3. Select `buildRelease` task and wait for it to finish.
4. The artifacts will be placed in *dcs-artifacts* directory, next to whoch the repository is cloned.
#### Through PowerShell
1. Open PowerShell (`pwsh`) and navigate to the directory which you cloned DisCatSharp to.
2. Execute `.\s_oneclick-rebuild-all.ps1 -configuration Release` and wait for the script to finish execution.
3. Once it's done, the artifacts will be available in *dcs-artifacts* directory, next to the directory to which the repository is cloned.
diff --git a/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..04fdce4bb 100644
--- a/DisCatSharp.Common/DisCatSharp.Common.csproj
+++ b/DisCatSharp.Common/DisCatSharp.Common.csproj
@@ -1,45 +1,54 @@
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
+
+ 1701;1702;;DV2001
+
+
+
+ 1701;1702;;DV2001
+
+
-
+
+
-
+
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
{
///