diff --git a/DisCatSharp.CommandsNext/CommandsNextExtension.cs b/DisCatSharp.CommandsNext/CommandsNextExtension.cs
index 9a03591a6..cb1507467 100644
--- a/DisCatSharp.CommandsNext/CommandsNextExtension.cs
+++ b/DisCatSharp.CommandsNext/CommandsNextExtension.cs
@@ -1,1086 +1,1081 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 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.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.EventArgs;
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 readonly CommandsNextConfiguration _config;
///
/// Gets the help formatter.
///
private readonly HelpFormatterFactory _helpFormatter;
///
/// Gets the convert generic.
///
private readonly MethodInfo _convertGeneric;
///
/// Gets the user friendly type names.
///
private readonly Dictionary _userFriendlyTypeNames;
///
/// 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(),
[typeof(DiscordScheduledEvent)] = new DiscordScheduledEventConverter(),
};
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",
[typeof(DiscordScheduledEvent)] = "event"
};
foreach (var xt in this.ArgumentConverters.Keys.ToArray())
{
var xti = xt.GetTypeInfo();
if (!xti.IsValueType)
continue;
var xcvt = typeof(NullableConverter<>).MakeGenericType(xt);
var xnt = typeof(Nullable<>).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 = this.GetType();
var ms = t.GetTypeInfo().DeclaredMethods;
var m = ms.FirstOrDefault(xm => xm.Name == "ConvertArgumentToObj" && xm.ContainsGenericParameters && !xm.IsStatic && xm.IsPrivate);
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();
foreach (var cb in tcmds)
cb.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[..mpos];
var cnt = e.Message.Content[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[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[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 readonly Dictionary _topLevelCommands;
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[0..^5];
else if (moduleName.EndsWith("Module") && moduleName != "Module")
moduleName = moduleName[0..^6];
else if (moduleName.EndsWith("Commands") && moduleName != "Commands")
moduleName = moduleName[0..^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[0..^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));
}
///
/// Unregister 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,
AttachmentsInternal = new List(),
EmbedsInternal = new List(),
TimestampRaw = now.ToString("yyyy-MM-ddTHH:mm:sszzz"),
ReactionsInternal = 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.MembersInternal.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.MentionedUsersInternal = mentionedUsers;
msg.MentionedRolesInternal = mentionedRoles;
msg.MentionedChannelsInternal = 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.
public async Task ConvertArgument(string value, CommandContext ctx)
{
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.
public async Task ConvertArgument(string value, CommandContext ctx, Type type)
{
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;
}
///
/// Unregister 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);
+ this.ArgumentConverters.Remove(t);
- if (this._userFriendlyTypeNames.ContainsKey(t))
- this._userFriendlyTypeNames.Remove(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];
+ if (this._userFriendlyTypeNames.TryGetValue(t, out var userFriendlyTypeName))
+ return userFriendlyTypeName;
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;
- }
+ if (!ti.IsGenericTypeDefinition || t.GetGenericTypeDefinition() != typeof(Nullable<>)) return t.Name;
+ var tn = ti.GenericTypeArguments[0];
+ return this._userFriendlyTypeNames.TryGetValue(tn, out var name) ? name : tn.Name;
- return t.Name;
}
#endregion
#region Helpers
///
/// Allows easier interoperability with reflection by turning the returned by
/// into a task containing , using the provided generic type information.
///
private async Task ConvertArgumentToObj(string value, CommandContext ctx)
=> await this.ConvertArgument(value, ctx).ConfigureAwait(false);
///
/// 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;
///
/// Fires when a command gets executed.
///
/// The command execution event arguments.
private Task OnCommandExecuted(CommandExecutionEventArgs e)
=> this._executed.InvokeAsync(this, e);
///
/// Fires when a command fails.
///
/// The command error event arguments.
private Task OnCommandErrored(CommandErrorEventArgs e)
=> this._error.InvokeAsync(this, e);
#endregion
}
diff --git a/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs b/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs
index 6e9ecae0f..03ff382cf 100644
--- a/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs
+++ b/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs
@@ -1,320 +1,318 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 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.Interactivity.Enums;
namespace DisCatSharp.Interactivity.EventHandling
{
///
/// The pagination request.
///
internal class PaginationRequest : IPaginationRequest
{
private TaskCompletionSource _tcs;
private readonly CancellationTokenSource _ct;
private readonly TimeSpan _timeout;
private readonly List _pages;
private readonly PaginationBehaviour _behaviour;
private readonly DiscordMessage _message;
private readonly PaginationEmojis _emojis;
private readonly DiscordUser _user;
private int _index;
///
/// Creates a new Pagination request
///
/// Message to paginate
/// User to allow control for
/// Behaviour during pagination
/// Behavior on pagination end
/// Emojis for this pagination object
/// Timeout time
/// Pagination pages
internal PaginationRequest(DiscordMessage message, DiscordUser user, PaginationBehaviour behaviour, PaginationDeletion deletion,
PaginationEmojis emojis, TimeSpan timeout, IEnumerable pages)
{
this._tcs = new TaskCompletionSource();
this._ct = new CancellationTokenSource(timeout);
this._ct.Token.Register(() => this._tcs.TrySetResult(true));
this._timeout = timeout;
this._message = message;
this._user = user;
this.PaginationDeletion = deletion;
this._behaviour = behaviour;
this._emojis = emojis;
this._pages = new List();
foreach (var p in pages)
{
this._pages.Add(p);
}
}
///
/// Gets the page count.
///
public int PageCount => this._pages.Count;
///
/// Gets the pagination deletion.
///
public PaginationDeletion PaginationDeletion { get; }
///
/// Gets the page async.
///
/// A Task.
public async Task GetPageAsync()
{
await Task.Yield();
return this._pages[this._index];
}
///
/// Skips the left async.
///
/// A Task.
public async Task SkipLeftAsync()
{
await Task.Yield();
this._index = 0;
}
///
/// Skips the right async.
///
/// A Task.
public async Task SkipRightAsync()
{
await Task.Yield();
this._index = this._pages.Count - 1;
}
///
/// Nexts the page async.
///
/// A Task.
public async Task NextPageAsync()
{
await Task.Yield();
switch (this._behaviour)
{
case PaginationBehaviour.Ignore:
if (this._index == this._pages.Count - 1)
break;
else
this._index++;
break;
case PaginationBehaviour.WrapAround:
if (this._index == this._pages.Count - 1)
this._index = 0;
else
this._index++;
break;
}
}
///
/// Previous the page async.
///
/// A Task.
public async Task PreviousPageAsync()
{
await Task.Yield();
switch (this._behaviour)
{
case PaginationBehaviour.Ignore:
if (this._index == 0)
break;
else
this._index--;
break;
case PaginationBehaviour.WrapAround:
if (this._index == 0)
this._index = this._pages.Count - 1;
else
this._index--;
break;
}
}
///
/// Gets the buttons async.
///
///
-#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
- public async Task> GetButtonsAsync()
-#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
+ public Task> GetButtonsAsync()
=> throw new NotSupportedException("This request does not support buttons.");
///
/// Gets the emojis async.
///
/// A Task.
public async Task GetEmojisAsync()
{
await Task.Yield();
return this._emojis;
}
///
/// Gets the message async.
///
/// A Task.
public async Task GetMessageAsync()
{
await Task.Yield();
return this._message;
}
///
/// Gets the user async.
///
/// A Task.
public async Task GetUserAsync()
{
await Task.Yield();
return this._user;
}
///
/// Dos the cleanup async.
///
/// A Task.
public async Task DoCleanupAsync()
{
switch (this.PaginationDeletion)
{
case PaginationDeletion.DeleteEmojis:
await this._message.DeleteAllReactionsAsync().ConfigureAwait(false);
break;
case PaginationDeletion.DeleteMessage:
await this._message.DeleteAsync().ConfigureAwait(false);
break;
case PaginationDeletion.KeepEmojis:
break;
}
}
///
/// Gets the task completion source async.
///
/// A Task.
public async Task> GetTaskCompletionSourceAsync()
{
await Task.Yield();
return this._tcs;
}
~PaginationRequest()
{
this.Dispose();
}
///
/// Disposes this PaginationRequest.
///
public void Dispose()
{
this._ct.Dispose();
this._tcs = null;
}
}
}
namespace DisCatSharp.Interactivity
{
///
/// The pagination emojis.
///
public class PaginationEmojis
{
public DiscordEmoji SkipLeft;
public DiscordEmoji SkipRight;
public DiscordEmoji Left;
public DiscordEmoji Right;
public DiscordEmoji Stop;
///
/// Initializes a new instance of the class.
///
public PaginationEmojis()
{
this.Left = DiscordEmoji.FromUnicode("◀");
this.Right = DiscordEmoji.FromUnicode("▶");
this.SkipLeft = DiscordEmoji.FromUnicode("⏮");
this.SkipRight = DiscordEmoji.FromUnicode("⏭");
this.Stop = DiscordEmoji.FromUnicode("⏹");
}
}
///
/// The page.
///
public class Page
{
///
/// Gets or sets the content.
///
public string Content { get; set; }
///
/// Gets or sets the embed.
///
public DiscordEmbed Embed { get; set; }
///
/// Initializes a new instance of the class.
///
/// The content.
/// The embed.
public Page(string content = "", DiscordEmbedBuilder embed = null)
{
this.Content = content;
this.Embed = embed?.Build();
}
}
}
diff --git a/DisCatSharp.Lavalink/LavalinkExtension.cs b/DisCatSharp.Lavalink/LavalinkExtension.cs
index c6757898b..5ce30b0d3 100644
--- a/DisCatSharp.Lavalink/LavalinkExtension.cs
+++ b/DisCatSharp.Lavalink/LavalinkExtension.cs
@@ -1,215 +1,215 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 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.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.Lavalink.EventArgs;
using DisCatSharp.Net;
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];
+ if (this._connectedNodes.TryGetValue(config.SocketEndpoint, out var async))
+ return async;
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;
+ => this._connectedNodes.TryGetValue(endpoint, out var ep) ? ep : 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.Length <= 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.ConnectedGuildsInternal.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.Lavalink/LavalinkNodeConnection.cs b/DisCatSharp.Lavalink/LavalinkNodeConnection.cs
index d26f55d8b..60cbca117 100644
--- a/DisCatSharp.Lavalink/LavalinkNodeConnection.cs
+++ b/DisCatSharp.Lavalink/LavalinkNodeConnection.cs
@@ -1,626 +1,626 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 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.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Lavalink.Entities;
using DisCatSharp.Lavalink.EventArgs;
using DisCatSharp.Net;
using DisCatSharp.Net.WebSocket;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DisCatSharp.Lavalink;
internal delegate void NodeDisconnectedEventHandler(LavalinkNodeConnection node);
///
/// Represents a connection to a Lavalink node.
///
public sealed class LavalinkNodeConnection
{
///
/// Triggered whenever Lavalink WebSocket throws an exception.
///
public event AsyncEventHandler LavalinkSocketErrored
{
add => this._lavalinkSocketError.Register(value);
remove => this._lavalinkSocketError.Unregister(value);
}
private readonly AsyncEvent _lavalinkSocketError;
///
/// Triggered when this node disconnects.
///
public event AsyncEventHandler Disconnected
{
add => this._disconnected.Register(value);
remove => this._disconnected.Unregister(value);
}
private readonly AsyncEvent _disconnected;
///
/// Triggered when this node receives a statistics update.
///
public event AsyncEventHandler StatisticsReceived
{
add => this._statsReceived.Register(value);
remove => this._statsReceived.Unregister(value);
}
private readonly AsyncEvent _statsReceived;
///
/// Triggered whenever any of the players on this node is updated.
///
public event AsyncEventHandler PlayerUpdated
{
add => this._playerUpdated.Register(value);
remove => this._playerUpdated.Unregister(value);
}
private readonly AsyncEvent _playerUpdated;
///
/// Triggered whenever playback of a track starts.
/// This is only available for version 3.3.1 and greater.
///
public event AsyncEventHandler PlaybackStarted
{
add => this._playbackStarted.Register(value);
remove => this._playbackStarted.Unregister(value);
}
private readonly AsyncEvent _playbackStarted;
///
/// Triggered whenever playback of a track finishes.
///
public event AsyncEventHandler PlaybackFinished
{
add => this._playbackFinished.Register(value);
remove => this._playbackFinished.Unregister(value);
}
private readonly AsyncEvent _playbackFinished;
///
/// Triggered whenever playback of a track gets stuck.
///
public event AsyncEventHandler TrackStuck
{
add => this._trackStuck.Register(value);
remove => this._trackStuck.Unregister(value);
}
private readonly AsyncEvent _trackStuck;
///
/// Triggered whenever playback of a track encounters an error.
///
public event AsyncEventHandler TrackException
{
add => this._trackException.Register(value);
remove => this._trackException.Unregister(value);
}
private readonly AsyncEvent _trackException;
///
/// Gets the remote endpoint of this Lavalink node connection.
///
public ConnectionEndpoint NodeEndpoint => this.Configuration.SocketEndpoint;
///
/// Gets whether the client is connected to Lavalink.
///
public bool IsConnected => !Volatile.Read(ref this._isDisposed);
private bool _isDisposed;
private int _backoff;
///
/// The minimum backoff.
///
private const int MINIMUM_BACKOFF = 7500;
///
/// The maximum backoff.
///
private const int MAXIMUM_BACKOFF = 120000;
///
/// Gets the current resource usage statistics.
///
public LavalinkStatistics Statistics { get; }
///
/// Gets a dictionary of Lavalink guild connections for this node.
///
public IReadOnlyDictionary ConnectedGuilds { get; }
internal ConcurrentDictionary ConnectedGuildsInternal = new();
///
/// Gets the REST client for this Lavalink connection.
///
public LavalinkRestClient Rest { get; }
///
/// Gets the parent extension which this node connection belongs to.
///
public LavalinkExtension Parent { get; }
///
/// Gets the Discord client this node connection belongs to.
///
public DiscordClient Discord { get; }
///
/// Gets the configuration.
///
internal LavalinkConfiguration Configuration { get; }
///
/// Gets the region.
///
internal DiscordVoiceRegion Region { get; }
///
/// Gets or sets the web socket.
///
private IWebSocketClient _webSocket;
///
/// Gets the voice state updates.
///
private readonly ConcurrentDictionary> _voiceStateUpdates;
///
/// Gets the voice server updates.
///
private readonly ConcurrentDictionary> _voiceServerUpdates;
///
/// Initializes a new instance of the class.
///
/// The client.
/// the event.tension.
/// The config.
internal LavalinkNodeConnection(DiscordClient client, LavalinkExtension extension, LavalinkConfiguration config)
{
this.Discord = client;
this.Parent = extension;
this.Configuration = new LavalinkConfiguration(config);
if (config.Region != null && this.Discord.VoiceRegions.Values.Contains(config.Region))
this.Region = config.Region;
this.ConnectedGuilds = new ReadOnlyConcurrentDictionary(this.ConnectedGuildsInternal);
this.Statistics = new LavalinkStatistics();
this._lavalinkSocketError = new AsyncEvent("LAVALINK_SOCKET_ERROR", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._disconnected = new AsyncEvent("LAVALINK_NODE_DISCONNECTED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._statsReceived = new AsyncEvent("LAVALINK_STATS_RECEIVED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._playerUpdated = new AsyncEvent("LAVALINK_PLAYER_UPDATED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._playbackStarted = new AsyncEvent("LAVALINK_PLAYBACK_STARTED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._playbackFinished = new AsyncEvent("LAVALINK_PLAYBACK_FINISHED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._trackStuck = new AsyncEvent("LAVALINK_TRACK_STUCK", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._trackException = new AsyncEvent("LAVALINK_TRACK_EXCEPTION", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._voiceServerUpdates = new ConcurrentDictionary>();
this._voiceStateUpdates = new ConcurrentDictionary>();
this.Discord.VoiceStateUpdated += this.Discord_VoiceStateUpdated;
this.Discord.VoiceServerUpdated += this.Discord_VoiceServerUpdated;
this.Rest = new LavalinkRestClient(this.Configuration, this.Discord);
Volatile.Write(ref this._isDisposed, false);
}
///
/// Establishes a connection to the Lavalink node.
///
///
internal async Task StartAsync()
{
if (this.Discord?.CurrentUser?.Id == null || this.Discord?.ShardCount == null)
throw new InvalidOperationException("This operation requires the Discord client to be fully initialized.");
this._webSocket = this.Discord.Configuration.WebSocketClientFactory(this.Discord.Configuration.Proxy, this.Discord.ServiceProvider);
this._webSocket.Connected += this.WebSocket_OnConnect;
this._webSocket.Disconnected += this.WebSocket_OnDisconnect;
this._webSocket.ExceptionThrown += this.WebSocket_OnException;
this._webSocket.MessageReceived += this.WebSocket_OnMessage;
this._webSocket.AddDefaultHeader("Authorization", this.Configuration.Password);
this._webSocket.AddDefaultHeader("Num-Shards", this.Discord.ShardCount.ToString(CultureInfo.InvariantCulture));
this._webSocket.AddDefaultHeader("User-Id", this.Discord.CurrentUser.Id.ToString(CultureInfo.InvariantCulture));
this._webSocket.AddDefaultHeader("Client-Name", $"DisCatSharp.Lavalink version {this.Discord.VersionString}");
if (this.Configuration.ResumeKey != null)
this._webSocket.AddDefaultHeader("Resume-Key", this.Configuration.ResumeKey);
do
{
try
{
if (this._backoff != 0)
{
await Task.Delay(this._backoff).ConfigureAwait(false);
this._backoff = Math.Min(this._backoff * 2, MAXIMUM_BACKOFF);
}
else
{
this._backoff = MINIMUM_BACKOFF;
}
await this._webSocket.ConnectAsync(new Uri(this.Configuration.SocketEndpoint.ToWebSocketString())).ConfigureAwait(false);
break;
}
catch (PlatformNotSupportedException)
{ throw; }
catch (NotImplementedException)
{ throw; }
catch (Exception ex)
{
if (!this.Configuration.SocketAutoReconnect || this._backoff == MAXIMUM_BACKOFF)
{
this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, ex, "Failed to connect to Lavalink.");
throw;
}
else
{
this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, ex, $"Failed to connect to Lavalink, retrying in {this._backoff} ms.");
}
}
}
while (this.Configuration.SocketAutoReconnect);
Volatile.Write(ref this._isDisposed, false);
}
///
/// Stops this Lavalink node connection and frees resources.
///
///
public async Task StopAsync()
{
foreach (var kvp in this.ConnectedGuildsInternal)
await kvp.Value.DisconnectAsync().ConfigureAwait(false);
this.NodeDisconnected?.Invoke(this);
Volatile.Write(ref this._isDisposed, true);
await this._webSocket.DisconnectAsync().ConfigureAwait(false);
// this should not be here, no?
//await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this)).ConfigureAwait(false);
}
///
/// Connects this Lavalink node to specified Discord channel.
///
/// Voice channel to connect to.
/// Channel connection, which allows for playback control.
public async Task ConnectAsync(DiscordChannel channel)
{
- if (this.ConnectedGuildsInternal.ContainsKey(channel.Guild.Id))
- return this.ConnectedGuildsInternal[channel.Guild.Id];
+ if (this.ConnectedGuildsInternal.TryGetValue(channel.Guild.Id, out var connectedGuild))
+ return connectedGuild;
if (channel.Guild == null || (channel.Type != ChannelType.Voice && channel.Type != ChannelType.Stage))
throw new ArgumentException("Invalid channel specified.", nameof(channel));
var vstut = new TaskCompletionSource();
var vsrut = new TaskCompletionSource();
this._voiceStateUpdates[channel.Guild.Id] = vstut;
this._voiceServerUpdates[channel.Guild.Id] = vsrut;
var vsd = new VoiceDispatch
{
OpCode = 4,
Payload = new VoiceStateUpdatePayload
{
GuildId = channel.Guild.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 vsru = await vsrut.Task.ConfigureAwait(false);
await this.SendPayloadAsync(new LavalinkVoiceUpdate(vstu, vsru)).ConfigureAwait(false);
var con = new LavalinkGuildConnection(this, channel, vstu);
con.ChannelDisconnected += this.Con_ChannelDisconnected;
con.PlayerUpdated += (s, e) => this._playerUpdated.InvokeAsync(s, e);
con.PlaybackStarted += (s, e) => this._playbackStarted.InvokeAsync(s, e);
con.PlaybackFinished += (s, e) => this._playbackFinished.InvokeAsync(s, e);
con.TrackStuck += (s, e) => this._trackStuck.InvokeAsync(s, e);
con.TrackException += (s, e) => this._trackException.InvokeAsync(s, e);
this.ConnectedGuildsInternal[channel.Guild.Id] = con;
return con;
}
///
/// Gets a Lavalink connection to specified Discord channel.
///
/// Guild to get connection for.
/// Channel connection, which allows for playback control.
public LavalinkGuildConnection GetGuildConnection(DiscordGuild guild)
=> this.ConnectedGuildsInternal.TryGetValue(guild.Id, out var lgc) && lgc.IsConnected ? lgc : null;
///
/// Sends the payload async.
///
/// The payload.
internal async Task SendPayloadAsync(LavalinkPayload payload)
=> await this.WsSendAsync(JsonConvert.SerializeObject(payload, Formatting.None)).ConfigureAwait(false);
///
/// Webs the socket_ on message.
///
/// The client.
/// the event.ent.
private async Task WebSocket_OnMessage(IWebSocketClient client, SocketMessageEventArgs e)
{
if (e is not SocketTextMessageEventArgs et)
{
this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, "Lavalink sent binary data - unable to process");
return;
}
this.Discord.Logger.LogTrace(LavalinkEvents.LavalinkWsRx, et.Message);
var json = et.Message;
var jsonData = JObject.Parse(json);
switch (jsonData["op"].ToString())
{
case "playerUpdate":
var gid = (ulong)jsonData["guildId"];
var state = jsonData["state"].ToObject();
if (this.ConnectedGuildsInternal.TryGetValue(gid, out var lvl))
await lvl.InternalUpdatePlayerStateAsync(state).ConfigureAwait(false);
break;
case "stats":
var statsRaw = jsonData.ToObject();
this.Statistics.Update(statsRaw);
await this._statsReceived.InvokeAsync(this, new StatisticsReceivedEventArgs(this.Discord.ServiceProvider, this.Statistics)).ConfigureAwait(false);
break;
case "event":
var evtype = jsonData["type"].ToObject();
var guildId = (ulong)jsonData["guildId"];
switch (evtype)
{
case EventType.TrackStartEvent:
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvtst))
await lvlEvtst.InternalPlaybackStartedAsync(jsonData["track"].ToString()).ConfigureAwait(false);
break;
case EventType.TrackEndEvent:
var reason = TrackEndReason.Cleanup;
switch (jsonData["reason"].ToString())
{
case "FINISHED":
reason = TrackEndReason.Finished;
break;
case "LOAD_FAILED":
reason = TrackEndReason.LoadFailed;
break;
case "STOPPED":
reason = TrackEndReason.Stopped;
break;
case "REPLACED":
reason = TrackEndReason.Replaced;
break;
case "CLEANUP":
reason = TrackEndReason.Cleanup;
break;
}
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvtf))
await lvlEvtf.InternalPlaybackFinishedAsync(new TrackFinishData { Track = jsonData["track"].ToString(), Reason = reason }).ConfigureAwait(false);
break;
case EventType.TrackStuckEvent:
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvts))
await lvlEvts.InternalTrackStuckAsync(new TrackStuckData { Track = jsonData["track"].ToString(), Threshold = (long)jsonData["thresholdMs"] }).ConfigureAwait(false);
break;
case EventType.TrackExceptionEvent:
var severity = LoadFailedSeverity.Common;
switch (jsonData["severity"].ToString())
{
case "COMMON":
severity = LoadFailedSeverity.Common;
break;
case "SUSPICIOUS":
severity = LoadFailedSeverity.Suspicious;
break;
case "FAULT":
severity = LoadFailedSeverity.Fault;
break;
}
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvte))
await lvlEvte.InternalTrackExceptionAsync(new LavalinkLoadFailedInfo { Message = jsonData["message"].ToString(), Severity = severity }, jsonData["track"].ToString()).ConfigureAwait(false);
break;
case EventType.WebSocketClosedEvent:
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEwsce))
{
lvlEwsce.VoiceWsDisconnectTcs.SetResult(true);
await lvlEwsce.InternalWebSocketClosedAsync(new WebSocketCloseEventArgs(jsonData["code"].ToObject(), jsonData["reason"].ToString(), jsonData["byRemote"].ToObject(), this.Discord.ServiceProvider)).ConfigureAwait(false);
}
break;
}
break;
}
}
///
/// Webs the socket_ on exception.
///
/// The client.
/// the event.
private Task WebSocket_OnException(IWebSocketClient client, SocketErrorEventArgs e)
=> this._lavalinkSocketError.InvokeAsync(this, new SocketErrorEventArgs(client.ServiceProvider) { Exception = e.Exception });
///
/// Webs the socket_ on disconnect.
///
/// The client.
/// the event.
private async Task WebSocket_OnDisconnect(IWebSocketClient client, SocketCloseEventArgs e)
{
if (this.IsConnected && e.CloseCode != 1001 && e.CloseCode != -1)
{
this.Discord.Logger.LogWarning(LavalinkEvents.LavalinkConnectionClosed, "Connection broken ({0}, '{1}'), reconnecting", e.CloseCode, e.CloseMessage);
await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, false)).ConfigureAwait(false);
if (this.Configuration.SocketAutoReconnect)
await this.StartAsync().ConfigureAwait(false);
}
else if (e.CloseCode != 1001 && e.CloseCode != -1)
{
this.Discord.Logger.LogInformation(LavalinkEvents.LavalinkConnectionClosed, "Connection closed ({0}, '{1}')", e.CloseCode, e.CloseMessage);
this.NodeDisconnected?.Invoke(this);
await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, true)).ConfigureAwait(false);
}
else
{
Volatile.Write(ref this._isDisposed, true);
this.Discord.Logger.LogWarning(LavalinkEvents.LavalinkConnectionClosed, "Lavalink died");
foreach (var kvp in this.ConnectedGuildsInternal)
{
await kvp.Value.SendVoiceUpdateAsync().ConfigureAwait(false);
_ = this.ConnectedGuildsInternal.TryRemove(kvp.Key, out _);
}
this.NodeDisconnected?.Invoke(this);
await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, false)).ConfigureAwait(false);
if (this.Configuration.SocketAutoReconnect)
await this.StartAsync().ConfigureAwait(false);
}
}
///
/// Webs the socket_ on connect.
///
/// The client.
/// the event..
private async Task WebSocket_OnConnect(IWebSocketClient client, SocketEventArgs ea)
{
this.Discord.Logger.LogDebug(LavalinkEvents.LavalinkConnected, "Connection to Lavalink node established");
this._backoff = 0;
if (this.Configuration.ResumeKey != null)
await this.SendPayloadAsync(new LavalinkConfigureResume(this.Configuration.ResumeKey, this.Configuration.ResumeTimeout)).ConfigureAwait(false);
}
///
/// Con_S the channel disconnected.
///
/// The con.
private void Con_ChannelDisconnected(LavalinkGuildConnection con)
=> this.ConnectedGuildsInternal.TryRemove(con.GuildId, out _);
///
/// Discord voice state updated.
///
/// The client.
/// the event.
private Task Discord_VoiceStateUpdated(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.Discord.CurrentUser.Id)
{
if (this.ConnectedGuildsInternal.TryGetValue(e.Guild.Id, out var lvlgc))
lvlgc.VoiceStateUpdate = e;
if (e.After.Channel == null && this.IsConnected && this.ConnectedGuildsInternal.ContainsKey(gld.Id))
{
_ = Task.Run(async () =>
{
var delayTask = Task.Delay(this.Configuration.WebSocketCloseTimeout);
var tcs = lvlgc.VoiceWsDisconnectTcs.Task;
_ = await Task.WhenAny(delayTask, tcs).ConfigureAwait(false);
await lvlgc.DisconnectInternalAsync(false, true).ConfigureAwait(false);
_ = this.ConnectedGuildsInternal.TryRemove(gld.Id, out _);
});
}
if (!string.IsNullOrWhiteSpace(e.SessionId) && e.Channel != null && this._voiceStateUpdates.TryRemove(gld.Id, out var xe))
xe.SetResult(e);
}
return Task.CompletedTask;
}
///
/// Discord voice server updated.
///
/// The client.
/// the event.
private Task Discord_VoiceServerUpdated(DiscordClient client, VoiceServerUpdateEventArgs e)
{
var gld = e.Guild;
if (gld == null)
return Task.CompletedTask;
if (this.ConnectedGuildsInternal.TryGetValue(e.Guild.Id, out var lvlgc))
{
var lvlp = new LavalinkVoiceUpdate(lvlgc.VoiceStateUpdate, e);
_ = Task.Run(() => this.WsSendAsync(JsonConvert.SerializeObject(lvlp)));
}
if (this._voiceServerUpdates.TryRemove(gld.Id, out var xe))
xe.SetResult(e);
return Task.CompletedTask;
}
///
/// Ws the send async.
///
/// The payload.
private async Task WsSendAsync(string payload)
{
this.Discord.Logger.LogTrace(LavalinkEvents.LavalinkWsTx, payload);
await this._webSocket.SendMessageAsync(payload).ConfigureAwait(false);
}
internal event NodeDisconnectedEventHandler NodeDisconnected;
}
diff --git a/DisCatSharp.VoiceNext/DiscordClientExtensions.cs b/DisCatSharp.VoiceNext/DiscordClientExtensions.cs
index 27f1d8301..690810f36 100644
--- a/DisCatSharp.VoiceNext/DiscordClientExtensions.cs
+++ b/DisCatSharp.VoiceNext/DiscordClientExtensions.cs
@@ -1,139 +1,136 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 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.Enums;
namespace DisCatSharp.VoiceNext;
///
/// The discord client extensions.
///
public static class DiscordClientExtensions
{
///
/// Creates a new VoiceNext client with default settings.
///
/// Discord client to create VoiceNext instance for.
/// VoiceNext client instance.
public static VoiceNextExtension UseVoiceNext(this DiscordClient client)
=> UseVoiceNext(client, new VoiceNextConfiguration());
///
/// Creates a new VoiceNext client with specified settings.
///
/// Discord client to create VoiceNext instance for.
/// Configuration for the VoiceNext client.
/// VoiceNext client instance.
public static VoiceNextExtension UseVoiceNext(this DiscordClient client, VoiceNextConfiguration config)
{
if (client.GetExtension() != null)
throw new InvalidOperationException("VoiceNext is already enabled for that client.");
var vnext = new VoiceNextExtension(config);
client.AddExtension(vnext);
return vnext;
}
///
/// Creates new VoiceNext clients on all shards in a given sharded client.
///
/// Discord sharded client to create VoiceNext instances for.
/// Configuration for the VoiceNext clients.
/// A dictionary of created VoiceNext clients.
public static async Task> UseVoiceNextAsync(this DiscordShardedClient client, VoiceNextConfiguration config)
{
var modules = new Dictionary();
await client.InitializeShardsAsync().ConfigureAwait(false);
foreach (var shard in client.ShardClients.Select(xkvp => xkvp.Value))
{
var vnext = shard.GetExtension();
vnext ??= shard.UseVoiceNext(config);
modules[shard.ShardId] = vnext;
}
return new ReadOnlyDictionary(modules);
}
///
/// Gets the active instance of VoiceNext client for the DiscordClient.
///
/// Discord client to get VoiceNext instance for.
/// VoiceNext client instance.
public static VoiceNextExtension GetVoiceNext(this DiscordClient client)
=> client.GetExtension();
///
/// Retrieves a instance for each shard.
///
/// The shard client to retrieve instances from.
/// A dictionary containing instances for each shard.
public static async Task> GetVoiceNextAsync(this DiscordShardedClient client)
{
await client.InitializeShardsAsync().ConfigureAwait(false);
var extensions = new Dictionary();
foreach (var shard in client.ShardClients.Values)
{
extensions.Add(shard.ShardId, shard.GetExtension());
}
return new ReadOnlyDictionary(extensions);
}
///
/// Connects to this voice channel using VoiceNext.
///
/// Channel to connect to.
/// If successful, the VoiceNext connection.
public static Task ConnectAsync(this DiscordChannel channel)
{
if (channel == null)
throw new NullReferenceException();
if (channel.Guild == null)
throw new InvalidOperationException("VoiceNext can only be used with guild channels.");
if (channel.Type != ChannelType.Voice && channel.Type != ChannelType.Stage)
throw new InvalidOperationException("You can only connect to voice or stage channels.");
if (channel.Discord is not DiscordClient discord || discord == null)
throw new NullReferenceException();
- var vnext = discord.GetVoiceNext();
- if (vnext == null)
- throw new InvalidOperationException("VoiceNext is not initialized for this Discord client.");
-
+ var vnext = discord.GetVoiceNext() ?? throw new InvalidOperationException("VoiceNext is not initialized for this Discord client.");
var vnc = vnext.GetConnection(channel.Guild);
return vnc != null
? throw new InvalidOperationException("VoiceNext is already connected in this guild.")
: vnext.ConnectAsync(channel);
}
}
diff --git a/DisCatSharp/Clients/DiscordClient.Dispatch.cs b/DisCatSharp/Clients/DiscordClient.Dispatch.cs
index 59d3f85ee..9c46ab501 100644
--- a/DisCatSharp/Clients/DiscordClient.Dispatch.cs
+++ b/DisCatSharp/Clients/DiscordClient.Dispatch.cs
@@ -1,3586 +1,3567 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Common;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Exceptions;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Serialization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
namespace DisCatSharp;
///
/// Represents a discord Logger.ent.L
///
public sealed partial class DiscordClient
{
#region Private Fields
private string _resumeGatewayUrl;
private string _sessionId;
private bool _guildDownloadCompleted;
private readonly Dictionary> _tempTimers = new();
///
/// Represents a timeout handler.
///
internal class TimeoutHandler
{
///
/// Gets the member.
///
internal readonly DiscordMember Member;
///
/// Gets the guild.
///
internal readonly DiscordGuild Guild;
///
/// Gets the old timeout value.
///
internal DateTime? TimeoutUntilOld;
///
/// Gets the new timeout value.
///
internal DateTime? TimeoutUntilNew;
///
/// Constructs a new .
///
/// The affected member.
/// The affected guild.
/// The old timeout value.
/// The new timeout value.
internal TimeoutHandler(DiscordMember mbr, DiscordGuild guild, DateTime? too, DateTime? ton)
{
this.Guild = guild;
this.Member = mbr;
this.TimeoutUntilOld = too;
this.TimeoutUntilNew = ton;
}
}
#endregion
#region Dispatch Handler
///
/// Handles the dispatch payloads.
///
/// The payload.
internal async Task HandleDispatchAsync(GatewayPayload payload)
{
if (payload.Data is not JObject dat)
{
this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Invalid payload body (this message is probably safe to ignore); opcode: {0} event: {1}; payload: {2}", payload.OpCode, payload.EventName, payload.Data);
return;
}
await this._payloadReceived.InvokeAsync(this, new PayloadReceivedEventArgs(this.ServiceProvider)
{
EventName = payload.EventName,
PayloadObject = dat
}).ConfigureAwait(false);
#region Default objects
DiscordChannel chn;
ulong gid;
ulong cid;
ulong uid;
DiscordStageInstance stg = default;
DiscordIntegration itg = default;
DiscordThreadChannel trd = default;
DiscordThreadChannelMember trdm = default;
DiscordScheduledEvent gse = default;
TransportUser usr = default;
TransportMember mbr = default;
TransportUser refUsr = default;
TransportMember refMbr = default;
JToken rawMbr = default;
var rawRefMsg = dat["referenced_message"];
#endregion
switch (payload.EventName.ToLowerInvariant())
{
#region Gateway Status
case "ready":
var glds = (JArray)dat["guilds"];
await this.OnReadyEventAsync(dat.ToObject(), glds).ConfigureAwait(false);
break;
case "resumed":
await this.OnResumedAsync().ConfigureAwait(false);
break;
#endregion
#region Channel
case "channel_create":
chn = dat.ToObject();
await this.OnChannelCreateEventAsync(chn).ConfigureAwait(false);
break;
case "channel_update":
await this.OnChannelUpdateEventAsync(dat.ToObject()).ConfigureAwait(false);
break;
case "channel_delete":
chn = dat.ToObject();
await this.OnChannelDeleteEventAsync(chn.IsPrivate ? dat.ToObject() : chn).ConfigureAwait(false);
break;
case "channel_pins_update":
cid = (ulong)dat["channel_id"];
var ts = (string)dat["last_pin_timestamp"];
await this.OnChannelPinsUpdateAsync((ulong?)dat["guild_id"], cid, ts != null ? DateTimeOffset.Parse(ts, CultureInfo.InvariantCulture) : default(DateTimeOffset?)).ConfigureAwait(false);
break;
#endregion
#region Guild
case "guild_create":
await this.OnGuildCreateEventAsync(dat.ToDiscordObject(), (JArray)dat["members"], dat["presences"].ToDiscordObject>()).ConfigureAwait(false);
break;
case "guild_update":
await this.OnGuildUpdateEventAsync(dat.ToDiscordObject(), (JArray)dat["members"]).ConfigureAwait(false);
break;
case "guild_delete":
await this.OnGuildDeleteEventAsync(dat.ToDiscordObject()).ConfigureAwait(false);
break;
case "guild_audit_log_entry_create":
gid = (ulong)dat["guild_id"];
dat.Remove("guild_id");
await this.OnGuildAuditLogEntryCreateEventAsync(this.GuildsInternal[gid], dat).ConfigureAwait(false);
break;
case "guild_sync":
gid = (ulong)dat["id"];
await this.OnGuildSyncEventAsync(this.GuildsInternal[gid], (bool)dat["large"], (JArray)dat["members"], dat["presences"].ToDiscordObject>()).ConfigureAwait(false);
break;
case "guild_emojis_update":
gid = (ulong)dat["guild_id"];
var ems = dat["emojis"].ToObject>();
await this.OnGuildEmojisUpdateEventAsync(this.GuildsInternal[gid], ems).ConfigureAwait(false);
break;
case "guild_stickers_update":
gid = (ulong)dat["guild_id"];
var strs = dat["stickers"].ToDiscordObject>();
await this.OnStickersUpdatedAsync(strs, gid).ConfigureAwait(false);
break;
case "guild_integrations_update":
gid = (ulong)dat["guild_id"];
// discord fires this event inconsistently if the current user leaves a guild.
if (!this.GuildsInternal.ContainsKey(gid))
return;
await this.OnGuildIntegrationsUpdateEventAsync(this.GuildsInternal[gid]).ConfigureAwait(false);
break;
/*
case "guild_join_request_create":
break;
case "guild_join_request_update":
break;
case "guild_join_request_delete":
break;
*/
#endregion
#region Guild Automod
case "auto_moderation_rule_create":
await this.OnAutomodRuleCreated(dat.ToDiscordObject());
break;
case "auto_moderation_rule_update":
await this.OnAutomodRuleUpdated(dat.ToDiscordObject());
break;
case "auto_moderation_rule_delete":
await this.OnAutomodRuleDeleted(dat.ToDiscordObject());
break;
case "auto_moderation_action_execution":
gid = (ulong)dat["guild_id"];
await this.OnAutomodActionExecuted(this.GuildsInternal[gid], dat);
break;
#endregion
#region Guild Ban
case "guild_ban_add":
usr = dat["user"].ToObject();
gid = (ulong)dat["guild_id"];
await this.OnGuildBanAddEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_ban_remove":
usr = dat["user"].ToObject();
gid = (ulong)dat["guild_id"];
await this.OnGuildBanRemoveEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
#endregion
#region Guild Event
case "guild_scheduled_event_create":
gse = dat.ToObject();
gid = (ulong)dat["guild_id"];
await this.OnGuildScheduledEventCreateEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_scheduled_event_update":
gse = dat.ToObject();
gid = (ulong)dat["guild_id"];
await this.OnGuildScheduledEventUpdateEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_scheduled_event_delete":
gse = dat.ToObject();
gid = (ulong)dat["guild_id"];
await this.OnGuildScheduledEventDeleteEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_scheduled_event_user_add":
gid = (ulong)dat["guild_id"];
uid = (ulong)dat["user_id"];
await this.OnGuildScheduledEventUserAddedEventAsync((ulong)dat["guild_scheduled_event_id"], uid, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_scheduled_event_user_remove":
gid = (ulong)dat["guild_id"];
uid = (ulong)dat["user_id"];
await this.OnGuildScheduledEventUserRemovedEventAsync((ulong)dat["guild_scheduled_event_id"], uid, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
#endregion
#region Guild Integration
case "integration_create":
gid = (ulong)dat["guild_id"];
itg = dat.ToObject();
// discord fires this event inconsistently if the current user leaves a guild.
if (!this.GuildsInternal.ContainsKey(gid))
return;
await this.OnGuildIntegrationCreateEventAsync(this.GuildsInternal[gid], itg).ConfigureAwait(false);
break;
case "integration_update":
gid = (ulong)dat["guild_id"];
itg = dat.ToObject();
// discord fires this event inconsistently if the current user leaves a guild.
if (!this.GuildsInternal.ContainsKey(gid))
return;
await this.OnGuildIntegrationUpdateEventAsync(this.GuildsInternal[gid], itg).ConfigureAwait(false);
break;
case "integration_delete":
gid = (ulong)dat["guild_id"];
// discord fires this event inconsistently if the current user leaves a guild.
if (!this.GuildsInternal.ContainsKey(gid))
return;
await this.OnGuildIntegrationDeleteEventAsync(this.GuildsInternal[gid], (ulong)dat["id"], (ulong?)dat["application_id"]).ConfigureAwait(false);
break;
#endregion
#region Guild Member
case "guild_member_add":
gid = (ulong)dat["guild_id"];
await this.OnGuildMemberAddEventAsync(dat.ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_member_remove":
gid = (ulong)dat["guild_id"];
usr = dat["user"].ToObject();
if (!this.GuildsInternal.ContainsKey(gid))
{
// discord fires this event inconsistently if the current user leaves a guild.
if (usr.Id != this.CurrentUser.Id)
this.Logger.LogError(LoggerEvents.WebSocketReceive, "Could not find {0} in guild cache", gid);
return;
}
await this.OnGuildMemberRemoveEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_member_update":
gid = (ulong)dat["guild_id"];
await this.OnGuildMemberUpdateEventAsync(dat.ToDiscordObject(), this.GuildsInternal[gid], dat["roles"].ToObject>(), (string)dat["nick"], (bool?)dat["pending"]).ConfigureAwait(false);
break;
case "guild_members_chunk":
await this.OnGuildMembersChunkEventAsync(dat).ConfigureAwait(false);
break;
#endregion
#region Guild Role
case "guild_role_create":
gid = (ulong)dat["guild_id"];
await this.OnGuildRoleCreateEventAsync(dat["role"].ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_role_update":
gid = (ulong)dat["guild_id"];
await this.OnGuildRoleUpdateEventAsync(dat["role"].ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_role_delete":
gid = (ulong)dat["guild_id"];
await this.OnGuildRoleDeleteEventAsync((ulong)dat["role_id"], this.GuildsInternal[gid]).ConfigureAwait(false);
break;
#endregion
#region Invite
case "invite_create":
gid = (ulong)dat["guild_id"];
cid = (ulong)dat["channel_id"];
await this.OnInviteCreateEventAsync(cid, gid, dat.ToObject()).ConfigureAwait(false);
break;
case "invite_delete":
gid = (ulong)dat["guild_id"];
cid = (ulong)dat["channel_id"];
await this.OnInviteDeleteEventAsync(cid, gid, dat).ConfigureAwait(false);
break;
#endregion
#region Message
case "message_ack":
cid = (ulong)dat["channel_id"];
var mid = (ulong)dat["message_id"];
await this.OnMessageAckEventAsync(this.InternalGetCachedChannel(cid), mid).ConfigureAwait(false);
break;
case "message_create":
rawMbr = dat["member"];
if (rawMbr != null)
mbr = rawMbr.ToObject();
if (rawRefMsg != null && rawRefMsg.HasValues)
{
if (rawRefMsg.SelectToken("author") != null)
{
refUsr = rawRefMsg.SelectToken("author").ToObject();
}
if (rawRefMsg.SelectToken("member") != null)
{
refMbr = rawRefMsg.SelectToken("member").ToObject();
}
}
await this.OnMessageCreateEventAsync(dat.ToDiscordObject(), dat["author"].ToObject(), mbr, refUsr, refMbr).ConfigureAwait(false);
break;
case "message_update":
rawMbr = dat["member"];
if (rawMbr != null)
mbr = rawMbr.ToObject();
if (rawRefMsg != null && rawRefMsg.HasValues)
{
if (rawRefMsg.SelectToken("author") != null)
{
refUsr = rawRefMsg.SelectToken("author").ToObject();
}
if (rawRefMsg.SelectToken("member") != null)
{
refMbr = rawRefMsg.SelectToken("member").ToObject();
}
}
await this.OnMessageUpdateEventAsync(dat.ToDiscordObject(), dat["author"]?.ToObject(), mbr, refUsr, refMbr).ConfigureAwait(false);
break;
// delete event does *not* include message object
case "message_delete":
await this.OnMessageDeleteEventAsync((ulong)dat["id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false);
break;
case "message_delete_bulk":
await this.OnMessageBulkDeleteEventAsync(dat["ids"].ToObject(), (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false);
break;
#endregion
#region Message Reaction
case "message_reaction_add":
rawMbr = dat["member"];
if (rawMbr != null)
mbr = rawMbr.ToObject();
// TODO: Add burst stuff
this.Logger.LogDebug("Reaction add: {object}", dat.ToString(Newtonsoft.Json.Formatting.Indented));
await this.OnMessageReactionAddAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], mbr, dat["emoji"].ToObject(), (bool)dat["burst"]).ConfigureAwait(false);
break;
case "message_reaction_remove":
this.Logger.LogDebug("Reaction removed: {object}", dat.ToString(Newtonsoft.Json.Formatting.Indented));
await this.OnMessageReactionRemoveAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], dat["emoji"].ToObject(), (bool)dat["burst"]).ConfigureAwait(false);
break;
case "message_reaction_remove_all":
await this.OnMessageReactionRemoveAllAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false);
break;
case "message_reaction_remove_emoji":
this.Logger.LogDebug(dat.ToString(Newtonsoft.Json.Formatting.Indented));
await this.OnMessageReactionRemoveEmojiAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong)dat["guild_id"], dat["emoji"]).ConfigureAwait(false);
break;
#endregion
#region Stage Instance
case "stage_instance_create":
stg = dat.ToObject();
await this.OnStageInstanceCreateEventAsync(stg).ConfigureAwait(false);
break;
case "stage_instance_update":
stg = dat.ToObject();
await this.OnStageInstanceUpdateEventAsync(stg).ConfigureAwait(false);
break;
case "stage_instance_delete":
stg = dat.ToObject();
await this.OnStageInstanceDeleteEventAsync(stg).ConfigureAwait(false);
break;
#endregion
#region Thread
case "thread_create":
trd = dat.ToObject();
await this.OnThreadCreateEventAsync(trd).ConfigureAwait(false);
break;
case "thread_update":
trd = dat.ToObject();
await this.OnThreadUpdateEventAsync(trd).ConfigureAwait(false);
break;
case "thread_delete":
trd = dat.ToObject();
await this.OnThreadDeleteEventAsync(trd).ConfigureAwait(false);
break;
case "thread_list_sync":
gid = (ulong)dat["guild_id"]; //get guild
await this.OnThreadListSyncEventAsync(this.GuildsInternal[gid], dat["channel_ids"].ToObject>(), dat["threads"].ToObject>(), dat["members"].ToObject>()).ConfigureAwait(false);
break;
case "thread_member_update":
trdm = dat.ToObject();
await this.OnThreadMemberUpdateEventAsync(trdm).ConfigureAwait(false);
break;
case "thread_members_update":
gid = (ulong)dat["guild_id"];
await this.OnThreadMembersUpdateEventAsync(this.GuildsInternal[gid], (ulong)dat["id"], (JArray)dat["added_members"], (JArray)dat["removed_member_ids"], (int)dat["member_count"]).ConfigureAwait(false);
break;
#endregion
#region Activities
case "embedded_activity_update":
gid = (ulong)dat["guild_id"];
cid = (ulong)dat["channel_id"];
await this.OnEmbeddedActivityUpdateAsync((JObject)dat["embedded_activity"], this.GuildsInternal[gid], cid, (JArray)dat["users"], (ulong)dat["embedded_activity"]["application_id"]).ConfigureAwait(false);
break;
#endregion
#region User/Presence Update
case "presence_update":
await this.OnPresenceUpdateEventAsync(dat, (JObject)dat["user"]).ConfigureAwait(false);
break;
case "user_settings_update":
await this.OnUserSettingsUpdateEventAsync(dat.ToObject()).ConfigureAwait(false);
break;
case "user_update":
await this.OnUserUpdateEventAsync(dat.ToObject()).ConfigureAwait(false);
break;
#endregion
#region Voice
case "voice_state_update":
await this.OnVoiceStateUpdateEventAsync(dat).ConfigureAwait(false);
break;
case "voice_server_update":
gid = (ulong)dat["guild_id"];
await this.OnVoiceServerUpdateEventAsync((string)dat["endpoint"], (string)dat["token"], this.GuildsInternal[gid]).ConfigureAwait(false);
break;
#endregion
#region Interaction/Integration/Application
case "interaction_create":
rawMbr = dat["member"];
if (rawMbr != null)
{
mbr = dat["member"].ToObject();
usr = mbr.User;
}
else
{
usr = dat["user"].ToObject();
}
cid = (ulong)dat["channel_id"];
// Console.WriteLine(dat.ToString()); // Get raw interaction payload.
await this.OnInteractionCreateAsync((ulong?)dat["guild_id"], cid, usr, mbr, dat.ToDiscordObject(), dat.ToString()).ConfigureAwait(false);
break;
case "application_command_create":
await this.OnApplicationCommandCreateAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false);
break;
case "application_command_update":
await this.OnApplicationCommandUpdateAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false);
break;
case "application_command_delete":
await this.OnApplicationCommandDeleteAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false);
break;
case "guild_application_command_counts_update":
var counts = dat["application_command_counts"];
await this.OnGuildApplicationCommandCountsUpdateAsync((int)counts["1"], (int)counts["2"], (int)counts["3"], (ulong)dat["guild_id"]).ConfigureAwait(false);
break;
case "guild_application_command_index_update":
// TODO: Implement.
break;
case "application_command_permissions_update":
var aid = (ulong)dat["application_id"];
if (aid != this.CurrentApplication.Id)
return;
var pms = dat["permissions"].ToObject>();
gid = (ulong)dat["guild_id"];
await this.OnApplicationCommandPermissionsUpdateAsync(pms, (ulong)dat["id"], gid, aid).ConfigureAwait(false);
break;
#endregion
#region Misc
case "gift_code_update": //Not supposed to be dispatched to bots
break;
case "typing_start":
cid = (ulong)dat["channel_id"];
rawMbr = dat["member"];
if (rawMbr != null)
mbr = rawMbr.ToObject();
await this.OnTypingStartEventAsync((ulong)dat["user_id"], cid, this.InternalGetCachedChannel(cid), (ulong?)dat["guild_id"], Utilities.GetDateTimeOffset((long)dat["timestamp"]), mbr).ConfigureAwait(false);
break;
case "webhooks_update":
gid = (ulong)dat["guild_id"];
cid = (ulong)dat["channel_id"];
await this.OnWebhooksUpdateAsync(this.GuildsInternal[gid].GetChannel(cid), this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_join_request_update":
case "guild_join_request_create":
case "guild_join_request_delete": // Deprecated
break;
default:
await this.OnUnknownEventAsync(payload).ConfigureAwait(false);
this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown event: {0}\npayload: {1}", payload.EventName, dat.ToString(Newtonsoft.Json.Formatting.Indented));
break;
#endregion
}
}
#endregion
#region Events
#region Gateway
///
/// Handles the ready event.
///
/// The ready payload.
/// The raw guilds.
internal async Task OnReadyEventAsync(ReadyPayload ready, JArray rawGuilds)
{
//ready.CurrentUser.Discord = this;
var rusr = ready.CurrentUser;
this.CurrentUser.Username = rusr.Username;
this.CurrentUser.Discriminator = rusr.Discriminator;
this.CurrentUser.AvatarHash = rusr.AvatarHash;
this.CurrentUser.MfaEnabled = rusr.MfaEnabled;
this.CurrentUser.Verified = rusr.Verified;
this.CurrentUser.IsBot = rusr.IsBot;
this.CurrentUser.Flags = rusr.Flags;
this.CurrentUser.GlobalName = rusr.GlobalName;
this.GatewayVersion = ready.GatewayVersion;
this._sessionId = ready.SessionId;
this._resumeGatewayUrl = ready.ResumeGatewayUrl;
var rawGuildIndex = rawGuilds.ToDictionary(xt => (ulong)xt["id"], xt => (JObject)xt);
this.GuildsInternal.Clear();
foreach (var guild in ready.Guilds)
{
guild.Discord = this;
guild.ChannelsInternal ??= new ConcurrentDictionary();
foreach (var xc in guild.Channels.Values)
{
xc.GuildId = guild.Id;
xc.Initialize(this);
}
guild.RolesInternal ??= new ConcurrentDictionary();
foreach (var xr in guild.Roles.Values)
{
xr.Discord = this;
xr.GuildId = guild.Id;
}
var rawGuild = rawGuildIndex[guild.Id];
var rawMembers = (JArray)rawGuild["members"];
if (guild.MembersInternal != null)
guild.MembersInternal.Clear();
else
guild.MembersInternal = new ConcurrentDictionary();
if (rawMembers != null)
{
foreach (var xj in rawMembers)
{
var xtm = xj.ToObject();
var xu = new DiscordUser(xtm.User) { Discord = this };
xu = this.UserCache.AddOrUpdate(xtm.User.Id, xu, (id, old) =>
{
old.Username = xu.Username;
old.Discriminator = xu.Discriminator;
old.AvatarHash = xu.AvatarHash;
old.GlobalName = xu.GlobalName;
return old;
});
guild.MembersInternal[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, GuildId = guild.Id };
}
}
guild.EmojisInternal ??= new ConcurrentDictionary();
foreach (var xe in guild.Emojis.Values)
xe.Discord = this;
guild.StickersInternal ??= new ConcurrentDictionary();
foreach (var xs in guild.Stickers.Values)
xs.Discord = this;
guild.VoiceStatesInternal ??= new ConcurrentDictionary();
foreach (var xvs in guild.VoiceStates.Values)
xvs.Discord = this;
guild.ThreadsInternal ??= new ConcurrentDictionary();
foreach (var xt in guild.ThreadsInternal.Values)
xt.Discord = this;
guild.StageInstancesInternal ??= new ConcurrentDictionary();
foreach (var xsi in guild.StageInstancesInternal.Values)
xsi.Discord = this;
guild.ScheduledEventsInternal ??= new ConcurrentDictionary();
foreach (var xse in guild.ScheduledEventsInternal.Values)
xse.Discord = this;
this.GuildsInternal[guild.Id] = guild;
}
await this._ready.InvokeAsync(this, new ReadyEventArgs(this.ServiceProvider)).ConfigureAwait(false);
}
///
/// Handles the resumed event.
///
internal Task OnResumedAsync()
{
this.Logger.LogInformation(LoggerEvents.SessionUpdate, "Session resumed");
return this._resumed.InvokeAsync(this, new ReadyEventArgs(this.ServiceProvider));
}
#endregion
#region Channel
///
/// Handles the channel create event.
///
/// The channel.
internal async Task OnChannelCreateEventAsync(DiscordChannel channel)
{
channel.Initialize(this);
this.GuildsInternal[channel.GuildId.Value].ChannelsInternal[channel.Id] = channel;
/*if (this.Configuration.AutoRefreshChannelCache)
{
await this.RefreshChannelsAsync(channel.Guild.Id);
}*/
await this._channelCreated.InvokeAsync(this, new ChannelCreateEventArgs(this.ServiceProvider) { Channel = channel, Guild = channel.Guild }).ConfigureAwait(false);
}
///
/// Handles the channel update event.
///
/// The channel.
internal async Task OnChannelUpdateEventAsync(DiscordChannel channel)
{
if (channel == null)
return;
channel.Discord = this;
var gld = channel.Guild;
var channelNew = this.InternalGetCachedChannel(channel.Id);
DiscordChannel channelOld = null;
if (channelNew != null)
{
channelOld = new DiscordChannel
{
Bitrate = channelNew.Bitrate,
Discord = this,
GuildId = channelNew.GuildId,
Id = channelNew.Id,
LastMessageId = channelNew.LastMessageId,
Name = channelNew.Name,
PermissionOverwritesInternal = new List(channelNew.PermissionOverwritesInternal),
Position = channelNew.Position,
Topic = channelNew.Topic,
Type = channelNew.Type,
UserLimit = channelNew.UserLimit,
ParentId = channelNew.ParentId,
IsNsfw = channelNew.IsNsfw,
PerUserRateLimit = channelNew.PerUserRateLimit,
RtcRegionId = channelNew.RtcRegionId,
QualityMode = channelNew.QualityMode,
DefaultAutoArchiveDuration = channelNew.DefaultAutoArchiveDuration,
};
channelNew.Bitrate = channel.Bitrate;
channelNew.Name = channel.Name;
channelNew.Position = channel.Position;
channelNew.Topic = channel.Topic;
channelNew.UserLimit = channel.UserLimit;
channelNew.ParentId = channel.ParentId;
channelNew.IsNsfw = channel.IsNsfw;
channelNew.PerUserRateLimit = channel.PerUserRateLimit;
channelNew.Type = channel.Type;
channelNew.RtcRegionId = channel.RtcRegionId;
channelNew.QualityMode = channel.QualityMode;
channelNew.DefaultAutoArchiveDuration = channel.DefaultAutoArchiveDuration;
channelNew.PermissionOverwritesInternal.Clear();
channel.Initialize(this);
channelNew.PermissionOverwritesInternal.AddRange(channel.PermissionOverwritesInternal);
if (channel.Type == ChannelType.Forum)
{
channelOld.PostCreateUserRateLimit = channelNew.PostCreateUserRateLimit;
channelOld.InternalAvailableTags = channelNew.InternalAvailableTags;
channelOld.Template = channelNew.Template;
channelOld.DefaultReactionEmoji = channelNew.DefaultReactionEmoji;
channelOld.DefaultSortOrder = channelNew.DefaultSortOrder;
channelNew.PostCreateUserRateLimit = channel.PostCreateUserRateLimit;
channelNew.Template = channel.Template;
channelNew.DefaultReactionEmoji = channel.DefaultReactionEmoji;
channelNew.DefaultSortOrder = channel.DefaultSortOrder;
if (channelNew.InternalAvailableTags != null && channelNew.InternalAvailableTags.Any())
channelNew.InternalAvailableTags.Clear();
if (channel.InternalAvailableTags != null && channel.InternalAvailableTags.Any())
channelNew.InternalAvailableTags.AddRange(channel.InternalAvailableTags);
}
else
{
channelOld.PostCreateUserRateLimit = null;
channelOld.InternalAvailableTags = null;
channelOld.Template = null;
channelOld.DefaultReactionEmoji = null;
channelOld.DefaultSortOrder = null;
channelNew.PostCreateUserRateLimit = null;
channelNew.InternalAvailableTags = null;
channelNew.Template = null;
channelNew.DefaultReactionEmoji = null;
channelNew.DefaultSortOrder = null;
}
channelOld.Initialize(this);
channelNew.Initialize(this);
if (this.Configuration.AutoRefreshChannelCache && gld != null)
{
await this.RefreshChannelsAsync(channel.Guild.Id);
}
}
else if (gld != null)
{
gld.ChannelsInternal[channel.Id] = channel;
if (this.Configuration.AutoRefreshChannelCache)
{
await this.RefreshChannelsAsync(channel.Guild.Id);
}
}
await this._channelUpdated.InvokeAsync(this, new ChannelUpdateEventArgs(this.ServiceProvider) { ChannelAfter = channelNew, Guild = gld, ChannelBefore = channelOld }).ConfigureAwait(false);
}
///
/// Handles the channel delete event.
///
/// The channel.
internal async Task OnChannelDeleteEventAsync(DiscordChannel channel)
{
if (channel == null)
return;
channel.Discord = this;
//if (channel.IsPrivate)
if (channel.Type == ChannelType.Group || channel.Type == ChannelType.Private)
{
var dmChannel = channel as DiscordDmChannel;
await this._dmChannelDeleted.InvokeAsync(this, new DmChannelDeleteEventArgs(this.ServiceProvider) { Channel = dmChannel }).ConfigureAwait(false);
}
else
{
var gld = channel.Guild;
if (gld.ChannelsInternal.TryRemove(channel.Id, out var cachedChannel)) channel = cachedChannel;
if (this.Configuration.AutoRefreshChannelCache)
{
await this.RefreshChannelsAsync(channel.Guild.Id);
}
await this._channelDeleted.InvokeAsync(this, new ChannelDeleteEventArgs(this.ServiceProvider) { Channel = channel, Guild = gld }).ConfigureAwait(false);
}
}
///
/// Refreshes the channels.
///
/// The guild id.
internal async Task RefreshChannelsAsync(ulong guildId)
{
var guild = this.InternalGetCachedGuild(guildId);
var channels = await this.ApiClient.GetGuildChannelsAsync(guildId);
guild.ChannelsInternal.Clear();
foreach (var channel in channels.ToList())
{
channel.Initialize(this);
guild.ChannelsInternal[channel.Id] = channel;
}
}
///
/// Handles the channel pins update event.
///
/// The optional guild id.
/// The channel id.
/// The optional last pin timestamp.
internal async Task OnChannelPinsUpdateAsync(ulong? guildId, ulong channelId, DateTimeOffset? lastPinTimestamp)
{
var guild = this.InternalGetCachedGuild(guildId);
var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId);
var ea = new ChannelPinsUpdateEventArgs(this.ServiceProvider)
{
Guild = guild,
Channel = channel,
LastPinTimestamp = lastPinTimestamp
};
await this._channelPinsUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Guild
///
/// Handles the guild create event.
///
/// The guild.
/// The raw members.
/// The presences.
internal async Task OnGuildCreateEventAsync(DiscordGuild guild, JArray rawMembers, IEnumerable presences)
{
if (presences != null)
{
foreach (var xp in presences)
{
xp.Discord = this;
xp.GuildId = guild.Id;
xp.Activity = new DiscordActivity(xp.RawActivity);
if (xp.RawActivities != null)
{
xp.InternalActivities = xp.RawActivities
.Select(x => new DiscordActivity(x)).ToArray();
}
this.PresencesInternal[xp.InternalUser.Id] = xp;
}
}
var exists = this.GuildsInternal.TryGetValue(guild.Id, out var foundGuild);
guild.Discord = this;
guild.IsUnavailable = false;
var eventGuild = guild;
if (exists)
guild = foundGuild;
guild.ChannelsInternal ??= new ConcurrentDictionary();
guild.ThreadsInternal ??= new ConcurrentDictionary();
guild.RolesInternal ??= new ConcurrentDictionary();
guild.ThreadsInternal ??= new ConcurrentDictionary();
guild.StickersInternal ??= new ConcurrentDictionary();
guild.EmojisInternal ??= new ConcurrentDictionary();
guild.VoiceStatesInternal ??= new ConcurrentDictionary();
guild.MembersInternal ??= new ConcurrentDictionary();
guild.ScheduledEventsInternal ??= new ConcurrentDictionary();
this.UpdateCachedGuild(eventGuild, rawMembers);
guild.JoinedAt = eventGuild.JoinedAt;
guild.IsLarge = eventGuild.IsLarge;
guild.MemberCount = Math.Max(eventGuild.MemberCount, guild.MembersInternal.Count);
guild.IsUnavailable = eventGuild.IsUnavailable;
guild.PremiumSubscriptionCount = eventGuild.PremiumSubscriptionCount;
guild.PremiumTier = eventGuild.PremiumTier;
guild.BannerHash = eventGuild.BannerHash;
guild.VanityUrlCode = eventGuild.VanityUrlCode;
guild.Description = eventGuild.Description;
guild.IsNsfw = eventGuild.IsNsfw;
foreach (var kvp in eventGuild.VoiceStatesInternal) guild.VoiceStatesInternal[kvp.Key] = kvp.Value;
foreach (var kvp in eventGuild.ChannelsInternal) guild.ChannelsInternal[kvp.Key] = kvp.Value;
foreach (var kvp in eventGuild.RolesInternal) guild.RolesInternal[kvp.Key] = kvp.Value;
foreach (var kvp in eventGuild.EmojisInternal) guild.EmojisInternal[kvp.Key] = kvp.Value;
foreach (var kvp in eventGuild.ThreadsInternal) guild.ThreadsInternal[kvp.Key] = kvp.Value;
foreach (var kvp in eventGuild.StickersInternal) guild.StickersInternal[kvp.Key] = kvp.Value;
foreach (var kvp in eventGuild.StageInstancesInternal) guild.StageInstancesInternal[kvp.Key] = kvp.Value;
foreach (var kvp in eventGuild.ScheduledEventsInternal) guild.ScheduledEventsInternal[kvp.Key] = kvp.Value;
foreach (var xc in guild.ChannelsInternal.Values)
{
xc.GuildId = guild.Id;
xc.Initialize(this);
}
foreach (var xt in guild.ThreadsInternal.Values)
{
xt.GuildId = guild.Id;
xt.Discord = this;
}
foreach (var xe in guild.EmojisInternal.Values)
xe.Discord = this;
foreach (var xs in guild.StickersInternal.Values)
xs.Discord = this;
foreach (var xvs in guild.VoiceStatesInternal.Values)
xvs.Discord = this;
foreach (var xsi in guild.StageInstancesInternal.Values)
{
xsi.Discord = this;
xsi.GuildId = guild.Id;
}
foreach (var xr in guild.RolesInternal.Values)
{
xr.Discord = this;
xr.GuildId = guild.Id;
}
foreach (var xse in guild.ScheduledEventsInternal.Values)
{
xse.Discord = this;
xse.GuildId = guild.Id;
if (xse.Creator != null)
xse.Creator.Discord = this;
}
var old = Volatile.Read(ref this._guildDownloadCompleted);
var dcompl = this.GuildsInternal.Values.All(xg => !xg.IsUnavailable);
Volatile.Write(ref this._guildDownloadCompleted, dcompl);
if (exists)
await this._guildAvailable.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false);
else
await this._guildCreated.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false);
if (dcompl && !old)
await this._guildDownloadCompletedEv.InvokeAsync(this, new GuildDownloadCompletedEventArgs(this.Guilds, this.ServiceProvider)).ConfigureAwait(false);
}
///
/// Handles the guild update event.
///
/// The guild.
/// The raw members.
internal async Task OnGuildUpdateEventAsync(DiscordGuild guild, JArray rawMembers)
{
DiscordGuild oldGuild;
if (!this.GuildsInternal.ContainsKey(guild.Id))
{
this.GuildsInternal[guild.Id] = guild;
oldGuild = null;
}
else
{
var gld = this.GuildsInternal[guild.Id];
oldGuild = new DiscordGuild
{
Discord = gld.Discord,
Name = gld.Name,
AfkChannelId = gld.AfkChannelId,
AfkTimeout = gld.AfkTimeout,
ApplicationId = gld.ApplicationId,
DefaultMessageNotifications = gld.DefaultMessageNotifications,
ExplicitContentFilter = gld.ExplicitContentFilter,
RawFeatures = gld.RawFeatures,
IconHash = gld.IconHash,
Id = gld.Id,
IsLarge = gld.IsLarge,
IsSynced = gld.IsSynced,
IsUnavailable = gld.IsUnavailable,
JoinedAt = gld.JoinedAt,
MemberCount = gld.MemberCount,
MaxMembers = gld.MaxMembers,
MaxPresences = gld.MaxPresences,
ApproximateMemberCount = gld.ApproximateMemberCount,
ApproximatePresenceCount = gld.ApproximatePresenceCount,
MaxVideoChannelUsers = gld.MaxVideoChannelUsers,
DiscoverySplashHash = gld.DiscoverySplashHash,
PreferredLocale = gld.PreferredLocale,
MfaLevel = gld.MfaLevel,
OwnerId = gld.OwnerId,
SplashHash = gld.SplashHash,
SystemChannelId = gld.SystemChannelId,
SystemChannelFlags = gld.SystemChannelFlags,
Description = gld.Description,
WidgetEnabled = gld.WidgetEnabled,
WidgetChannelId = gld.WidgetChannelId,
VerificationLevel = gld.VerificationLevel,
RulesChannelId = gld.RulesChannelId,
PublicUpdatesChannelId = gld.PublicUpdatesChannelId,
VoiceRegionId = gld.VoiceRegionId,
IsNsfw = gld.IsNsfw,
PremiumProgressBarEnabled = gld.PremiumProgressBarEnabled,
PremiumSubscriptionCount = gld.PremiumSubscriptionCount,
PremiumTier = gld.PremiumTier,
ChannelsInternal = new ConcurrentDictionary(),
ThreadsInternal = new ConcurrentDictionary(),
EmojisInternal = new ConcurrentDictionary(),
StickersInternal = new ConcurrentDictionary(),
MembersInternal = new ConcurrentDictionary(),
RolesInternal = new ConcurrentDictionary(),
StageInstancesInternal = new ConcurrentDictionary(),
VoiceStatesInternal = new ConcurrentDictionary(),
ScheduledEventsInternal = new ConcurrentDictionary()
};
foreach (var kvp in gld.ChannelsInternal) oldGuild.ChannelsInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.ThreadsInternal) oldGuild.ThreadsInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.EmojisInternal) oldGuild.EmojisInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.StickersInternal) oldGuild.StickersInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.RolesInternal) oldGuild.RolesInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.VoiceStatesInternal) oldGuild.VoiceStatesInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.MembersInternal) oldGuild.MembersInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.StageInstancesInternal) oldGuild.StageInstancesInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.ScheduledEventsInternal) oldGuild.ScheduledEventsInternal[kvp.Key] = kvp.Value;
}
guild.Discord = this;
guild.IsUnavailable = false;
var eventGuild = guild;
guild = this.GuildsInternal[eventGuild.Id];
guild.ChannelsInternal ??= new ConcurrentDictionary();
guild.ThreadsInternal ??= new ConcurrentDictionary();
guild.RolesInternal ??= new ConcurrentDictionary();
guild.EmojisInternal ??= new ConcurrentDictionary();
guild.StickersInternal ??= new ConcurrentDictionary();
guild.VoiceStatesInternal ??= new ConcurrentDictionary();
guild.StageInstancesInternal ??= new ConcurrentDictionary();
guild.MembersInternal ??= new ConcurrentDictionary();
guild.ScheduledEventsInternal ??= new ConcurrentDictionary();
this.UpdateCachedGuild(eventGuild, rawMembers);
foreach (var xc in guild.ChannelsInternal.Values)
{
xc.GuildId = guild.Id;
xc.Initialize(this);
}
foreach (var xc in guild.ThreadsInternal.Values)
{
xc.GuildId = guild.Id;
xc.Discord = this;
}
foreach (var xe in guild.EmojisInternal.Values)
xe.Discord = this;
foreach (var xs in guild.StickersInternal.Values)
xs.Discord = this;
foreach (var xvs in guild.VoiceStatesInternal.Values)
xvs.Discord = this;
foreach (var xr in guild.RolesInternal.Values)
{
xr.Discord = this;
xr.GuildId = guild.Id;
}
foreach (var xsi in guild.StageInstancesInternal.Values)
{
xsi.Discord = this;
xsi.GuildId = guild.Id;
}
foreach (var xse in guild.ScheduledEventsInternal.Values)
{
xse.Discord = this;
xse.GuildId = guild.Id;
if (xse.Creator != null)
xse.Creator.Discord = this;
}
await this._guildUpdated.InvokeAsync(this, new GuildUpdateEventArgs(this.ServiceProvider) { GuildBefore = oldGuild, GuildAfter = guild }).ConfigureAwait(false);
}
///
/// Handles the guild delete event.
///
/// The guild.
internal async Task OnGuildDeleteEventAsync(DiscordGuild guild)
{
if (guild.IsUnavailable)
{
if (!this.GuildsInternal.TryGetValue(guild.Id, out var gld))
return;
gld.IsUnavailable = true;
await this._guildUnavailable.InvokeAsync(this, new GuildDeleteEventArgs(this.ServiceProvider) { Guild = guild, Unavailable = true }).ConfigureAwait(false);
}
else
{
if (!this.GuildsInternal.TryRemove(guild.Id, out var gld))
return;
await this._guildDeleted.InvokeAsync(this, new GuildDeleteEventArgs(this.ServiceProvider) { Guild = gld }).ConfigureAwait(false);
}
}
///
/// Handles the guild audit log entry create event.
///
/// The guild where the audit log entry was created.
/// The auditlog event.
internal async Task OnGuildAuditLogEntryCreateEventAsync(DiscordGuild guild, JObject auditLogCreateEntry)
{
try
{
var auditLogAction = DiscordJson.ToDiscordObject(auditLogCreateEntry);
List workaroundAuditLogEntryList = new()
{
new AuditLog()
{
Entries = new List()
{
auditLogAction
}
}
};
var dataList = await guild.ProcessAuditLog(workaroundAuditLogEntryList);
- var data = dataList.First();
- await this._guildAuditLogEntryCreated.InvokeAsync(this, new(this.ServiceProvider) { Guild = guild, AuditLogEntry = data });
+ await this._guildAuditLogEntryCreated.InvokeAsync(this, new(this.ServiceProvider) { Guild = guild, AuditLogEntry = dataList[0] });
}
catch (Exception)
{ }
}
///
/// Handles the guild sync event.
///
/// The guild.
/// Whether the guild is a large guild..
/// The raw members.
/// The presences.
internal async Task OnGuildSyncEventAsync(DiscordGuild guild, bool isLarge, JArray rawMembers, IEnumerable presences)
{
presences = presences.Select(xp => { xp.Discord = this; xp.Activity = new DiscordActivity(xp.RawActivity); return xp; });
foreach (var xp in presences)
this.PresencesInternal[xp.InternalUser.Id] = xp;
guild.IsSynced = true;
guild.IsLarge = isLarge;
this.UpdateCachedGuild(guild, rawMembers);
await this._guildAvailable.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false);
}
///
/// Handles the guild emojis update event.
///
/// The guild.
/// The new emojis.
internal async Task OnGuildEmojisUpdateEventAsync(DiscordGuild guild, IEnumerable newEmojis)
{
var oldEmojis = new ConcurrentDictionary(guild.EmojisInternal);
guild.EmojisInternal.Clear();
foreach (var emoji in newEmojis)
{
emoji.Discord = this;
guild.EmojisInternal[emoji.Id] = emoji;
}
var ea = new GuildEmojisUpdateEventArgs(this.ServiceProvider)
{
Guild = guild,
EmojisAfter = guild.Emojis,
EmojisBefore = new ReadOnlyConcurrentDictionary(oldEmojis)
};
await this._guildEmojisUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the stickers updated.
///
/// The new stickers.
/// The guild id.
internal async Task OnStickersUpdatedAsync(IEnumerable newStickers, ulong guildId)
{
var guild = this.InternalGetCachedGuild(guildId);
var oldStickers = new ConcurrentDictionary(guild.StickersInternal);
guild.StickersInternal.Clear();
foreach (var nst in newStickers)
{
if (nst.User is not null)
{
nst.User.Discord = this;
this.UserCache.AddOrUpdate(nst.User.Id, nst.User, (old, @new) => @new);
}
nst.Discord = this;
guild.StickersInternal[nst.Id] = nst;
}
var sea = new GuildStickersUpdateEventArgs(this.ServiceProvider)
{
Guild = guild,
StickersBefore = oldStickers,
StickersAfter = guild.Stickers
};
await this._guildStickersUpdated.InvokeAsync(this, sea).ConfigureAwait(false);
}
///
/// Handles the created rule.
///
/// The new added rule.
internal async Task OnAutomodRuleCreated(AutomodRule newRule)
{
var sea = new AutomodRuleCreateEventArgs(this.ServiceProvider)
{
Rule = newRule
};
await this._automodRuleCreated.InvokeAsync(this, sea).ConfigureAwait(false);
}
///
/// Handles the updated rule.
///
/// The updated rule.
internal async Task OnAutomodRuleUpdated(AutomodRule updatedRule)
{
var sea = new AutomodRuleUpdateEventArgs(this.ServiceProvider)
{
Rule = updatedRule
};
await this._automodRuleUpdated.InvokeAsync(this, sea).ConfigureAwait(false);
}
///
/// Handles the deleted rule.
///
/// The deleted rule.
internal async Task OnAutomodRuleDeleted(AutomodRule deletedRule)
{
var sea = new AutomodRuleDeleteEventArgs(this.ServiceProvider)
{
Rule = deletedRule
};
await this._automodRuleDeleted.InvokeAsync(this, sea).ConfigureAwait(false);
}
///
/// Handles the rule action execution.
///
/// The guild.
/// The raw payload.
internal async Task OnAutomodActionExecuted(DiscordGuild guild, JObject rawPayload)
{
var executedAction = rawPayload["action"].ToObject();
var ruleId = (ulong)rawPayload["rule_id"];
var triggerType = rawPayload["rule_trigger_type"].ToObject();
var userId = (ulong)rawPayload["user_id"];
- var channelId = rawPayload.ContainsKey("channel_id") ? (ulong?)rawPayload["channel_id"] : null;
- var messageId = rawPayload.ContainsKey("message_id") ? (ulong?)rawPayload["message_id"] : null;
- var alertMessageId = rawPayload.ContainsKey("alert_system_message_id") ? (ulong?)rawPayload["alert_system_message_id"] : null;
-#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
-#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
- string? content = rawPayload.ContainsKey("content") ?(string?)rawPayload["content"] : null;
-#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
-#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
-#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
-#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
- string? matchedKeyword = rawPayload.ContainsKey("matched_keyword") ? (string?)rawPayload["matched_keyword"] : null;
-#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
-#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
-#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
-#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
- string? matchedContent = rawPayload.ContainsKey("matched_content") ? (string?)rawPayload["matched_content"] : null;
-#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
-#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
+ var channelId = rawPayload.TryGetValue("channel_id", out var value) ? (ulong?)value : null;
+ var messageId = rawPayload.TryGetValue("message_id", out var value1) ? (ulong?)value1 : null;
+ var alertMessageId = rawPayload.TryGetValue("alert_system_message_id", out var value2) ? (ulong?)value2 : null;
+ var content = rawPayload.TryGetValue("content", out var value3) ?(string?)value3 : null;
+ var matchedKeyword = rawPayload.TryGetValue("matched_keyword", out var value4) ? (string?)value4 : null;
+ var matchedContent = rawPayload.TryGetValue("matched_content", out var value5) ? (string?)value5 : null;
var ea = new AutomodActionExecutedEventArgs(this.ServiceProvider)
{
Guild = guild,
Action = executedAction,
RuleId = ruleId,
TriggerType = triggerType,
UserId = userId,
ChannelId = channelId,
MessageId = messageId,
AlertMessageId = alertMessageId,
MessageContent = content,
MatchedKeyword = matchedKeyword,
MatchedContent = matchedContent
};
await this._automodActionExecuted.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Guild Ban
///
/// Handles the guild ban add event.
///
/// The transport user.
/// The guild.
internal async Task OnGuildBanAddEventAsync(TransportUser user, DiscordGuild guild)
{
var usr = new DiscordUser(user) { Discord = this };
usr = this.UserCache.AddOrUpdate(user.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
old.GlobalName = usr.GlobalName;
return old;
});
if (!guild.Members.TryGetValue(user.Id, out var mbr))
mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id };
var ea = new GuildBanAddEventArgs(this.ServiceProvider)
{
Guild = guild,
Member = mbr
};
await this._guildBanAdded.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the guild ban remove event.
///
/// The transport user.
/// The guild.
internal async Task OnGuildBanRemoveEventAsync(TransportUser user, DiscordGuild guild)
{
var usr = new DiscordUser(user) { Discord = this };
usr = this.UserCache.AddOrUpdate(user.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
old.GlobalName = usr.GlobalName;
return old;
});
if (!guild.Members.TryGetValue(user.Id, out var mbr))
mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id };
var ea = new GuildBanRemoveEventArgs(this.ServiceProvider)
{
Guild = guild,
Member = mbr
};
await this._guildBanRemoved.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Guild Scheduled Event
///
/// Handles the scheduled event create event.
///
/// The created event.
/// The guild.
internal async Task OnGuildScheduledEventCreateEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild)
{
scheduledEvent.Discord = this;
guild.ScheduledEventsInternal.AddOrUpdate(scheduledEvent.Id, scheduledEvent, (old, newScheduledEvent) => newScheduledEvent);
if (scheduledEvent.Creator != null)
{
scheduledEvent.Creator.Discord = this;
this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) =>
{
old.Username = scheduledEvent.Creator.Username;
old.Discriminator = scheduledEvent.Creator.Discriminator;
old.AvatarHash = scheduledEvent.Creator.AvatarHash;
old.Flags = scheduledEvent.Creator.Flags;
old.GlobalName = scheduledEvent.Creator.GlobalName;
return old;
});
}
await this._guildScheduledEventCreated.InvokeAsync(this, new GuildScheduledEventCreateEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = scheduledEvent.Guild }).ConfigureAwait(false);
}
///
/// Handles the scheduled event update event.
///
/// The updated event.
/// The guild.
internal async Task OnGuildScheduledEventUpdateEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild)
{
if (guild == null)
return;
DiscordScheduledEvent oldEvent;
if (!guild.ScheduledEventsInternal.ContainsKey(scheduledEvent.Id))
{
oldEvent = null;
}
else
{
var ev = guild.ScheduledEventsInternal[scheduledEvent.Id];
oldEvent = new DiscordScheduledEvent
{
Id = ev.Id,
ChannelId = ev.ChannelId,
EntityId = ev.EntityId,
EntityMetadata = ev.EntityMetadata,
CreatorId = ev.CreatorId,
Creator = ev.Creator,
Discord = this,
Description = ev.Description,
EntityType = ev.EntityType,
ScheduledStartTimeRaw = ev.ScheduledStartTimeRaw,
ScheduledEndTimeRaw = ev.ScheduledEndTimeRaw,
GuildId = ev.GuildId,
Status = ev.Status,
Name = ev.Name,
UserCount = ev.UserCount,
CoverImageHash = ev.CoverImageHash
};
}
if (scheduledEvent.Creator != null)
{
scheduledEvent.Creator.Discord = this;
this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) =>
{
old.Username = scheduledEvent.Creator.Username;
old.Discriminator = scheduledEvent.Creator.Discriminator;
old.AvatarHash = scheduledEvent.Creator.AvatarHash;
old.Flags = scheduledEvent.Creator.Flags;
old.GlobalName = scheduledEvent.Creator.GlobalName;
return old;
});
}
if (scheduledEvent.Status == ScheduledEventStatus.Completed)
{
guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent);
await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, Reason = ScheduledEventStatus.Completed }).ConfigureAwait(false);
}
else if (scheduledEvent.Status == ScheduledEventStatus.Canceled)
{
guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent);
scheduledEvent.Status = ScheduledEventStatus.Canceled;
await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, Reason = ScheduledEventStatus.Canceled }).ConfigureAwait(false);
}
else
{
this.UpdateScheduledEvent(scheduledEvent, guild);
await this._guildScheduledEventUpdated.InvokeAsync(this, new GuildScheduledEventUpdateEventArgs(this.ServiceProvider) { ScheduledEventBefore = oldEvent, ScheduledEventAfter = scheduledEvent, Guild = guild }).ConfigureAwait(false);
}
}
///
/// Handles the scheduled event delete event.
///
/// The deleted event.
/// The guild.
internal async Task OnGuildScheduledEventDeleteEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild)
{
scheduledEvent.Discord = this;
if (scheduledEvent.Status == ScheduledEventStatus.Scheduled)
scheduledEvent.Status = ScheduledEventStatus.Canceled;
if (scheduledEvent.Creator != null)
{
scheduledEvent.Creator.Discord = this;
this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) =>
{
old.Username = scheduledEvent.Creator.Username;
old.Discriminator = scheduledEvent.Creator.Discriminator;
old.AvatarHash = scheduledEvent.Creator.AvatarHash;
old.Flags = scheduledEvent.Creator.Flags;
old.GlobalName = scheduledEvent.Creator.GlobalName;
return old;
});
}
await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = scheduledEvent.Guild, Reason = scheduledEvent.Status }).ConfigureAwait(false);
guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent);
}
///
/// Handles the scheduled event user add event.
/// The event.
/// The added user id.
/// The guild.
///
internal async Task OnGuildScheduledEventUserAddedEventAsync(ulong guildScheduledEventId, ulong userId, DiscordGuild guild)
{
var scheduledEvent = this.InternalGetCachedScheduledEvent(guildScheduledEventId) ?? this.UpdateScheduledEvent(new DiscordScheduledEvent
{
Id = guildScheduledEventId,
GuildId = guild.Id,
Discord = this,
UserCount = 0
}, guild);
scheduledEvent.UserCount++;
scheduledEvent.Discord = this;
guild.Discord = this;
var user = this.GetUserAsync(userId, true).Result;
user.Discord = this;
var member = guild.Members.TryGetValue(userId, out var mem) ? mem : guild.GetMemberAsync(userId).Result;
member.Discord = this;
await this._guildScheduledEventUserAdded.InvokeAsync(this, new GuildScheduledEventUserAddEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, User = user, Member = member }).ConfigureAwait(false);
}
///
/// Handles the scheduled event user remove event.
/// The event.
/// The removed user id.
/// The guild.
///
internal async Task OnGuildScheduledEventUserRemovedEventAsync(ulong guildScheduledEventId, ulong userId, DiscordGuild guild)
{
var scheduledEvent = this.InternalGetCachedScheduledEvent(guildScheduledEventId) ?? this.UpdateScheduledEvent(new DiscordScheduledEvent
{
Id = guildScheduledEventId,
GuildId = guild.Id,
Discord = this,
UserCount = 0
}, guild);
scheduledEvent.UserCount = scheduledEvent.UserCount == 0 ? 0 : scheduledEvent.UserCount - 1;
scheduledEvent.Discord = this;
guild.Discord = this;
var user = this.GetUserAsync(userId, true).Result;
user.Discord = this;
var member = guild.Members.TryGetValue(userId, out var mem) ? mem : guild.GetMemberAsync(userId).Result;
member.Discord = this;
await this._guildScheduledEventUserRemoved.InvokeAsync(this, new GuildScheduledEventUserRemoveEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, User = user, Member = member }).ConfigureAwait(false);
}
#endregion
#region Guild Integration
///
/// Handles the guild integration create event.
///
/// The guild.
/// The integration.
internal async Task OnGuildIntegrationCreateEventAsync(DiscordGuild guild, DiscordIntegration integration)
{
integration.Discord = this;
await this._guildIntegrationCreated.InvokeAsync(this, new GuildIntegrationCreateEventArgs(this.ServiceProvider) { Integration = integration, Guild = guild }).ConfigureAwait(false);
}
///
/// Handles the guild integration update event.
///
/// The guild.
/// The integration.
internal async Task OnGuildIntegrationUpdateEventAsync(DiscordGuild guild, DiscordIntegration integration)
{
integration.Discord = this;
await this._guildIntegrationUpdated.InvokeAsync(this, new GuildIntegrationUpdateEventArgs(this.ServiceProvider) { Integration = integration, Guild = guild }).ConfigureAwait(false);
}
///
/// Handles the guild integrations update event.
///
/// The guild.
internal async Task OnGuildIntegrationsUpdateEventAsync(DiscordGuild guild)
{
var ea = new GuildIntegrationsUpdateEventArgs(this.ServiceProvider)
{
Guild = guild
};
await this._guildIntegrationsUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the guild integration delete event.
///
/// The guild.
/// The integration id.
/// The optional application id.
internal async Task OnGuildIntegrationDeleteEventAsync(DiscordGuild guild, ulong integrationId, ulong? applicationId)
=> await this._guildIntegrationDeleted.InvokeAsync(this, new GuildIntegrationDeleteEventArgs(this.ServiceProvider) { Guild = guild, IntegrationId = integrationId, ApplicationId = applicationId }).ConfigureAwait(false);
#endregion
#region Guild Member
///
/// Handles the guild member add event.
///
/// The transport member.
/// The guild.
internal async Task OnGuildMemberAddEventAsync(TransportMember member, DiscordGuild guild)
{
var usr = new DiscordUser(member.User) { Discord = this };
usr = this.UserCache.AddOrUpdate(member.User.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
old.GlobalName = usr.GlobalName;
return old;
});
var mbr = new DiscordMember(member)
{
Discord = this,
GuildId = guild.Id
};
guild.MembersInternal[mbr.Id] = mbr;
guild.MemberCount++;
var ea = new GuildMemberAddEventArgs(this.ServiceProvider)
{
Guild = guild,
Member = mbr
};
await this._guildMemberAdded.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the guild member remove event.
///
/// The transport user.
/// The guild.
internal async Task OnGuildMemberRemoveEventAsync(TransportUser user, DiscordGuild guild)
{
var usr = new DiscordUser(user);
if (!guild.MembersInternal.TryRemove(user.Id, out var mbr))
mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id };
guild.MemberCount--;
_ = this.UserCache.AddOrUpdate(user.Id, usr, (old, @new) => @new);
var ea = new GuildMemberRemoveEventArgs(this.ServiceProvider)
{
Guild = guild,
Member = mbr
};
await this._guildMemberRemoved.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the guild member update event.
///
/// The transport member.
/// The guild.
/// The roles.
/// The nick.
/// Whether the member is pending.
internal async Task OnGuildMemberUpdateEventAsync(TransportMember member, DiscordGuild guild, IEnumerable roles, string nick, bool? pending)
{
var usr = new DiscordUser(member.User) { Discord = this };
usr = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
old.GlobalName = usr.GlobalName;
return old;
});
if (!guild.Members.TryGetValue(member.User.Id, out var mbr))
mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id };
var old = mbr;
var gAvOld = old.GuildAvatarHash;
var avOld = old.AvatarHash;
var nickOld = mbr.Nickname;
var pendingOld = mbr.IsPending;
var rolesOld = new ReadOnlyCollection(new List(mbr.Roles));
var cduOld = mbr.CommunicationDisabledUntil;
mbr.MemberFlags = member.MemberFlags;
mbr.AvatarHashInternal = member.AvatarHash;
mbr.GuildAvatarHash = member.GuildAvatarHash;
mbr.Nickname = nick;
mbr.GuildPronouns = member.GuildPronouns;
mbr.IsPending = pending;
mbr.CommunicationDisabledUntil = member.CommunicationDisabledUntil;
mbr.RoleIdsInternal.Clear();
mbr.RoleIdsInternal.AddRange(roles);
guild.MembersInternal.AddOrUpdate(member.User.Id, mbr, (id, oldMbr) => oldMbr);
var timeoutUntil = member.CommunicationDisabledUntil;
/*this.Logger.LogTrace($"Timeout:\nBefore - {cduOld}\nAfter - {timeoutUntil}");
if ((timeoutUntil.HasValue && cduOld.HasValue) || (timeoutUntil == null && cduOld.HasValue) || (timeoutUntil.HasValue && cduOld == null))
{
// We are going to add a scheduled timer to assure that we get a auditlog entry.
var id = $"tt-{mbr.Id}-{guild.Id}-{DateTime.Now.ToLongTimeString()}";
this._tempTimers.Add(
id,
new(
new TimeoutHandler(
mbr,
guild,
cduOld,
timeoutUntil
),
new Timer(
this.TimeoutTimer,
id,
2000,
Timeout.Infinite
)
)
);
this.Logger.LogTrace("Scheduling timeout event.");
return;
}*/
//this.Logger.LogTrace("No timeout detected. Continuing on normal operation.");
var eargs = new GuildMemberUpdateEventArgs(this.ServiceProvider)
{
Guild = guild,
Member = mbr,
NicknameAfter = mbr.Nickname,
RolesAfter = new ReadOnlyCollection(new List(mbr.Roles)),
PendingAfter = mbr.IsPending,
TimeoutAfter = mbr.CommunicationDisabledUntil,
AvatarHashAfter = mbr.AvatarHash,
GuildAvatarHashAfter = mbr.GuildAvatarHash,
NicknameBefore = nickOld,
RolesBefore = rolesOld,
PendingBefore = pendingOld,
TimeoutBefore = cduOld,
AvatarHashBefore = avOld,
GuildAvatarHashBefore = gAvOld
};
await this._guildMemberUpdated.InvokeAsync(this, eargs).ConfigureAwait(false);
}
///
/// Handles timeout events.
///
/// Internally used as uid for the timer data.
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "")]
private async void TimeoutTimer(object state)
{
var tid = (string)state;
var data = this._tempTimers.First(x=> x.Key == tid).Value.Key;
var timer = this._tempTimers.First(x=> x.Key == tid).Value.Value;
IReadOnlyList auditlog = null;
DiscordAuditLogMemberUpdateEntry filtered = null;
try
{
auditlog = await data.Guild.GetAuditLogsAsync(10, null, AuditLogActionType.MemberUpdate);
var preFiltered = auditlog.Select(x => x as DiscordAuditLogMemberUpdateEntry).Where(x => x.Target.Id == data.Member.Id);
filtered = preFiltered.First();
}
catch (UnauthorizedException) { }
catch (Exception)
{
this.Logger.LogTrace("Failing timeout event.");
await timer.DisposeAsync();
this._tempTimers.Remove(tid);
return;
}
var actor = filtered?.UserResponsible as DiscordMember;
this.Logger.LogTrace("Trying to execute timeout event.");
if (data.TimeoutUntilOld.HasValue && data.TimeoutUntilNew.HasValue)
{
// A timeout was updated.
if (filtered != null && auditlog == null)
{
this.Logger.LogTrace("Re-scheduling timeout event.");
timer.Change(2000, Timeout.Infinite);
return;
}
var ea = new GuildMemberTimeoutUpdateEventArgs(this.ServiceProvider)
{
Guild = data.Guild,
Target = data.Member,
TimeoutBefore = data.TimeoutUntilOld.Value,
TimeoutAfter = data.TimeoutUntilNew.Value,
Actor = actor,
AuditLogId = filtered?.Id,
AuditLogReason = filtered?.Reason
};
await this._guildMemberTimeoutChanged.InvokeAsync(this, ea).ConfigureAwait(false);
}
else if (!data.TimeoutUntilOld.HasValue && data.TimeoutUntilNew.HasValue)
{
// A timeout was added.
if (filtered != null && auditlog == null)
{
this.Logger.LogTrace("Re-scheduling timeout event.");
timer.Change(2000, Timeout.Infinite);
return;
}
var ea = new GuildMemberTimeoutAddEventArgs(this.ServiceProvider)
{
Guild = data.Guild,
Target = data.Member,
Timeout = data.TimeoutUntilNew.Value,
Actor = actor,
AuditLogId = filtered?.Id,
AuditLogReason = filtered?.Reason
};
await this._guildMemberTimeoutAdded.InvokeAsync(this, ea).ConfigureAwait(false);
}
else if (data.TimeoutUntilOld.HasValue && !data.TimeoutUntilNew.HasValue)
{
// A timeout was removed.
if (filtered != null && auditlog == null)
{
this.Logger.LogTrace("Re-scheduling timeout event.");
timer.Change(2000, Timeout.Infinite);
return;
}
var ea = new GuildMemberTimeoutRemoveEventArgs(this.ServiceProvider)
{
Guild = data.Guild,
Target = data.Member,
TimeoutBefore = data.TimeoutUntilOld.Value,
Actor = actor,
AuditLogId = filtered?.Id,
AuditLogReason = filtered?.Reason
};
await this._guildMemberTimeoutRemoved.InvokeAsync(this, ea).ConfigureAwait(false);
}
// Ending timer because it worked.
this.Logger.LogTrace("Removing timeout event.");
await timer.DisposeAsync();
this._tempTimers.Remove(tid);
}
///
/// Handles the guild members chunk event.
///
/// The raw chunk data.
internal async Task OnGuildMembersChunkEventAsync(JObject dat)
{
var guild = this.Guilds[(ulong)dat["guild_id"]];
var chunkIndex = (int)dat["chunk_index"];
var chunkCount = (int)dat["chunk_count"];
var nonce = (string)dat["nonce"];
var mbrs = new HashSet();
var pres = new HashSet();
var members = dat["members"].ToObject();
foreach (var member in members)
{
var mbr = new DiscordMember(member) { Discord = this, GuildId = guild.Id };
if (!this.UserCache.ContainsKey(mbr.Id))
this.UserCache[mbr.Id] = new DiscordUser(member.User) { Discord = this };
guild.MembersInternal[mbr.Id] = mbr;
mbrs.Add(mbr);
}
guild.MemberCount = guild.MembersInternal.Count;
var ea = new GuildMembersChunkEventArgs(this.ServiceProvider)
{
Guild = guild,
Members = new ReadOnlySet(mbrs),
ChunkIndex = chunkIndex,
ChunkCount = chunkCount,
Nonce = nonce,
};
if (dat["presences"] != null)
{
var presences = dat["presences"].ToObject();
var presCount = presences.Length;
foreach (var presence in presences)
{
presence.Discord = this;
presence.Activity = new DiscordActivity(presence.RawActivity);
if (presence.RawActivities != null)
{
presence.InternalActivities = presence.RawActivities
.Select(x => new DiscordActivity(x)).ToArray();
}
pres.Add(presence);
}
ea.Presences = new ReadOnlySet(pres);
}
if (dat["not_found"] != null)
{
var nf = dat["not_found"].ToObject>();
ea.NotFound = new ReadOnlySet(nf);
}
await this._guildMembersChunked.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Guild Role
///
/// Handles the guild role create event.
///
/// The role.
/// The guild.
internal async Task OnGuildRoleCreateEventAsync(DiscordRole role, DiscordGuild guild)
{
role.Discord = this;
role.GuildId = guild.Id;
guild.RolesInternal[role.Id] = role;
var ea = new GuildRoleCreateEventArgs(this.ServiceProvider)
{
Guild = guild,
Role = role
};
await this._guildRoleCreated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the guild role update event.
///
/// The role.
/// The guild.
internal async Task OnGuildRoleUpdateEventAsync(DiscordRole role, DiscordGuild guild)
{
var newRole = guild.GetRole(role.Id);
var oldRole = new DiscordRole
{
GuildId = guild.Id,
ColorInternal = newRole.ColorInternal,
Discord = this,
IsHoisted = newRole.IsHoisted,
Id = newRole.Id,
IsManaged = newRole.IsManaged,
IsMentionable = newRole.IsMentionable,
Name = newRole.Name,
Permissions = newRole.Permissions,
Position = newRole.Position,
IconHash = newRole.IconHash,
Tags = newRole.Tags ?? null,
UnicodeEmojiString = newRole.UnicodeEmojiString
};
newRole.GuildId = guild.Id;
newRole.ColorInternal = role.ColorInternal;
newRole.IsHoisted = role.IsHoisted;
newRole.IsManaged = role.IsManaged;
newRole.IsMentionable = role.IsMentionable;
newRole.Name = role.Name;
newRole.Permissions = role.Permissions;
newRole.Position = role.Position;
newRole.IconHash = role.IconHash;
newRole.Tags = role.Tags ?? null;
newRole.UnicodeEmojiString = role.UnicodeEmojiString;
var ea = new GuildRoleUpdateEventArgs(this.ServiceProvider)
{
Guild = guild,
RoleAfter = newRole,
RoleBefore = oldRole
};
await this._guildRoleUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the guild role delete event.
///
/// The role id.
/// The guild.
internal async Task OnGuildRoleDeleteEventAsync(ulong roleId, DiscordGuild guild)
{
if (!guild.RolesInternal.TryRemove(roleId, out var role))
this.Logger.LogWarning($"Attempted to delete a nonexistent role ({roleId}) from guild ({guild}).");
var ea = new GuildRoleDeleteEventArgs(this.ServiceProvider)
{
Guild = guild,
Role = role
};
await this._guildRoleDeleted.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Invite
///
/// Handles the invite create event.
///
/// The channel id.
/// The guild id.
/// The invite.
internal async Task OnInviteCreateEventAsync(ulong channelId, ulong guildId, DiscordInvite invite)
{
var guild = this.InternalGetCachedGuild(guildId);
var channel = this.InternalGetCachedChannel(channelId);
invite.Discord = this;
if (invite.Inviter is not null)
{
invite.Inviter.Discord = this;
this.UserCache.AddOrUpdate(invite.Inviter.Id, invite.Inviter, (old, @new) => @new);
}
guild.Invites[invite.Code] = invite;
var ea = new InviteCreateEventArgs(this.ServiceProvider)
{
Channel = channel,
Guild = guild,
Invite = invite
};
await this._inviteCreated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the invite delete event.
///
/// The channel id.
/// The guild id.
/// The raw invite.
internal async Task OnInviteDeleteEventAsync(ulong channelId, ulong guildId, JToken dat)
{
var guild = this.InternalGetCachedGuild(guildId);
var channel = this.InternalGetCachedChannel(channelId);
if (!guild.Invites.TryRemove(dat["code"].ToString(), out var invite))
{
invite = dat.ToObject();
invite.Discord = this;
}
invite.IsRevoked = true;
var ea = new InviteDeleteEventArgs(this.ServiceProvider)
{
Channel = channel,
Guild = guild,
Invite = invite
};
await this._inviteDeleted.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Message
///
/// Handles the message acknowledge event.
///
/// The channel.
/// The message id.
internal async Task OnMessageAckEventAsync(DiscordChannel chn, ulong messageId)
{
if (this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == chn.Id, out var msg))
{
msg = new DiscordMessage
{
Id = messageId,
ChannelId = chn.Id,
Discord = this,
};
}
await this._messageAcknowledged.InvokeAsync(this, new MessageAcknowledgeEventArgs(this.ServiceProvider) { Message = msg }).ConfigureAwait(false);
}
///
/// Handles the message create event.
///
/// The message.
/// The transport user (author).
/// The transport member.
/// The reference transport user (author).
/// The reference transport member.
internal async Task OnMessageCreateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember)
{
message.Discord = this;
this.PopulateMessageReactionsAndCache(message, author, member);
message.PopulateMentions();
if (message.Channel == null && message.ChannelId == default)
this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Channel which the last message belongs to is not in cache - cache state might be invalid!");
if (message.ReferencedMessage != null)
{
message.ReferencedMessage.Discord = this;
this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember);
message.ReferencedMessage.PopulateMentions();
}
foreach (var sticker in message.Stickers)
sticker.Discord = this;
var ea = new MessageCreateEventArgs(this.ServiceProvider)
{
Message = message,
MentionedUsers = new ReadOnlyCollection(message.MentionedUsersInternal),
MentionedRoles = message.MentionedRolesInternal != null ? new ReadOnlyCollection(message.MentionedRolesInternal) : null,
MentionedChannels = message.MentionedChannelsInternal != null ? new ReadOnlyCollection(message.MentionedChannelsInternal) : null
};
await this._messageCreated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the message update event.
///
/// The message.
/// The transport user (author).
/// The transport member.
/// The reference transport user (author).
/// The reference transport member.
internal async Task OnMessageUpdateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember)
{
DiscordGuild guild;
message.Discord = this;
var eventMessage = message;
DiscordMessage oldmsg = null;
if (this.Configuration.MessageCacheSize == 0
|| this.MessageCache == null
|| !this.MessageCache.TryGet(xm => xm.Id == eventMessage.Id && xm.ChannelId == eventMessage.ChannelId, out message))
{
message = eventMessage;
this.PopulateMessageReactionsAndCache(message, author, member);
guild = message.Channel?.Guild;
if (message.ReferencedMessage != null)
{
message.ReferencedMessage.Discord = this;
this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember);
message.ReferencedMessage.PopulateMentions();
}
}
else
{
oldmsg = new DiscordMessage(message);
guild = message.Channel?.Guild;
message.EditedTimestampRaw = eventMessage.EditedTimestampRaw;
if (eventMessage.Content != null)
message.Content = eventMessage.Content;
message.EmbedsInternal.Clear();
message.EmbedsInternal.AddRange(eventMessage.EmbedsInternal);
message.Pinned = eventMessage.Pinned;
message.IsTts = eventMessage.IsTts;
}
message.PopulateMentions();
var ea = new MessageUpdateEventArgs(this.ServiceProvider)
{
Message = message,
MessageBefore = oldmsg,
MentionedUsers = new ReadOnlyCollection(message.MentionedUsersInternal),
MentionedRoles = message.MentionedRolesInternal != null ? new ReadOnlyCollection(message.MentionedRolesInternal) : null,
MentionedChannels = message.MentionedChannelsInternal != null ? new ReadOnlyCollection