diff --git a/DisCatSharp.CommandsNext/CommandsNextExtension.cs b/DisCatSharp.CommandsNext/CommandsNextExtension.cs
index e9447807a..9b7cab031 100644
--- a/DisCatSharp.CommandsNext/CommandsNextExtension.cs
+++ b/DisCatSharp.CommandsNext/CommandsNextExtension.cs
@@ -1,1086 +1,1095 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 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 DisCatSharp.Net.Abstractions;
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;
+ 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();
+ mentionedRoles = Utilities.GetRoleMentions(msg).ToList();
+ mentionedChannels = Utilities.GetChannelMentions(msg).Select(xid =>
+ new ChannelMention()
+ {
+ Discord = msg.Discord,
+ Name = msg.Channel.Guild.GetChannel(xid).Name,
+ GuildId = msg.Channel.GuildId,
+ Id = xid,
+ Type = msg.Channel.Guild.GetChannel(xid).Type
+ }).ToList();
}
else
{
mentionedUsers = Utilities.GetUserMentions(msg).Select(this.Client.GetCachedOrEmptyUserInternal).ToList();
}
}
msg.MentionedUsersInternal = mentionedUsers;
- msg.MentionedRolesInternal = mentionedRoles;
+ msg.MentionedRoleIdsInternal = 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);
if (this._userFriendlyTypeNames.ContainsKey(t))
this._userFriendlyTypeNames.Remove(t);
if (!ti.IsValueType)
return;
var nullableType = typeof(Nullable<>).MakeGenericType(t);
if (!this.ArgumentConverters.ContainsKey(nullableType))
return;
this.ArgumentConverters.Remove(nullableType);
this._userFriendlyTypeNames.Remove(nullableType);
}
///
/// Registers a user-friendly type name.
///
/// Type to register the name for.
/// Name to register.
public void RegisterUserFriendlyTypeName(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentNullException(nameof(value), "Name cannot be null or empty.");
var t = typeof(T);
var ti = t.GetTypeInfo();
if (!this.ArgumentConverters.ContainsKey(t))
throw new InvalidOperationException("Cannot register a friendly name for a type which has no associated converter.");
this._userFriendlyTypeNames[t] = value;
if (!ti.IsValueType)
return;
var nullableType = typeof(Nullable<>).MakeGenericType(t);
this._userFriendlyTypeNames[nullableType] = value;
}
///
/// Converts a type into user-friendly type name.
///
/// Type to convert.
/// User-friendly type name.
public string GetUserFriendlyTypeName(Type t)
{
if (this._userFriendlyTypeNames.ContainsKey(t))
return this._userFriendlyTypeNames[t];
var ti = t.GetTypeInfo();
if (ti.IsGenericTypeDefinition && t.GetGenericTypeDefinition() == typeof(Nullable<>))
{
var tn = ti.GenericTypeArguments[0];
return this._userFriendlyTypeNames.ContainsKey(tn) ? this._userFriendlyTypeNames[tn] : tn.Name;
}
return t.Name;
}
#endregion
#region Helpers
///
/// 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/Clients/Standalone/Dispatch/DiscordClient.Dispatch.cs b/DisCatSharp/Clients/Standalone/Dispatch/DiscordClient.Dispatch.cs
index 89170e045..ae7f774db 100644
--- a/DisCatSharp/Clients/Standalone/Dispatch/DiscordClient.Dispatch.cs
+++ b/DisCatSharp/Clients/Standalone/Dispatch/DiscordClient.Dispatch.cs
@@ -1,3578 +1,3606 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 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.Diagnostics.Metrics;
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 client.
///
public sealed partial class DiscordClient
{
#region Private Fields
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_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 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":
await this.OnMessageUpdateRaw(dat).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();
await this.OnMessageReactionAddAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], mbr, dat["emoji"].ToObject()).ConfigureAwait(false);
break;
case "message_reaction_remove":
await this.OnMessageReactionRemoveAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], dat["emoji"].ToObject()).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":
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()).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;
default:
await this.OnUnknownEventAsync(payload).ConfigureAwait(false);
this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown event: {0}\npayload: {1}", payload.EventName, payload.Data);
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.GatewayVersion = ready.GatewayVersion;
this._sessionId = ready.SessionId;
var rawGuildIndex = rawGuilds.ToDictionary(xt => (ulong)xt["id"], xt => (JObject)xt);
this.GuildsInternal.Clear();
foreach (var guild in ready.Guilds)
{
guild.Discord = this;
if (guild.ChannelsInternal == null)
guild.ChannelsInternal = new ConcurrentDictionary();
foreach (var xc in guild.Channels.Values)
{
xc.GuildId = guild.Id;
xc.Discord = this;
foreach (var xo in xc.PermissionOverwritesInternal)
{
xo.Discord = this;
xo.ChannelId = xc.Id;
}
}
if (guild.RolesInternal == null)
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;
return old;
});
guild.MembersInternal[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, GuildId = guild.Id };
}
}
if (guild.EmojisInternal == null)
guild.EmojisInternal = new ConcurrentDictionary();
foreach (var xe in guild.Emojis.Values)
xe.Discord = this;
if (guild.StickersInternal == null)
guild.StickersInternal = new ConcurrentDictionary();
foreach (var xs in guild.Stickers.Values)
xs.Discord = this;
if (guild.VoiceStatesInternal == null)
guild.VoiceStatesInternal = new ConcurrentDictionary();
foreach (var xvs in guild.VoiceStates.Values)
xvs.Discord = this;
if (guild.ThreadsInternal == null)
guild.ThreadsInternal = new ConcurrentDictionary();
foreach (var xt in guild.ThreadsInternal.Values)
xt.Discord = this;
if (guild.StageInstancesInternal == null)
guild.StageInstancesInternal = new ConcurrentDictionary();
foreach (var xsi in guild.StageInstancesInternal.Values)
xsi.Discord = this;
if (guild.ScheduledEventsInternal == null)
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.Discord = this;
foreach (var xo in channel.PermissionOverwritesInternal)
{
xo.Discord = this;
xo.ChannelId = channel.Id;
}
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();
foreach (var po in channel.PermissionOverwritesInternal)
{
po.Discord = this;
po.ChannelId = channel.Id;
}
channelNew.PermissionOverwritesInternal.AddRange(channel.PermissionOverwritesInternal);
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.Discord = this;
foreach (var xo in channel.PermissionOverwritesInternal)
{
xo.Discord = this;
xo.ChannelId = channel.Id;
}
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;
if (guild.ChannelsInternal == null)
guild.ChannelsInternal = new ConcurrentDictionary();
if (guild.ThreadsInternal == null)
guild.ThreadsInternal = new ConcurrentDictionary();
if (guild.RolesInternal == null)
guild.RolesInternal = new ConcurrentDictionary();
if (guild.ThreadsInternal == null)
guild.ThreadsInternal = new ConcurrentDictionary();
if (guild.StickersInternal == null)
guild.StickersInternal = new ConcurrentDictionary();
if (guild.EmojisInternal == null)
guild.EmojisInternal = new ConcurrentDictionary();
if (guild.VoiceStatesInternal == null)
guild.VoiceStatesInternal = new ConcurrentDictionary();
if (guild.MembersInternal == null)
guild.MembersInternal = new ConcurrentDictionary();
if (guild.ScheduledEventsInternal == null)
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.Discord = this;
foreach (var xo in xc.PermissionOverwritesInternal)
{
xo.Discord = this;
xo.ChannelId = xc.Id;
}
}
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];
if (guild.ChannelsInternal == null)
guild.ChannelsInternal = new ConcurrentDictionary();
if (guild.ThreadsInternal == null)
guild.ThreadsInternal = new ConcurrentDictionary();
if (guild.RolesInternal == null)
guild.RolesInternal = new ConcurrentDictionary();
if (guild.EmojisInternal == null)
guild.EmojisInternal = new ConcurrentDictionary();
if (guild.StickersInternal == null)
guild.StickersInternal = new ConcurrentDictionary();
if (guild.VoiceStatesInternal == null)
guild.VoiceStatesInternal = new ConcurrentDictionary();
if (guild.StageInstancesInternal == null)
guild.StageInstancesInternal = new ConcurrentDictionary();
if (guild.MembersInternal == null)
guild.MembersInternal = new ConcurrentDictionary();
if (guild.ScheduledEventsInternal == null)
guild.ScheduledEventsInternal = new ConcurrentDictionary();
this.UpdateCachedGuild(eventGuild, rawMembers);
foreach (var xc in guild.ChannelsInternal.Values)
{
xc.GuildId = guild.Id;
xc.Discord = this;
foreach (var xo in xc.PermissionOverwritesInternal)
{
xo.Discord = this;
xo.ChannelId = xc.Id;
}
}
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 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);
}
#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;
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;
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;
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;
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;
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;
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;
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.
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.TryGetLastOrDefault(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 = message.MentionedUsers.ToList(),
MentionedRoles = message.MentionedRoles.ToList(),
MentionedChannels = message.MentionedChannels.ToList()
};
await this._messageCreated.InvokeAsync(this, ea).ConfigureAwait(false);
}
internal async Task OnMessageUpdateRaw(JObject data)
{
var guild = data["guild_id"];
if (guild.ToString() == "858089274087309313")
this.Logger.LogDebug(data.ToString());
// Valid
// To check
var rawRefMsg = data["referenced_message"];
TransportUser refUsr = null;
TransportMember refMbr = null;
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.OnMessageUpdateEventAsyncV2(data.ToDiscordObject(), refUsr, refMbr).ConfigureAwait(false);
}
internal async Task OnMessageUpdateEventAsyncV2(DiscordMessage message, TransportUser referenceAuthor, TransportMember referenceMember)
{
DiscordMessage oldMessage = null;
message.Discord = this;
// If we have a guild id, we try to get the guild out of the cache, if not cached, we fetch from the api, otherwise null.
var guild = message.GuildId != null && message.GuildId.HasValue ?
this.InternalGetCachedGuild(message.GuildId) ?? await this.GetGuildAsync(message.GuildId.Value, false)
: null;
message.Guild = guild;
// We try to get the channel from the guild if guild id is given. This depends on the cache. If not cached, we fetch it from the api.
message.Channel = guild != null ?
this.InternalGetCachedChannel(message.ChannelId, guild.Id) ?? await this.ApiClient.GetChannelAsync(message.ChannelId)
: await this.ApiClient.GetChannelAsync(message.ChannelId);
// When the message cache is not disabled and we get an old message
if (this.Configuration.MessageCacheSize != 0 && this.MessageCache != null && this.MessageCache.TryGetLastOrDefault(xm => xm.Id == message.Id && xm.ChannelId == message.ChannelId, out oldMessage))
{
oldMessage.Discord = this;
oldMessage.Guild = message.Guild;
oldMessage.Channel = message.Channel;
}
// If we have a transport member, we convert it to a discord member and update the members cache.
if (message.MemberInternal != null)
{
message.MemberInternal.User = message.AuthorInternal;
message.Member = new(message.MemberInternal)
{
Discord = this
};
guild.MembersInternal.AddOrUpdate(message.AuthorInternal.Id, message.Member, (id, oldMbr) => oldMbr);
}
this.PopulateMessageReactionsAndCache(message, message.AuthorInternal, message.MemberInternal);
if (message.ReferencedMessage != null)
{
message.ReferencedMessage.Discord = this;
this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember);
message.ReferencedMessage.PopulateMentions();
}
// Handling of user mentions
if (message.MentionedUsersInternal.Any())
foreach (var user in message.MentionedUsers)
{
user.Discord = this;
if (user.Id != message.Author.Id)
this.UserCache.AddOrUpdate(user.Id, user, (id, oldUsr) => oldUsr);
}
// This is fucking hacky
if (!message.MentionedChannelsInternal.Any() && message.Content != null && Utilities.ContainsChannelMentions(message.Content))
{
List mentions = new();
var channelMentions = Utilities.GetChannelMentions(message);
foreach(var mention in channelMentions)
{
- var channel = this.InternalGetCachedChannel(mention) ?? this.InternalGetCachedThread(mention) ?? await this.ApiClient.GetChannelAsync(mention);
- if (channel.Name == null)
+ try
+ {
+ var channel = this.InternalGetCachedChannel(mention) ?? this.InternalGetCachedThread(mention) ?? await this.ApiClient.GetChannelAsync(mention);
+ if (channel.Name == null)
+ {
+ mentions.Add(new ChannelMention()
+ {
+ Id = mention,
+ GuildId = null,
+ Name = null,
+ Discord = this,
+ Type = null
+ });
+ continue;
+ }
+ if (channel != null && channel.GuildId.HasValue)
+ mentions.Add(new ChannelMention()
+ {
+ Id = channel.Id,
+ GuildId = channel.GuildId,
+ Name = channel.Name,
+ Discord = this,
+ Type = channel.Type
+ });
+ else
+ mentions.Add(new ChannelMention()
+ {
+ Id = mention,
+ GuildId = null,
+ Name = null,
+ Discord = this,
+ Type = null
+ });
+ continue;
+ }
+ catch (UnauthorizedException)
{
mentions.Add(new ChannelMention()
{
Id = mention,
GuildId = null,
Name = null,
Discord = this,
Type = null
});
continue;
}
- if (channel != null && channel.GuildId.HasValue)
- mentions.Add(new ChannelMention()
- {
- Id = channel.Id,
- GuildId = channel.GuildId,
- Name = channel.Name,
- Discord = this,
- Type = channel.Type
- });
- else
+ catch (NotFoundException)
+ {
mentions.Add(new ChannelMention()
{
Id = mention,
GuildId = null,
Name = null,
Discord = this,
Type = null
});
+ continue;
+ }
}
message.MentionedChannelsInternal.AddRange(mentions);
}
message.PopulateMentions();
// embed cache is broken, component cache is broken
var ea = new MessageUpdateEventArgs(this.ServiceProvider)
{
Message = message,
MessageBefore = oldMessage,
MentionedUsers = message.MentionedUsers.ToList(),
MentionedRoles = message.MentionedRoles.ToList(),
MentionedChannels = message.MentionedChannels.ToList()
};
await this._messageUpdated.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.TryGetFirstOrDefault(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.Pinned = eventMessage.Pinned;
message.IsTts = eventMessage.IsTts;
}
if (message.EmbedsInternal != null)
{
message.EmbedsInternal.Clear();
message.EmbedsInternal.AddRange(eventMessage.EmbedsInternal ?? new());
}
else
message.EmbedsInternal = new(eventMessage.EmbedsInternal ?? new());
if (message.ComponentsInternal != null)
{
message.ComponentsInternal.Clear();
message.ComponentsInternal.AddRange(eventMessage.ComponentsInternal ?? new());
}
else
message.ComponentsInternal = new(eventMessage.ComponentsInternal ?? new());
if (message.MentionedUsersInternal != null)
{
message.MentionedUsersInternal.Clear();
message.MentionedUsersInternal.AddRange(eventMessage.MentionedUsersInternal ?? new());
}
else
message.MentionedUsersInternal = new(eventMessage.MentionedUsersInternal ?? new());
if (message.MentionedRolesInternal != null)
{
message.MentionedRolesInternal.Clear();
message.MentionedRolesInternal.AddRange(eventMessage.MentionedRolesInternal ?? new());
}
else
message.MentionedRolesInternal = new(eventMessage.MentionedRolesInternal ?? new());
if (message.MentionedChannelsInternal != null)
{
message.MentionedChannelsInternal.Clear();
message.MentionedChannelsInternal.AddRange(eventMessage.MentionedChannelsInternal ?? new());
}
else
message.MentionedChannelsInternal = new(eventMessage.MentionedChannelsInternal ?? new());
message.MentionEveryone = eventMessage.MentionEveryone;
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(message.MentionedChannelsInternal) : null
};
await this._messageUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
*/
///
/// Handles the message delete event.
///
/// The message id.
/// The channel id.
/// The optional guild id.
internal async Task OnMessageDeleteEventAsync(ulong messageId, ulong channelId, ulong? guildId)
{
var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId);
var guild = this.InternalGetCachedGuild(guildId);
if (channel == null
|| this.Configuration.MessageCacheSize == 0
|| this.MessageCache == null
|| !this.MessageCache.TryGetLastOrDefault(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg))
{
msg = new DiscordMessage
{
Id = messageId,
ChannelId = channelId,
Discord = this,
};
}
if (this.Configuration.MessageCacheSize > 0)
this.MessageCache?.Remove(xm => xm.Id == msg.Id && xm.ChannelId == channelId);
var ea = new MessageDeleteEventArgs(this.ServiceProvider)
{
Channel = channel,
Message = msg,
Guild = guild
};
await this._messageDeleted.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the message bulk delete event.
///
/// The message ids.
/// The channel id.
/// The optional guild id.
internal async Task OnMessageBulkDeleteEventAsync(ulong[] messageIds, ulong channelId, ulong? guildId)
{
var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId);
var msgs = new List(messageIds.Length);
foreach (var messageId in messageIds)
{
if (channel == null
|| this.Configuration.MessageCacheSize == 0
|| this.MessageCache == null
|| !this.MessageCache.TryGetLastOrDefault(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg))
{
msg = new DiscordMessage
{
Id = messageId,
ChannelId = channelId,
Discord = this,
};
}
if (this.Configuration.MessageCacheSize > 0)
this.MessageCache?.Remove(xm => xm.Id == msg.Id && xm.ChannelId == channelId);
msgs.Add(msg);
}
var guild = this.InternalGetCachedGuild(guildId);
var ea = new MessageBulkDeleteEventArgs(this.ServiceProvider)
{
Channel = channel,
Messages = new ReadOnlyCollection(msgs),
Guild = guild
};
await this._messagesBulkDeleted.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Message Reaction
///
/// Handles the message reaction add event.
///
/// The user id.
/// The message id.
/// The channel id.
/// The optional guild id.
/// The transport member.
/// The emoji.
internal async Task OnMessageReactionAddAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, TransportMember mbr, DiscordEmoji emoji)
{
var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId);
var guild = this.InternalGetCachedGuild(guildId);
emoji.Discord = this;
var usr = this.UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr);
if (channel == null
|| this.Configuration.MessageCacheSize == 0
|| this.MessageCache == null
|| !this.MessageCache.TryGetLastOrDefault(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg))
{
msg = new DiscordMessage
{
Id = messageId,
ChannelId = channelId,
Discord = this,
ReactionsInternal = new List()
};
}
var react = msg.ReactionsInternal.FirstOrDefault(xr => xr.Emoji == emoji);
if (react == null)
{
msg.ReactionsInternal.Add(react = new DiscordReaction
{
Count = 1,
Emoji = emoji,
IsMe = this.CurrentUser.Id == userId
});
}
else
{
react.Count++;
react.IsMe |= this.CurrentUser.Id == userId;
}
var ea = new MessageReactionAddEventArgs(this.ServiceProvider)
{
Message = msg,
User = usr,
Guild = guild,
Emoji = emoji
};
await this._messageReactionAdded.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the message reaction remove event.
///
/// The user id.
/// The message id.
/// The channel id.
/// The guild id.
/// The emoji.
internal async Task OnMessageReactionRemoveAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, DiscordEmoji emoji)
{
var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId);
emoji.Discord = this;
if (!this.UserCache.TryGetValue(userId, out var usr))
usr = new DiscordUser { Id = userId, Discord = this };
if (channel?.Guild != null)
usr = channel.Guild.Members.TryGetValue(userId, out var member)
? member
: new DiscordMember(usr) { Discord = this, GuildId = channel.GuildId.Value };
if (channel == null
|| this.Configuration.MessageCacheSize == 0
|| this.MessageCache == null
|| !this.MessageCache.TryGetLastOrDefault(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg))
{
msg = new DiscordMessage
{
Id = messageId,
ChannelId = channelId,
Discord = this
};
}
var react = msg.ReactionsInternal?.FirstOrDefault(xr => xr.Emoji == emoji);
if (react != null)
{
react.Count--;
react.IsMe &= this.CurrentUser.Id != userId;
if (msg.ReactionsInternal != null && react.Count <= 0) // shit happens
msg.ReactionsInternal.RemoveFirst(x => x.Emoji == emoji);
}
var guild = this.InternalGetCachedGuild(guildId);
var ea = new MessageReactionRemoveEventArgs(this.ServiceProvider)
{
Message = msg,
User = usr,
Guild = guild,
Emoji = emoji
};
await this._messageReactionRemoved.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the message reaction remove event.
/// Fired when all message reactions were removed.
///
/// The message id.
/// The channel id.
/// The optional guild id.
internal async Task OnMessageReactionRemoveAllAsync(ulong messageId, ulong channelId, ulong? guildId)
{
var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId);
if (channel == null
|| this.Configuration.MessageCacheSize == 0
|| this.MessageCache == null
|| !this.MessageCache.TryGetLastOrDefault(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg))
{
msg = new DiscordMessage
{
Id = messageId,
ChannelId = channelId,
Discord = this
};
}
msg.ReactionsInternal?.Clear();
var guild = this.InternalGetCachedGuild(guildId);
var ea = new MessageReactionsClearEventArgs(this.ServiceProvider)
{
Message = msg
};
await this._messageReactionsCleared.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the message reaction remove event.
/// Fired when a emoji got removed.
///
/// The message id.
/// The channel id.
/// The guild id.
/// The raw discord emoji.
internal async Task OnMessageReactionRemoveEmojiAsync(ulong messageId, ulong channelId, ulong guildId, JToken dat)
{
var guild = this.InternalGetCachedGuild(guildId);
var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId);
if (channel == null
|| this.Configuration.MessageCacheSize == 0
|| this.MessageCache == null
|| !this.MessageCache.TryGetLastOrDefault(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg))
{
msg = new DiscordMessage
{
Id = messageId,
ChannelId = channelId,
Discord = this
};
}
var partialEmoji = dat.ToObject();
if (!guild.EmojisInternal.TryGetValue(partialEmoji.Id, out var emoji))
{
emoji = partialEmoji;
emoji.Discord = this;
}
msg.ReactionsInternal?.RemoveAll(r => r.Emoji.Equals(emoji));
var ea = new MessageReactionRemoveEmojiEventArgs(this.ServiceProvider)
{
Channel = channel,
Guild = guild,
Message = msg,
Emoji = emoji
};
await this._messageReactionRemovedEmoji.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Stage Instance
///
/// Handles the stage instance create event.
///
/// The created stage instance.
internal async Task OnStageInstanceCreateEventAsync(DiscordStageInstance stage)
{
stage.Discord = this;
var guild = this.InternalGetCachedGuild(stage.GuildId);
guild.StageInstancesInternal[stage.Id] = stage;
await this._stageInstanceCreated.InvokeAsync(this, new StageInstanceCreateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false);
}
///
/// Handles the stage instance update event.
///
/// The updated stage instance.
internal async Task OnStageInstanceUpdateEventAsync(DiscordStageInstance stage)
{
stage.Discord = this;
var guild = this.InternalGetCachedGuild(stage.GuildId);
guild.StageInstancesInternal[stage.Id] = stage;
await this._stageInstanceUpdated.InvokeAsync(this, new StageInstanceUpdateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false);
}
///
/// Handles the stage instance delete event.
///
/// The deleted stage instance.
internal async Task OnStageInstanceDeleteEventAsync(DiscordStageInstance stage)
{
stage.Discord = this;
var guild = this.InternalGetCachedGuild(stage.GuildId);
guild.StageInstancesInternal[stage.Id] = stage;
await this._stageInstanceDeleted.InvokeAsync(this, new StageInstanceDeleteEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false);
}
#endregion
#region Thread
///
/// Handles the thread create event.
///
/// The created thread.
internal async Task OnThreadCreateEventAsync(DiscordThreadChannel thread)
{
thread.Discord = this;
this.InternalGetCachedGuild(thread.GuildId).ThreadsInternal.AddOrUpdate(thread.Id, thread, (oldThread, newThread) => newThread);
await this._threadCreated.InvokeAsync(this, new ThreadCreateEventArgs(this.ServiceProvider) { Thread = thread, Guild = thread.Guild, Parent = thread.Parent }).ConfigureAwait(false);
}
///
/// Handles the thread update event.
///
/// The updated thread.
internal async Task OnThreadUpdateEventAsync(DiscordThreadChannel thread)
{
if (thread == null)
return;
thread.Discord = this;
var guild = thread.Guild;
var threadNew = this.InternalGetCachedThread(thread.Id);
DiscordThreadChannel threadOld = null;
ThreadUpdateEventArgs updateEvent;
if (threadNew != null)
{
threadOld = new DiscordThreadChannel
{
Discord = this,
Type = threadNew.Type,
ThreadMetadata = thread.ThreadMetadata,
ThreadMembersInternal = threadNew.ThreadMembersInternal,
ParentId = thread.ParentId,
OwnerId = thread.OwnerId,
Name = thread.Name,
LastMessageId = threadNew.LastMessageId,
MessageCount = thread.MessageCount,
MemberCount = thread.MemberCount,
GuildId = thread.GuildId,
LastPinTimestampRaw = threadNew.LastPinTimestampRaw,
PerUserRateLimit = threadNew.PerUserRateLimit,
CurrentMember = threadNew.CurrentMember
};
threadNew.ThreadMetadata = thread.ThreadMetadata;
threadNew.ParentId = thread.ParentId;
threadNew.OwnerId = thread.OwnerId;
threadNew.Name = thread.Name;
threadNew.LastMessageId = thread.LastMessageId.HasValue ? thread.LastMessageId : threadOld.LastMessageId;
threadNew.MessageCount = thread.MessageCount;
threadNew.MemberCount = thread.MemberCount;
threadNew.GuildId = thread.GuildId;
updateEvent = new ThreadUpdateEventArgs(this.ServiceProvider)
{
ThreadAfter = thread,
ThreadBefore = threadOld,
Guild = thread.Guild,
Parent = thread.Parent
};
}
else
{
updateEvent = new ThreadUpdateEventArgs(this.ServiceProvider)
{
ThreadAfter = thread,
Guild = thread.Guild,
Parent = thread.Parent
};
guild.ThreadsInternal[thread.Id] = thread;
}
await this._threadUpdated.InvokeAsync(this, updateEvent).ConfigureAwait(false);
}
///
/// Handles the thread delete event.
///
/// The deleted thread.
internal async Task OnThreadDeleteEventAsync(DiscordThreadChannel thread)
{
if (thread == null)
return;
thread.Discord = this;
var gld = thread.Guild;
if (gld.ThreadsInternal.TryRemove(thread.Id, out var cachedThread))
thread = cachedThread;
await this._threadDeleted.InvokeAsync(this, new ThreadDeleteEventArgs(this.ServiceProvider) { Thread = thread, Guild = thread.Guild, Parent = thread.Parent, Type = thread.Type }).ConfigureAwait(false);
}
///
/// Handles the thread list sync event.
///
/// The synced guild.
/// The synced channel ids.
/// The synced threads.
/// The synced thread members.
internal async Task OnThreadListSyncEventAsync(DiscordGuild guild, IReadOnlyList channelIds, IReadOnlyList threads, IReadOnlyList members)
{
guild.Discord = this;
var channels = channelIds.Select(x => guild.GetChannel(x.Value)); //getting channel objects
foreach (var chan in channels)
{
chan.Discord = this;
}
_ = threads.Select(x => x.Discord = this);
await this._threadListSynced.InvokeAsync(this, new ThreadListSyncEventArgs(this.ServiceProvider) { Guild = guild, Channels = channels.ToList().AsReadOnly(), Threads = threads, Members = members.ToList().AsReadOnly() }).ConfigureAwait(false);
}
///
/// Handles the thread member update event.
///
/// The updated member.
internal async Task OnThreadMemberUpdateEventAsync(DiscordThreadChannelMember member)
{
member.Discord = this;
var thread = this.InternalGetCachedThread(member.Id);
if (thread == null)
{
var tempThread = await this.ApiClient.GetThreadAsync(member.Id);
thread = this.GuildsInternal[member.GuildId].ThreadsInternal.AddOrUpdate(member.Id, tempThread, (old, newThread) => newThread);
}
thread.CurrentMember = member;
thread.Guild.ThreadsInternal.AddOrUpdate(member.Id, thread, (oldThread, newThread) => newThread);
await this._threadMemberUpdated.InvokeAsync(this, new ThreadMemberUpdateEventArgs(this.ServiceProvider) { ThreadMember = member, Thread = thread }).ConfigureAwait(false);
}
///
/// Handles the thread members update event.
///
/// The target guild.
/// The thread id of the target thread this update belongs to.
/// The added members.
/// The ids of the removed members.
/// The new member count.
internal async Task OnThreadMembersUpdateEventAsync(DiscordGuild guild, ulong threadId, JArray membersAdded, JArray membersRemoved, int memberCount)
{
var thread = this.InternalGetCachedThread(threadId);
if (thread == null)
{
var tempThread = await this.ApiClient.GetThreadAsync(threadId);
thread = guild.ThreadsInternal.AddOrUpdate(threadId, tempThread, (old, newThread) => newThread);
}
thread.Discord = this;
guild.Discord = this;
List addedMembers = new();
List removedMemberIds = new();
if (membersAdded != null)
{
foreach (var xj in membersAdded)
{
var xtm = xj.ToDiscordObject();
xtm.Discord = this;
xtm.GuildId = guild.Id;
if (xtm != null)
addedMembers.Add(xtm);
if (xtm.Id == this.CurrentUser.Id)
thread.CurrentMember = xtm;
}
}
var removedMembers = new List();
if (membersRemoved != null)
{
foreach (var removedId in membersRemoved)
{
removedMembers.Add(guild.MembersInternal.TryGetValue((ulong)removedId, out var member) ? member : new DiscordMember { Id = (ulong)removedId, GuildId = guild.Id, Discord = this });
}
}
if (removedMemberIds.Contains(this.CurrentUser.Id)) //indicates the bot was removed from the thread
thread.CurrentMember = null;
thread.MemberCount = memberCount;
var threadMembersUpdateArg = new ThreadMembersUpdateEventArgs(this.ServiceProvider)
{
Guild = guild,
Thread = thread,
AddedMembers = addedMembers,
RemovedMembers = removedMembers,
MemberCount = memberCount
};
await this._threadMembersUpdated.InvokeAsync(this, threadMembersUpdateArg).ConfigureAwait(false);
}
#endregion
#region Activities
///
/// Dispatches the event.
///
/// The transport activity.
/// The guild.
/// The channel id.
/// The users in the activity.
/// The application id.
internal async Task OnEmbeddedActivityUpdateAsync(JObject trActivity, DiscordGuild guild, ulong channelId, JArray jUsers, ulong appId)
=> await Task.Delay(20);
/*{
try
{
var users = j_users?.ToObject>();
DiscordActivity old = null;
var uid = $"{guild.Id}_{channel_id}_{app_id}";
if (this._embeddedActivities.TryGetValue(uid, out var activity))
{
old = new DiscordActivity(activity);
DiscordJson.PopulateObject(tr_activity, activity);
}
else
{
activity = tr_activity.ToObject();
this._embeddedActivities[uid] = activity;
}
var activity_users = new List();
var channel = this.InternalGetCachedChannel(channel_id) ?? await this.ApiClient.GetChannelAsync(channel_id);
if (users != null)
{
foreach (var user in users)
{
var activity_user = guild._members.TryGetValue(user, out var member) ? member : new DiscordMember { Id = user, _guild_id = guild.Id, Discord = this };
activity_users.Add(activity_user);
}
}
else
activity_users = null;
var ea = new EmbeddedActivityUpdateEventArgs(this.ServiceProvider)
{
Guild = guild,
Users = activity_users,
Channel = channel
};
await this._embeddedActivityUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
} catch (Exception ex)
{
this.Logger.LogError(ex, ex.Message);
}
}*/
#endregion
#region User/Presence Update
///
/// Handles the presence update event.
///
/// The raw presence.
/// The raw user.
internal async Task OnPresenceUpdateEventAsync(JObject rawPresence, JObject rawUser)
{
var uid = (ulong)rawUser["id"];
DiscordPresence old = null;
if (this.PresencesInternal.TryGetValue(uid, out var presence))
{
old = new DiscordPresence(presence);
DiscordJson.PopulateObject(rawPresence, presence);
}
else
{
presence = rawPresence.ToObject();
presence.Discord = this;
presence.Activity = new DiscordActivity(presence.RawActivity);
this.PresencesInternal[presence.InternalUser.Id] = presence;
}
// reuse arrays / avoid linq (this is a hot zone)
if (presence.Activities == null || rawPresence["activities"] == null)
{
presence.InternalActivities = Array.Empty();
}
else
{
if (presence.InternalActivities.Length != presence.RawActivities.Length)
presence.InternalActivities = new DiscordActivity[presence.RawActivities.Length];
for (var i = 0; i < presence.InternalActivities.Length; i++)
presence.InternalActivities[i] = new DiscordActivity(presence.RawActivities[i]);
if (presence.InternalActivities.Length > 0)
{
presence.RawActivity = presence.RawActivities[0];
if (presence.Activity != null)
presence.Activity.UpdateWith(presence.RawActivity);
else
presence.Activity = new DiscordActivity(presence.RawActivity);
}
}
if (this.UserCache.TryGetValue(uid, out var usr))
{
if (old != null)
{
old.InternalUser.Username = usr.Username;
old.InternalUser.Discriminator = usr.Discriminator;
old.InternalUser.AvatarHash = usr.AvatarHash;
}
if (rawUser["username"] is object)
usr.Username = (string)rawUser["username"];
if (rawUser["discriminator"] is object)
usr.Discriminator = (string)rawUser["discriminator"];
if (rawUser["avatar"] is object)
usr.AvatarHash = (string)rawUser["avatar"];
presence.InternalUser.Username = usr.Username;
presence.InternalUser.Discriminator = usr.Discriminator;
presence.InternalUser.AvatarHash = usr.AvatarHash;
}
var usrafter = usr ?? new DiscordUser(presence.InternalUser);
var ea = new PresenceUpdateEventArgs(this.ServiceProvider)
{
Status = presence.Status,
Activity = presence.Activity,
User = usr,
PresenceBefore = old,
PresenceAfter = presence,
UserBefore = old != null ? new DiscordUser(old.InternalUser) : usrafter,
UserAfter = usrafter
};
await this._presenceUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the user settings update event.
///
/// The transport user.
internal async Task OnUserSettingsUpdateEventAsync(TransportUser user)
{
var usr = new DiscordUser(user) { Discord = this };
var ea = new UserSettingsUpdateEventArgs(this.ServiceProvider)
{
User = usr
};
await this._userSettingsUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the user update event.
///
/// The transport user.
internal async Task OnUserUpdateEventAsync(TransportUser user)
{
var usrOld = new DiscordUser
{
AvatarHash = this.CurrentUser.AvatarHash,
Discord = this,
Discriminator = this.CurrentUser.Discriminator,
Email = this.CurrentUser.Email,
Id = this.CurrentUser.Id,
IsBot = this.CurrentUser.IsBot,
MfaEnabled = this.CurrentUser.MfaEnabled,
Username = this.CurrentUser.Username,
Verified = this.CurrentUser.Verified
};
this.CurrentUser.AvatarHash = user.AvatarHash;
this.CurrentUser.Discriminator = user.Discriminator;
this.CurrentUser.Email = user.Email;
this.CurrentUser.Id = user.Id;
this.CurrentUser.IsBot = user.IsBot;
this.CurrentUser.MfaEnabled = user.MfaEnabled;
this.CurrentUser.Username = user.Username;
this.CurrentUser.Verified = user.Verified;
var ea = new UserUpdateEventArgs(this.ServiceProvider)
{
UserAfter = this.CurrentUser,
UserBefore = usrOld
};
await this._userUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Voice
///
/// Handles the voice state update event.
///
/// The raw voice state update object.
internal async Task OnVoiceStateUpdateEventAsync(JObject raw)
{
var gid = (ulong)raw["guild_id"];
var uid = (ulong)raw["user_id"];
var gld = this.GuildsInternal[gid];
var vstateNew = raw.ToObject();
vstateNew.Discord = this;
gld.VoiceStatesInternal.TryRemove(uid, out var vstateOld);
if (vstateNew.Channel != null)
{
gld.VoiceStatesInternal[vstateNew.UserId] = vstateNew;
}
if (gld.MembersInternal.TryGetValue(uid, out var mbr))
{
mbr.IsMuted = vstateNew.IsServerMuted;
mbr.IsDeafened = vstateNew.IsServerDeafened;
}
else
{
var transportMbr = vstateNew.TransportMember;
this.UpdateUser(new DiscordUser(transportMbr.User) { Discord = this }, gid, gld, transportMbr);
}
var ea = new VoiceStateUpdateEventArgs(this.ServiceProvider)
{
Guild = vstateNew.Guild,
Channel = vstateNew.Channel,
User = vstateNew.User,
SessionId = vstateNew.SessionId,
Before = vstateOld,
After = vstateNew
};
await this._voiceStateUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the voice server update event.
///
/// The new endpoint.
/// The new token.
/// The guild.
internal async Task OnVoiceServerUpdateEventAsync(string endpoint, string token, DiscordGuild guild)
{
var ea = new VoiceServerUpdateEventArgs(this.ServiceProvider)
{
Endpoint = endpoint,
VoiceToken = token,
Guild = guild
};
await this._voiceServerUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Commands
///
/// Handles the application command create event.
///
/// The application command.
/// The optional guild id.
internal async Task OnApplicationCommandCreateAsync(DiscordApplicationCommand cmd, ulong? guildId)
{
cmd.Discord = this;
var guild = this.InternalGetCachedGuild(guildId);
if (guild == null && guildId.HasValue)
{
guild = new DiscordGuild
{
Id = guildId.Value,
Discord = this
};
}
var ea = new ApplicationCommandEventArgs(this.ServiceProvider)
{
Guild = guild,
Command = cmd
};
await this._applicationCommandCreated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the application command update event.
///
/// The application command.
/// The optional guild id.
internal async Task OnApplicationCommandUpdateAsync(DiscordApplicationCommand cmd, ulong? guildId)
{
cmd.Discord = this;
var guild = this.InternalGetCachedGuild(guildId);
if (guild == null && guildId.HasValue)
{
guild = new DiscordGuild
{
Id = guildId.Value,
Discord = this
};
}
var ea = new ApplicationCommandEventArgs(this.ServiceProvider)
{
Guild = guild,
Command = cmd
};
await this._applicationCommandUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the application command delete event.
///
/// The application command.
/// The optional guild id.
internal async Task OnApplicationCommandDeleteAsync(DiscordApplicationCommand cmd, ulong? guildId)
{
cmd.Discord = this;
var guild = this.InternalGetCachedGuild(guildId);
if (guild == null && guildId.HasValue)
{
guild = new DiscordGuild
{
Id = guildId.Value,
Discord = this
};
}
var ea = new ApplicationCommandEventArgs(this.ServiceProvider)
{
Guild = guild,
Command = cmd
};
await this._applicationCommandDeleted.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the guild application command counts update event.
///
/// The count.
/// The count.
/// The count.
/// The guild id.
/// Count of application commands.
internal async Task OnGuildApplicationCommandCountsUpdateAsync(int chatInputCommandCount, int userContextMenuCommandCount, int messageContextMenuCount, ulong guildId)
{
var guild = this.InternalGetCachedGuild(guildId);
if (guild == null)
{
guild = new DiscordGuild
{
Id = guildId,
Discord = this
};
}
var ea = new GuildApplicationCommandCountEventArgs(this.ServiceProvider)
{
SlashCommands = chatInputCommandCount,
UserContextMenuCommands = userContextMenuCommandCount,
MessageContextMenuCommands = messageContextMenuCount,
Guild = guild
};
await this._guildApplicationCommandCountUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the application command permissions update event.
///
/// The new permissions.
/// The command id.
/// The guild id.
/// The application id.
internal async Task OnApplicationCommandPermissionsUpdateAsync(IEnumerable perms, ulong channelId, ulong guildId, ulong applicationId)
{
if (applicationId != this.CurrentApplication.Id)
return;
var guild = this.InternalGetCachedGuild(guildId);
DiscordApplicationCommand cmd;
try
{
cmd = await this.GetGuildApplicationCommandAsync(guildId, channelId);
}
catch (NotFoundException)
{
cmd = await this.GetGlobalApplicationCommandAsync(channelId);
}
if (guild == null)
{
guild = new DiscordGuild
{
Id = guildId,
Discord = this
};
}
var ea = new ApplicationCommandPermissionsUpdateEventArgs(this.ServiceProvider)
{
Permissions = perms.ToList(),
Command = cmd,
ApplicationId = applicationId,
Guild = guild
};
await this._applicationCommandPermissionsUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Interaction
///
/// Handles the interaction create event.
///
/// The guild id.
/// The channel id.
/// The transport user.
/// The transport member.
/// The interaction.
internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, TransportUser user, TransportMember member, DiscordInteraction interaction)
{
var usr = new DiscordUser(user) { Discord = this };
interaction.ChannelId = channelId;
interaction.GuildId = guildId;
interaction.Discord = this;
interaction.Data.Discord = this;
if (member != null)
{
usr = new DiscordMember(member) { GuildId = guildId.Value, Discord = this };
this.UpdateUser(usr, guildId, interaction.Guild, member);
}
else
{
this.UserCache.AddOrUpdate(usr.Id, usr, (old, @new) => @new);
}
interaction.User = usr;
var resolved = interaction.Data.Resolved;
if (resolved != null)
{
if (resolved.Users != null)
{
foreach (var c in resolved.Users)
{
c.Value.Discord = this;
this.UserCache.AddOrUpdate(c.Value.Id, c.Value, (old, @new) => @new);
}
}
if (resolved.Members != null)
{
foreach (var c in resolved.Members)
{
c.Value.Discord = this;
c.Value.Id = c.Key;
c.Value.GuildId = guildId.Value;
c.Value.User.Discord = this;
this.UserCache.AddOrUpdate(c.Value.User.Id, c.Value.User, (old, @new) => @new);
}
}
if (resolved.Channels != null)
{
foreach (var c in resolved.Channels)
{
c.Value.Discord = this;
if (guildId.HasValue)
c.Value.GuildId = guildId.Value;
}
}
if (resolved.Roles != null)
{
foreach (var c in resolved.Roles)
{
c.Value.Discord = this;
if (guildId.HasValue)
c.Value.GuildId = guildId.Value;
}
}
if (resolved.Messages != null)
{
foreach (var m in resolved.Messages)
{
m.Value.Discord = this;
if (guildId.HasValue)
m.Value.GuildId = guildId.Value;
}
}
if (resolved.Attachments != null)
foreach (var a in resolved.Attachments)
a.Value.Discord = this;
}
if (interaction.Type is InteractionType.Component || interaction.Type is InteractionType.ModalSubmit)
{
if (interaction.Message != null)
{
interaction.Message.Discord = this;
interaction.Message.ChannelId = interaction.ChannelId;
}
var cea = new ComponentInteractionCreateEventArgs(this.ServiceProvider)
{
Message = interaction.Message,
Interaction = interaction
};
await this._componentInteractionCreated.InvokeAsync(this, cea).ConfigureAwait(false);
}
else
{
if (interaction.Data.Target.HasValue) // Context-Menu. //
{
var targetId = interaction.Data.Target.Value;
DiscordUser targetUser = null;
DiscordMember targetMember = null;
DiscordMessage targetMessage = null;
interaction.Data.Resolved.Messages?.TryGetValue(targetId, out targetMessage);
interaction.Data.Resolved.Members?.TryGetValue(targetId, out targetMember);
interaction.Data.Resolved.Users?.TryGetValue(targetId, out targetUser);
var ctea = new ContextMenuInteractionCreateEventArgs(this.ServiceProvider)
{
Interaction = interaction,
TargetUser = targetMember ?? targetUser,
TargetMessage = targetMessage,
Type = interaction.Data.Type,
};
await this._contextMenuInteractionCreated.InvokeAsync(this, ctea).ConfigureAwait(false);
}
else
{
var ea = new InteractionCreateEventArgs(this.ServiceProvider)
{
Interaction = interaction
};
await this._interactionCreated.InvokeAsync(this, ea).ConfigureAwait(false);
}
}
}
#endregion
#region Misc
///
/// Handles the typing start event.
///
/// The user id.
/// The channel id.
/// The channel.
/// The optional guild id.
/// The time when the user started typing.
/// The transport member.
internal async Task OnTypingStartEventAsync(ulong userId, ulong channelId, DiscordChannel channel, ulong? guildId, DateTimeOffset started, TransportMember mbr)
{
if (channel == null)
{
channel = new DiscordChannel
{
Discord = this,
Id = channelId,
GuildId = guildId ?? default,
};
}
var guild = this.InternalGetCachedGuild(guildId);
var usr = this.UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr);
var ea = new TypingStartEventArgs(this.ServiceProvider)
{
Channel = channel,
User = usr,
Guild = guild,
StartedAt = started
};
await this._typingStarted.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the webhooks update.
///
/// The channel.
/// The guild.
internal async Task OnWebhooksUpdateAsync(DiscordChannel channel, DiscordGuild guild)
{
var ea = new WebhooksUpdateEventArgs(this.ServiceProvider)
{
Channel = channel,
Guild = guild
};
await this._webhooksUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles all unknown events.
///
/// The payload.
internal async Task OnUnknownEventAsync(GatewayPayload payload)
{
var ea = new UnknownEventArgs(this.ServiceProvider) { EventName = payload.EventName, Json = (payload.Data as JObject)?.ToString() };
await this._unknownEvent.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#endregion
}
diff --git a/DisCatSharp/Entities/Message/DiscordMessage.cs b/DisCatSharp/Entities/Message/DiscordMessage.cs
index 791e03faa..26ff2e7b8 100644
--- a/DisCatSharp/Entities/Message/DiscordMessage.cs
+++ b/DisCatSharp/Entities/Message/DiscordMessage.cs
@@ -1,864 +1,864 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 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.Globalization;
using System.Linq;
using System.Threading.Tasks;
using DisCatSharp.Enums;
using DisCatSharp.Net.Abstractions;
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents a Discord text message.
///
public class DiscordMessage : SnowflakeObject, IEquatable
{
///
/// Initializes a new instance of the class.
///
internal DiscordMessage()
{
this._reactionsLazy = new Lazy>(() => new ReadOnlyCollection(this.ReactionsInternal));
this._stickersLazy = new Lazy>(() => new ReadOnlyCollection(this.StickersInternal));
this._jumpLink = new Lazy(() =>
{
string gid = null;
if (this.Channel != null)
gid = this.Channel is DiscordDmChannel
? "@me"
: this.Channel is DiscordThreadChannel
? this.InternalThread.GuildId.Value.ToString(CultureInfo.InvariantCulture)
: this.Channel.GuildId.Value.ToString(CultureInfo.InvariantCulture);
var cid = this.ChannelId.ToString(CultureInfo.InvariantCulture);
var mid = this.Id.ToString(CultureInfo.InvariantCulture);
return new Uri($"https://{(this.Discord.Configuration.UseCanary ? "canary.discord.com" : this.Discord.Configuration.UsePtb ? "ptb.discord.com" : "discord.com")}/channels/{gid}/{cid}/{mid}");
});
}
///
/// Initializes a new instance of the class.
///
/// The other message.
internal DiscordMessage(DiscordMessage other)
: this()
{
this.Discord = other.Discord;
this.AttachmentsInternal = new(other.AttachmentsInternal);
this.EmbedsInternal = new(other.EmbedsInternal);
this.ComponentsInternal = new(other.ComponentsInternal);
if (other.MentionedChannelsInternal != null)
this.MentionedChannelsInternal = new(other.MentionedChannelsInternal);
if (other.MentionedRoleIdsInternal != null)
this.MentionedRoleIdsInternal = new(other.MentionedRoleIdsInternal);
this.MentionedUsersInternal = new(other.MentionedUsersInternal);
this.ReactionsInternal = new(other.ReactionsInternal);
this.StickersInternal = new(other.StickersInternal);
this.Author = other.Author;
this.ChannelId = other.ChannelId;
this.Content = other.Content;
this.EditedTimestampRaw = other.EditedTimestampRaw;
this.Id = other.Id;
this.IsTts = other.IsTts;
this.MessageType = other.MessageType;
this.Pinned = other.Pinned;
this.TimestampRaw = other.TimestampRaw;
this.WebhookId = other.WebhookId;
}
///
/// Gets the channel in which the message was sent.
///
[JsonIgnore]
public DiscordChannel Channel
{
get => (this.Discord as DiscordClient)?.InternalGetCachedChannel(this.ChannelId) ?? this._channel;
internal set => this._channel = value;
}
private DiscordChannel _channel;
///
/// Gets the thread in which the message was sent.
///
[JsonIgnore]
internal DiscordThreadChannel InternalThread
{
get => (this.Discord as DiscordClient)?.InternalGetCachedThread(this.ChannelId) ?? this._thread;
set => this._thread = value;
}
private DiscordThreadChannel _thread;
///
/// Gets the ID of the channel in which the message was sent.
///
[JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong ChannelId { get; internal set; }
///
/// Gets the components this message was sent with.
///
[JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)]
internal List ComponentsInternal;
[JsonIgnore]
public IReadOnlyList Components
=> new List(this.ComponentsInternal);
///
/// Gets the user or member that sent the message.
///
[JsonProperty("author", NullValueHandling = NullValueHandling.Ignore)]
internal TransportUser AuthorInternal;
[JsonIgnore]
public DiscordUser Author { get; internal set; }
[JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)]
internal TransportMember MemberInternal;
[JsonIgnore]
public DiscordMember Member { get; internal set; }
///
/// Gets the message's content.
///
[JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)]
public string Content { get; internal set; }
///
/// Gets the message's creation timestamp.
///
[JsonIgnore]
public DateTimeOffset Timestamp
=> !string.IsNullOrWhiteSpace(this.TimestampRaw) && DateTimeOffset.TryParse(this.TimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ?
dto : this.CreationTimestamp;
///
/// Gets the message's creation timestamp as raw string.
///
[JsonProperty("timestamp", NullValueHandling = NullValueHandling.Ignore)]
internal string TimestampRaw { get; set; }
///
/// Gets the message's edit timestamp. Will be null if the message was not edited.
///
[JsonIgnore]
public DateTimeOffset? EditedTimestamp
=> !string.IsNullOrWhiteSpace(this.EditedTimestampRaw) && DateTimeOffset.TryParse(this.EditedTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ?
(DateTimeOffset?)dto : null;
///
/// Gets the message's edit timestamp as raw string. Will be null if the message was not edited.
///
[JsonProperty("edited_timestamp", NullValueHandling = NullValueHandling.Ignore)]
internal string EditedTimestampRaw { get; set; }
///
/// Gets whether this message was edited.
///
[JsonIgnore]
public bool IsEdited
=> !string.IsNullOrWhiteSpace(this.EditedTimestampRaw);
///
/// Gets whether the message is a text-to-speech message.
///
[JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)]
public bool IsTts { get; internal set; }
///
/// Gets whether the message mentions everyone.
///
[JsonProperty("mention_everyone", NullValueHandling = NullValueHandling.Ignore)]
public bool MentionEveryone { get; internal set; }
///
/// Gets users or members mentioned by this message.
///
[JsonIgnore]
public IReadOnlyCollection MentionedUsers
- => this.MentionedUsersInternal.Any() ? this.MentionedUsersInternal.Select(x => new DiscordUser(x)).ToArray(): Array.Empty();
+ => this.MentionedUsersInternal.Any() ? this.MentionedUsersInternal.ToArray(): Array.Empty();
[JsonProperty("mentions", NullValueHandling = NullValueHandling.Ignore)]
- internal List MentionedUsersInternal = new();
+ internal List MentionedUsersInternal = new();
///
/// Gets roles mentioned by this message.
///
[JsonIgnore]
public IReadOnlyCollection MentionedRoles
=> this.MentionedRoleIdsInternal.Any() ? this.MentionedRolesInternal : Array.Empty();
[JsonIgnore]
internal List MentionedRolesInternal
=> this.GuildId != null && this.Guild != null && this.MentionedRoleIdsInternal.Any() ? this.MentionedRoleIdsInternal.Select(x => this.Guild.GetRole(x)).ToList() : new();
[JsonProperty("mention_roles")]
internal List MentionedRoleIdsInternal = new();
///
/// Gets channels mentioned by this message.
///
[JsonIgnore]
public IReadOnlyCollection MentionedChannels
=> this.MentionedChannelsInternal.Any()
? this.MentionedChannelsInternal.Select(
x =>
x.Type == ChannelType.NewsThread || x.Type == ChannelType.PublicThread || x.Type == ChannelType.PrivateThread
? x.GuildId.HasValue && this.Discord.Guilds.TryGetValue(x.GuildId.Value, out var guild) ? guild.GetThread(x.Id) : new DiscordThreadChannel(x.Name, x.Id, x.GuildId, x.Type ?? ChannelType.Unknown) { Discord = this.Discord }
: x.GuildId.HasValue && this.Discord.Guilds.TryGetValue(x.GuildId.Value, out var guild2) ? guild2.GetChannel(x.Id) : new DiscordChannel(x.Name, x.Id, x.GuildId, x.Type ?? ChannelType.Unknown) { Discord = this.Discord }
).ToArray()
: Array.Empty();
[JsonProperty("mention_channels", NullValueHandling = NullValueHandling.Ignore)]
internal List MentionedChannelsInternal = new();
///
/// Gets files attached to this message.
///
[JsonIgnore]
public IReadOnlyList Attachments
=> this.AttachmentsInternal;
[JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)]
internal List AttachmentsInternal = new();
///
/// Gets embeds attached to this message.
///
[JsonIgnore]
public IReadOnlyList Embeds
=> this.EmbedsInternal;
[JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)]
internal List EmbedsInternal = new();
///
/// Gets reactions used on this message.
///
[JsonIgnore]
public IReadOnlyList Reactions
=> this._reactionsLazy.Value;
[JsonProperty("reactions", NullValueHandling = NullValueHandling.Ignore)]
internal List ReactionsInternal = new();
[JsonIgnore]
private readonly Lazy> _reactionsLazy;
///
/// Gets the nonce sent with the message, if the message was sent by the client.
///
[JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)]
public string Nonce { get; internal set; }
///
/// Gets whether the message is pinned.
///
[JsonProperty("pinned", NullValueHandling = NullValueHandling.Ignore)]
public bool Pinned { get; internal set; }
///
/// Gets the id of the webhook that generated this message.
///
[JsonProperty("webhook_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong? WebhookId { get; internal set; }
///
/// Gets the type of the message.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public MessageType? MessageType { get; internal set; }
///
/// Gets the message activity in the Rich Presence embed.
///
[JsonProperty("activity", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMessageActivity Activity { get; internal set; }
///
/// Gets the message application in the Rich Presence embed.
///
[JsonProperty("application", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMessageApplication Application { get; internal set; }
///
/// Gets the message application id in the Rich Presence embed.
///
[JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong ApplicationId { get; internal set; }
///
/// Gets the internal reference.
///
[JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)]
internal InternalDiscordMessageReference? InternalReference { get; set; }
///
/// Gets the original message reference from the crossposted message.
///
[JsonIgnore]
public DiscordMessageReference Reference
=> this.InternalReference.HasValue ? this?.InternalBuildMessageReference() : null;
///
/// Gets the bitwise flags for this message.
///
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public MessageFlags? Flags { get; internal set; }
///
/// Gets whether the message originated from a webhook.
///
[JsonIgnore]
public bool WebhookMessage
=> this.WebhookId != null;
///
/// Gets the jump link to this message.
///
[JsonIgnore]
public Uri JumpLink => this._jumpLink.Value;
private readonly Lazy _jumpLink;
///
/// Gets stickers for this message.
///
[JsonIgnore]
public IReadOnlyList Stickers
=> this._stickersLazy.Value;
[JsonProperty("sticker_items", NullValueHandling = NullValueHandling.Ignore)]
internal List StickersInternal = new();
[JsonIgnore]
private readonly Lazy> _stickersLazy;
///
/// Gets the guild id.
///
[JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)]
internal ulong? GuildId { get; set; }
///
/// Gets the guild.
///
[JsonIgnore]
internal DiscordGuild Guild { get; set; }
///
/// Gets the message object for the referenced message
///
[JsonProperty("referenced_message", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMessage ReferencedMessage { get; internal set; }
///
/// Gets whether the message is a response to an interaction.
///
[JsonProperty("interaction", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMessageInteraction Interaction { get; internal set; }
///
/// Gets the thread that was started from this message.
///
[JsonProperty("thread", NullValueHandling = NullValueHandling.Ignore)]
public DiscordThreadChannel Thread { get; internal set; }
///
/// Build the message reference.
///
internal DiscordMessageReference InternalBuildMessageReference()
{
var client = this.Discord as DiscordClient;
var guildId = this.InternalReference.Value.GuildId;
var channelId = this.InternalReference.Value.ChannelId;
var messageId = this.InternalReference.Value.MessageId;
var reference = new DiscordMessageReference();
if (guildId.HasValue)
reference.Guild = client.GuildsInternal.TryGetValue(guildId.Value, out var g)
? g
: new DiscordGuild
{
Id = guildId.Value,
Discord = client
};
var channel = client.InternalGetCachedChannel(channelId.Value);
if (channel == null)
{
reference.Channel = new DiscordChannel
{
Id = channelId.Value,
Discord = client
};
if (guildId.HasValue)
reference.Channel.GuildId = guildId.Value;
}
else reference.Channel = channel;
if (client.MessageCache != null && client.MessageCache.TryGetLastOrDefault(m => m.Id == messageId.Value && m.ChannelId == channelId, out var msg))
reference.Message = msg;
else
{
reference.Message = new DiscordMessage
{
ChannelId = this.ChannelId,
Discord = client
};
if (messageId.HasValue)
reference.Message.Id = messageId.Value;
}
return reference;
}
///
/// Gets the mentions.
///
/// An array of IMentions.
private List GetMentions()
{
var mentions = new List();
if (this.ReferencedMessage != null && this.MentionedUsers.Contains(this.ReferencedMessage.Author))
mentions.Add(new RepliedUserMention());
if (this.MentionedUsersInternal.Any())
mentions.AddRange(this.MentionedUsers.Select(m => (IMention)new UserMention(m)));
if (this.MentionedRoleIdsInternal.Any())
mentions.AddRange(this.MentionedRoleIdsInternal.Select(r => (IMention)new RoleMention(r)));
return mentions;
}
///
/// Populates the mentions.
///
internal void PopulateMentions()
{
var guild = this.Channel?.Guild;
this.MentionedUsersInternal ??= new();
this.MentionedRoleIdsInternal ??= new();
this.MentionedChannelsInternal ??= new();
var mentionedUsers = new HashSet(new DiscordUserComparer());
if (guild != null)
{
foreach (var usr in this.MentionedUsers)
{
usr.Discord = this.Discord;
this.Discord.UserCache.AddOrUpdate(usr.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
return old;
});
}
}
}
///
/// Edits the message.
///
/// New content.
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Optional content)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, default, this.GetMentions(), default, default, Array.Empty(), default);
///
/// Edits the message.
///
/// New embed.
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Optional embed = default)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, embed.Map(v => new[] { v }).ValueOr(Array.Empty()), this.GetMentions(), default, default, Array.Empty(), default);
///
/// Edits the message.
///
/// New content.
/// New embed.
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Optional content, Optional embed = default)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embed.Map(v => new[] { v }).ValueOr(Array.Empty()), this.GetMentions(), default, default, Array.Empty(), default);
///
/// Edits the message.
///
/// New content.
/// New embeds.
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Optional content, Optional> embeds = default)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embeds, this.GetMentions(), default, default, Array.Empty(), default);
///
/// Edits the message.
///
/// The builder of the message to edit.
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ModifyAsync(DiscordMessageBuilder builder)
{
builder.Validate(true);
return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, Optional.Some(builder.Embeds.AsEnumerable()), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? Optional.Some(builder.Attachments.AsEnumerable()) : builder.KeepAttachmentsInternal.HasValue ? builder.KeepAttachmentsInternal.Value ? Optional.Some(this.Attachments.AsEnumerable()) : Array.Empty() : null);
}
///
/// Edits the message embed suppression.
///
/// Suppress embeds.
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifySuppressionAsync(bool suppress = false)
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, default, default, suppress, default, default);
///
/// Clears all attachments from the message.
///
///
public Task ClearAttachmentsAsync()
=> this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, this.GetMentions(), default, default, default, Array.Empty());
///
/// Edits the message.
///
/// The builder of the message to edit.
/// Thrown when the client tried to modify a message not sent by them.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ModifyAsync(Action action)
{
var builder = new DiscordMessageBuilder();
action(builder);
builder.Validate(true);
return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, Optional.Some(builder.Embeds.AsEnumerable()), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? Optional.Some(builder.Attachments.AsEnumerable()) : builder.KeepAttachmentsInternal.HasValue ? builder.KeepAttachmentsInternal.Value ? Optional.Some(this.Attachments.AsEnumerable()) : Array.Empty() : null);
}
///
/// Deletes the message.
///
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteAsync(string reason = null)
=> this.Discord.ApiClient.DeleteMessageAsync(this.ChannelId, this.Id, reason);
///
/// Creates a thread.
/// Depending on the of the parent channel it's either a or a .
///
/// The name of the thread.
/// till it gets archived. Defaults to
/// The per user ratelimit, aka slowdown.
/// The reason.
/// Thrown when the client does not have the or permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
/// Thrown when the cannot be modified.
public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration autoArchiveDuration = ThreadAutoArchiveDuration.OneHour, int? rateLimitPerUser = null, string reason = null) =>
Utilities.CheckThreadAutoArchiveDurationFeature(this.Channel.Guild, autoArchiveDuration)
? await this.Discord.ApiClient.CreateThreadAsync(this.ChannelId, this.Id, name, autoArchiveDuration, this.Channel.Type == ChannelType.News ? ChannelType.NewsThread : ChannelType.PublicThread, rateLimitPerUser, reason)
: throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.");
///
/// Pins the message in its channel.
///
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task PinAsync()
=> this.Discord.ApiClient.PinMessageAsync(this.ChannelId, this.Id);
///
/// Unpins the message in its channel.
///
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task UnpinAsync()
=> this.Discord.ApiClient.UnpinMessageAsync(this.ChannelId, this.Id);
///
/// Responds to the message. This produces a reply.
///
/// Message content to respond with.
/// The sent message.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RespondAsync(string content)
=> this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false);
///
/// Responds to the message. This produces a reply.
///
/// Embed to attach to the message.
/// The sent message.
/// Thrown when the client does not have the