diff --git a/DisCatSharp.CommandsNext/CommandsNextExtension.cs b/DisCatSharp.CommandsNext/CommandsNextExtension.cs
index 89a21f217..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 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.Common/Attributes/DateTimeFormatAttribute.cs b/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs
index 9c8cc3159..55fcd8beb 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.
///
CurrentLocaleLong = 2,
///
/// 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".
///
InvariantLocaleLong = 4,
///
/// 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/UnixTimestampAttributes.cs b/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs
index 758145715..1579de6d5 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.
//
// 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.Interactivity/Extensions/ChannelExtensions.cs b/DisCatSharp.Interactivity/Extensions/ChannelExtensions.cs
index c5341a108..871d321b6 100644
--- a/DisCatSharp.Interactivity/Extensions/ChannelExtensions.cs
+++ b/DisCatSharp.Interactivity/Extensions/ChannelExtensions.cs
@@ -1,150 +1,150 @@
// 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.Threading;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.EventArgs;
using DisCatSharp.Interactivity.Enums;
using DisCatSharp.Interactivity.EventHandling;
namespace DisCatSharp.Interactivity.Extensions
{
///
/// Interactivity extension methods for .
///
public static class ChannelExtensions
{
///
/// Waits for the next message sent in this channel that satisfies the predicate.
///
/// The channel to monitor.
/// A predicate that should return if a message matches.
/// Overrides the timeout set in
- /// Thrown if interactivity is not enabled for the client associated with the channel.
+ /// Thrown if interactivity is not enabled for the client associated with the channel.
public static Task> GetNextMessageAsync(this DiscordChannel channel, Func predicate, TimeSpan? timeoutOverride = null)
=> GetInteractivity(channel).WaitForMessageAsync(msg => msg.ChannelId == channel.Id && predicate(msg), timeoutOverride);
///
/// Waits for the next message sent in this channel.
///
/// The channel to monitor.
/// Overrides the timeout set in
- /// Thrown if interactivity is not enabled for the client associated with the channel.
+ /// Thrown if interactivity is not enabled for the client associated with the channel.
public static Task> GetNextMessageAsync(this DiscordChannel channel, TimeSpan? timeoutOverride = null)
=> channel.GetNextMessageAsync(msg => true, timeoutOverride);
///
/// Waits for the next message sent in this channel from a specific user.
///
/// The channel to monitor.
/// The target user.
/// Overrides the timeout set in
- /// Thrown if interactivity is not enabled for the client associated with the channel.
+ /// Thrown if interactivity is not enabled for the client associated with the channel.
public static Task> GetNextMessageAsync(this DiscordChannel channel, DiscordUser user, TimeSpan? timeoutOverride = null)
=> channel.GetNextMessageAsync(msg => msg.Author.Id == user.Id, timeoutOverride);
///
/// Waits for a specific user to start typing in this channel.
///
/// The target channel.
/// The target user.
/// Overrides the timeout set in
- /// Thrown if interactivity is not enabled for the client associated with the channel.
+ /// Thrown if interactivity is not enabled for the client associated with the channel.
public static Task> WaitForUserTypingAsync(this DiscordChannel channel, DiscordUser user, TimeSpan? timeoutOverride = null)
=> GetInteractivity(channel).WaitForUserTypingAsync(user, channel, timeoutOverride);
///
/// Sends a new paginated message.
///
/// Target channel.
/// The user that'll be able to control the pages.
/// A collection of to display.
/// Pagination emojis.
/// Pagination behaviour (when hitting max and min indices).
/// Deletion behaviour.
/// Override timeout period.
- /// Thrown if interactivity is not enabled for the client associated with the channel.
+ /// Thrown if interactivity is not enabled for the client associated with the channel.
public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis, PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutoverride = null)
=> GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, emojis, behaviour, deletion, timeoutoverride);
///
/// Sends a new paginated message with buttons.
///
/// Target channel.
/// The user that'll be able to control the pages.
/// A collection of to display.
/// Pagination buttons (leave null to default to ones on configuration).
/// Pagination behaviour.
/// Deletion behaviour
/// A custom cancellation token that can be cancelled at any point.
- /// Thrown if interactivity is not enabled for the client associated with the channel.
+ /// Thrown if interactivity is not enabled for the client associated with the channel.
public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
=> GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, token);
///
public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
=> channel.SendPaginatedMessageAsync(user, pages, default, behaviour, deletion, token);
///
/// Sends a new paginated message with buttons.
///
/// Target channel.
/// The user that'll be able to control the pages.
/// A collection of to display.
/// Pagination buttons (leave null to default to ones on configuration).
/// Pagination behaviour.
/// Deletion behaviour
/// Override timeout period.
- /// Thrown if interactivity is not enabled for the client associated with the channel.
+ /// Thrown if interactivity is not enabled for the client associated with the channel.
public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default)
=> GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, buttons, timeoutoverride, behaviour, deletion);
///
/// Sends the paginated message async.
///
/// The channel.
/// The user.
/// The pages.
/// The timeoutoverride.
/// The behaviour.
/// The deletion.
/// A Task.
public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default)
=> channel.SendPaginatedMessageAsync(user, pages, default, timeoutoverride, behaviour, deletion);
///
/// Retrieves an interactivity instance from a channel instance.
///
private static InteractivityExtension GetInteractivity(DiscordChannel channel)
{
var client = (DiscordClient)channel.Discord;
var interactivity = client.GetInteractivity();
return interactivity ?? throw new InvalidOperationException($"Interactivity is not enabled for this {(client._isShard ? "shard" : "client")}.");
}
}
}
diff --git a/DisCatSharp.Interactivity/Extensions/ClientExtensions.cs b/DisCatSharp.Interactivity/Extensions/ClientExtensions.cs
index 58ed4dec6..1c18425d6 100644
--- a/DisCatSharp.Interactivity/Extensions/ClientExtensions.cs
+++ b/DisCatSharp.Interactivity/Extensions/ClientExtensions.cs
@@ -1,100 +1,100 @@
// 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.Threading.Tasks;
namespace DisCatSharp.Interactivity.Extensions
{
///
/// Interactivity extension methods for and .
///
public static class ClientExtensions
{
///
/// Enables interactivity for this instance.
///
/// The client to enable interactivity for.
/// A configuration instance. Default configuration values will be used if none is provided.
/// A brand new instance.
- /// Thrown if interactivity has already been enabled for the client instance.
+ /// Thrown if interactivity has already been enabled for the client instance.
public static InteractivityExtension UseInteractivity(this DiscordClient client, InteractivityConfiguration configuration = null)
{
if (client.GetExtension() != null) throw new InvalidOperationException($"Interactivity is already enabled for this {(client._isShard ? "shard" : "client")}.");
configuration ??= new InteractivityConfiguration();
var extension = new InteractivityExtension(configuration);
client.AddExtension(extension);
return extension;
}
///
/// Enables interactivity for each shard.
///
/// The shard client to enable interactivity for.
/// Configuration to use for all shards. If one isn't provided, default configuration values will be used.
/// A dictionary containing new instances for each shard.
public static async Task> UseInteractivityAsync(this DiscordShardedClient client, InteractivityConfiguration configuration = null)
{
var extensions = new Dictionary();
await client.InitializeShardsAsync().ConfigureAwait(false);
foreach (var shard in client.ShardClients.Select(xkvp => xkvp.Value))
{
var extension = shard.GetExtension() ?? shard.UseInteractivity(configuration);
extensions.Add(shard.ShardId, extension);
}
return new ReadOnlyDictionary(extensions);
}
///
/// Retrieves the registered instance for this client.
///
/// The client to retrieve an instance from.
/// An existing instance, or if interactivity is not enabled for the instance.
public static InteractivityExtension GetInteractivity(this DiscordClient client)
=> client.GetExtension();
///
/// Retrieves a instance for each shard.
///
/// The shard client to retrieve interactivity instances from.
/// A dictionary containing instances for each shard.
public static async Task> GetInteractivityAsync(this DiscordShardedClient client)
{
await client.InitializeShardsAsync().ConfigureAwait(false);
var extensions = new Dictionary();
foreach (var shard in client.ShardClients.Select(xkvp => xkvp.Value))
{
extensions.Add(shard.ShardId, shard.GetExtension());
}
return new ReadOnlyDictionary(extensions);
}
}
}
diff --git a/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs b/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs
index 1b3a13998..4b3878ae5 100644
--- a/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs
+++ b/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs
@@ -1,243 +1,243 @@
// 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.Threading;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.EventArgs;
using DisCatSharp.Interactivity.Enums;
using DisCatSharp.Interactivity.EventHandling;
namespace DisCatSharp.Interactivity.Extensions
{
///
/// Interactivity extension methods for .
///
public static class MessageExtensions
{
///
/// Waits for the next message that has the same author and channel as this message.
///
/// Original message.
/// Overrides the timeout set in
public static Task> GetNextMessageAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null)
=> message.Channel.GetNextMessageAsync(message.Author, timeoutOverride);
///
/// Waits for the next message with the same author and channel as this message, which also satisfies a predicate.
///
/// Original message.
/// A predicate that should return if a message matches.
/// Overrides the timeout set in
public static Task> GetNextMessageAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null)
=> message.Channel.GetNextMessageAsync(msg => msg.Author.Id == message.Author.Id && message.ChannelId == msg.ChannelId && predicate(msg), timeoutOverride);
///
/// Waits for any button to be pressed on the specified message.
///
/// The message to wait on.
public static Task> WaitForButtonAsync(this DiscordMessage message)
=> GetInteractivity(message).WaitForButtonAsync(message);
///
/// Waits for any button to be pressed on the specified message.
///
/// The message to wait on.
/// Overrides the timeout set in
public static Task> WaitForButtonAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForButtonAsync(message, timeoutOverride);
///
/// Waits for any button to be pressed on the specified message.
///
/// The message to wait on.
/// A custom cancellation token that can be cancelled at any point.
public static Task> WaitForButtonAsync(this DiscordMessage message, CancellationToken token)
=> GetInteractivity(message).WaitForButtonAsync(message, token);
///
/// Waits for a button with the specified Id to be pressed on the specified message.
///
/// The message to wait on.
/// The Id of the button to wait for.
/// Overrides the timeout set in
public static Task> WaitForButtonAsync(this DiscordMessage message, string id, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForButtonAsync(message, id, timeoutOverride);
///
/// Waits for a button with the specified Id to be pressed on the specified message.
///
/// The message to wait on.
/// The Id of the button to wait for.
/// A custom cancellation token that can be cancelled at any point.
public static Task> WaitForButtonAsync(this DiscordMessage message, string id, CancellationToken token)
=> GetInteractivity(message).WaitForButtonAsync(message, id, token);
///
/// Waits for any button to be pressed on the specified message by the specified user.
///
/// The message to wait on.
/// The user to wait for button input from.
/// Overrides the timeout set in
public static Task> WaitForButtonAsync(this DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForButtonAsync(message, user, timeoutOverride);
///
/// Waits for any button to be pressed on the specified message by the specified user.
///
/// The message to wait on.
/// The user to wait for button input from.
/// A custom cancellation token that can be cancelled at any point.
public static Task> WaitForButtonAsync(this DiscordMessage message, DiscordUser user, CancellationToken token)
=> GetInteractivity(message).WaitForButtonAsync(message, user, token);
///
/// Waits for any button to be interacted with.
///
/// The message to wait on.
/// The predicate to filter interactions by.
/// Override the timeout specified in
public static Task> WaitForButtonAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForButtonAsync(message, predicate, timeoutOverride);
///
/// Waits for any button to be interacted with.
///
/// The message to wait on.
/// The predicate to filter interactions by.
/// A token to cancel interactivity with at any time. Pass to wait indefinitely.
public static Task> WaitForButtonAsync(this DiscordMessage message, Func predicate, CancellationToken token)
=> GetInteractivity(message).WaitForButtonAsync(message, predicate, token);
///
/// Waits for any dropdown to be interacted with.
///
/// The message to wait for.
/// A filter predicate.
/// Override the timeout period specified in .
/// Thrown when the message doesn't contain any dropdowns
public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForSelectAsync(message, predicate, timeoutOverride);
///
/// Waits for any dropdown to be interacted with.
///
/// The message to wait for.
/// A filter predicate.
/// A token that can be used to cancel interactivity. Pass to wait indefinitely.
/// Thrown when the message doesn't contain any dropdowns
public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, CancellationToken token)
=> GetInteractivity(message).WaitForSelectAsync(message, predicate, token);
///
/// Waits for a dropdown to be interacted with.
///
/// The message to wait on.
/// The Id of the dropdown to wait for.
/// Overrides the timeout set in
public static Task> WaitForSelectAsync(this DiscordMessage message, string id, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForSelectAsync(message, id, timeoutOverride);
///
/// Waits for a dropdown to be interacted with.
///
/// The message to wait on.
/// The Id of the dropdown to wait for.
/// A custom cancellation token that can be cancelled at any point.
public static Task> WaitForSelectAsync(this DiscordMessage message, string id, CancellationToken token)
=> GetInteractivity(message).WaitForSelectAsync(message, id, token);
///
/// Waits for a dropdown to be interacted with by the specified user.
///
/// The message to wait on.
/// The user to wait for.
/// The Id of the dropdown to wait for.
///
public static Task> WaitForSelectAsync(this DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForSelectAsync(message, user, id, timeoutOverride);
///
/// Waits for a dropdown to be interacted with by the specified user.
///
/// The message to wait on.
/// The user to wait for.
/// The Id of the dropdown to wait for.
/// A custom cancellation token that can be cancelled at any point.
public static Task> WaitForSelectAsync(this DiscordMessage message, DiscordUser user, string id, CancellationToken token)
=> GetInteractivity(message).WaitForSelectAsync(message, user, id, token);
///
/// Waits for a reaction on this message from a specific user.
///
/// Target message.
/// The target user.
/// Overrides the timeout set in
- /// Thrown if interactivity is not enabled for the client associated with the message.
+ /// Thrown if interactivity is not enabled for the client associated with the message.
public static Task> WaitForReactionAsync(this DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForReactionAsync(message, user, timeoutOverride);
///
/// Waits for a specific reaction on this message from the specified user.
///
/// Target message.
/// The target user.
/// The target emoji.
/// Overrides the timeout set in
- /// Thrown if interactivity is not enabled for the client associated with the message.
+ /// Thrown if interactivity is not enabled for the client associated with the message.
public static Task> WaitForReactionAsync(this DiscordMessage message, DiscordUser user, DiscordEmoji emoji, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).WaitForReactionAsync(e => e.Emoji == emoji, message, user, timeoutOverride);
///
/// Collects all reactions on this message within the timeout duration.
///
/// The message to collect reactions from.
/// Overrides the timeout set in
- /// Thrown if interactivity is not enabled for the client associated with the message.
+ /// Thrown if interactivity is not enabled for the client associated with the message.
public static Task> CollectReactionsAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).CollectReactionsAsync(message, timeoutOverride);
///
/// Begins a poll using this message.
///
/// Target message.
/// Options for this poll.
/// Overrides the action set in
/// Overrides the timeout set in
- /// Thrown if interactivity is not enabled for the client associated with the message.
+ /// Thrown if interactivity is not enabled for the client associated with the message.
public static Task> DoPollAsync(this DiscordMessage message, IEnumerable emojis, PollBehaviour? behaviorOverride = null, TimeSpan? timeoutOverride = null)
=> GetInteractivity(message).DoPollAsync(message, emojis, behaviorOverride, timeoutOverride);
///
/// Retrieves an interactivity instance from a message instance.
///
internal static InteractivityExtension GetInteractivity(DiscordMessage message)
{
var client = (DiscordClient)message.Discord;
var interactivity = client.GetInteractivity();
return interactivity ?? throw new InvalidOperationException($"Interactivity is not enabled for this {(client._isShard ? "shard" : "client")}.");
}
}
}
diff --git a/DisCatSharp.Interactivity/InteractivityExtension.cs b/DisCatSharp.Interactivity/InteractivityExtension.cs
index 45321747d..a49fab9b6 100644
--- a/DisCatSharp.Interactivity/InteractivityExtension.cs
+++ b/DisCatSharp.Interactivity/InteractivityExtension.cs
@@ -1,930 +1,930 @@
// 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.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.EventArgs;
using DisCatSharp.Interactivity.Enums;
using DisCatSharp.Interactivity.EventHandling;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Enums;
using System.Threading;
using System.Globalization;
namespace DisCatSharp.Interactivity
{
///
/// Extension class for DisCatSharp.Interactivity
///
public class InteractivityExtension : BaseExtension
{
#pragma warning disable IDE1006 // Naming Styles
///
/// Gets the config.
///
internal InteractivityConfiguration Config { get; }
private EventWaiter MessageCreatedWaiter;
private EventWaiter MessageReactionAddWaiter;
private EventWaiter TypingStartWaiter;
private EventWaiter ComponentInteractionWaiter;
private ComponentEventWaiter ComponentEventWaiter;
private ReactionCollector ReactionCollector;
private Poller Poller;
private Paginator Paginator;
private ComponentPaginator _compPaginator;
#pragma warning restore IDE1006 // Naming Styles
///
/// Initializes a new instance of the class.
///
/// The configuration.
internal InteractivityExtension(InteractivityConfiguration cfg)
{
this.Config = new InteractivityConfiguration(cfg);
}
///
/// Setups the Interactivity Extension.
///
/// Discord client.
protected internal override void Setup(DiscordClient client)
{
this.Client = client;
this.MessageCreatedWaiter = new EventWaiter(this.Client);
this.MessageReactionAddWaiter = new EventWaiter(this.Client);
this.ComponentInteractionWaiter = new EventWaiter(this.Client);
this.TypingStartWaiter = new EventWaiter(this.Client);
this.Poller = new Poller(this.Client);
this.ReactionCollector = new ReactionCollector(this.Client);
this.Paginator = new Paginator(this.Client);
this._compPaginator = new(this.Client, this.Config);
this.ComponentEventWaiter = new(this.Client, this.Config);
}
///
/// Makes a poll and returns poll results.
///
/// Message to create poll on.
/// Emojis to use for this poll.
/// What to do when the poll ends.
/// override timeout period.
///
public async Task> DoPollAsync(DiscordMessage m, IEnumerable emojis, PollBehaviour? behaviour = default, TimeSpan? timeout = null)
{
if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No reaction intents are enabled.");
if (!emojis.Any())
throw new ArgumentException("You need to provide at least one emoji for a poll!");
foreach (var em in emojis)
await m.CreateReactionAsync(em).ConfigureAwait(false);
var res = await this.Poller.DoPollAsync(new PollRequest(m, timeout ?? this.Config.Timeout, emojis)).ConfigureAwait(false);
var pollbehaviour = behaviour ?? this.Config.PollBehaviour;
var thismember = await m.Channel.Guild.GetMemberAsync(this.Client.CurrentUser.Id).ConfigureAwait(false);
if (pollbehaviour == PollBehaviour.DeleteEmojis && m.Channel.PermissionsFor(thismember).HasPermission(Permissions.ManageMessages))
await m.DeleteAllReactionsAsync().ConfigureAwait(false);
return new ReadOnlyCollection(res.ToList());
}
///
/// Waits for any button in the specified collection to be pressed.
///
/// The message to wait on.
/// A collection of buttons to listen for.
/// Override the timeout period in .
/// A with the result of button that was pressed, if any.
- /// Thrown when attempting to wait for a message that is not authored by the current user.
+ /// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, buttons, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button in the specified collection to be pressed.
///
/// The message to wait on.
/// A collection of buttons to listen for.
/// A custom cancellation token that can be cancelled at any point.
/// A with the result of button that was pressed, if any.
- /// Thrown when attempting to wait for a message that is not authored by the current user.
+ /// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!buttons.Any())
throw new ArgumentException("You must specify at least one button to listen for.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Provided Message does not contain any button components.");
var res = await this.ComponentEventWaiter
.WaitForMatchAsync(new(message,
c =>
c.Interaction.Data.ComponentType == ComponentType.Button &&
buttons.Any(b => b.CustomId == c.Id), token)).ConfigureAwait(false);
return new(res is null, res);
}
///
/// Waits for any button on the specified message to be pressed.
///
/// The message to wait for the button on.
/// Override the timeout period specified in .
/// A with the result of button that was pressed, if any.
- /// Thrown when attempting to wait for a message that is not authored by the current user.
+ /// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button on the specified message to be pressed.
///
/// The message to wait for the button on.
/// A custom cancellation token that can be cancelled at any point.
/// A with the result of button that was pressed, if any.
- /// Thrown when attempting to wait for a message that is not authored by the current user.
+ /// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
var ids = message.Components.SelectMany(m => m.Components).Select(c => c.CustomId);
var result =
await this
.ComponentEventWaiter
.WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && ids.Contains(c.Id), token))
.ConfigureAwait(false);
return new(result is null, result);
}
///
/// Waits for any button on the specified message to be pressed by the specified user.
///
/// The message to wait for the button on.
/// The user to wait for the button press from.
/// Override the timeout period specified in .
/// A with the result of button that was pressed, if any.
- /// Thrown when attempting to wait for a message that is not authored by the current user.
+ /// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, user, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button on the specified message to be pressed by the specified user.
///
/// The message to wait for the button on.
/// The user to wait for the button press from.
/// A custom cancellation token that can be cancelled at any point.
/// A with the result of button that was pressed, if any.
- /// Thrown when attempting to wait for a message that is not authored by the current user.
+ /// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
var result = await this
.ComponentEventWaiter
.WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.User == user, token))
.ConfigureAwait(false);
return new(result is null, result);
}
///
/// Waits for a button with the specified Id to be pressed.
///
/// The message to wait for the button on.
/// The Id of the button to wait for.
/// Override the timeout period specified in .
/// A with the result of the operation.
- /// Thrown when attempting to wait for a message that is not authored by the current user.
+ /// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, id, this.GetCancellationToken(timeoutOverride));
///
/// Waits for a button with the specified Id to be pressed.
///
/// The message to wait for the button on.
/// The Id of the button to wait for.
/// Override the timeout period specified in .
/// A with the result of the operation.
- /// Thrown when attempting to wait for a message that is not authored by the current user.
+ /// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, string id, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
if (!message.Components.SelectMany(c => c.Components).OfType().Any(c => c.CustomId == id))
throw new ArgumentException($"Message does not contain button with Id of '{id}'.");
var result = await this
.ComponentEventWaiter
.WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.Id == id, token))
.ConfigureAwait(false);
return new(result is null, result);
}
///
/// Waits for any button to be interacted with.
///
/// The message to wait on.
/// The predicate to filter interactions by.
/// Override the timeout specified in
public Task> WaitForButtonAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, predicate, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button to be interacted with.
///
/// The message to wait on.
/// The predicate to filter interactions by.
/// A token to cancel interactivity with at any time. Pass to wait indefinitely.
public async Task> WaitForButtonAsync(DiscordMessage message, Func predicate, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
var result = await this
.ComponentEventWaiter
.WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType is ComponentType.Button && predicate(c), token))
.ConfigureAwait(false);
return new(result is null, result);
}
///
/// Waits for any dropdown to be interacted with.
///
/// The message to wait for.
/// A filter predicate.
/// Override the timeout period specified in .
/// Thrown when the Provided message does not contain any dropdowns
public Task> WaitForSelectAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null)
=> this.WaitForSelectAsync(message, predicate, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any dropdown to be interacted with.
///
/// The message to wait for.
/// A filter predicate.
/// A token that can be used to cancel interactivity. Pass to wait indefinitely.
/// Thrown when the Provided message does not contain any dropdowns
public async Task> WaitForSelectAsync(DiscordMessage message, Func predicate, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select))
throw new ArgumentException("Message does not contain any select components.");
var result = await this
.ComponentEventWaiter
.WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType is ComponentType.Select && predicate(c), token))
.ConfigureAwait(false);
return new(result is null, result);
}
///
/// Waits for a dropdown to be interacted with.
///
/// This is here for backwards-compatibility and will internally create a cancellation token.
/// The message to wait on.
/// The Id of the dropdown to wait on.
/// Override the timeout period specified in .
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
public Task> WaitForSelectAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null)
=> this.WaitForSelectAsync(message, id, this.GetCancellationToken(timeoutOverride));
///
/// Waits for a dropdown to be interacted with.
///
/// The message to wait on.
/// The Id of the dropdown to wait on.
/// A custom cancellation token that can be cancelled at any point.
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
public async Task> WaitForSelectAsync(DiscordMessage message, string id, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select))
throw new ArgumentException("Message does not contain any select components.");
if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id))
throw new ArgumentException($"Message does not contain select component with Id of '{id}'.");
var result = await this
.ComponentEventWaiter
.WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Select && c.Id == id, token))
.ConfigureAwait(false);
return new(result is null, result);
}
///
/// Waits for a dropdown to be interacted with by a specific user.
///
/// The message to wait on.
/// The user to wait on.
/// The Id of the dropdown to wait on.
/// Override the timeout period specified in .
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
public Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null)
=> this.WaitForSelectAsync(message, user, id, this.GetCancellationToken(timeoutOverride));
///
/// Waits for a dropdown to be interacted with by a specific user.
///
/// The message to wait on.
/// The user to wait on.
/// The Id of the dropdown to wait on.
/// A custom cancellation token that can be cancelled at any point.
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
public async Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select))
throw new ArgumentException("Message does not contain any select components.");
if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id))
throw new ArgumentException($"Message does not contain select with Id of '{id}'.");
var result = await this
.ComponentEventWaiter
.WaitForMatchAsync(new(message, (c) => c.Id == id && c.User == user, token)).ConfigureAwait(false);
return new(result is null, result);
}
///
/// Waits for a specific message.
///
/// Predicate to match.
/// override timeout period.
public async Task> WaitForMessageAsync(Func predicate,
TimeSpan? timeoutoverride = null)
{
if (!Utilities.HasMessageIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No message intents are enabled.");
var timeout = timeoutoverride ?? this.Config.Timeout;
var returns = await this.MessageCreatedWaiter.WaitForMatchAsync(new MatchRequest(x => predicate(x.Message), timeout)).ConfigureAwait(false);
return new InteractivityResult(returns == null, returns?.Message);
}
///
/// Wait for a specific reaction.
///
/// Predicate to match.
/// override timeout period.
public async Task> WaitForReactionAsync(Func predicate,
TimeSpan? timeoutoverride = null)
{
if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No reaction intents are enabled.");
var timeout = timeoutoverride ?? this.Config.Timeout;
var returns = await this.MessageReactionAddWaiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false);
return new InteractivityResult(returns == null, returns);
}
///
/// Wait for a specific reaction.
/// For this Event you need the intent specified in
///
/// Message reaction was added to.
/// User that made the reaction.
/// override timeout period.
public async Task> WaitForReactionAsync(DiscordMessage message, DiscordUser user,
TimeSpan? timeoutoverride = null)
=> await this.WaitForReactionAsync(x => x.User.Id == user.Id && x.Message.Id == message.Id, timeoutoverride).ConfigureAwait(false);
///
/// Waits for a specific reaction.
/// For this Event you need the intent specified in
///
/// Predicate to match.
/// Message reaction was added to.
/// User that made the reaction.
/// override timeout period.
public async Task> WaitForReactionAsync(Func predicate,
DiscordMessage message, DiscordUser user, TimeSpan? timeoutoverride = null)
=> await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id && x.Message.Id == message.Id, timeoutoverride).ConfigureAwait(false);
///
/// Waits for a specific reaction.
/// For this Event you need the intent specified in
///
/// predicate to match.
/// User that made the reaction.
/// Override timeout period.
public async Task> WaitForReactionAsync(Func predicate,
DiscordUser user, TimeSpan? timeoutoverride = null)
=> await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id, timeoutoverride).ConfigureAwait(false);
///
/// Waits for a user to start typing.
///
/// User that starts typing.
/// Channel the user is typing in.
/// Override timeout period.
public async Task> WaitForUserTypingAsync(DiscordUser user,
DiscordChannel channel, TimeSpan? timeoutoverride = null)
{
if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No typing intents are enabled.");
var timeout = timeoutoverride ?? this.Config.Timeout;
var returns = await this.TypingStartWaiter.WaitForMatchAsync(
new MatchRequest(x => x.User.Id == user.Id && x.Channel.Id == channel.Id, timeout))
.ConfigureAwait(false);
return new InteractivityResult(returns == null, returns);
}
///
/// Waits for a user to start typing.
///
/// User that starts typing.
/// Override timeout period.
public async Task> WaitForUserTypingAsync(DiscordUser user, TimeSpan? timeoutoverride = null)
{
if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No typing intents are enabled.");
var timeout = timeoutoverride ?? this.Config.Timeout;
var returns = await this.TypingStartWaiter.WaitForMatchAsync(
new MatchRequest(x => x.User.Id == user.Id, timeout))
.ConfigureAwait(false);
return new InteractivityResult(returns == null, returns);
}
///
/// Waits for any user to start typing.
///
/// Channel to type in.
/// Override timeout period.
public async Task> WaitForTypingAsync(DiscordChannel channel, TimeSpan? timeoutoverride = null)
{
if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No typing intents are enabled.");
var timeout = timeoutoverride ?? this.Config.Timeout;
var returns = await this.TypingStartWaiter.WaitForMatchAsync(
new MatchRequest(x => x.Channel.Id == channel.Id, timeout))
.ConfigureAwait(false);
return new InteractivityResult(returns == null, returns);
}
///
/// Collects reactions on a specific message.
///
/// Message to collect reactions on.
/// Override timeout period.
public async Task> CollectReactionsAsync(DiscordMessage m, TimeSpan? timeoutoverride = null)
{
if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No reaction intents are enabled.");
var timeout = timeoutoverride ?? this.Config.Timeout;
var collection = await this.ReactionCollector.CollectAsync(new ReactionCollectRequest(m, timeout)).ConfigureAwait(false);
return collection;
}
///
/// Waits for specific event args to be received. Make sure the appropriate are registered, if needed.
///
///
/// The predicate.
/// Override timeout period.
public async Task> WaitForEventArgsAsync(Func predicate, TimeSpan? timeoutoverride = null) where T : AsyncEventArgs
{
var timeout = timeoutoverride ?? this.Config.Timeout;
using var waiter = new EventWaiter(this.Client);
var res = await waiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false);
return new InteractivityResult(res == null, res);
}
///
/// Collects the event arguments.
///
/// The predicate.
/// Override timeout period.
public async Task> CollectEventArgsAsync(Func predicate, TimeSpan? timeoutoverride = null) where T : AsyncEventArgs
{
var timeout = timeoutoverride ?? this.Config.Timeout;
using var waiter = new EventWaiter(this.Client);
var res = await waiter.CollectMatchesAsync(new CollectRequest(predicate, timeout)).ConfigureAwait(false);
return res;
}
///
/// Sends a paginated message with buttons.
///
/// The channel to send it on.
/// User to give control.
/// The pages.
/// Pagination buttons (pass null to use buttons defined in ).
/// Pagination behaviour.
/// Deletion behaviour
/// A custom cancellation token that can be cancelled at any point.
public async Task SendPaginatedMessageAsync(
DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons,
PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
{
var bhv = behaviour ?? this.Config.PaginationBehaviour;
var del = deletion ?? this.Config.ButtonBehavior;
var bts = buttons ?? this.Config.PaginationButtons;
bts = new(bts);
if (bhv is PaginationBehaviour.Ignore)
{
bts.SkipLeft.Disable();
bts.Left.Disable();
}
var builder = new DiscordMessageBuilder()
.WithContent(pages.First().Content)
.WithEmbed(pages.First().Embed)
.AddComponents(bts.ButtonArray);
var message = await builder.SendAsync(channel).ConfigureAwait(false);
var req = new ButtonPaginationRequest(message, user, bhv, del, bts, pages.ToArray(), token == default ? this.GetCancellationToken() : token);
await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false);
}
///
/// Sends a paginated message with buttons.
///
/// The channel to send it on.
/// User to give control.
/// The pages.
/// Pagination buttons (pass null to use buttons defined in ).
/// Pagination behaviour.
/// Deletion behaviour
/// Override timeout period.
public Task SendPaginatedMessageAsync(
DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutoverride,
PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default)
=> this.SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, this.GetCancellationToken(timeoutoverride));
///
/// Sends the paginated message.
///
/// The channel.
/// The user.
/// The pages.
/// The behaviour.
/// The deletion.
/// The token.
/// A Task.
public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
=> this.SendPaginatedMessageAsync(channel, user, pages, default, behaviour, deletion, token);
///
/// Sends the paginated message.
///
/// The channel.
/// The user.
/// The pages.
/// The timeoutoverride.
/// The behaviour.
/// The deletion.
/// A Task.
public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default)
=> this.SendPaginatedMessageAsync(channel, user, pages, timeoutoverride, behaviour, deletion);
///
/// Sends a paginated message.
/// For this Event you need the intent specified in
///
/// Channel to send paginated message in.
/// User to give control.
/// Pages.
/// Pagination emojis.
/// Pagination behaviour (when hitting max and min indices).
/// Deletion behaviour.
/// Override timeout period.
public async Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis,
PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutoverride = null)
{
var builder = new DiscordMessageBuilder()
.WithContent(pages.First().Content)
.WithEmbed(pages.First().Embed);
var m = await builder.SendAsync(channel).ConfigureAwait(false);
var timeout = timeoutoverride ?? this.Config.Timeout;
var bhv = behaviour ?? this.Config.PaginationBehaviour;
var del = deletion ?? this.Config.PaginationDeletion;
var ems = emojis ?? this.Config.PaginationEmojis;
var prequest = new PaginationRequest(m, user, bhv, del, ems, timeout, pages.ToArray());
await this.Paginator.DoPaginationAsync(prequest).ConfigureAwait(false);
}
///
/// Sends a paginated message in response to an interaction.
///
/// Pass the interaction directly. Interactivity will ACK it.
///
///
/// The interaction to create a response to.
/// Whether the response should be ephemeral.
/// The user to listen for button presses from.
/// The pages to paginate.
/// Optional: custom buttons
/// Pagination behaviour.
/// Deletion behaviour
/// A custom cancellation token that can be cancelled at any point.
public async Task SendPaginatedResponseAsync(DiscordInteraction interaction, bool ephemeral, DiscordUser user, IEnumerable pages, PaginationButtons buttons = null, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default)
{
var bhv = behaviour ?? this.Config.PaginationBehaviour;
var del = deletion ?? this.Config.ButtonBehavior;
var bts = buttons ?? this.Config.PaginationButtons;
bts = new(bts);
if (bhv is PaginationBehaviour.Ignore)
{
bts.SkipLeft.Disable();
bts.Left.Disable();
}
var builder = new DiscordInteractionResponseBuilder()
.WithContent(pages.First().Content)
.AddEmbed(pages.First().Embed)
.AsEphemeral(ephemeral)
.AddComponents(bts.ButtonArray);
await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder).ConfigureAwait(false);
var message = await interaction.GetOriginalResponseAsync().ConfigureAwait(false);
var req = new InteractionPaginationRequest(interaction, message, user, bhv, del, bts, pages, token);
await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false);
}
///
/// Waits for a custom pagination request to finish.
/// This does NOT handle removing emojis after finishing for you.
///
///
///
public async Task WaitForCustomPaginationAsync(IPaginationRequest request) => await this.Paginator.DoPaginationAsync(request).ConfigureAwait(false);
///
/// Waits for custom button-based pagination request to finish.
///
/// This does not invoke .
///
/// The request to wait for.
public async Task WaitForCustomComponentPaginationAsync(IPaginationRequest request) => await this._compPaginator.DoPaginationAsync(request).ConfigureAwait(false);
///
/// Generates pages from a string, and puts them in message content.
///
/// Input string.
/// How to split input string.
///
public IEnumerable GeneratePagesInContent(string input, SplitType splittype = SplitType.Character)
{
if (string.IsNullOrEmpty(input))
throw new ArgumentException("You must provide a string that is not null or empty!");
var result = new List();
List split;
switch (splittype)
{
default:
case SplitType.Character:
split = this.SplitString(input, 500).ToList();
break;
case SplitType.Line:
var subsplit = input.Split('\n');
split = new List();
var s = "";
for (var i = 0; i < subsplit.Length; i++)
{
s += subsplit[i];
if (i >= 15 && i % 15 == 0)
{
split.Add(s);
s = "";
}
}
if (split.All(x => x != s))
split.Add(s);
break;
}
var page = 1;
foreach (var s in split)
{
result.Add(new Page($"Page {page}:\n{s}"));
page++;
}
return result;
}
///
/// Generates pages from a string, and puts them in message embeds.
///
/// Input string.
/// How to split input string.
/// Base embed for output embeds.
///
public IEnumerable GeneratePagesInEmbed(string input, SplitType splittype = SplitType.Character, DiscordEmbedBuilder embedbase = null)
{
if (string.IsNullOrEmpty(input))
throw new ArgumentException("You must provide a string that is not null or empty!");
var embed = embedbase ?? new DiscordEmbedBuilder();
var result = new List();
List split;
switch (splittype)
{
default:
case SplitType.Character:
split = this.SplitString(input, 500).ToList();
break;
case SplitType.Line:
var subsplit = input.Split('\n');
split = new List();
var s = "";
for (var i = 0; i < subsplit.Length; i++)
{
s += $"{subsplit[i]}\n";
if (i % 15 == 0 && i != 0)
{
split.Add(s);
s = "";
}
}
if (!split.Any(x => x == s))
split.Add(s);
break;
}
var page = 1;
foreach (var s in split)
{
result.Add(new Page("", new DiscordEmbedBuilder(embed).WithDescription(s).WithFooter($"Page {page}/{split.Count}")));
page++;
}
return result;
}
///
/// Splits the string.
///
/// The string.
/// The chunk size.
private List SplitString(string str, int chunkSize)
{
var res = new List();
var len = str.Length;
var i = 0;
while (i < len)
{
var size = Math.Min(len - i, chunkSize);
res.Add(str.Substring(i, size));
i += size;
}
return res;
}
///
/// Gets the cancellation token.
///
/// The timeout.
private CancellationToken GetCancellationToken(TimeSpan? timeout = null) => new CancellationTokenSource(timeout ?? this.Config.Timeout).Token;
///
/// Handles an invalid interaction.
///
/// The interaction.
private async Task HandleInvalidInteraction(DiscordInteraction interaction)
{
var at = this.Config.ResponseBehavior switch
{
InteractionResponseBehavior.Ack => interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate),
InteractionResponseBehavior.Respond => interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new() { Content = this.Config.ResponseMessage, IsEphemeral = true}),
InteractionResponseBehavior.Ignore => Task.CompletedTask,
_ => throw new ArgumentException("Unknown enum value.")
};
await at;
}
}
}
diff --git a/DisCatSharp.Lavalink/LavalinkExtension.cs b/DisCatSharp.Lavalink/LavalinkExtension.cs
index fe1a40d54..4ebea4ce5 100644
--- a/DisCatSharp.Lavalink/LavalinkExtension.cs
+++ b/DisCatSharp.Lavalink/LavalinkExtension.cs
@@ -1,215 +1,215 @@
// 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.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Lavalink.EventArgs;
using DisCatSharp.Net;
using DisCatSharp.Common.Utilities;
namespace DisCatSharp.Lavalink
{
///
/// The lavalink extension.
///
public sealed class LavalinkExtension : BaseExtension
{
///
/// Triggered whenever a node disconnects.
///
public event AsyncEventHandler NodeDisconnected
{
add { this._nodeDisconnected.Register(value); }
remove { this._nodeDisconnected.Unregister(value); }
}
private AsyncEvent _nodeDisconnected;
///
/// Gets a dictionary of connected Lavalink nodes for the extension.
///
public IReadOnlyDictionary ConnectedNodes { get; }
private readonly ConcurrentDictionary _connectedNodes = new();
///
/// Creates a new instance of this Lavalink extension.
///
internal LavalinkExtension()
{
this.ConnectedNodes = new ReadOnlyConcurrentDictionary(this._connectedNodes);
}
///
/// 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._nodeDisconnected = new AsyncEvent("LAVALINK_NODE_DISCONNECTED", TimeSpan.Zero, this.Client.EventErrorHandler);
}
///
/// Connect to a Lavalink node.
///
/// Lavalink client configuration.
/// The established Lavalink connection.
public async Task ConnectAsync(LavalinkConfiguration config)
{
if (this._connectedNodes.ContainsKey(config.SocketEndpoint))
return this._connectedNodes[config.SocketEndpoint];
var con = new LavalinkNodeConnection(this.Client, this, config);
con.NodeDisconnected += this.Con_NodeDisconnected;
con.Disconnected += this.Con_Disconnected;
this._connectedNodes[con.NodeEndpoint] = con;
try
{
await con.StartAsync().ConfigureAwait(false);
}
catch
{
this.Con_NodeDisconnected(con);
throw;
}
return con;
}
///
/// Gets the Lavalink node connection for the specified endpoint.
///
/// Endpoint at which the node resides.
/// Lavalink node connection.
public LavalinkNodeConnection GetNodeConnection(ConnectionEndpoint endpoint)
=> this._connectedNodes.ContainsKey(endpoint) ? this._connectedNodes[endpoint] : null;
///
/// Gets a Lavalink node connection based on load balancing and an optional voice region.
///
/// The region to compare with the node's , if any.
/// The least load affected node connection, or null if no nodes are present.
public LavalinkNodeConnection GetIdealNodeConnection(DiscordVoiceRegion region = null)
{
if (this._connectedNodes.Count <= 1)
return this._connectedNodes.Values.FirstOrDefault();
var nodes = this._connectedNodes.Values.ToArray();
if (region != null)
{
var regionPredicate = new Func(x => x.Region == region);
if (nodes.Any(regionPredicate))
nodes = nodes.Where(regionPredicate).ToArray();
if (nodes.Count() <= 1)
return nodes.FirstOrDefault();
}
return this.FilterByLoad(nodes);
}
///
/// Gets a Lavalink guild connection from a .
///
/// The guild the connection is on.
/// The found guild connection, or null if one could not be found.
public LavalinkGuildConnection GetGuildConnection(DiscordGuild guild)
{
var nodes = this._connectedNodes.Values;
var node = nodes.FirstOrDefault(x => x._connectedGuilds.ContainsKey(guild.Id));
return node?.GetGuildConnection(guild);
}
///
/// Filters the by load.
///
/// The nodes.
private LavalinkNodeConnection FilterByLoad(LavalinkNodeConnection[] nodes)
{
Array.Sort(nodes, (a, b) =>
{
if (!a.Statistics._updated || !b.Statistics._updated)
return 0;
//https://github.com/FredBoat/Lavalink-Client/blob/48bc27784f57be5b95d2ff2eff6665451b9366f5/src/main/java/lavalink/client/io/LavalinkLoadBalancer.java#L122
//https://github.com/briantanner/eris-lavalink/blob/master/src/PlayerManager.js#L329
//player count
var aPenaltyCount = a.Statistics.ActivePlayers;
var bPenaltyCount = b.Statistics.ActivePlayers;
//cpu load
aPenaltyCount += (int)Math.Pow(1.05d, (100 * (a.Statistics.CpuSystemLoad / a.Statistics.CpuCoreCount) * 10) - 10);
bPenaltyCount += (int)Math.Pow(1.05d, (100 * (b.Statistics.CpuSystemLoad / a.Statistics.CpuCoreCount) * 10) - 10);
//frame load
if (a.Statistics.AverageDeficitFramesPerMinute > 0)
{
//deficit frame load
aPenaltyCount += (int)((Math.Pow(1.03d, 500f * (a.Statistics.AverageDeficitFramesPerMinute / 3000f)) * 600) - 600);
//null frame load
aPenaltyCount += (int)((Math.Pow(1.03d, 500f * (a.Statistics.AverageNulledFramesPerMinute / 3000f)) * 300) - 300);
}
//frame load
if (b.Statistics.AverageDeficitFramesPerMinute > 0)
{
//deficit frame load
bPenaltyCount += (int)((Math.Pow(1.03d, 500f * (b.Statistics.AverageDeficitFramesPerMinute / 3000f)) * 600) - 600);
//null frame load
bPenaltyCount += (int)((Math.Pow(1.03d, 500f * (b.Statistics.AverageNulledFramesPerMinute / 3000f)) * 300) - 300);
}
return aPenaltyCount - bPenaltyCount;
});
return nodes[0];
}
///
/// Removes a node.
///
/// The node to be removed.
private void Con_NodeDisconnected(LavalinkNodeConnection node)
=> this._connectedNodes.TryRemove(node.NodeEndpoint, out _);
///
/// Disconnects a node.
///
/// The affected node.
/// The node disconnected event args.
private Task Con_Disconnected(LavalinkNodeConnection node, NodeDisconnectedEventArgs e)
=> this._nodeDisconnected.InvokeAsync(node, e);
}
}
diff --git a/DisCatSharp.VoiceNext/VoiceNextExtension.cs b/DisCatSharp.VoiceNext/VoiceNextExtension.cs
index cb94f4006..7d5e1ce7d 100644
--- a/DisCatSharp.VoiceNext/VoiceNextExtension.cs
+++ b/DisCatSharp.VoiceNext/VoiceNextExtension.cs
@@ -1,261 +1,261 @@
// 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.Concurrent;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.EventArgs;
using DisCatSharp.Net;
using DisCatSharp.VoiceNext.Entities;
using Newtonsoft.Json;
namespace DisCatSharp.VoiceNext
{
///
/// Represents VoiceNext extension, which acts as Discord voice client.
///
public sealed class VoiceNextExtension : BaseExtension
{
///
/// Gets or sets the configuration.
///
private VoiceNextConfiguration Configuration { get; set; }
///
/// Gets or sets the active connections.
///
private ConcurrentDictionary ActiveConnections { get; set; }
///
/// Gets or sets the voice state updates.
///
private ConcurrentDictionary> VoiceStateUpdates { get; set; }
///
/// Gets or sets the voice server updates.
///
private ConcurrentDictionary> VoiceServerUpdates { get; set; }
///
/// Gets whether this connection has incoming voice enabled.
///
public bool IsIncomingEnabled { get; }
///
/// Initializes a new instance of the class.
///
/// The config.
internal VoiceNextExtension(VoiceNextConfiguration config)
{
this.Configuration = new VoiceNextConfiguration(config);
this.IsIncomingEnabled = config.EnableIncoming;
this.ActiveConnections = new ConcurrentDictionary();
this.VoiceStateUpdates = new ConcurrentDictionary>();
this.VoiceServerUpdates = new ConcurrentDictionary>();
}
///
/// 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.Client.VoiceStateUpdated += this.Client_VoiceStateUpdate;
this.Client.VoiceServerUpdated += this.Client_VoiceServerUpdate;
}
///
/// Create a VoiceNext connection for the specified channel.
///
/// Channel to connect to.
/// VoiceNext connection for this channel.
public async Task ConnectAsync(DiscordChannel channel)
{
if (channel.Type != ChannelType.Voice && channel.Type != ChannelType.Stage)
throw new ArgumentException(nameof(channel), "Invalid channel specified; needs to be voice or stage channel");
if (channel.Guild == null)
throw new ArgumentException(nameof(channel), "Invalid channel specified; needs to be guild channel");
if (!channel.PermissionsFor(channel.Guild.CurrentMember).HasPermission(Permissions.AccessChannels | Permissions.UseVoice))
throw new InvalidOperationException("You need AccessChannels and UseVoice permission to connect to this voice channel");
var gld = channel.Guild;
if (this.ActiveConnections.ContainsKey(gld.Id))
throw new InvalidOperationException("This guild already has a voice connection");
var vstut = new TaskCompletionSource();
var vsrut = new TaskCompletionSource();
this.VoiceStateUpdates[gld.Id] = vstut;
this.VoiceServerUpdates[gld.Id] = vsrut;
var vsd = new VoiceDispatch
{
OpCode = 4,
Payload = new VoiceStateUpdatePayload
{
GuildId = gld.Id,
ChannelId = channel.Id,
Deafened = false,
Muted = false
}
};
var vsj = JsonConvert.SerializeObject(vsd, Formatting.None);
await (channel.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false);
var vstu = await vstut.Task.ConfigureAwait(false);
var vstup = new VoiceStateUpdatePayload
{
SessionId = vstu.SessionId,
UserId = vstu.User.Id
};
var vsru = await vsrut.Task.ConfigureAwait(false);
var vsrup = new VoiceServerUpdatePayload
{
Endpoint = vsru.Endpoint,
GuildId = vsru.Guild.Id,
Token = vsru.VoiceToken
};
var vnc = new VoiceNextConnection(this.Client, gld, channel, this.Configuration, vsrup, vstup);
vnc.VoiceDisconnected += this.Vnc_VoiceDisconnected;
await vnc.ConnectAsync().ConfigureAwait(false);
await vnc.WaitForReadyAsync().ConfigureAwait(false);
this.ActiveConnections[gld.Id] = vnc;
return vnc;
}
///
/// Gets a VoiceNext connection for specified guild.
///
/// Guild to get VoiceNext connection for.
/// VoiceNext connection for the specified guild.
public VoiceNextConnection GetConnection(DiscordGuild guild) => this.ActiveConnections.ContainsKey(guild.Id) ? this.ActiveConnections[guild.Id] : null;
///
/// Vnc_S the voice disconnected.
///
/// The guild.
/// A Task.
private async Task Vnc_VoiceDisconnected(DiscordGuild guild)
{
VoiceNextConnection vnc = null;
if (this.ActiveConnections.ContainsKey(guild.Id))
this.ActiveConnections.TryRemove(guild.Id, out vnc);
var vsd = new VoiceDispatch
{
OpCode = 4,
Payload = new VoiceStateUpdatePayload
{
GuildId = guild.Id,
ChannelId = null
}
};
var vsj = JsonConvert.SerializeObject(vsd, Formatting.None);
await (guild.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false);
}
///
/// Client_S the voice state update.
///
/// The client.
/// The e.
/// A Task.
private Task Client_VoiceStateUpdate(DiscordClient client, VoiceStateUpdateEventArgs e)
{
var gld = e.Guild;
if (gld == null)
return Task.CompletedTask;
if (e.User == null)
return Task.CompletedTask;
if (e.User.Id == this.Client.CurrentUser.Id)
{
if (e.After.Channel == null && this.ActiveConnections.TryRemove(gld.Id, out var ac))
ac.Disconnect();
if (this.ActiveConnections.TryGetValue(e.Guild.Id, out var vnc))
vnc.TargetChannel = e.Channel;
if (!string.IsNullOrWhiteSpace(e.SessionId) && e.Channel != null && this.VoiceStateUpdates.TryRemove(gld.Id, out var xe))
xe.SetResult(e);
}
return Task.CompletedTask;
}
///
/// Client_S the voice server update.
///
/// The client.
/// The e.
/// A Task.
private async Task Client_VoiceServerUpdate(DiscordClient client, VoiceServerUpdateEventArgs e)
{
var gld = e.Guild;
if (gld == null)
return;
if (this.ActiveConnections.TryGetValue(e.Guild.Id, out var vnc))
{
vnc.ServerData = new VoiceServerUpdatePayload
{
Endpoint = e.Endpoint,
GuildId = e.Guild.Id,
Token = e.VoiceToken
};
var eps = e.Endpoint;
var epi = eps.LastIndexOf(':');
var eph = string.Empty;
var epp = 443;
if (epi != -1)
{
eph = eps.Substring(0, epi);
epp = int.Parse(eps.Substring(epi + 1));
}
else
{
eph = eps;
}
vnc.WebSocketEndpoint = new ConnectionEndpoint { Hostname = eph, Port = epp };
vnc.Resume = false;
await vnc.ReconnectAsync().ConfigureAwait(false);
}
if (this.VoiceServerUpdates.ContainsKey(gld.Id))
{
this.VoiceServerUpdates.TryRemove(gld.Id, out var xe);
xe.SetResult(e);
}
}
}
}
diff --git a/DisCatSharp/Clients/DiscordClient.cs b/DisCatSharp/Clients/DiscordClient.cs
index c2a8461a7..2b2007d39 100644
--- a/DisCatSharp/Clients/DiscordClient.cs
+++ b/DisCatSharp/Clients/DiscordClient.cs
@@ -1,1314 +1,1314 @@
// 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.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.EventArgs;
using DisCatSharp.Exceptions;
using DisCatSharp.Net;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Models;
using DisCatSharp.Net.Serialization;
using DisCatSharp.Common.Utilities;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using DisCatSharp.Enums;
using System.Globalization;
namespace DisCatSharp
{
///
/// A Discord API wrapper.
///
public sealed partial class DiscordClient : BaseDiscordClient
{
#region Internal Fields/Properties
internal bool _isShard = false;
///
/// Gets the message cache.
///
internal RingBuffer MessageCache { get; }
private List _extensions = new();
private StatusUpdate _status = null;
///
/// Gets the connection lock.
///
private ManualResetEventSlim ConnectionLock { get; } = new ManualResetEventSlim(true);
#endregion
#region Public Fields/Properties
///
/// Gets the gateway protocol version.
///
public int GatewayVersion { get; internal set; }
///
/// Gets the gateway session information for this client.
///
public GatewayInfo GatewayInfo { get; internal set; }
///
/// Gets the gateway URL.
///
public Uri GatewayUri { get; internal set; }
///
/// Gets the total number of shards the bot is connected to.
///
public int ShardCount => this.GatewayInfo != null
? this.GatewayInfo.ShardCount
: this.Configuration.ShardCount;
///
/// Gets the currently connected shard ID.
///
public int ShardId
=> this.Configuration.ShardId;
///
/// Gets the intents configured for this client.
///
public DiscordIntents Intents
=> this.Configuration.Intents;
///
/// Gets a dictionary of guilds that this client is in. The dictionary's key is the guild ID. Note that the
/// guild objects in this dictionary will not be filled in if the specific guilds aren't available (the
/// or events haven't been fired yet)
///
public override IReadOnlyDictionary Guilds { get; }
internal ConcurrentDictionary _guilds = new();
///
/// Gets the WS latency for this client.
///
public int Ping
=> Volatile.Read(ref this._ping);
private int _ping;
///
/// Gets the collection of presences held by this client.
///
public IReadOnlyDictionary Presences
=> this._presencesLazy.Value;
internal Dictionary _presences = new();
private Lazy> _presencesLazy;
///
/// Gets the collection of presences held by this client.
///
public IReadOnlyDictionary EmbeddedActivities
=> this._embeddedActivitiesLazy.Value;
internal Dictionary _embeddedActivities = new();
private Lazy> _embeddedActivitiesLazy;
#endregion
#region Constructor/Internal Setup
///
/// Initializes a new instance of .
///
/// Specifies configuration parameters.
public DiscordClient(DiscordConfiguration config)
: base(config)
{
if (this.Configuration.MessageCacheSize > 0)
{
var intents = this.Configuration.Intents;
this.MessageCache = intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages)
? new RingBuffer(this.Configuration.MessageCacheSize)
: null;
}
this.InternalSetup();
this.Guilds = new ReadOnlyConcurrentDictionary(this._guilds);
}
///
/// Internal setup of the Client.
///
internal void InternalSetup()
{
this._clientErrored = new AsyncEvent("CLIENT_ERRORED", EventExecutionLimit, this.Goof);
this._socketErrored = new AsyncEvent("SOCKET_ERRORED", EventExecutionLimit, this.Goof);
this._socketOpened = new AsyncEvent("SOCKET_OPENED", EventExecutionLimit, this.EventErrorHandler);
this._socketClosed = new AsyncEvent("SOCKET_CLOSED", EventExecutionLimit, this.EventErrorHandler);
this._ready = new AsyncEvent("READY", EventExecutionLimit, this.EventErrorHandler);
this._resumed = new AsyncEvent("RESUMED", EventExecutionLimit, this.EventErrorHandler);
this._channelCreated = new AsyncEvent("CHANNEL_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._channelUpdated = new AsyncEvent("CHANNEL_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._channelDeleted = new AsyncEvent("CHANNEL_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._dmChannelDeleted = new AsyncEvent("DM_CHANNEL_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._channelPinsUpdated = new AsyncEvent("CHANNEL_PINS_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildCreated = new AsyncEvent("GUILD_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._guildAvailable = new AsyncEvent("GUILD_AVAILABLE", EventExecutionLimit, this.EventErrorHandler);
this._guildUpdated = new AsyncEvent("GUILD_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildDeleted = new AsyncEvent("GUILD_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._guildUnavailable = new AsyncEvent("GUILD_UNAVAILABLE", EventExecutionLimit, this.EventErrorHandler);
this._guildDownloadCompletedEv = new AsyncEvent("GUILD_DOWNLOAD_COMPLETED", EventExecutionLimit, this.EventErrorHandler);
this._inviteCreated = new AsyncEvent("INVITE_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._inviteDeleted = new AsyncEvent("INVITE_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._messageCreated = new AsyncEvent("MESSAGE_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._presenceUpdated = new AsyncEvent("PRESENCE_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildBanAdded = new AsyncEvent("GUILD_BAN_ADD", EventExecutionLimit, this.EventErrorHandler);
this._guildBanRemoved = new AsyncEvent("GUILD_BAN_REMOVED", EventExecutionLimit, this.EventErrorHandler);
this._guildEmojisUpdated = new AsyncEvent("GUILD_EMOJI_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildStickersUpdated = new AsyncEvent("GUILD_STICKER_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildIntegrationsUpdated = new AsyncEvent("GUILD_INTEGRATIONS_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildMemberAdded = new AsyncEvent("GUILD_MEMBER_ADD", EventExecutionLimit, this.EventErrorHandler);
this._guildMemberRemoved = new AsyncEvent("GUILD_MEMBER_REMOVED", EventExecutionLimit, this.EventErrorHandler);
this._guildMemberUpdated = new AsyncEvent("GUILD_MEMBER_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildRoleCreated = new AsyncEvent("GUILD_ROLE_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._guildRoleUpdated = new AsyncEvent("GUILD_ROLE_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildRoleDeleted = new AsyncEvent("GUILD_ROLE_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._messageAcknowledged = new AsyncEvent("MESSAGE_ACKNOWLEDGED", EventExecutionLimit, this.EventErrorHandler);
this._messageUpdated = new AsyncEvent("MESSAGE_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._messageDeleted = new AsyncEvent("MESSAGE_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._messagesBulkDeleted = new AsyncEvent("MESSAGE_BULK_DELETED", EventExecutionLimit, this.EventErrorHandler);
this._interactionCreated = new AsyncEvent("INTERACTION_CREATED", EventExecutionLimit, this.EventErrorHandler);
this._componentInteractionCreated = new AsyncEvent("COMPONENT_INTERACTED", EventExecutionLimit, this.EventErrorHandler);
this._contextMenuInteractionCreated = new AsyncEvent("CONTEXT_MENU_INTERACTED", EventExecutionLimit, this.EventErrorHandler);
this._typingStarted = new AsyncEvent("TYPING_STARTED", EventExecutionLimit, this.EventErrorHandler);
this._userSettingsUpdated = new AsyncEvent("USER_SETTINGS_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._userUpdated = new AsyncEvent("USER_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._voiceStateUpdated = new AsyncEvent("VOICE_STATE_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._voiceServerUpdated = new AsyncEvent("VOICE_SERVER_UPDATED", EventExecutionLimit, this.EventErrorHandler);
this._guildMembersChunked = new AsyncEvent("GUILD_MEMBERS_CHUNKED", EventExecutionLimit, this.EventErrorHandler);
this._unknownEvent = new AsyncEvent("UNKNOWN_EVENT", EventExecutionLimit, this.EventErrorHandler);
this._messageReactionAdded = new AsyncEvent