diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index f73a27a11..90d3f5d57 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,20 +1,21 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"features": {
"ghcr.io/devcontainers/features/common-utils:1": {
"version": "latest"
},
"ghcr.io/devcontainers/features/github-cli:1": {
"version": "latest"
},
"ghcr.io/devcontainers/features/git-lfs:1": {
"version": "latest"
},
"ghcr.io/devcontainers/features/sshd:1": {
"version": "latest"
},
"ghcr.io/devcontainers/features/dotnet:1": {
- "version": "7.0.100"
+ "version": "7.0.100",
+ "installUsingApt": false
}
}
}
diff --git a/DisCatSharp.CommandsNext/CommandsNextExtension.cs b/DisCatSharp.CommandsNext/CommandsNextExtension.cs
index 717168ad7..a914f0332 100644
--- a/DisCatSharp.CommandsNext/CommandsNextExtension.cs
+++ b/DisCatSharp.CommandsNext/CommandsNextExtension.cs
@@ -1,1086 +1,1084 @@
// 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 Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace DisCatSharp.CommandsNext;
///
/// This is the class which handles command registration, management, and execution.
///
public class CommandsNextExtension : BaseExtension
{
///
/// Gets the config.
///
private readonly CommandsNextConfiguration _config;
///
/// Gets the help formatter.
///
private readonly HelpFormatterFactory _helpFormatter;
///
/// Gets the convert generic.
///
private readonly MethodInfo _convertGeneric;
///
/// Gets the user friendly type names.
///
private readonly Dictionary _userFriendlyTypeNames;
///
/// Gets the argument converters.
///
internal Dictionary ArgumentConverters { get; }
///
/// Gets the service provider this CommandsNext module was configured with.
///
public IServiceProvider Services
=> this._config.ServiceProvider;
///
/// Initializes a new instance of the class.
///
/// The cfg.
internal CommandsNextExtension(CommandsNextConfiguration cfg)
{
this._config = new CommandsNextConfiguration(cfg);
this._topLevelCommands = new Dictionary();
this._registeredCommandsLazy = new Lazy>(() => new ReadOnlyDictionary(this._topLevelCommands));
this._helpFormatter = new HelpFormatterFactory();
this._helpFormatter.SetFormatterType();
this.ArgumentConverters = new Dictionary
{
[typeof(string)] = new StringConverter(),
[typeof(bool)] = new BoolConverter(),
[typeof(sbyte)] = new Int8Converter(),
[typeof(byte)] = new Uint8Converter(),
[typeof(short)] = new Int16Converter(),
[typeof(ushort)] = new Uint16Converter(),
[typeof(int)] = new Int32Converter(),
[typeof(uint)] = new Uint32Converter(),
[typeof(long)] = new Int64Converter(),
[typeof(ulong)] = new Uint64Converter(),
[typeof(float)] = new Float32Converter(),
[typeof(double)] = new Float64Converter(),
[typeof(decimal)] = new Float128Converter(),
[typeof(DateTime)] = new DateTimeConverter(),
[typeof(DateTimeOffset)] = new DateTimeOffsetConverter(),
[typeof(TimeSpan)] = new TimeSpanConverter(),
[typeof(Uri)] = new UriConverter(),
[typeof(DiscordUser)] = new DiscordUserConverter(),
[typeof(DiscordMember)] = new DiscordMemberConverter(),
[typeof(DiscordRole)] = new DiscordRoleConverter(),
[typeof(DiscordChannel)] = new DiscordChannelConverter(),
[typeof(DiscordGuild)] = new DiscordGuildConverter(),
[typeof(DiscordMessage)] = new DiscordMessageConverter(),
[typeof(DiscordEmoji)] = new DiscordEmojiConverter(),
[typeof(DiscordThreadChannel)] = new DiscordThreadChannelConverter(),
[typeof(DiscordInvite)] = new DiscordInviteConverter(),
[typeof(DiscordColor)] = new DiscordColorConverter(),
[typeof(DiscordScheduledEvent)] = new DiscordScheduledEventConverter(),
};
this._userFriendlyTypeNames = new Dictionary()
{
[typeof(string)] = "string",
[typeof(bool)] = "boolean",
[typeof(sbyte)] = "signed byte",
[typeof(byte)] = "byte",
[typeof(short)] = "short",
[typeof(ushort)] = "unsigned short",
[typeof(int)] = "int",
[typeof(uint)] = "unsigned int",
[typeof(long)] = "long",
[typeof(ulong)] = "unsigned long",
[typeof(float)] = "float",
[typeof(double)] = "double",
[typeof(decimal)] = "decimal",
[typeof(DateTime)] = "date and time",
[typeof(DateTimeOffset)] = "date and time",
[typeof(TimeSpan)] = "time span",
[typeof(Uri)] = "URL",
[typeof(DiscordUser)] = "user",
[typeof(DiscordMember)] = "member",
[typeof(DiscordRole)] = "role",
[typeof(DiscordChannel)] = "channel",
[typeof(DiscordGuild)] = "guild",
[typeof(DiscordMessage)] = "message",
[typeof(DiscordEmoji)] = "emoji",
[typeof(DiscordThreadChannel)] = "thread",
[typeof(DiscordInvite)] = "invite",
[typeof(DiscordColor)] = "color",
[typeof(DiscordScheduledEvent)] = "event"
};
foreach (var xt in this.ArgumentConverters.Keys.ToArray())
{
var xti = xt.GetTypeInfo();
if (!xti.IsValueType)
continue;
var xcvt = typeof(NullableConverter<>).MakeGenericType(xt);
var xnt = typeof(Nullable<>).MakeGenericType(xt);
if (this.ArgumentConverters.ContainsKey(xcvt))
continue;
var xcv = Activator.CreateInstance(xcvt) as IArgumentConverter;
this.ArgumentConverters[xnt] = xcv;
this._userFriendlyTypeNames[xnt] = this._userFriendlyTypeNames[xt];
}
var t = this.GetType();
var ms = t.GetTypeInfo().DeclaredMethods;
var m = ms.FirstOrDefault(xm => xm.Name == "ConvertArgumentToObj" && xm.ContainsGenericParameters && !xm.IsStatic && xm.IsPrivate);
this._convertGeneric = m;
}
///
/// Sets the help formatter to use with the default help command.
///
/// Type of the formatter to use.
public void SetHelpFormatter() where T : BaseHelpFormatter => this._helpFormatter.SetFormatterType();
#region DiscordClient Registration
///
/// DO NOT USE THIS MANUALLY.
///
/// DO NOT USE THIS MANUALLY.
///
protected internal override void Setup(DiscordClient client)
{
if (this.Client != null)
throw new InvalidOperationException("What did I tell you?");
this.Client = client;
this._executed = new AsyncEvent("COMMAND_EXECUTED", TimeSpan.Zero, this.Client.EventErrorHandler);
this._error = new AsyncEvent("COMMAND_ERRORED", TimeSpan.Zero, this.Client.EventErrorHandler);
if (this._config.UseDefaultCommandHandler)
this.Client.MessageCreated += this.HandleCommandsAsync;
else
this.Client.Logger.LogWarning(CommandsNextEvents.Misc, "Not attaching default command handler - if this is intentional, you can ignore this message");
if (this._config.EnableDefaultHelp)
{
this.RegisterCommands(typeof(DefaultHelpModule), null, null, out var tcmds);
if (this._config.DefaultHelpChecks != null)
{
var checks = this._config.DefaultHelpChecks.ToArray();
foreach (var cb in tcmds)
cb.WithExecutionChecks(checks);
}
if (tcmds != null)
foreach (var xc in tcmds)
this.AddToCommandDictionary(xc.Build(null));
}
}
#endregion
#region Command Handling
///
/// Handles the commands async.
///
/// The sender.
/// The e.
/// A Task.
private async Task HandleCommandsAsync(DiscordClient sender, MessageCreateEventArgs e)
{
if (e.Author.IsBot) // bad bot
return;
if (!this._config.EnableDms && e.Channel.IsPrivate)
return;
var mpos = -1;
if (this._config.EnableMentionPrefix)
mpos = e.Message.GetMentionPrefixLength(this.Client.CurrentUser);
if (this._config.StringPrefixes?.Any() == true)
foreach (var pfix in this._config.StringPrefixes)
if (mpos == -1 && !string.IsNullOrWhiteSpace(pfix))
mpos = e.Message.GetStringPrefixLength(pfix, this._config.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
if (mpos == -1 && this._config.PrefixResolver != null)
mpos = await this._config.PrefixResolver(e.Message).ConfigureAwait(false);
if (mpos == -1)
return;
var pfx = e.Message.Content[..mpos];
var cnt = e.Message.Content[mpos..];
var __ = 0;
var fname = cnt.ExtractNextArgument(ref __);
var cmd = this.FindCommand(cnt, out var args);
var ctx = this.CreateContext(e.Message, pfx, cmd, args);
if (cmd == null)
{
await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = new CommandNotFoundException(fname) }).ConfigureAwait(false);
return;
}
_ = Task.Run(async () => await this.ExecuteCommandAsync(ctx).ConfigureAwait(false));
}
///
/// Finds a specified command by its qualified name, then separates arguments.
///
/// Qualified name of the command, optionally with arguments.
/// Separated arguments.
/// Found command or null if none was found.
public Command FindCommand(string commandString, out string rawArguments)
{
rawArguments = null;
var ignoreCase = !this._config.CaseSensitive;
var pos = 0;
var next = commandString.ExtractNextArgument(ref pos);
if (next == null)
return null;
if (!this.RegisteredCommands.TryGetValue(next, out var cmd))
{
if (!ignoreCase)
return null;
next = next.ToLowerInvariant();
var cmdKvp = this.RegisteredCommands.FirstOrDefault(x => x.Key.ToLowerInvariant() == next);
if (cmdKvp.Value == null)
return null;
cmd = cmdKvp.Value;
}
if (cmd is not CommandGroup)
{
rawArguments = commandString[pos..].Trim();
return cmd;
}
while (cmd is CommandGroup)
{
var cm2 = cmd as CommandGroup;
var oldPos = pos;
next = commandString.ExtractNextArgument(ref pos);
if (next == null)
break;
if (ignoreCase)
{
next = next.ToLowerInvariant();
cmd = cm2.Children.FirstOrDefault(x => x.Name.ToLowerInvariant() == next || x.Aliases?.Any(xx => xx.ToLowerInvariant() == next) == true);
}
else
{
cmd = cm2.Children.FirstOrDefault(x => x.Name == next || x.Aliases?.Contains(next) == true);
}
if (cmd == null)
{
cmd = cm2;
pos = oldPos;
break;
}
}
rawArguments = commandString[pos..].Trim();
return cmd;
}
///
/// Creates a command execution context from specified arguments.
///
/// Message to use for context.
/// Command prefix, used to execute commands.
/// Command to execute.
/// Raw arguments to pass to command.
/// Created command execution context.
public CommandContext CreateContext(DiscordMessage msg, string prefix, Command cmd, string rawArguments = null)
{
var ctx = new CommandContext
{
Client = this.Client,
Command = cmd,
Message = msg,
Config = this._config,
RawArgumentString = rawArguments ?? "",
Prefix = prefix,
CommandsNext = this,
Services = this.Services
};
if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null))
{
var scope = ctx.Services.CreateScope();
ctx.ServiceScopeContext = new CommandContext.ServiceContext(ctx.Services, scope);
ctx.Services = scope.ServiceProvider;
}
return ctx;
}
///
/// Executes specified command from given context.
///
/// Context to execute command from.
///
public async Task ExecuteCommandAsync(CommandContext ctx)
{
try
{
var cmd = ctx.Command;
await this.RunAllChecksAsync(cmd, ctx).ConfigureAwait(false);
var res = await cmd.ExecuteAsync(ctx).ConfigureAwait(false);
if (res.IsSuccessful)
await this._executed.InvokeAsync(this, new CommandExecutionEventArgs(this.Client.ServiceProvider) { Context = res.Context }).ConfigureAwait(false);
else
await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = res.Context, Exception = res.Exception }).ConfigureAwait(false);
}
catch (Exception ex)
{
await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = ex }).ConfigureAwait(false);
}
finally
{
if (ctx.ServiceScopeContext.IsInitialized)
ctx.ServiceScopeContext.Dispose();
}
}
///
/// Runs the all checks async.
///
/// The cmd.
/// The ctx.
/// A Task.
private async Task RunAllChecksAsync(Command cmd, CommandContext ctx)
{
if (cmd.Parent != null)
await this.RunAllChecksAsync(cmd.Parent, ctx).ConfigureAwait(false);
var fchecks = await cmd.RunChecksAsync(ctx, false).ConfigureAwait(false);
if (fchecks.Any())
throw new ChecksFailedException(cmd, ctx, fchecks);
}
#endregion
#region Command Registration
///
/// Gets a dictionary of registered top-level commands.
///
public IReadOnlyDictionary RegisteredCommands
=> this._registeredCommandsLazy.Value;
///
/// Gets or sets the top level commands.
///
private readonly Dictionary _topLevelCommands;
private readonly Lazy> _registeredCommandsLazy;
///
/// Registers all commands from a given assembly. The command classes need to be public to be considered for registration.
///
/// Assembly to register commands from.
public void RegisterCommands(Assembly assembly)
{
var types = assembly.ExportedTypes.Where(xt =>
{
var xti = xt.GetTypeInfo();
return xti.IsModuleCandidateType() && !xti.IsNested;
});
foreach (var xt in types)
this.RegisterCommands(xt);
}
///
/// Registers all commands from a given command class.
///
/// Class which holds commands to register.
public void RegisterCommands() where T : BaseCommandModule
{
var t = typeof(T);
this.RegisterCommands(t);
}
///
/// Registers all commands from a given command class.
///
/// Type of the class which holds commands to register.
public void RegisterCommands(Type t)
{
if (t == null)
throw new ArgumentNullException(nameof(t), "Type cannot be null.");
if (!t.IsModuleCandidateType())
throw new ArgumentNullException(nameof(t), "Type must be a class, which cannot be abstract or static.");
this.RegisterCommands(t, null, null, out var tempCommands);
if (tempCommands != null)
foreach (var command in tempCommands)
this.AddToCommandDictionary(command.Build(null));
}
///
/// Registers the commands.
///
/// The type.
/// The current parent.
/// The inherited checks.
/// The found commands.
private void RegisterCommands(Type t, CommandGroupBuilder currentParent, IEnumerable inheritedChecks, out List foundCommands)
{
var ti = t.GetTypeInfo();
var lifespan = ti.GetCustomAttribute();
var moduleLifespan = lifespan != null ? lifespan.Lifespan : ModuleLifespan.Singleton;
var module = new CommandModuleBuilder()
.WithType(t)
.WithLifespan(moduleLifespan)
.Build(this.Services);
// restrict parent lifespan to more or equally restrictive
if (currentParent?.Module is TransientCommandModule && moduleLifespan != ModuleLifespan.Transient)
throw new InvalidOperationException("In a transient module, child modules can only be transient.");
// check if we are anything
var groupBuilder = new CommandGroupBuilder(module);
var isModule = false;
var moduleAttributes = ti.GetCustomAttributes();
var moduleHidden = false;
var moduleChecks = new List();
foreach (var xa in moduleAttributes)
{
switch (xa)
{
case GroupAttribute g:
isModule = true;
var moduleName = g.Name;
if (moduleName == null)
{
moduleName = ti.Name;
if (moduleName.EndsWith("Group") && moduleName != "Group")
moduleName = moduleName[0..^5];
else if (moduleName.EndsWith("Module") && moduleName != "Module")
moduleName = moduleName[0..^6];
else if (moduleName.EndsWith("Commands") && moduleName != "Commands")
moduleName = moduleName[0..^8];
}
if (!this._config.CaseSensitive)
moduleName = moduleName.ToLowerInvariant();
groupBuilder.WithName(moduleName);
if (inheritedChecks != null)
foreach (var chk in inheritedChecks)
groupBuilder.WithExecutionCheck(chk);
foreach (var mi in ti.DeclaredMethods.Where(x => x.IsCommandCandidate(out _) && x.GetCustomAttribute() != null))
groupBuilder.WithOverload(new CommandOverloadBuilder(mi));
break;
case AliasesAttribute a:
foreach (var xalias in a.Aliases)
groupBuilder.WithAlias(this._config.CaseSensitive ? xalias : xalias.ToLowerInvariant());
break;
case HiddenAttribute h:
groupBuilder.WithHiddenStatus(true);
moduleHidden = true;
break;
case DescriptionAttribute d:
groupBuilder.WithDescription(d.Description);
break;
case CheckBaseAttribute c:
moduleChecks.Add(c);
groupBuilder.WithExecutionCheck(c);
break;
default:
groupBuilder.WithCustomAttribute(xa);
break;
}
}
if (!isModule)
{
groupBuilder = null;
if (inheritedChecks != null)
moduleChecks.AddRange(inheritedChecks);
}
// candidate methods
var methods = ti.DeclaredMethods;
var commands = new List();
var commandBuilders = new Dictionary();
foreach (var m in methods)
{
if (!m.IsCommandCandidate(out _))
continue;
var attrs = m.GetCustomAttributes();
if (attrs.FirstOrDefault(xa => xa is CommandAttribute) is not CommandAttribute cattr)
continue;
var commandName = cattr.Name;
if (commandName == null)
{
commandName = m.Name;
if (commandName.EndsWith("Async") && commandName != "Async")
commandName = commandName[0..^5];
}
if (!this._config.CaseSensitive)
commandName = commandName.ToLowerInvariant();
if (!commandBuilders.TryGetValue(commandName, out var commandBuilder))
{
commandBuilders.Add(commandName, commandBuilder = new CommandBuilder(module).WithName(commandName));
if (!isModule)
if (currentParent != null)
currentParent.WithChild(commandBuilder);
else
commands.Add(commandBuilder);
else
groupBuilder.WithChild(commandBuilder);
}
commandBuilder.WithOverload(new CommandOverloadBuilder(m));
if (!isModule && moduleChecks.Any())
foreach (var chk in moduleChecks)
commandBuilder.WithExecutionCheck(chk);
foreach (var xa in attrs)
{
switch (xa)
{
case AliasesAttribute a:
foreach (var xalias in a.Aliases)
commandBuilder.WithAlias(this._config.CaseSensitive ? xalias : xalias.ToLowerInvariant());
break;
case CheckBaseAttribute p:
commandBuilder.WithExecutionCheck(p);
break;
case DescriptionAttribute d:
commandBuilder.WithDescription(d.Description);
break;
case HiddenAttribute h:
commandBuilder.WithHiddenStatus(true);
break;
default:
commandBuilder.WithCustomAttribute(xa);
break;
}
}
if (!isModule && moduleHidden)
commandBuilder.WithHiddenStatus(true);
}
// candidate types
var types = ti.DeclaredNestedTypes
.Where(xt => xt.IsModuleCandidateType() && xt.DeclaredConstructors.Any(xc => xc.IsPublic));
foreach (var type in types)
{
this.RegisterCommands(type.AsType(),
groupBuilder,
!isModule ? moduleChecks : null,
out var tempCommands);
if (isModule)
foreach (var chk in moduleChecks)
groupBuilder.WithExecutionCheck(chk);
if (isModule && tempCommands != null)
foreach (var xtcmd in tempCommands)
groupBuilder.WithChild(xtcmd);
else if (tempCommands != null)
commands.AddRange(tempCommands);
}
if (isModule && currentParent == null)
commands.Add(groupBuilder);
else if (isModule)
currentParent.WithChild(groupBuilder);
foundCommands = commands;
}
///
/// Builds and registers all supplied commands.
///
/// Commands to build and register.
public void RegisterCommands(params CommandBuilder[] cmds)
{
foreach (var cmd in cmds)
this.AddToCommandDictionary(cmd.Build(null));
}
///
/// Unregister specified commands from CommandsNext.
///
/// Commands to unregister.
public void UnregisterCommands(params Command[] cmds)
{
if (cmds.Any(x => x.Parent != null))
throw new InvalidOperationException("Cannot unregister nested commands.");
var keys = this.RegisteredCommands.Where(x => cmds.Contains(x.Value)).Select(x => x.Key).ToList();
foreach (var key in keys)
this._topLevelCommands.Remove(key);
}
///
/// Adds the to command dictionary.
///
/// The cmd.
private void AddToCommandDictionary(Command cmd)
{
if (cmd.Parent != null)
return;
if (this._topLevelCommands.ContainsKey(cmd.Name) || (cmd.Aliases != null && cmd.Aliases.Any(xs => this._topLevelCommands.ContainsKey(xs))))
throw new DuplicateCommandException(cmd.QualifiedName);
this._topLevelCommands[cmd.Name] = cmd;
if (cmd.Aliases != null)
foreach (var xs in cmd.Aliases)
this._topLevelCommands[xs] = cmd;
}
#endregion
#region Default Help
///
/// Represents the default help module.
///
[ModuleLifespan(ModuleLifespan.Transient)]
public class DefaultHelpModule : BaseCommandModule
{
///
/// Defaults the help async.
///
/// The ctx.
/// The command.
/// A Task.
[Command("help"), Description("Displays command help.")]
public async Task DefaultHelpAsync(CommandContext ctx, [Description("Command to provide help for.")] params string[] command)
{
var topLevel = ctx.CommandsNext._topLevelCommands.Values.Distinct();
var helpBuilder = ctx.CommandsNext._helpFormatter.Create(ctx);
if (command != null && command.Any())
{
Command cmd = null;
var searchIn = topLevel;
foreach (var c in command)
{
if (searchIn == null)
{
cmd = null;
break;
}
cmd = ctx.Config.CaseSensitive
? searchIn.FirstOrDefault(xc => xc.Name == c || (xc.Aliases != null && xc.Aliases.Contains(c)))
: searchIn.FirstOrDefault(xc => xc.Name.ToLowerInvariant() == c.ToLowerInvariant() || (xc.Aliases != null && xc.Aliases.Select(xs => xs.ToLowerInvariant()).Contains(c.ToLowerInvariant())));
if (cmd == null)
break;
var failedChecks = await cmd.RunChecksAsync(ctx, true).ConfigureAwait(false);
if (failedChecks.Any())
throw new ChecksFailedException(cmd, ctx, failedChecks);
searchIn = cmd is CommandGroup ? (cmd as CommandGroup).Children : null;
}
if (cmd == null)
throw new CommandNotFoundException(string.Join(" ", command));
helpBuilder.WithCommand(cmd);
if (cmd is CommandGroup group)
{
var commandsToSearch = group.Children.Where(xc => !xc.IsHidden);
var eligibleCommands = new List();
foreach (var candidateCommand in commandsToSearch)
{
if (candidateCommand.ExecutionChecks == null || !candidateCommand.ExecutionChecks.Any())
{
eligibleCommands.Add(candidateCommand);
continue;
}
var candidateFailedChecks = await candidateCommand.RunChecksAsync(ctx, true).ConfigureAwait(false);
if (!candidateFailedChecks.Any())
eligibleCommands.Add(candidateCommand);
}
if (eligibleCommands.Any())
helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name));
}
}
else
{
var commandsToSearch = topLevel.Where(xc => !xc.IsHidden);
var eligibleCommands = new List();
foreach (var sc in commandsToSearch)
{
if (sc.ExecutionChecks == null || !sc.ExecutionChecks.Any())
{
eligibleCommands.Add(sc);
continue;
}
var candidateFailedChecks = await sc.RunChecksAsync(ctx, true).ConfigureAwait(false);
if (!candidateFailedChecks.Any())
eligibleCommands.Add(sc);
}
if (eligibleCommands.Any())
helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name));
}
var helpMessage = helpBuilder.Build();
var builder = new DiscordMessageBuilder().WithContent(helpMessage.Content).WithEmbed(helpMessage.Embed);
if (!ctx.Config.DmHelp || ctx.Channel is DiscordDmChannel || ctx.Guild == null)
await ctx.RespondAsync(builder).ConfigureAwait(false);
else
await ctx.Member.SendMessageAsync(builder).ConfigureAwait(false);
}
}
#endregion
#region Sudo
///
/// Creates a fake command context to execute commands with.
///
/// The user or member to use as message author.
/// The channel the message is supposed to appear from.
/// Contents of the message.
/// Command prefix, used to execute commands.
/// Command to execute.
/// Raw arguments to pass to command.
/// Created fake context.
public CommandContext CreateFakeContext(DiscordUser actor, DiscordChannel channel, string messageContents, string prefix, Command cmd, string rawArguments = null)
{
var epoch = new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero);
var now = DateTimeOffset.UtcNow;
var timeSpan = (ulong)(now - epoch).TotalMilliseconds;
// create fake message
var msg = new DiscordMessage
{
Discord = this.Client,
Author = actor,
ChannelId = channel.Id,
Content = messageContents,
Id = timeSpan << 22,
Pinned = false,
MentionEveryone = messageContents.Contains("@everyone"),
IsTts = false,
AttachmentsInternal = new List(),
EmbedsInternal = new List(),
TimestampRaw = now.ToString("yyyy-MM-ddTHH:mm:sszzz"),
ReactionsInternal = new List()
};
var mentionedUsers = new List();
var mentionedRoles = msg.Channel.Guild != null ? new List() : null;
var mentionedChannels = msg.Channel.Guild != null ? new List() : null;
if (!string.IsNullOrWhiteSpace(msg.Content))
{
if (msg.Channel.Guild != null)
{
mentionedUsers = Utilities.GetUserMentions(msg).Select(xid => msg.Channel.Guild.MembersInternal.TryGetValue(xid, out var member) ? member : null).Cast().ToList();
mentionedRoles = Utilities.GetRoleMentions(msg).Select(xid => msg.Channel.Guild.GetRole(xid)).ToList();
mentionedChannels = Utilities.GetChannelMentions(msg).Select(xid => msg.Channel.Guild.GetChannel(xid)).ToList();
}
else
{
mentionedUsers = Utilities.GetUserMentions(msg).Select(this.Client.GetCachedOrEmptyUserInternal).ToList();
}
}
msg.MentionedUsersInternal = mentionedUsers;
msg.MentionedRolesInternal = mentionedRoles;
msg.MentionedChannelsInternal = mentionedChannels;
var ctx = new CommandContext
{
Client = this.Client,
Command = cmd,
Message = msg,
Config = this._config,
RawArgumentString = rawArguments ?? "",
Prefix = prefix,
CommandsNext = this,
Services = this.Services
};
if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null))
{
var scope = ctx.Services.CreateScope();
ctx.ServiceScopeContext = new CommandContext.ServiceContext(ctx.Services, scope);
ctx.Services = scope.ServiceProvider;
}
return ctx;
}
#endregion
#region Type Conversion
///
/// Converts a string to specified type.
///
/// Type to convert to.
/// Value to convert.
/// Context in which to convert to.
/// Converted object.
public async Task ConvertArgument(string value, CommandContext ctx)
{
var t = typeof(T);
if (!this.ArgumentConverters.ContainsKey(t))
throw new ArgumentException("There is no converter specified for given type.", nameof(T));
if (this.ArgumentConverters[t] is not IArgumentConverter cv)
throw new ArgumentException("Invalid converter registered for this type.", nameof(T));
var cvr = await cv.ConvertAsync(value, ctx).ConfigureAwait(false);
return !cvr.HasValue ? throw new ArgumentException("Could not convert specified value to given type.", nameof(value)) : cvr.Value;
}
///
/// Converts a string to specified type.
///
/// Value to convert.
/// Context in which to convert to.
/// Type to convert to.
/// Converted object.
public async Task ConvertArgument(string value, CommandContext ctx, Type type)
{
var m = this._convertGeneric.MakeGenericMethod(type);
try
{
return await (m.Invoke(this, new object[] { value, ctx }) as Task).ConfigureAwait(false);
}
catch (TargetInvocationException ex)
{
throw ex.InnerException;
}
}
///
/// Registers an argument converter for specified type.
///
/// Type for which to register the converter.
/// Converter to register.
public void RegisterConverter(IArgumentConverter converter)
{
if (converter == null)
throw new ArgumentNullException(nameof(converter), "Converter cannot be null.");
var t = typeof(T);
var ti = t.GetTypeInfo();
this.ArgumentConverters[t] = converter;
if (!ti.IsValueType)
return;
var nullableConverterType = typeof(NullableConverter<>).MakeGenericType(t);
var nullableType = typeof(Nullable<>).MakeGenericType(t);
if (this.ArgumentConverters.ContainsKey(nullableType))
return;
var nullableConverter = Activator.CreateInstance(nullableConverterType) as IArgumentConverter;
this.ArgumentConverters[nullableType] = nullableConverter;
}
///
/// Unregister an argument converter for specified type.
///
/// Type for which to unregister the converter.
public void UnregisterConverter()
{
var t = typeof(T);
var ti = t.GetTypeInfo();
- if (this.ArgumentConverters.ContainsKey(t))
- this.ArgumentConverters.Remove(t);
+ this.ArgumentConverters.Remove(t);
- if (this._userFriendlyTypeNames.ContainsKey(t))
- this._userFriendlyTypeNames.Remove(t);
+ this._userFriendlyTypeNames.Remove(t);
if (!ti.IsValueType)
return;
var nullableType = typeof(Nullable<>).MakeGenericType(t);
if (!this.ArgumentConverters.ContainsKey(nullableType))
return;
this.ArgumentConverters.Remove(nullableType);
this._userFriendlyTypeNames.Remove(nullableType);
}
///
/// Registers a user-friendly type name.
///
/// Type to register the name for.
/// Name to register.
public void RegisterUserFriendlyTypeName(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentNullException(nameof(value), "Name cannot be null or empty.");
var t = typeof(T);
var ti = t.GetTypeInfo();
if (!this.ArgumentConverters.ContainsKey(t))
throw new InvalidOperationException("Cannot register a friendly name for a type which has no associated converter.");
this._userFriendlyTypeNames[t] = value;
if (!ti.IsValueType)
return;
var nullableType = typeof(Nullable<>).MakeGenericType(t);
this._userFriendlyTypeNames[nullableType] = value;
}
///
/// Converts a type into user-friendly type name.
///
/// Type to convert.
/// User-friendly type name.
public string GetUserFriendlyTypeName(Type t)
{
- if (this._userFriendlyTypeNames.ContainsKey(t))
- return this._userFriendlyTypeNames[t];
+ if (this._userFriendlyTypeNames.TryGetValue(t, out var val))
+ return val;
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 this._userFriendlyTypeNames.TryGetValue(tn, out var value) ? value : 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.CommandsNext/Converters/ArgumentBindingResult.cs b/DisCatSharp.CommandsNext/Converters/ArgumentBindingResult.cs
index 0f898cfa8..cfbec2be1 100644
--- a/DisCatSharp.CommandsNext/Converters/ArgumentBindingResult.cs
+++ b/DisCatSharp.CommandsNext/Converters/ArgumentBindingResult.cs
@@ -1,74 +1,74 @@
// 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;
namespace DisCatSharp.CommandsNext.Converters;
///
/// Represents a argument binding result.
///
-public struct ArgumentBindingResult
+public readonly struct ArgumentBindingResult
{
///
/// Gets a value indicating whether the binding is successful.
///
public bool IsSuccessful { get; }
///
/// Gets the converted.
///
public object[] Converted { get; }
///
/// Gets the raw.
///
public IReadOnlyList Raw { get; }
///
/// Gets the reason.
///
public Exception Reason { get; }
///
/// Initializes a new instance of the class.
///
/// The converted.
/// The raw.
public ArgumentBindingResult(object[] converted, IReadOnlyList raw)
{
this.IsSuccessful = true;
this.Reason = null;
this.Converted = converted;
this.Raw = raw;
}
///
/// Initializes a new instance of the class.
///
/// The ex.
public ArgumentBindingResult(Exception ex)
{
this.IsSuccessful = false;
this.Reason = ex;
this.Converted = null;
this.Raw = null;
}
}
diff --git a/DisCatSharp.CommandsNext/Entities/CommandHelpMessage.cs b/DisCatSharp.CommandsNext/Entities/CommandHelpMessage.cs
index cd4e26d96..597ccfaf2 100644
--- a/DisCatSharp.CommandsNext/Entities/CommandHelpMessage.cs
+++ b/DisCatSharp.CommandsNext/Entities/CommandHelpMessage.cs
@@ -1,52 +1,52 @@
// 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 DisCatSharp.Entities;
namespace DisCatSharp.CommandsNext.Entities;
///
/// Represents a formatted help message.
///
-public struct CommandHelpMessage
+public readonly struct CommandHelpMessage
{
///
/// Gets the contents of the help message.
///
public string Content { get; }
///
/// Gets the embed attached to the help message.
///
public DiscordEmbed Embed { get; }
///
/// Creates a new instance of a help message.
///
/// Contents of the message.
/// Embed to attach to the message.
public CommandHelpMessage(string content = null, DiscordEmbed embed = null)
{
this.Content = content;
this.Embed = embed;
}
}
diff --git a/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs b/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs
index 69cba3138..3fa4d24cf 100644
--- a/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs
+++ b/DisCatSharp.CommandsNext/EventArgs/CommandContext.cs
@@ -1,207 +1,207 @@
// 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.Threading.Tasks;
using DisCatSharp.Entities;
using Microsoft.Extensions.DependencyInjection;
namespace DisCatSharp.CommandsNext;
///
/// Represents a context in which a command is executed.
///
public sealed class CommandContext
{
///
/// Gets the client which received the message.
///
public DiscordClient Client { get; internal set; }
///
/// Gets the message that triggered the execution.
///
public DiscordMessage Message { get; internal set; }
///
/// Gets the channel in which the execution was triggered,
///
public DiscordChannel Channel
=> this.Message.Channel;
///
/// Gets the guild in which the execution was triggered. This property is null for commands sent over direct messages.
///
public DiscordGuild Guild
=> this.Channel.Guild;
///
/// Gets the user who triggered the execution.
///
public DiscordUser User
=> this.Message.Author;
///
/// Gets the member who triggered the execution. This property is null for commands sent over direct messages.
///
public DiscordMember Member
=> this._lazyMember.Value;
private readonly Lazy _lazyMember;
///
/// Gets the CommandsNext service instance that handled this command.
///
public CommandsNextExtension CommandsNext { get; internal set; }
///
/// Gets the service provider for this CNext instance.
///
public IServiceProvider Services { get; internal set; }
///
/// Gets the command that is being executed.
///
public Command Command { get; internal set; }
///
/// Gets the overload of the command that is being executed.
///
public CommandOverload Overload { get; internal set; }
///
/// Gets the list of raw arguments passed to the command.
///
public IReadOnlyList RawArguments { get; internal set; }
///
/// Gets the raw string from which the arguments were extracted.
///
public string RawArgumentString { get; internal set; }
///
/// Gets the prefix used to invoke the command.
///
public string Prefix { get; internal set; }
///
/// Gets or sets the config.
///
internal CommandsNextConfiguration Config { get; set; }
///
/// Gets or sets the service scope context.
///
internal ServiceContext ServiceScopeContext { get; set; }
///
/// Initializes a new instance of the class.
///
internal CommandContext()
{
this._lazyMember = new Lazy(() => this.Guild != null && this.Guild.Members.TryGetValue(this.User.Id, out var member) ? member : this.Guild?.GetMemberAsync(this.User.Id).ConfigureAwait(false).GetAwaiter().GetResult());
}
///
/// Quickly respond to the message that triggered the command.
///
/// Message to respond with.
///
public Task RespondAsync(string content)
=> this.Message.RespondAsync(content);
///
/// Quickly respond to the message that triggered the command.
///
/// Embed to attach.
///
public Task RespondAsync(DiscordEmbed embed)
=> this.Message.RespondAsync(embed);
///
/// Quickly respond to the message that triggered the command.
///
/// Message to respond with.
/// Embed to attach.
///
public Task RespondAsync(string content, DiscordEmbed embed)
=> this.Message.RespondAsync(content, embed);
///
/// Quickly respond to the message that triggered the command.
///
/// The Discord Message builder.
///
public Task RespondAsync(DiscordMessageBuilder builder)
=> this.Message.RespondAsync(builder);
///
/// Quickly respond to the message that triggered the command.
///
/// The Discord Message builder.
///
public Task RespondAsync(Action action)
=> this.Message.RespondAsync(action);
///
/// Triggers typing in the channel containing the message that triggered the command.
///
///
public Task TriggerTypingAsync()
=> this.Channel.TriggerTypingAsync();
- internal struct ServiceContext : IDisposable
+ internal readonly struct ServiceContext : IDisposable
{
///
/// Gets the provider.
///
public IServiceProvider Provider { get; }
///
/// Gets the scope.
///
public IServiceScope Scope { get; }
///
/// Gets a value indicating whether is initialized.
///
public bool IsInitialized { get; }
///
/// Initializes a new instance of the class.
///
/// The services.
/// The scope.
public ServiceContext(IServiceProvider services, IServiceScope scope)
{
this.Provider = services;
this.Scope = scope;
this.IsInitialized = true;
}
///
/// Disposes the command context.
///
public void Dispose() => this.Scope?.Dispose();
}
}
diff --git a/DisCatSharp.Lavalink/Entities/LavalinkEqualizerTypes.cs b/DisCatSharp.Lavalink/Entities/LavalinkEqualizerTypes.cs
index c06ba9c94..1d4af2375 100644
--- a/DisCatSharp.Lavalink/Entities/LavalinkEqualizerTypes.cs
+++ b/DisCatSharp.Lavalink/Entities/LavalinkEqualizerTypes.cs
@@ -1,84 +1,84 @@
// 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 Newtonsoft.Json;
namespace DisCatSharp.Lavalink;
///
/// Represents Lavalink equalizer band adjustment. This is used to alter the sound output by using Lavalink's equalizer.
///
-public struct LavalinkBandAdjustment
+public readonly struct LavalinkBandAdjustment
{
///
/// Gets the ID of the band to adjust.
///
[JsonProperty("band")]
public int BandId { get; }
///
/// Gets the gain of the specified band.
///
[JsonProperty("gain")]
public float Gain { get; }
///
/// Creates a new band adjustment with specified parameters.
///
/// Which band to adjust. Must be in 0-14 range.
/// By how much to adjust the band. Must be greater than or equal to -0.25 (muted), and less than or equal to +1.0. +0.25 means the band is doubled.
public LavalinkBandAdjustment(int bandId, float gain)
{
if (bandId < 0 || bandId > 14)
throw new ArgumentOutOfRangeException(nameof(bandId), "Band ID cannot be lower than 0 or greater than 14.");
if (gain < -0.25 || gain > 1.0)
throw new ArgumentOutOfRangeException(nameof(gain), "Gain cannot be lower than -0.25 or greater than 1.0.");
this.BandId = bandId;
this.Gain = gain;
}
}
///
/// The lavalink band adjustment comparer.
///
internal class LavalinkBandAdjustmentComparer : IEqualityComparer
{
///
/// Whether two band adjustments are equal.
///
/// The first band adjustments.
/// The second band adjustments.
public bool Equals(LavalinkBandAdjustment x, LavalinkBandAdjustment y)
=> x.BandId == y.BandId;
///
/// Gets the hash code.
///
/// The band adjustments.
public int GetHashCode(LavalinkBandAdjustment obj)
=> obj.BandId;
}
diff --git a/DisCatSharp.Lavalink/LavalinkExtension.cs b/DisCatSharp.Lavalink/LavalinkExtension.cs
index 7f2159754..18d65f7ea 100644
--- a/DisCatSharp.Lavalink/LavalinkExtension.cs
+++ b/DisCatSharp.Lavalink/LavalinkExtension.cs
@@ -1,215 +1,215 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-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.Linq;
using System.Threading.Tasks;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.Lavalink.EventArgs;
using DisCatSharp.Net;
namespace DisCatSharp.Lavalink;
///
/// The lavalink extension.
///
public sealed class LavalinkExtension : BaseExtension
{
///
/// Triggered whenever a node disconnects.
///
public event AsyncEventHandler NodeDisconnected
{
add => this._nodeDisconnected.Register(value);
remove => this._nodeDisconnected.Unregister(value);
}
private AsyncEvent _nodeDisconnected;
///
/// Gets a dictionary of connected Lavalink nodes for the extension.
///
public IReadOnlyDictionary ConnectedNodes { get; }
private readonly ConcurrentDictionary _connectedNodes = new();
///
/// Creates a new instance of this Lavalink extension.
///
internal LavalinkExtension()
{
this.ConnectedNodes = new ReadOnlyConcurrentDictionary(this._connectedNodes);
}
///
/// DO NOT USE THIS MANUALLY.
///
/// DO NOT USE THIS MANUALLY.
///
protected internal override void Setup(DiscordClient client)
{
if (this.Client != null)
throw new InvalidOperationException("What did I tell you?");
this.Client = client;
this._nodeDisconnected = new AsyncEvent("LAVALINK_NODE_DISCONNECTED", TimeSpan.Zero, this.Client.EventErrorHandler);
}
///
/// Connect to a Lavalink node.
///
/// Lavalink client configuration.
/// The established Lavalink connection.
public async Task ConnectAsync(LavalinkConfiguration config)
{
- if (this._connectedNodes.ContainsKey(config.SocketEndpoint))
- return this._connectedNodes[config.SocketEndpoint];
+ if (this._connectedNodes.TryGetValue(config.SocketEndpoint, out var endpoint))
+ return endpoint;
var con = new LavalinkNodeConnection(this.Client, this, config);
con.NodeDisconnected += this.Con_NodeDisconnected;
con.Disconnected += this.Con_Disconnected;
this._connectedNodes[con.NodeEndpoint] = con;
try
{
await con.StartAsync().ConfigureAwait(false);
}
catch
{
this.Con_NodeDisconnected(con);
throw;
}
return con;
}
///
/// Gets the Lavalink node connection for the specified endpoint.
///
/// Endpoint at which the node resides.
/// Lavalink node connection.
- public LavalinkNodeConnection GetNodeConnection(ConnectionEndpoint endpoint)
- => this._connectedNodes.ContainsKey(endpoint) ? this._connectedNodes[endpoint] : null;
+ public LavalinkNodeConnection? GetNodeConnection(ConnectionEndpoint endpoint)
+ => this._connectedNodes.TryGetValue(endpoint, out var value) ? value : null;
///
/// Gets a Lavalink node connection based on load balancing and an optional voice region.
///
/// The region to compare with the node's , if any.
/// The least load affected node connection, or null if no nodes are present.
- public LavalinkNodeConnection GetIdealNodeConnection(DiscordVoiceRegion region = null)
+ public LavalinkNodeConnection? GetIdealNodeConnection(DiscordVoiceRegion? region = null)
{
if (this._connectedNodes.Count <= 1)
return this._connectedNodes.Values.FirstOrDefault();
var nodes = this._connectedNodes.Values.ToArray();
if (region != null)
{
var regionPredicate = new Func(x => x.Region == region);
if (nodes.Any(regionPredicate))
nodes = nodes.Where(regionPredicate).ToArray();
if (nodes.Length <= 1)
return nodes.FirstOrDefault();
}
return this.FilterByLoad(nodes);
}
///
/// Gets a Lavalink guild connection from a .
///
/// The guild the connection is on.
/// The found guild connection, or null if one could not be found.
public LavalinkGuildConnection GetGuildConnection(DiscordGuild guild)
{
var nodes = this._connectedNodes.Values;
var node = nodes.FirstOrDefault(x => x.ConnectedGuildsInternal.ContainsKey(guild.Id));
return node?.GetGuildConnection(guild);
}
///
/// Filters the by load.
///
/// The nodes.
private LavalinkNodeConnection FilterByLoad(LavalinkNodeConnection[] nodes)
{
Array.Sort(nodes, (a, b) =>
{
if (!a.Statistics.Updated || !b.Statistics.Updated)
return 0;
//https://github.com/FredBoat/Lavalink-Client/blob/48bc27784f57be5b95d2ff2eff6665451b9366f5/src/main/java/lavalink/client/io/LavalinkLoadBalancer.java#L122
//https://github.com/briantanner/eris-lavalink/blob/master/src/PlayerManager.js#L329
//player count
var aPenaltyCount = a.Statistics.ActivePlayers;
var bPenaltyCount = b.Statistics.ActivePlayers;
//cpu load
aPenaltyCount += (int)Math.Pow(1.05d, (100 * (a.Statistics.CpuSystemLoad / a.Statistics.CpuCoreCount) * 10) - 10);
bPenaltyCount += (int)Math.Pow(1.05d, (100 * (b.Statistics.CpuSystemLoad / a.Statistics.CpuCoreCount) * 10) - 10);
//frame load
if (a.Statistics.AverageDeficitFramesPerMinute > 0)
{
//deficit frame load
aPenaltyCount += (int)((Math.Pow(1.03d, 500f * (a.Statistics.AverageDeficitFramesPerMinute / 3000f)) * 600) - 600);
//null frame load
aPenaltyCount += (int)((Math.Pow(1.03d, 500f * (a.Statistics.AverageNulledFramesPerMinute / 3000f)) * 300) - 300);
}
//frame load
if (b.Statistics.AverageDeficitFramesPerMinute > 0)
{
//deficit frame load
bPenaltyCount += (int)((Math.Pow(1.03d, 500f * (b.Statistics.AverageDeficitFramesPerMinute / 3000f)) * 600) - 600);
//null frame load
bPenaltyCount += (int)((Math.Pow(1.03d, 500f * (b.Statistics.AverageNulledFramesPerMinute / 3000f)) * 300) - 300);
}
return aPenaltyCount - bPenaltyCount;
});
return nodes[0];
}
///
/// Removes a node.
///
/// The node to be removed.
private void Con_NodeDisconnected(LavalinkNodeConnection node)
=> this._connectedNodes.TryRemove(node.NodeEndpoint, out _);
///
/// Disconnects a node.
///
/// The affected node.
/// The node disconnected event args.
private Task Con_Disconnected(LavalinkNodeConnection node, NodeDisconnectedEventArgs e)
=> this._nodeDisconnected.InvokeAsync(node, e);
}
diff --git a/DisCatSharp.Lavalink/LavalinkNodeConnection.cs b/DisCatSharp.Lavalink/LavalinkNodeConnection.cs
index ded9d142b..accda9dd1 100644
--- a/DisCatSharp.Lavalink/LavalinkNodeConnection.cs
+++ b/DisCatSharp.Lavalink/LavalinkNodeConnection.cs
@@ -1,626 +1,626 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-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.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Lavalink.Entities;
using DisCatSharp.Lavalink.EventArgs;
using DisCatSharp.Net;
using DisCatSharp.Net.WebSocket;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DisCatSharp.Lavalink;
internal delegate void NodeDisconnectedEventHandler(LavalinkNodeConnection node);
///
/// Represents a connection to a Lavalink node.
///
public sealed class LavalinkNodeConnection
{
///
/// Triggered whenever Lavalink WebSocket throws an exception.
///
public event AsyncEventHandler LavalinkSocketErrored
{
add => this._lavalinkSocketError.Register(value);
remove => this._lavalinkSocketError.Unregister(value);
}
private readonly AsyncEvent _lavalinkSocketError;
///
/// Triggered when this node disconnects.
///
public event AsyncEventHandler Disconnected
{
add => this._disconnected.Register(value);
remove => this._disconnected.Unregister(value);
}
private readonly AsyncEvent _disconnected;
///
/// Triggered when this node receives a statistics update.
///
public event AsyncEventHandler StatisticsReceived
{
add => this._statsReceived.Register(value);
remove => this._statsReceived.Unregister(value);
}
private readonly AsyncEvent _statsReceived;
///
/// Triggered whenever any of the players on this node is updated.
///
public event AsyncEventHandler PlayerUpdated
{
add => this._playerUpdated.Register(value);
remove => this._playerUpdated.Unregister(value);
}
private readonly AsyncEvent _playerUpdated;
///
/// Triggered whenever playback of a track starts.
/// This is only available for version 3.3.1 and greater.
///
public event AsyncEventHandler PlaybackStarted
{
add => this._playbackStarted.Register(value);
remove => this._playbackStarted.Unregister(value);
}
private readonly AsyncEvent _playbackStarted;
///
/// Triggered whenever playback of a track finishes.
///
public event AsyncEventHandler PlaybackFinished
{
add => this._playbackFinished.Register(value);
remove => this._playbackFinished.Unregister(value);
}
private readonly AsyncEvent _playbackFinished;
///
/// Triggered whenever playback of a track gets stuck.
///
public event AsyncEventHandler TrackStuck
{
add => this._trackStuck.Register(value);
remove => this._trackStuck.Unregister(value);
}
private readonly AsyncEvent _trackStuck;
///
/// Triggered whenever playback of a track encounters an error.
///
public event AsyncEventHandler TrackException
{
add => this._trackException.Register(value);
remove => this._trackException.Unregister(value);
}
private readonly AsyncEvent _trackException;
///
/// Gets the remote endpoint of this Lavalink node connection.
///
public ConnectionEndpoint NodeEndpoint => this.Configuration.SocketEndpoint;
///
/// Gets whether the client is connected to Lavalink.
///
public bool IsConnected => !Volatile.Read(ref this._isDisposed);
private bool _isDisposed;
private int _backoff;
///
/// The minimum backoff.
///
private const int MINIMUM_BACKOFF = 7500;
///
/// The maximum backoff.
///
private const int MAXIMUM_BACKOFF = 120000;
///
/// Gets the current resource usage statistics.
///
public LavalinkStatistics Statistics { get; }
///
/// Gets a dictionary of Lavalink guild connections for this node.
///
public IReadOnlyDictionary ConnectedGuilds { get; }
internal ConcurrentDictionary ConnectedGuildsInternal = new();
///
/// Gets the REST client for this Lavalink connection.
///
public LavalinkRestClient Rest { get; }
///
/// Gets the parent extension which this node connection belongs to.
///
public LavalinkExtension Parent { get; }
///
/// Gets the Discord client this node connection belongs to.
///
public DiscordClient Discord { get; }
///
/// Gets the configuration.
///
internal LavalinkConfiguration Configuration { get; }
///
/// Gets the region.
///
internal DiscordVoiceRegion Region { get; }
///
/// Gets or sets the web socket.
///
private IWebSocketClient _webSocket;
///
/// Gets the voice state updates.
///
private readonly ConcurrentDictionary> _voiceStateUpdates;
///
/// Gets the voice server updates.
///
private readonly ConcurrentDictionary> _voiceServerUpdates;
///
/// Initializes a new instance of the class.
///
/// The client.
/// the event.tension.
/// The config.
internal LavalinkNodeConnection(DiscordClient client, LavalinkExtension extension, LavalinkConfiguration config)
{
this.Discord = client;
this.Parent = extension;
this.Configuration = new LavalinkConfiguration(config);
if (config.Region != null && this.Discord.VoiceRegions.Values.Contains(config.Region))
this.Region = config.Region;
this.ConnectedGuilds = new ReadOnlyConcurrentDictionary(this.ConnectedGuildsInternal);
this.Statistics = new LavalinkStatistics();
this._lavalinkSocketError = new AsyncEvent("LAVALINK_SOCKET_ERROR", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._disconnected = new AsyncEvent("LAVALINK_NODE_DISCONNECTED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._statsReceived = new AsyncEvent("LAVALINK_STATS_RECEIVED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._playerUpdated = new AsyncEvent("LAVALINK_PLAYER_UPDATED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._playbackStarted = new AsyncEvent("LAVALINK_PLAYBACK_STARTED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._playbackFinished = new AsyncEvent("LAVALINK_PLAYBACK_FINISHED", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._trackStuck = new AsyncEvent("LAVALINK_TRACK_STUCK", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._trackException = new AsyncEvent("LAVALINK_TRACK_EXCEPTION", TimeSpan.Zero, this.Discord.EventErrorHandler);
this._voiceServerUpdates = new ConcurrentDictionary>();
this._voiceStateUpdates = new ConcurrentDictionary>();
this.Discord.VoiceStateUpdated += this.Discord_VoiceStateUpdated;
this.Discord.VoiceServerUpdated += this.Discord_VoiceServerUpdated;
this.Rest = new LavalinkRestClient(this.Configuration, this.Discord);
Volatile.Write(ref this._isDisposed, false);
}
///
/// Establishes a connection to the Lavalink node.
///
///
internal async Task StartAsync()
{
if (this.Discord?.CurrentUser?.Id == null || this.Discord?.ShardCount == null)
throw new InvalidOperationException("This operation requires the Discord client to be fully initialized.");
this._webSocket = this.Discord.Configuration.WebSocketClientFactory(this.Discord.Configuration.Proxy, this.Discord.ServiceProvider);
this._webSocket.Connected += this.WebSocket_OnConnect;
this._webSocket.Disconnected += this.WebSocket_OnDisconnect;
this._webSocket.ExceptionThrown += this.WebSocket_OnException;
this._webSocket.MessageReceived += this.WebSocket_OnMessage;
this._webSocket.AddDefaultHeader("Authorization", this.Configuration.Password);
this._webSocket.AddDefaultHeader("Num-Shards", this.Discord.ShardCount.ToString(CultureInfo.InvariantCulture));
this._webSocket.AddDefaultHeader("User-Id", this.Discord.CurrentUser.Id.ToString(CultureInfo.InvariantCulture));
this._webSocket.AddDefaultHeader("Client-Name", $"DisCatSharp.Lavalink version {this.Discord.VersionString}");
if (this.Configuration.ResumeKey != null)
this._webSocket.AddDefaultHeader("Resume-Key", this.Configuration.ResumeKey);
do
{
try
{
if (this._backoff != 0)
{
await Task.Delay(this._backoff).ConfigureAwait(false);
this._backoff = Math.Min(this._backoff * 2, MAXIMUM_BACKOFF);
}
else
{
this._backoff = MINIMUM_BACKOFF;
}
await this._webSocket.ConnectAsync(new Uri(this.Configuration.SocketEndpoint.ToWebSocketString())).ConfigureAwait(false);
break;
}
catch (PlatformNotSupportedException)
{ throw; }
catch (NotImplementedException)
{ throw; }
catch (Exception ex)
{
if (!this.Configuration.SocketAutoReconnect || this._backoff == MAXIMUM_BACKOFF)
{
this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, ex, "Failed to connect to Lavalink.");
throw;
}
else
{
this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, ex, $"Failed to connect to Lavalink, retrying in {this._backoff} ms.");
}
}
}
while (this.Configuration.SocketAutoReconnect);
Volatile.Write(ref this._isDisposed, false);
}
///
/// Stops this Lavalink node connection and frees resources.
///
///
public async Task StopAsync()
{
foreach (var kvp in this.ConnectedGuildsInternal)
await kvp.Value.DisconnectAsync().ConfigureAwait(false);
this.NodeDisconnected?.Invoke(this);
Volatile.Write(ref this._isDisposed, true);
await this._webSocket.DisconnectAsync().ConfigureAwait(false);
// this should not be here, no?
//await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this)).ConfigureAwait(false);
}
///
/// Connects this Lavalink node to specified Discord channel.
///
/// Voice channel to connect to.
/// Channel connection, which allows for playback control.
public async Task ConnectAsync(DiscordChannel channel)
{
- if (this.ConnectedGuildsInternal.ContainsKey(channel.Guild.Id))
- return this.ConnectedGuildsInternal[channel.Guild.Id];
+ if (this.ConnectedGuildsInternal.TryGetValue(channel.Guild.Id, out var connection))
+ return connection;
if (channel.Guild == null || (channel.Type != ChannelType.Voice && channel.Type != ChannelType.Stage))
throw new ArgumentException("Invalid channel specified.", nameof(channel));
var vstut = new TaskCompletionSource();
var vsrut = new TaskCompletionSource();
this._voiceStateUpdates[channel.Guild.Id] = vstut;
this._voiceServerUpdates[channel.Guild.Id] = vsrut;
var vsd = new VoiceDispatch
{
OpCode = 4,
Payload = new VoiceStateUpdatePayload
{
GuildId = channel.Guild.Id,
ChannelId = channel.Id,
Deafened = false,
Muted = false
}
};
var vsj = JsonConvert.SerializeObject(vsd, Formatting.None);
await (channel.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false);
var vstu = await vstut.Task.ConfigureAwait(false);
var vsru = await vsrut.Task.ConfigureAwait(false);
await this.SendPayloadAsync(new LavalinkVoiceUpdate(vstu, vsru)).ConfigureAwait(false);
var con = new LavalinkGuildConnection(this, channel, vstu);
con.ChannelDisconnected += this.Con_ChannelDisconnected;
con.PlayerUpdated += (s, e) => this._playerUpdated.InvokeAsync(s, e);
con.PlaybackStarted += (s, e) => this._playbackStarted.InvokeAsync(s, e);
con.PlaybackFinished += (s, e) => this._playbackFinished.InvokeAsync(s, e);
con.TrackStuck += (s, e) => this._trackStuck.InvokeAsync(s, e);
con.TrackException += (s, e) => this._trackException.InvokeAsync(s, e);
this.ConnectedGuildsInternal[channel.Guild.Id] = con;
return con;
}
///
/// Gets a Lavalink connection to specified Discord channel.
///
/// Guild to get connection for.
/// Channel connection, which allows for playback control.
public LavalinkGuildConnection GetGuildConnection(DiscordGuild guild)
=> this.ConnectedGuildsInternal.TryGetValue(guild.Id, out var lgc) && lgc.IsConnected ? lgc : null;
///
/// Sends the payload async.
///
/// The payload.
internal async Task SendPayloadAsync(LavalinkPayload payload)
=> await this.WsSendAsync(JsonConvert.SerializeObject(payload, Formatting.None)).ConfigureAwait(false);
///
/// Webs the socket_ on message.
///
/// The client.
/// the event.ent.
private async Task WebSocket_OnMessage(IWebSocketClient client, SocketMessageEventArgs e)
{
if (e is not SocketTextMessageEventArgs et)
{
this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, "Lavalink sent binary data - unable to process");
return;
}
this.Discord.Logger.LogTrace(LavalinkEvents.LavalinkWsRx, et.Message);
var json = et.Message;
var jsonData = JObject.Parse(json);
switch (jsonData["op"].ToString())
{
case "playerUpdate":
var gid = (ulong)jsonData["guildId"];
var state = jsonData["state"].ToObject();
if (this.ConnectedGuildsInternal.TryGetValue(gid, out var lvl))
await lvl.InternalUpdatePlayerStateAsync(state).ConfigureAwait(false);
break;
case "stats":
var statsRaw = jsonData.ToObject();
this.Statistics.Update(statsRaw);
await this._statsReceived.InvokeAsync(this, new StatisticsReceivedEventArgs(this.Discord.ServiceProvider, this.Statistics)).ConfigureAwait(false);
break;
case "event":
var evtype = jsonData["type"].ToObject();
var guildId = (ulong)jsonData["guildId"];
switch (evtype)
{
case EventType.TrackStartEvent:
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvtst))
await lvlEvtst.InternalPlaybackStartedAsync(jsonData["track"].ToString()).ConfigureAwait(false);
break;
case EventType.TrackEndEvent:
var reason = TrackEndReason.Cleanup;
switch (jsonData["reason"].ToString())
{
case "FINISHED":
reason = TrackEndReason.Finished;
break;
case "LOAD_FAILED":
reason = TrackEndReason.LoadFailed;
break;
case "STOPPED":
reason = TrackEndReason.Stopped;
break;
case "REPLACED":
reason = TrackEndReason.Replaced;
break;
case "CLEANUP":
reason = TrackEndReason.Cleanup;
break;
}
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvtf))
await lvlEvtf.InternalPlaybackFinishedAsync(new TrackFinishData { Track = jsonData["track"].ToString(), Reason = reason }).ConfigureAwait(false);
break;
case EventType.TrackStuckEvent:
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvts))
await lvlEvts.InternalTrackStuckAsync(new TrackStuckData { Track = jsonData["track"].ToString(), Threshold = (long)jsonData["thresholdMs"] }).ConfigureAwait(false);
break;
case EventType.TrackExceptionEvent:
var severity = LoadFailedSeverity.Common;
switch (jsonData["severity"].ToString())
{
case "COMMON":
severity = LoadFailedSeverity.Common;
break;
case "SUSPICIOUS":
severity = LoadFailedSeverity.Suspicious;
break;
case "FAULT":
severity = LoadFailedSeverity.Fault;
break;
}
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvte))
await lvlEvte.InternalTrackExceptionAsync(new LavalinkLoadFailedInfo { Message = jsonData["message"].ToString(), Severity = severity }, jsonData["track"].ToString()).ConfigureAwait(false);
break;
case EventType.WebSocketClosedEvent:
if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEwsce))
{
lvlEwsce.VoiceWsDisconnectTcs.SetResult(true);
await lvlEwsce.InternalWebSocketClosedAsync(new WebSocketCloseEventArgs(jsonData["code"].ToObject(), jsonData["reason"].ToString(), jsonData["byRemote"].ToObject(), this.Discord.ServiceProvider)).ConfigureAwait(false);
}
break;
}
break;
}
}
///
/// Webs the socket_ on exception.
///
/// The client.
/// the event.
private Task WebSocket_OnException(IWebSocketClient client, SocketErrorEventArgs e)
=> this._lavalinkSocketError.InvokeAsync(this, new SocketErrorEventArgs(client.ServiceProvider) { Exception = e.Exception });
///
/// Webs the socket_ on disconnect.
///
/// The client.
/// the event.
private async Task WebSocket_OnDisconnect(IWebSocketClient client, SocketCloseEventArgs e)
{
if (this.IsConnected && e.CloseCode != 1001 && e.CloseCode != -1)
{
this.Discord.Logger.LogWarning(LavalinkEvents.LavalinkConnectionClosed, "Connection broken ({0}, '{1}'), reconnecting", e.CloseCode, e.CloseMessage);
await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, false)).ConfigureAwait(false);
if (this.Configuration.SocketAutoReconnect)
await this.StartAsync().ConfigureAwait(false);
}
else if (e.CloseCode != 1001 && e.CloseCode != -1)
{
this.Discord.Logger.LogInformation(LavalinkEvents.LavalinkConnectionClosed, "Connection closed ({0}, '{1}')", e.CloseCode, e.CloseMessage);
this.NodeDisconnected?.Invoke(this);
await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, true)).ConfigureAwait(false);
}
else
{
Volatile.Write(ref this._isDisposed, true);
this.Discord.Logger.LogWarning(LavalinkEvents.LavalinkConnectionClosed, "Lavalink died");
foreach (var kvp in this.ConnectedGuildsInternal)
{
await kvp.Value.SendVoiceUpdateAsync().ConfigureAwait(false);
_ = this.ConnectedGuildsInternal.TryRemove(kvp.Key, out _);
}
this.NodeDisconnected?.Invoke(this);
await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, false)).ConfigureAwait(false);
if (this.Configuration.SocketAutoReconnect)
await this.StartAsync().ConfigureAwait(false);
}
}
///
/// Webs the socket_ on connect.
///
/// The client.
/// the event..
private async Task WebSocket_OnConnect(IWebSocketClient client, SocketEventArgs ea)
{
this.Discord.Logger.LogDebug(LavalinkEvents.LavalinkConnected, "Connection to Lavalink node established");
this._backoff = 0;
if (this.Configuration.ResumeKey != null)
await this.SendPayloadAsync(new LavalinkConfigureResume(this.Configuration.ResumeKey, this.Configuration.ResumeTimeout)).ConfigureAwait(false);
}
///
/// Con_S the channel disconnected.
///
/// The con.
private void Con_ChannelDisconnected(LavalinkGuildConnection con)
=> this.ConnectedGuildsInternal.TryRemove(con.GuildId, out _);
///
/// Discord voice state updated.
///
/// The client.
/// the event.
private Task Discord_VoiceStateUpdated(DiscordClient client, VoiceStateUpdateEventArgs e)
{
var gld = e.Guild;
if (gld == null)
return Task.CompletedTask;
if (e.User == null)
return Task.CompletedTask;
if (e.User.Id == this.Discord.CurrentUser.Id)
{
if (this.ConnectedGuildsInternal.TryGetValue(e.Guild.Id, out var lvlgc))
lvlgc.VoiceStateUpdate = e;
if (e.After.Channel == null && this.IsConnected && this.ConnectedGuildsInternal.ContainsKey(gld.Id))
{
_ = Task.Run(async () =>
{
var delayTask = Task.Delay(this.Configuration.WebSocketCloseTimeout);
var tcs = lvlgc.VoiceWsDisconnectTcs.Task;
_ = await Task.WhenAny(delayTask, tcs).ConfigureAwait(false);
await lvlgc.DisconnectInternalAsync(false, true).ConfigureAwait(false);
_ = this.ConnectedGuildsInternal.TryRemove(gld.Id, out _);
});
}
if (!string.IsNullOrWhiteSpace(e.SessionId) && e.Channel != null && this._voiceStateUpdates.TryRemove(gld.Id, out var xe))
xe.SetResult(e);
}
return Task.CompletedTask;
}
///
/// Discord voice server updated.
///
/// The client.
/// the event.
private Task Discord_VoiceServerUpdated(DiscordClient client, VoiceServerUpdateEventArgs e)
{
var gld = e.Guild;
if (gld == null)
return Task.CompletedTask;
if (this.ConnectedGuildsInternal.TryGetValue(e.Guild.Id, out var lvlgc))
{
var lvlp = new LavalinkVoiceUpdate(lvlgc.VoiceStateUpdate, e);
_ = Task.Run(() => this.WsSendAsync(JsonConvert.SerializeObject(lvlp)));
}
if (this._voiceServerUpdates.TryRemove(gld.Id, out var xe))
xe.SetResult(e);
return Task.CompletedTask;
}
///
/// Ws the send async.
///
/// The payload.
private async Task WsSendAsync(string payload)
{
this.Discord.Logger.LogTrace(LavalinkEvents.LavalinkWsTx, payload);
await this._webSocket.SendMessageAsync(payload).ConfigureAwait(false);
}
internal event NodeDisconnectedEventHandler NodeDisconnected;
}
diff --git a/DisCatSharp.Tests/DisCatSharp.EventHandlers.Tests/GlobalSuppressions.cs b/DisCatSharp.Tests/DisCatSharp.EventHandlers.Tests/GlobalSuppressions.cs
new file mode 100644
index 000000000..affff1aa2
--- /dev/null
+++ b/DisCatSharp.Tests/DisCatSharp.EventHandlers.Tests/GlobalSuppressions.cs
@@ -0,0 +1,20 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.EventHandlers.Tests.BasicEventHandlerTests.HandlerB.MessageCreated(DisCatSharp.DiscordClient,DisCatSharp.EventArgs.MessageCreateEventArgs)~System.Threading.Tasks.Task")]
+[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "", Scope = "member", Target = "~M:DisCatSharp.EventHandlers.Tests.BasicEventHandlerTests.BadHandlerA.MessageCreated(System.Object,System.Object)~System.Int32")]
+[assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~M:DisCatSharp.EventHandlers.Tests.BasicEventHandlerTests.BadHandlerB.ThisEventDoesNotExist~System.Threading.Tasks.Task")]
+[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "", Scope = "member", Target = "~M:DisCatSharp.EventHandlers.Tests.BasicEventHandlerTests.HandlerB.MessageCreated(DisCatSharp.DiscordClient,DisCatSharp.EventArgs.MessageCreateEventArgs)~System.Threading.Tasks.Task")]
+[assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~M:DisCatSharp.EventHandlers.Tests.BasicEventHandlerTests.HandlerB.SomeEvent(DisCatSharp.DiscordClient,DisCatSharp.EventArgs.MessageDeleteEventArgs)~System.Threading.Tasks.Task")]
+[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "", Scope = "member", Target = "~M:DisCatSharp.EventHandlers.Tests.BasicEventHandlerTests.HandlerB.SomeEvent(DisCatSharp.DiscordClient,DisCatSharp.EventArgs.MessageDeleteEventArgs)~System.Threading.Tasks.Task")]
+[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "", Scope = "member", Target = "~M:DisCatSharp.EventHandlers.Tests.BasicEventHandlerTests.HandlerC.MessageCreated(DisCatSharp.DiscordClient,DisCatSharp.EventArgs.MessageCreateEventArgs)~System.Threading.Tasks.Task")]
+[assembly: SuppressMessage("Style", "IDE0057:Use range operator", Justification = "", Scope = "member", Target = "~M:DisCatSharp.EventHandlers.Tests.BasicEventHandlerTests.IsEventRegistered(System.String)~System.Boolean")]
+[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.EventHandlers.Tests.BasicEventHandlerTests.BadHandlerA.MessageCreated(System.Object,System.Object)~System.Int32")]
+[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "", Scope = "member", Target = "~M:DisCatSharp.EventHandlers.Tests.BasicEventHandlerTests.HandlerD.ChannelCreated(DisCatSharp.DiscordClient,DisCatSharp.EventArgs.ChannelCreateEventArgs)~System.Threading.Tasks.Task")]
+[assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~M:DisCatSharp.EventHandlers.Tests.EventsEnumIntegrityTests.TestEnumToEvent")]
+[assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "", Scope = "member", Target = "~M:DisCatSharp.EventHandlers.Tests.EventsEnumIntegrityTests.TestEventToEnum")]
+[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "", Scope = "member", Target = "~M:DisCatSharp.EventHandlers.Tests.ServiceProviderTests.Handler.#ctor(DisCatSharp.EventHandlers.Tests.ServiceProviderTests.Resource)")]
diff --git a/DisCatSharp.VoiceNext/AudioFormat.cs b/DisCatSharp.VoiceNext/AudioFormat.cs
index a2405bc26..2b037f789 100644
--- a/DisCatSharp.VoiceNext/AudioFormat.cs
+++ b/DisCatSharp.VoiceNext/AudioFormat.cs
@@ -1,163 +1,163 @@
// 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.Runtime.CompilerServices;
namespace DisCatSharp.VoiceNext;
///
/// Defines the format of PCM data consumed or produced by Opus.
///
-public struct AudioFormat
+public readonly struct AudioFormat
{
///
/// Gets the collection of sampling rates (in Hz) the Opus encoder can use.
///
public static IReadOnlyCollection AllowedSampleRates { get; } = new ReadOnlyCollection(new[] { 8000, 12000, 16000, 24000, 48000 });
///
/// Gets the collection of channel counts the Opus encoder can use.
///
public static IReadOnlyCollection AllowedChannelCounts { get; } = new ReadOnlyCollection(new[] { 1, 2 });
///
/// Gets the collection of sample durations (in ms) the Opus encoder can use.
///
public static IReadOnlyCollection AllowedSampleDurations { get; } = new ReadOnlyCollection(new[] { 5, 10, 20, 40, 60 });
///
/// Gets the default audio format. This is a format configured for 48kHz sampling rate, 2 channels, with music quality preset.
///
public static AudioFormat Default { get; } = new(48000, 2, VoiceApplication.Music);
///
/// Gets the audio sampling rate in Hz.
///
public int SampleRate { get; }
///
/// Gets the audio channel count.
///
public int ChannelCount { get; }
///
/// Gets the voice application, which dictates the quality preset.
///
public VoiceApplication VoiceApplication { get; }
///
/// Creates a new audio format for use with Opus encoder.
///
/// Audio sampling rate in Hz.
/// Number of audio channels in the data.
/// Encoder preset to use.
public AudioFormat(int sampleRate = 48000, int channelCount = 2, VoiceApplication voiceApplication = VoiceApplication.Music)
{
if (!AllowedSampleRates.Contains(sampleRate))
throw new ArgumentOutOfRangeException(nameof(sampleRate), "Invalid sample rate specified.");
if (!AllowedChannelCounts.Contains(channelCount))
throw new ArgumentOutOfRangeException(nameof(channelCount), "Invalid channel count specified.");
if (voiceApplication != VoiceApplication.Music && voiceApplication != VoiceApplication.Voice && voiceApplication != VoiceApplication.LowLatency)
throw new ArgumentOutOfRangeException(nameof(voiceApplication), "Invalid voice application specified.");
this.SampleRate = sampleRate;
this.ChannelCount = channelCount;
this.VoiceApplication = voiceApplication;
}
///
/// Calculates a sample size in bytes.
///
/// Millisecond duration of a sample.
/// Calculated sample size in bytes.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int CalculateSampleSize(int sampleDuration)
{
if (!AllowedSampleDurations.Contains(sampleDuration))
throw new ArgumentOutOfRangeException(nameof(sampleDuration), "Invalid sample duration specified.");
// Sample size in bytes is a product of the following:
// - duration in milliseconds
// - number of channels
// - sample rate in kHz
// - size of data (in this case, sizeof(int16_t))
// which comes down to below:
return sampleDuration * this.ChannelCount * (this.SampleRate / 1000) * 2;
}
///
/// Gets the maximum buffer size for decoding. This method should be called when decoding Opus data to PCM, to ensure sufficient buffer size.
///
/// Buffer size required to decode data.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetMaximumBufferSize()
=> this.CalculateMaximumFrameSize();
///
/// Calculates the sample duration.
///
/// The sample size.
/// An int.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal int CalculateSampleDuration(int sampleSize)
=> sampleSize / (this.SampleRate / 1000) / this.ChannelCount / 2 /* sizeof(int16_t) */;
///
/// Calculates the frame size.
///
/// The sample duration.
/// An int.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal int CalculateFrameSize(int sampleDuration)
=> sampleDuration * (this.SampleRate / 1000);
///
/// Calculates the maximum frame size.
///
/// An int.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal int CalculateMaximumFrameSize()
=> 120 * (this.SampleRate / 1000);
///
/// Samples the count to sample size.
///
/// The sample count.
/// An int.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal int SampleCountToSampleSize(int sampleCount)
=> sampleCount * this.ChannelCount * 2 /* sizeof(int16_t) */;
///
/// Are the valid.
///
/// A bool.
internal bool IsValid()
=> AllowedSampleRates.Contains(this.SampleRate) && AllowedChannelCounts.Contains(this.ChannelCount) &&
(this.VoiceApplication == VoiceApplication.Music || this.VoiceApplication == VoiceApplication.Voice || this.VoiceApplication == VoiceApplication.LowLatency);
}
diff --git a/DisCatSharp.VoiceNext/Codec/Interop.cs b/DisCatSharp.VoiceNext/Codec/Interop.cs
index 9d9a732e3..dcd8e6736 100644
--- a/DisCatSharp.VoiceNext/Codec/Interop.cs
+++ b/DisCatSharp.VoiceNext/Codec/Interop.cs
@@ -1,392 +1,408 @@
// 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.Runtime.InteropServices;
namespace DisCatSharp.VoiceNext.Codec;
///
/// This is an interop class. It contains wrapper methods for Opus and Sodium.
///
-internal static class Interop
+internal static partial class Interop
{
#region Sodium wrapper
///
/// The sodium library name.
///
private const string SODIUM_LIBRARY_NAME = "libsodium";
///
/// Gets the Sodium key size for xsalsa20_poly1305 algorithm.
///
public static int SodiumKeySize { get; } = (int)_SodiumSecretBoxKeySize();
///
/// Gets the Sodium nonce size for xsalsa20_poly1305 algorithm.
///
public static int SodiumNonceSize { get; } = (int)_SodiumSecretBoxNonceSize();
///
/// Gets the Sodium MAC size for xsalsa20_poly1305 algorithm.
///
public static int SodiumMacSize { get; } = (int)_SodiumSecretBoxMacSize();
///
/// _S the sodium secret box key size.
///
/// An UIntPtr.
- [DllImport(SODIUM_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "crypto_secretbox_xsalsa20poly1305_keybytes")]
+ [LibraryImport(SODIUM_LIBRARY_NAME, EntryPoint = "crypto_secretbox_xsalsa20poly1305_keybytes")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
[return: MarshalAs(UnmanagedType.SysUInt)]
- private static extern UIntPtr _SodiumSecretBoxKeySize();
+ private static partial UIntPtr _SodiumSecretBoxKeySize();
///
/// _S the sodium secret box nonce size.
///
/// An UIntPtr.
- [DllImport(SODIUM_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "crypto_secretbox_xsalsa20poly1305_noncebytes")]
+ [LibraryImport(SODIUM_LIBRARY_NAME, EntryPoint = "crypto_secretbox_xsalsa20poly1305_noncebytes")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
[return: MarshalAs(UnmanagedType.SysUInt)]
- private static extern UIntPtr _SodiumSecretBoxNonceSize();
+ private static partial UIntPtr _SodiumSecretBoxNonceSize();
///
/// _S the sodium secret box mac size.
///
/// An UIntPtr.
- [DllImport(SODIUM_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "crypto_secretbox_xsalsa20poly1305_macbytes")]
+ [LibraryImport(SODIUM_LIBRARY_NAME, EntryPoint = "crypto_secretbox_xsalsa20poly1305_macbytes")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
[return: MarshalAs(UnmanagedType.SysUInt)]
- private static extern UIntPtr _SodiumSecretBoxMacSize();
+ private static partial UIntPtr _SodiumSecretBoxMacSize();
///
/// _S the sodium secret box create.
///
/// The buffer.
/// The message.
/// The message length.
/// The nonce.
/// The key.
/// An int.
- [DllImport(SODIUM_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "crypto_secretbox_easy")]
- private static unsafe extern int _SodiumSecretBoxCreate(byte* buffer, byte* message, ulong messageLength, byte* nonce, byte* key);
+ [LibraryImport(SODIUM_LIBRARY_NAME, EntryPoint = "crypto_secretbox_easy")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static unsafe partial int _SodiumSecretBoxCreate(byte* buffer, byte* message, ulong messageLength, byte* nonce, byte* key);
///
/// _S the sodium secret box open.
///
/// The buffer.
/// The encrypted message.
/// The encrypted length.
/// The nonce.
/// The key.
/// An int.
- [DllImport(SODIUM_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "crypto_secretbox_open_easy")]
- private static unsafe extern int _SodiumSecretBoxOpen(byte* buffer, byte* encryptedMessage, ulong encryptedLength, byte* nonce, byte* key);
+ [LibraryImport(SODIUM_LIBRARY_NAME, EntryPoint = "crypto_secretbox_open_easy")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static unsafe partial int _SodiumSecretBoxOpen(byte* buffer, byte* encryptedMessage, ulong encryptedLength, byte* nonce, byte* key);
///
/// Encrypts supplied buffer using xsalsa20_poly1305 algorithm, using supplied key and nonce to perform encryption.
///
/// Contents to encrypt.
/// Buffer to encrypt to.
/// Key to use for encryption.
/// Nonce to use for encryption.
/// Encryption status.
public static unsafe int Encrypt(ReadOnlySpan source, Span target, ReadOnlySpan key, ReadOnlySpan nonce)
{
var status = 0;
fixed (byte* sourcePtr = &source.GetPinnableReference())
fixed (byte* targetPtr = &target.GetPinnableReference())
fixed (byte* keyPtr = &key.GetPinnableReference())
fixed (byte* noncePtr = &nonce.GetPinnableReference())
status = _SodiumSecretBoxCreate(targetPtr, sourcePtr, (ulong)source.Length, noncePtr, keyPtr);
return status;
}
///
/// Decrypts supplied buffer using xsalsa20_poly1305 algorithm, using supplied key and nonce to perform decryption.
///
/// Buffer to decrypt from.
/// Decrypted message buffer.
/// Key to use for decryption.
/// Nonce to use for decryption.
/// Decryption status.
public static unsafe int Decrypt(ReadOnlySpan source, Span target, ReadOnlySpan key, ReadOnlySpan nonce)
{
var status = 0;
fixed (byte* sourcePtr = &source.GetPinnableReference())
fixed (byte* targetPtr = &target.GetPinnableReference())
fixed (byte* keyPtr = &key.GetPinnableReference())
fixed (byte* noncePtr = &nonce.GetPinnableReference())
status = _SodiumSecretBoxOpen(targetPtr, sourcePtr, (ulong)source.Length, noncePtr, keyPtr);
return status;
}
#endregion
#region Opus wrapper
///
/// The opus library name.
///
private const string OPUS_LIBRARY_NAME = "libopus";
///
/// _S the opus create encoder.
///
/// The sample rate.
/// The channels.
/// The application.
/// The error.
/// An IntPtr.
- [DllImport(OPUS_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "opus_encoder_create")]
- private static extern IntPtr _OpusCreateEncoder(int sampleRate, int channels, int application, out OpusError error);
+ [LibraryImport(OPUS_LIBRARY_NAME, EntryPoint = "opus_encoder_create")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial IntPtr _OpusCreateEncoder(int sampleRate, int channels, int application, out OpusError error);
///
/// Opuses the destroy encoder.
///
/// The encoder.
- [DllImport(OPUS_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "opus_encoder_destroy")]
- public static extern void OpusDestroyEncoder(IntPtr encoder);
+ [LibraryImport(OPUS_LIBRARY_NAME, EntryPoint = "opus_encoder_destroy")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ public static partial void OpusDestroyEncoder(IntPtr encoder);
///
/// _S the opus encode.
///
/// The encoder.
/// The pcm data.
/// The frame size.
/// The data.
/// The max data bytes.
/// An int.
- [DllImport(OPUS_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "opus_encode")]
- private static unsafe extern int _OpusEncode(IntPtr encoder, byte* pcmData, int frameSize, byte* data, int maxDataBytes);
+ [LibraryImport(OPUS_LIBRARY_NAME, EntryPoint = "opus_encode")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static unsafe partial int _OpusEncode(IntPtr encoder, byte* pcmData, int frameSize, byte* data, int maxDataBytes);
///
/// _S the opus encoder control.
///
/// The encoder.
/// The request.
/// The value.
/// An OpusError.
- [DllImport(OPUS_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "opus_encoder_ctl")]
- private static extern OpusError _OpusEncoderControl(IntPtr encoder, OpusControl request, int value);
+ [LibraryImport(OPUS_LIBRARY_NAME, EntryPoint = "opus_encoder_ctl")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial OpusError _OpusEncoderControl(IntPtr encoder, OpusControl request, int value);
///
/// _S the opus create decoder.
///
/// The sample rate.
/// The channels.
/// The error.
/// An IntPtr.
- [DllImport(OPUS_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "opus_decoder_create")]
- private static extern IntPtr _OpusCreateDecoder(int sampleRate, int channels, out OpusError error);
+ [LibraryImport(OPUS_LIBRARY_NAME, EntryPoint = "opus_decoder_create")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial IntPtr _OpusCreateDecoder(int sampleRate, int channels, out OpusError error);
///
/// Opuses the destroy decoder.
///
/// The decoder.
- [DllImport(OPUS_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "opus_decoder_destroy")]
- public static extern void OpusDestroyDecoder(IntPtr decoder);
+ [LibraryImport(OPUS_LIBRARY_NAME, EntryPoint = "opus_decoder_destroy")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ public static partial void OpusDestroyDecoder(IntPtr decoder);
///
/// _S the opus decode.
///
/// The decoder.
/// The opus data.
/// The opus data length.
/// The data.
/// The frame size.
/// The decode fec.
/// An int.
- [DllImport(OPUS_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "opus_decode")]
- private static unsafe extern int _OpusDecode(IntPtr decoder, byte* opusData, int opusDataLength, byte* data, int frameSize, int decodeFec);
+ [LibraryImport(OPUS_LIBRARY_NAME, EntryPoint = "opus_decode")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static unsafe partial int _OpusDecode(IntPtr decoder, byte* opusData, int opusDataLength, byte* data, int frameSize, int decodeFec);
///
/// _S the opus get packet channel count.
///
/// The opus data.
/// An int.
- [DllImport(OPUS_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "opus_packet_get_nb_channels")]
- private static unsafe extern int _OpusGetPacketChannelCount(byte* opusData);
+ [LibraryImport(OPUS_LIBRARY_NAME, EntryPoint = "opus_packet_get_nb_channels")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static unsafe partial int _OpusGetPacketChannelCount(byte* opusData);
///
/// _S the opus get packet frame count.
///
/// The opus data.
/// The length.
/// An int.
- [DllImport(OPUS_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "opus_packet_get_nb_frames")]
- private static unsafe extern int _OpusGetPacketFrameCount(byte* opusData, int length);
+ [LibraryImport(OPUS_LIBRARY_NAME, EntryPoint = "opus_packet_get_nb_frames")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static unsafe partial int _OpusGetPacketFrameCount(byte* opusData, int length);
///
/// _S the opus get packet sample per frame count.
///
/// The opus data.
/// The sampling rate.
/// An int.
- [DllImport(OPUS_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "opus_packet_get_samples_per_frame")]
- private static unsafe extern int _OpusGetPacketSamplePerFrameCount(byte* opusData, int samplingRate);
+ [LibraryImport(OPUS_LIBRARY_NAME, EntryPoint = "opus_packet_get_samples_per_frame")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static unsafe partial int _OpusGetPacketSamplePerFrameCount(byte* opusData, int samplingRate);
///
/// _S the opus decoder control.
///
/// The decoder.
/// The request.
/// The value.
/// An int.
- [DllImport(OPUS_LIBRARY_NAME, CallingConvention = CallingConvention.Cdecl, EntryPoint = "opus_decoder_ctl")]
- private static extern int _OpusDecoderControl(IntPtr decoder, OpusControl request, out int value);
+ [LibraryImport(OPUS_LIBRARY_NAME, EntryPoint = "opus_decoder_ctl")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial int _OpusDecoderControl(IntPtr decoder, OpusControl request, out int value);
///
/// Opuses the create encoder.
///
/// The audio format.
/// An IntPtr.
public static IntPtr OpusCreateEncoder(AudioFormat audioFormat)
{
var encoder = _OpusCreateEncoder(audioFormat.SampleRate, audioFormat.ChannelCount, (int)audioFormat.VoiceApplication, out var error);
return error != OpusError.Ok ? throw new Exception($"Could not instantiate Opus encoder: {error} ({(int)error}).") : encoder;
}
///
/// Opuses the set encoder option.
///
/// The encoder.
/// The option.
/// The value.
public static void OpusSetEncoderOption(IntPtr encoder, OpusControl option, int value)
{
var error = OpusError.Ok;
if ((error = _OpusEncoderControl(encoder, option, value)) != OpusError.Ok)
throw new Exception($"Could not set Opus encoder option: {error} ({(int)error}).");
}
///
/// Opuses the encode.
///
/// The encoder.
/// The pcm.
/// The frame size.
/// The opus.
public static unsafe void OpusEncode(IntPtr encoder, ReadOnlySpan pcm, int frameSize, ref Span opus)
{
var len = 0;
fixed (byte* pcmPtr = &pcm.GetPinnableReference())
fixed (byte* opusPtr = &opus.GetPinnableReference())
len = _OpusEncode(encoder, pcmPtr, frameSize, opusPtr, opus.Length);
if (len < 0)
{
var error = (OpusError)len;
throw new Exception($"Could not encode PCM data to Opus: {error} ({(int)error}).");
}
opus = opus[..len];
}
///
/// Opuses the create decoder.
///
/// The audio format.
/// An IntPtr.
public static IntPtr OpusCreateDecoder(AudioFormat audioFormat)
{
var decoder = _OpusCreateDecoder(audioFormat.SampleRate, audioFormat.ChannelCount, out var error);
return error != OpusError.Ok ? throw new Exception($"Could not instantiate Opus decoder: {error} ({(int)error}).") : decoder;
}
///
/// Opuses the decode.
///
/// The decoder.
/// The opus.
/// The frame size.
/// The pcm.
/// If true, use fec.
/// An int.
public static unsafe int OpusDecode(IntPtr decoder, ReadOnlySpan opus, int frameSize, Span pcm, bool useFec)
{
var len = 0;
fixed (byte* opusPtr = &opus.GetPinnableReference())
fixed (byte* pcmPtr = &pcm.GetPinnableReference())
len = _OpusDecode(decoder, opusPtr, opus.Length, pcmPtr, frameSize, useFec ? 1 : 0);
if (len < 0)
{
var error = (OpusError)len;
throw new Exception($"Could not decode PCM data from Opus: {error} ({(int)error}).");
}
return len;
}
///
/// Opuses the decode.
///
/// The decoder.
/// The frame size.
/// The pcm.
/// An int.
public static unsafe int OpusDecode(IntPtr decoder, int frameSize, Span pcm)
{
var len = 0;
fixed (byte* pcmPtr = &pcm.GetPinnableReference())
len = _OpusDecode(decoder, null, 0, pcmPtr, frameSize, 1);
if (len < 0)
{
var error = (OpusError)len;
throw new Exception($"Could not decode PCM data from Opus: {error} ({(int)error}).");
}
return len;
}
///
/// Opuses the get packet metrics.
///
/// The opus.
/// The sampling rate.
/// The channels.
/// The frames.
/// The samples per frame.
/// The frame size.
public static unsafe void OpusGetPacketMetrics(ReadOnlySpan opus, int samplingRate, out int channels, out int frames, out int samplesPerFrame, out int frameSize)
{
fixed (byte* opusPtr = &opus.GetPinnableReference())
{
frames = _OpusGetPacketFrameCount(opusPtr, opus.Length);
samplesPerFrame = _OpusGetPacketSamplePerFrameCount(opusPtr, samplingRate);
channels = _OpusGetPacketChannelCount(opusPtr);
}
frameSize = frames * samplesPerFrame;
}
///
/// Opuses the get last packet duration.
///
/// The decoder.
/// The sample count.
public static void OpusGetLastPacketDuration(IntPtr decoder, out int sampleCount)
=> _OpusDecoderControl(decoder, OpusControl.GetLastPacketDuration, out sampleCount);
#endregion
}
diff --git a/DisCatSharp.VoiceNext/Interop/Bindings.cs b/DisCatSharp.VoiceNext/Interop/Bindings.cs
index 3698be98f..06fdd2dca 100644
--- a/DisCatSharp.VoiceNext/Interop/Bindings.cs
+++ b/DisCatSharp.VoiceNext/Interop/Bindings.cs
@@ -1,155 +1,166 @@
// 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.Runtime.InteropServices;
namespace DisCatSharp.VoiceNext.Interop
{
- internal static unsafe class Bindings
+ internal static unsafe partial class Bindings
{
- [DllImport("libopus", CallingConvention = CallingConvention.Cdecl)]
- private static extern IntPtr opus_encoder_create(int samplingRate, int channels, int application, out OpusError error);
+ [LibraryImport("libopus")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial IntPtr opus_encoder_create(int samplingRate, int channels, int application, out OpusError error);
- [DllImport("libopus", CallingConvention = CallingConvention.Cdecl)]
- private static extern void opus_encoder_destroy(IntPtr encoder);
+ [LibraryImport("libopus")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial void opus_encoder_destroy(IntPtr encoder);
- [DllImport("libopus", CallingConvention = CallingConvention.Cdecl)]
- private static extern int opus_encode(IntPtr encoder, byte* pcm, int frameSize, byte* data, int maxDataBytes);
+ [LibraryImport("libopus")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial int opus_encode(IntPtr encoder, byte* pcm, int frameSize, byte* data, int maxDataBytes);
- [DllImport("libopus", CallingConvention = CallingConvention.Cdecl)]
- private static extern OpusError opus_encoder_ctl(IntPtr encoder, OpusControl ctl, int value);
+ [LibraryImport("libopus")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial OpusError opus_encoder_ctl(IntPtr encoder, OpusControl ctl, int value);
- [DllImport("libopus", CallingConvention = CallingConvention.Cdecl)]
- private static extern IntPtr opus_decoder_create(int sampleRate, int channels, out OpusError error);
+ [LibraryImport("libopus")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial IntPtr opus_decoder_create(int sampleRate, int channels, out OpusError error);
- [DllImport("libopus", CallingConvention = CallingConvention.Cdecl)]
- private static extern void opus_decoder_destroy(IntPtr decoder);
+ [LibraryImport("libopus")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial void opus_decoder_destroy(IntPtr decoder);
- [DllImport("libopus", CallingConvention = CallingConvention.Cdecl)]
- private static extern int opus_decode(IntPtr decoder, byte* opusData, int opusDataLength, byte* data, int frameSize, int decodeFec);
+ [LibraryImport("libopus")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial int opus_decode(IntPtr decoder, byte* opusData, int opusDataLength, byte* data, int frameSize, int decodeFec);
- [DllImport("libopus", CallingConvention = CallingConvention.Cdecl)]
- private static extern int opus_packet_get_nb_channels(byte* data);
+ [LibraryImport("libopus")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial int opus_packet_get_nb_channels(byte* data);
- [DllImport("libopus", CallingConvention = CallingConvention.Cdecl)]
- private static extern int opus_packet_get_nb_frames(byte* data, int length);
+ [LibraryImport("libopus")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial int opus_packet_get_nb_frames(byte* data, int length);
- [DllImport("libopus", CallingConvention = CallingConvention.Cdecl)]
- private static extern int opus_packet_get_samples_per_frame(byte* data, int samplingRate);
+ [LibraryImport("libopus")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial int opus_packet_get_samples_per_frame(byte* data, int samplingRate);
- [DllImport("libopus", CallingConvention = CallingConvention.Cdecl)]
- private static extern int opus_decoder_ctl(IntPtr decoder, OpusControl ctl, out int value);
+ [LibraryImport("libopus")]
+ [UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ private static partial int opus_decoder_ctl(IntPtr decoder, OpusControl ctl, out int value);
public static IntPtr CreateEncoder(int sampleRate, int channelCount, int application)
{
var encoder = opus_encoder_create(sampleRate, channelCount, application, out var error);
return error == OpusError.Ok ? encoder : throw new Exception($"Failed to instantiate Opus encoder: {error} ({(int)error})");
}
public static void SetEncoderOption(IntPtr encoder, OpusControl option, int value)
{
var error = OpusError.Ok;
if ((error = opus_encoder_ctl(encoder, option, value)) != OpusError.Ok)
throw new Exception($"Failed to set Opus encoder option: ${error} ({(int)error})");
}
public static void Encode(IntPtr encoder, ReadOnlySpan pcm, int frameSize, ref Span data)
{
var length = 0;
fixed (byte* pcmPointer = pcm)
fixed (byte* dataPointer = data)
length = opus_encode(encoder, pcmPointer, frameSize, dataPointer, data.Length);
if (length < 0)
{
var error = (OpusError)length;
throw new Exception($"Failed to encode PCM data: {error} ({length})");
}
data = data[..length];
}
public static IntPtr CreateDecoder(int sampleRate, int channelCount)
{
var decoder = opus_decoder_create(sampleRate, channelCount, out var error);
return error == OpusError.Ok ? decoder : throw new Exception($"Failed to instantiate Opus decoder: {error} ({(int)error})");
}
public static int Decode(IntPtr decoder, ReadOnlySpan data, int frameSize, Span pcm, bool useFec)
{
var length = 0;
fixed (byte* dataPointer = data)
fixed (byte* pcmPointer = pcm)
length = opus_decode(decoder, dataPointer, data.Length, pcmPointer, frameSize, useFec ? 1 : 0);
if (length < 0)
{
var error = (OpusError)length;
throw new Exception($"Failed to decode PCM data: {error} ({length})");
}
return length;
}
public static int Decode(IntPtr decoder, int frameSize, Span pcm)
{
var length = 0;
fixed (byte* pcmPointer = pcm)
length = opus_decode(decoder, null, 0, pcmPointer, frameSize, 1);
if (length < 0)
{
var error = (OpusError)length;
throw new Exception($"Failed to decode PCM data: {error} ({length})");
}
return length;
}
public static OpusPacketMetrics GetPacketMetrics(ReadOnlySpan data, int samplingRate)
{
int channels, frames, samplesPerFrame;
fixed (byte* dataPointer = data)
{
frames = opus_packet_get_nb_frames(dataPointer, data.Length);
samplesPerFrame = opus_packet_get_samples_per_frame(dataPointer, samplingRate);
channels = opus_packet_get_nb_channels(dataPointer);
}
return new()
{
ChannelCount = channels,
FrameCount = frames,
SamplesPerFrame = samplesPerFrame,
FrameSize = frames * samplesPerFrame
};
}
public static void GetLastPacketDuration(IntPtr decoder, out int sampleCount)
=> opus_decoder_ctl(decoder, OpusControl.GetLastPacketDuration, out sampleCount);
}
}
diff --git a/DisCatSharp.VoiceNext/VoiceNextExtension.cs b/DisCatSharp.VoiceNext/VoiceNextExtension.cs
index 3ab318245..caac30fdb 100644
--- a/DisCatSharp.VoiceNext/VoiceNextExtension.cs
+++ b/DisCatSharp.VoiceNext/VoiceNextExtension.cs
@@ -1,265 +1,264 @@
// 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.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Net;
using DisCatSharp.VoiceNext.Entities;
using Newtonsoft.Json;
namespace DisCatSharp.VoiceNext;
///
/// Represents VoiceNext extension, which acts as Discord voice client.
///
public sealed class VoiceNextExtension : BaseExtension
{
///
/// Gets or sets the configuration.
///
private readonly VoiceNextConfiguration _configuration;
///
/// Gets or sets the active connections.
///
private readonly ConcurrentDictionary _activeConnections;
///
/// Gets or sets the voice state updates.
///
private readonly ConcurrentDictionary> _voiceStateUpdates;
///
/// Gets or sets the voice server updates.
///
private readonly ConcurrentDictionary> _voiceServerUpdates;
///
/// Gets whether this connection has incoming voice enabled.
///
public bool IsIncomingEnabled { get; }
///
/// Initializes a new instance of the class.
///
/// The config.
internal VoiceNextExtension(VoiceNextConfiguration config)
{
this._configuration = new VoiceNextConfiguration(config);
this.IsIncomingEnabled = config.EnableIncoming;
this._activeConnections = new ConcurrentDictionary();
this._voiceStateUpdates = new ConcurrentDictionary>();
this._voiceServerUpdates = new ConcurrentDictionary>();
}
///
/// DO NOT USE THIS MANUALLY.
///
/// DO NOT USE THIS MANUALLY.
///
protected internal override void Setup(DiscordClient client)
{
if (this.Client != null)
throw new InvalidOperationException("What did I tell you?");
this.Client = client;
this.Client.VoiceStateUpdated += this.Client_VoiceStateUpdate;
this.Client.VoiceServerUpdated += this.Client_VoiceServerUpdate;
}
///
/// Create a VoiceNext connection for the specified channel.
///
/// Channel to connect to.
/// VoiceNext connection for this channel.
public async Task ConnectAsync(DiscordChannel channel)
{
if (channel.Type != ChannelType.Voice && channel.Type != ChannelType.Stage)
throw new ArgumentException("Invalid channel specified; needs to be voice or stage channel", nameof(channel));
if (channel.Guild == null)
throw new ArgumentException("Invalid channel specified; needs to be guild channel", nameof(channel));
if (!channel.PermissionsFor(channel.Guild.CurrentMember).HasPermission(Permissions.AccessChannels | Permissions.UseVoice))
throw new InvalidOperationException("You need AccessChannels and UseVoice permission to connect to this voice channel");
var gld = channel.Guild;
if (this._activeConnections.ContainsKey(gld.Id))
throw new InvalidOperationException("This guild already has a voice connection");
var vstut = new TaskCompletionSource();
var vsrut = new TaskCompletionSource();
this._voiceStateUpdates[gld.Id] = vstut;
this._voiceServerUpdates[gld.Id] = vsrut;
var vsd = new VoiceDispatch
{
OpCode = 4,
Payload = new VoiceStateUpdatePayload
{
GuildId = gld.Id,
ChannelId = channel.Id,
Deafened = false,
Muted = false
}
};
var vsj = JsonConvert.SerializeObject(vsd, Formatting.None);
await (channel.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false);
var vstu = await vstut.Task.ConfigureAwait(false);
var vstup = new VoiceStateUpdatePayload
{
SessionId = vstu.SessionId,
UserId = vstu.User.Id
};
var vsru = await vsrut.Task.ConfigureAwait(false);
var vsrup = new VoiceServerUpdatePayload
{
Endpoint = vsru.Endpoint,
GuildId = vsru.Guild.Id,
Token = vsru.VoiceToken
};
var vnc = new VoiceNextConnection(this.Client, gld, channel, this._configuration, vsrup, vstup);
vnc.VoiceDisconnected += this.Vnc_VoiceDisconnected;
await vnc.ConnectAsync().ConfigureAwait(false);
await vnc.WaitForReadyAsync().ConfigureAwait(false);
this._activeConnections[gld.Id] = vnc;
return vnc;
}
///
/// Gets a VoiceNext connection for specified guild.
///
/// Guild to get VoiceNext connection for.
/// VoiceNext connection for the specified guild.
- public VoiceNextConnection GetConnection(DiscordGuild guild) => this._activeConnections.ContainsKey(guild.Id) ? this._activeConnections[guild.Id] : null;
+ public VoiceNextConnection? GetConnection(DiscordGuild guild) => this._activeConnections.TryGetValue(guild.Id, out var connection) ? connection : null;
///
/// Vnc_S the voice disconnected.
///
/// The guild.
/// A Task.
private async Task Vnc_VoiceDisconnected(DiscordGuild guild)
{
- VoiceNextConnection vnc = null;
if (this._activeConnections.ContainsKey(guild.Id))
- this._activeConnections.TryRemove(guild.Id, out vnc);
+ this._activeConnections.TryRemove(guild.Id, out _);
var vsd = new VoiceDispatch
{
OpCode = 4,
Payload = new VoiceStateUpdatePayload
{
GuildId = guild.Id,
ChannelId = null
}
};
var vsj = JsonConvert.SerializeObject(vsd, Formatting.None);
await (guild.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false);
}
///
/// Client_S the voice state update.
///
/// The client.
/// The e.
/// A Task.
private Task Client_VoiceStateUpdate(DiscordClient client, VoiceStateUpdateEventArgs e)
{
var gld = e.Guild;
if (gld == null)
return Task.CompletedTask;
if (e.User == null)
return Task.CompletedTask;
if (e.User.Id == this.Client.CurrentUser.Id)
{
if (e.After.Channel == null && this._activeConnections.TryRemove(gld.Id, out var ac))
ac.Disconnect();
if (this._activeConnections.TryGetValue(e.Guild.Id, out var vnc))
vnc.TargetChannel = e.Channel;
if (!string.IsNullOrWhiteSpace(e.SessionId) && e.Channel != null && this._voiceStateUpdates.TryRemove(gld.Id, out var xe))
xe.SetResult(e);
}
return Task.CompletedTask;
}
///
/// Client_S the voice server update.
///
/// The client.
/// The e.
/// A Task.
private async Task Client_VoiceServerUpdate(DiscordClient client, VoiceServerUpdateEventArgs e)
{
var gld = e.Guild;
if (gld == null)
return;
if (this._activeConnections.TryGetValue(e.Guild.Id, out var vnc))
{
vnc.ServerData = new VoiceServerUpdatePayload
{
Endpoint = e.Endpoint,
GuildId = e.Guild.Id,
Token = e.VoiceToken
};
var eps = e.Endpoint;
var epi = eps.LastIndexOf(':');
var eph = string.Empty;
var epp = 443;
if (epi != -1)
{
eph = eps[..epi];
epp = int.Parse(eps[(epi + 1)..]);
}
else
{
eph = eps;
}
vnc.WebSocketEndpoint = new ConnectionEndpoint { Hostname = eph, Port = epp };
vnc.Resume = false;
await vnc.ReconnectAsync().ConfigureAwait(false);
}
if (this._voiceServerUpdates.ContainsKey(gld.Id))
{
this._voiceServerUpdates.TryRemove(gld.Id, out var xe);
xe.SetResult(e);
}
}
}
diff --git a/DisCatSharp.code-workspace b/DisCatSharp.code-workspace
index 5f055bdae..52664b383 100644
--- a/DisCatSharp.code-workspace
+++ b/DisCatSharp.code-workspace
@@ -1,18 +1,20 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"discord.lowerDetailsEditing": "Editing {file_name}:{current_line}/{total_lines}",
"discord.detailsEditing": "Working on {git_repo_name} ({git_branch})",
"discord.lowerDetailsDebugging": "Debugging: {file_name}",
"discord.detailsDebugging": "Working on {git_repo_name} ({git_branch})",
"licenser.author": "Aiko IT Systems",
"licenser.customHeader": "This file is part of the DisCatSharp project, a fork of DSharpPlus.\\n\\nCopyright (c) 2021 AITSYS\\n\\nPermission is hereby granted, free of charge, to any person obtaining a copy\\nof this software and associated documentation files (the \"Software\"), to deal\\nin the Software without restriction, including without limitation the rights\\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\\ncopies of the Software, and to permit persons to whom the Software is\\nfurnished to do so, subject to the following conditions:\\n\\nThe above copyright notice and this permission notice shall be included in all\\ncopies or substantial portions of the Software.\\n\\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\\nSOFTWARE.",
"licenser.license": "MIT",
"licenser.projectName": "DisCatSharp",
- "licenser.useSingleLineStyle": false
+ "licenser.useSingleLineStyle": false,
+ "gitlens.views.repositories.showIncomingActivity": true,
+ "gitlens.graph.showRemoteNames": true
}
}
diff --git a/DisCatSharp/Entities/Channel/DiscordChannel.cs b/DisCatSharp/Entities/Channel/DiscordChannel.cs
index 2325338d2..39920a6b5 100644
--- a/DisCatSharp/Entities/Channel/DiscordChannel.cs
+++ b/DisCatSharp/Entities/Channel/DiscordChannel.cs
@@ -1,1468 +1,1468 @@
// 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.IO;
using System.Linq;
using System.Threading.Tasks;
using DisCatSharp.Enums;
using DisCatSharp.Exceptions;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Models;
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents a discord channel.
///
public class DiscordChannel : SnowflakeObject, IEquatable
{
///
/// Gets ID of the guild to which this channel belongs.
///
[JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong? GuildId { get; internal set; }
///
/// Gets ID of the category that contains this channel.
///
[JsonProperty("parent_id", NullValueHandling = NullValueHandling.Include)]
public ulong? ParentId { get; internal set; }
///
/// Gets the category that contains this channel.
///
[JsonIgnore]
public DiscordChannel Parent
=> this.ParentId.HasValue ? this.Guild.GetChannel(this.ParentId.Value) : null;
///
/// Gets the name of this channel.
///
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; internal set; }
///
/// Gets the type of this channel.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public ChannelType Type { get; internal set; }
///
/// Gets the template for new posts in this channel.
/// Applicable if forum channel.
///
[JsonProperty("template", NullValueHandling = NullValueHandling.Ignore)]
public string Template { get; internal set; }
///
/// Gets the position of this channel.
///
[JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)]
public int Position { get; internal set; }
///
/// Gets the flags of this channel.
///
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public ChannelFlags Flags { get; internal set; }
///
/// Gets the maximum available position to move the channel to.
/// This can contain outdated information.
///
public int GetMaxPosition()
{
var channels = this.Guild.Channels.Values;
return this.ParentId != null
? this.Type == ChannelType.Text || this.Type == ChannelType.News
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)).OrderBy(xc => xc.Position).Last().Position
: this.Type == ChannelType.Voice || this.Type == ChannelType.Stage
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)).OrderBy(xc => xc.Position).Last().Position
: channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).Last().Position
: channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).Last().Position;
}
///
/// Gets the minimum available position to move the channel to.
///
public int GetMinPosition()
{
var channels = this.Guild.Channels.Values;
return this.ParentId != null
? this.Type == ChannelType.Text || this.Type == ChannelType.News
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)).OrderBy(xc => xc.Position).First().Position
: this.Type == ChannelType.Voice || this.Type == ChannelType.Stage
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)).OrderBy(xc => xc.Position).First().Position
: channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).First().Position
: channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).First().Position;
}
///
/// Gets whether this channel is a DM channel.
///
[JsonIgnore]
public bool IsPrivate
=> this.Type is ChannelType.Private or ChannelType.Group;
///
/// Gets whether this channel is a channel category.
///
[JsonIgnore]
public bool IsCategory
=> this.Type == ChannelType.Category;
///
/// Gets whether this channel is a stage channel.
///
[JsonIgnore]
public bool IsStage
=> this.Type == ChannelType.Stage;
///
/// Gets the guild to which this channel belongs.
///
[JsonIgnore]
public DiscordGuild Guild
=> this.GuildId.HasValue && this.Discord.Guilds.TryGetValue(this.GuildId.Value, out var guild) ? guild : null;
///
/// Gets a collection of permission overwrites for this channel.
///
[JsonIgnore]
public IReadOnlyList PermissionOverwrites
=> this._permissionOverwritesLazy.Value;
[JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)]
internal List PermissionOverwritesInternal = new();
[JsonIgnore]
private readonly Lazy> _permissionOverwritesLazy;
///
/// Gets the channel's topic. This is applicable to text channels only.
///
[JsonProperty("topic", NullValueHandling = NullValueHandling.Ignore)]
public string Topic { get; internal set; }
///
/// Gets the ID of the last message sent in this channel. This is applicable to text channels only.
///
[JsonProperty("last_message_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong? LastMessageId { get; internal set; }
///
/// Gets this channel's bitrate. This is applicable to voice channels only.
///
[JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)]
public int? Bitrate { get; internal set; }
///
/// Gets this channel's user limit. This is applicable to voice channels only.
///
[JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)]
public int? UserLimit { get; internal set; }
///
/// Gets the slow mode delay configured for this channel.
/// All bots, as well as users with or permissions in the channel are exempt from slow mode.
///
[JsonProperty("rate_limit_per_user", NullValueHandling = NullValueHandling.Ignore)]
public int? PerUserRateLimit { get; internal set; }
///
/// Gets the slow mode delay configured for this channel for post creations.
/// All bots, as well as users with or permissions in the channel are exempt from slow mode.
///
[JsonProperty("default_thread_rate_limit_per_user", NullValueHandling = NullValueHandling.Ignore)]
public int? PostCreateUserRateLimit { get; internal set; }
///
/// Gets this channel's video quality mode. This is applicable to voice channels only.
///
[JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)]
public VideoQualityMode? QualityMode { get; internal set; }
///
/// List of available tags for forum posts.
///
[JsonIgnore]
public IReadOnlyList AvailableTags => this.InternalAvailableTags;
///
/// List of available tags for forum posts.
///
[JsonProperty("available_tags", NullValueHandling = NullValueHandling.Ignore)]
internal List InternalAvailableTags { get; set; } = new();
///
/// List of available tags for forum posts.
///
[JsonProperty("default_reaction_emoji", NullValueHandling = NullValueHandling.Ignore)]
public ForumReactionEmoji DefaultReactionEmoji { get; internal set; }
[JsonProperty("default_sort_order", NullValueHandling = NullValueHandling.Include)]
public ForumPostSortOrder? DefaultSortOrder { get; internal set; }
///
/// Gets when the last pinned message was pinned.
///
[JsonIgnore]
public DateTimeOffset? LastPinTimestamp
=> !string.IsNullOrWhiteSpace(this.LastPinTimestampRaw) && DateTimeOffset.TryParse(this.LastPinTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ?
dto : null;
///
/// Gets when the last pinned message was pinned as raw string.
///
[JsonProperty("last_pin_timestamp", NullValueHandling = NullValueHandling.Ignore)]
internal string LastPinTimestampRaw { get; set; }
///
/// Gets this channel's default duration for newly created threads, in minutes, to automatically archive the thread after recent activity.
///
[JsonProperty("default_auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)]
public ThreadAutoArchiveDuration? DefaultAutoArchiveDuration { get; internal set; }
///
/// Gets this channel's mention string.
///
[JsonIgnore]
public string Mention
=> Formatter.Mention(this);
///
/// Gets this channel's children. This applies only to channel categories.
///
[JsonIgnore]
public IReadOnlyList Children =>
!this.IsCategory
? throw new ArgumentException("Only channel categories contain children.")
: this.Guild.ChannelsInternal.Values.Where(e => e.ParentId == this.Id).ToList();
///
/// Gets the list of members currently in the channel (if voice channel), or members who can see the channel (otherwise).
///
[JsonIgnore]
public virtual IReadOnlyList Users =>
this.Guild == null
? throw new InvalidOperationException("Cannot query users outside of guild channels.")
: this.IsVoiceJoinable()
? this.Guild.Members.Values.Where(x => x.VoiceState?.ChannelId == this.Id).ToList()
: this.Guild.Members.Values.Where(x => (this.PermissionsFor(x) & Permissions.AccessChannels) == Permissions.AccessChannels).ToList();
///
/// Gets whether this channel is an NSFW channel.
///
[JsonProperty("nsfw")]
public bool IsNsfw { get; internal set; }
///
/// Gets this channel's region id (if voice channel).
///
[JsonProperty("rtc_region", NullValueHandling = NullValueHandling.Ignore)]
internal string RtcRegionId { get; set; }
///
/// Gets this channel's region override (if voice channel).
///
[JsonIgnore]
public DiscordVoiceRegion RtcRegion
=> this.RtcRegionId != null ? this.Discord.VoiceRegions[this.RtcRegionId] : null;
///
/// Only sent on the resolved channels of interaction responses for application commands.
/// Gets the permissions of the user in this channel who invoked the command.
///
[JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)]
public Permissions? UserPermissions { get; internal set; }
///
/// Initializes a new instance of the class.
///
internal DiscordChannel()
{
this._permissionOverwritesLazy = new Lazy>(() => new ReadOnlyCollection(this.PermissionOverwritesInternal));
}
#region Methods
///
/// Sends a message to this channel.
///
/// Content of the message to send.
/// The sent message.
/// Thrown when the client does not have the permission if TTS is true and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(string content) =>
!this.IsWritable()
? throw new ArgumentException("Cannot send a text message to a non-text channel.")
: this.Discord.ApiClient.CreateMessageAsync(this.Id, content, null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
///
/// Sends a message to this channel.
///
/// Embed to attach to the message.
/// The sent message.
/// Thrown when the client does not have the permission and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(DiscordEmbed embed) =>
!this.IsWritable()
? throw new ArgumentException("Cannot send a text message to a non-text channel.")
: this.Discord.ApiClient.CreateMessageAsync(this.Id, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
///
/// Sends a message to this channel.
///
/// Embed to attach to the message.
/// Content of the message to send.
/// The sent message.
/// Thrown when the client does not have the permission if TTS is true and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(string content, DiscordEmbed embed) =>
!this.IsWritable()
? throw new ArgumentException("Cannot send a text message to a non-text channel.")
: this.Discord.ApiClient.CreateMessageAsync(this.Id, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false);
///
/// Sends a message to this channel.
///
/// The builder with all the items to send.
/// The sent message.
/// Thrown when the client does not have the permission TTS is true and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(DiscordMessageBuilder builder)
=> this.Discord.ApiClient.CreateMessageAsync(this.Id, builder);
///
/// Sends a message to this channel.
///
/// The builder with all the items to send.
/// The sent message.
/// Thrown when the client does not have the permission TTS is true and if TTS is true.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SendMessageAsync(Action action)
{
var builder = new DiscordMessageBuilder();
action(builder);
return !this.IsWritable()
? throw new ArgumentException("Cannot send a text message to a non-text channel.")
: this.Discord.ApiClient.CreateMessageAsync(this.Id, builder);
}
///
/// Deletes a guild channel
///
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel 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.DeleteChannelAsync(this.Id, reason);
///
/// Clones this channel. This operation will create a channel with identical settings to this one. Note that this will not copy messages.
///
/// Reason for audit logs.
/// Newly-created channel.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CloneAsync(string reason = null)
{
if (this.Guild == null)
throw new InvalidOperationException("Non-guild channels cannot be cloned.");
var ovrs = new List();
foreach (var ovr in this.PermissionOverwritesInternal)
ovrs.Add(await new DiscordOverwriteBuilder().FromAsync(ovr).ConfigureAwait(false));
// TODO: Add forum tags option missing?
var bitrate = this.Bitrate;
var userLimit = this.UserLimit;
Optional perUserRateLimit = this.PerUserRateLimit;
if (!this.IsVoiceJoinable())
{
bitrate = null;
userLimit = null;
}
if (this.Type == ChannelType.Stage)
{
userLimit = null;
}
if (!this.IsWritable())
{
perUserRateLimit = Optional.None;
}
return await this.Guild.CreateChannelAsync(this.Name, this.Type, this.Parent, this.Topic, bitrate, userLimit, ovrs, this.IsNsfw, perUserRateLimit, this.QualityMode, this.DefaultAutoArchiveDuration, this.Flags, reason).ConfigureAwait(false);
}
///
/// Gets a specific message.
///
/// The id of the message
/// Whether to bypass the cache. Defaults to false.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetMessageAsync(ulong id, bool fetch = false) =>
this.Discord.Configuration.MessageCacheSize > 0
&& !fetch
&& this.Discord is DiscordClient dc
&& dc.MessageCache != null
&& dc.MessageCache.TryGet(xm => xm.Id == id && xm.ChannelId == this.Id, out var msg)
? msg
: await this.Discord.ApiClient.GetMessageAsync(this.Id, id).ConfigureAwait(false);
///
/// Tries to get a specific message.
///
/// The id of the message
/// Whether to bypass the cache. Defaults to true.
/// Thrown when the client does not have the permission.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task TryGetMessageAsync(ulong id, bool fetch = true)
{
try
{
return await this.GetMessageAsync(id, fetch).ConfigureAwait(false);
}
catch (NotFoundException)
{
return null;
}
}
///
/// Modifies the current channel.
///
/// Action to perform on this channel
/// Thrown when the client does not have the .
/// Thrown when the client does not have the correct for modifying the .
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyAsync(Action action)
{
if (this.Type == ChannelType.Forum)
throw new NotSupportedException("Cannot execute this request on a forum channel.");
var mdl = new ChannelEditModel();
action(mdl);
if (mdl.DefaultAutoArchiveDuration.HasValue)
if (!Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.DefaultAutoArchiveDuration.Value))
throw new NotSupportedException($"Cannot modify DefaultAutoArchiveDuration. Guild needs boost tier {(mdl.DefaultAutoArchiveDuration.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.");
return this.Discord.ApiClient.ModifyChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Nsfw,
mdl.Parent.Map(p => p?.Id), mdl.Bitrate, mdl.UserLimit, mdl.PerUserRateLimit, mdl.RtcRegion.Map(r => r?.Id),
mdl.QualityMode, mdl.DefaultAutoArchiveDuration, mdl.Type, mdl.PermissionOverwrites, mdl.Flags, mdl.AuditLogReason);
}
///
/// Modifies the current forum channel.
///
/// Action to perform on this channel
/// Thrown when the client does not have the .
/// Thrown when the client does not have the correct for modifying the .
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyForumAsync(Action action)
{
if (this.Type != ChannelType.Forum)
throw new NotSupportedException("Cannot execute this request on a non-forum channel.");
var mdl = new ForumChannelEditModel();
action(mdl);
if (mdl.DefaultAutoArchiveDuration.HasValue && mdl.DefaultAutoArchiveDuration.Value.HasValue)
if (!Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.DefaultAutoArchiveDuration.Value.Value))
throw new NotSupportedException($"Cannot modify DefaultAutoArchiveDuration. Guild needs boost tier {(mdl.DefaultAutoArchiveDuration.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.");
return mdl.AvailableTags.HasValue && mdl.AvailableTags.Value.Count > 20
? throw new NotSupportedException("Cannot have more than 20 tags in a forum channel.")
: (Task)this.Discord.ApiClient.ModifyForumChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Template, mdl.Nsfw,
mdl.Parent.Map(p => p?.Id), mdl.AvailableTags, mdl.DefaultReactionEmoji, mdl.PerUserRateLimit, mdl.PostCreateUserRateLimit,
mdl.DefaultSortOrder, mdl.DefaultAutoArchiveDuration, mdl.PermissionOverwrites, mdl.Flags, mdl.AuditLogReason);
}
///
/// Updates the channel position when it doesn't have a category.
///
/// Use for moving to other categories.
/// Use to move out of a category.
/// Use for moving within a category.
///
/// Position the channel should be moved to.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyPositionAsync(int position, string reason = null)
{
if (this.Guild == null)
throw new ArgumentException("Cannot modify order of non-guild channels.");
if (!this.IsMovable())
throw new NotSupportedException("You can't move this type of channel in categories.");
if (this.ParentId != null)
throw new ArgumentException("Cannot modify order of channels within a category. Use ModifyPositionInCategoryAsync instead.");
var pmds = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type).OrderBy(xc => xc.Position)
.Select(x => new RestGuildChannelReorderPayload
{
ChannelId = x.Id,
Position = x.Id == this.Id ? position : x.Position >= position ? x.Position + 1 : x.Position
});
return this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason);
}
///
/// Updates the channel position within it's own category.
///
/// Use for moving to other categories.
/// Use to move out of a category.
/// Use to move channels outside a category.
///
/// The position.
/// The reason.
/// Thrown when the client does not have the 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 is out of range.
/// Thrown when function is called on a channel without a parent channel.
public async Task ModifyPositionInCategoryAsync(int position, string reason = null)
{
if (!this.IsMovableInParent())
throw new NotSupportedException("You can't move this type of channel in categories.");
var isUp = position > this.Position;
var channels = await this.InternalRefreshChannelsAsync();
var chns = this.ParentId != null
? this.Type == ChannelType.Text || this.Type == ChannelType.News
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News))
: this.Type == ChannelType.Voice || this.Type == ChannelType.Stage
? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage))
: channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type)
: this.Type == ChannelType.Text || this.Type == ChannelType.News
? channels.Where(xc => xc.ParentId == null && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News))
: this.Type == ChannelType.Voice || this.Type == ChannelType.Stage
? channels.Where(xc => xc.ParentId == null && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage))
: channels.Where(xc => xc.ParentId == null && xc.Type == this.Type);
var ochns = chns.OrderBy(xc => xc.Position).ToArray();
var min = ochns.First().Position;
var max = ochns.Last().Position;
if (position > max || position < min)
throw new IndexOutOfRangeException($"Position is not in range. {position} is {(position > max ? "greater then the maximal" : "lower then the minimal")} position.");
var pmds = ochns.Select(x =>
new RestGuildChannelReorderPayload
{
ChannelId = x.Id,
Position = x.Id == this.Id
? position
: isUp
? x.Position <= position && x.Position > this.Position ? x.Position - 1 : x.Position
: x.Position >= position && x.Position < this.Position ? x.Position + 1 : x.Position
}
);
await this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason).ConfigureAwait(false);
}
///
/// Internally refreshes the channel list.
///
private async Task> InternalRefreshChannelsAsync()
{
await this.RefreshPositionsAsync();
return this.Guild.Channels.Values.ToList().AsReadOnly();
}
internal void Initialize(BaseDiscordClient client)
{
this.Discord = client;
foreach (var xo in this.PermissionOverwritesInternal)
{
xo.Discord = this.Discord;
xo.ChannelId = this.Id;
}
if (this.InternalAvailableTags != null)
{
foreach (var xo in this.InternalAvailableTags)
{
xo.Discord = this.Discord;
xo.ChannelId = this.Id;
xo.Channel = this;
}
}
}
///
/// Refreshes the positions.
///
public async Task RefreshPositionsAsync()
{
var channels = await this.Discord.ApiClient.GetGuildChannelsAsync(this.Guild.Id);
this.Guild.ChannelsInternal.Clear();
foreach (var channel in channels.ToList())
{
channel.Initialize(this.Discord);
this.Guild.ChannelsInternal[channel.Id] = channel;
}
}
///
/// Updates the channel position within it's own category.
/// Valid modes: '+' or 'down' to move a channel down | '-' or 'up' to move a channel up.
///
/// Use for moving to other categories.
/// Use to move out of a category.
/// Use to move channels outside a category.
///
/// The mode. Valid: '+' or 'down' to move a channel down | '-' or 'up' to move a channel up
/// The position.
/// The reason.
/// Thrown when the client does not have the 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 is out of range.
/// Thrown when function is called on a channel without a parent channel, a wrong mode is given or given position is zero.
public Task ModifyPositionInCategorySmartAsync(string mode, int position, string reason = null)
{
if (!this.IsMovableInParent())
throw new NotSupportedException("You can't move this type of channel in categories.");
if (mode != "+" && mode != "-" && mode != "down" && mode != "up")
throw new ArgumentException("Error with the selected mode: Valid is '+' or 'down' to move a channel down and '-' or 'up' to move a channel up");
var positive = mode == "+" || mode == "positive" || mode == "down";
var negative = mode == "-" || mode == "negative" || mode == "up";
return positive
? position < this.GetMaxPosition()
? this.ModifyPositionInCategoryAsync(this.Position + position, reason)
: throw new IndexOutOfRangeException($"Position is not in range of category.")
: negative
? position > this.GetMinPosition()
? this.ModifyPositionInCategoryAsync(this.Position - position, reason)
: throw new IndexOutOfRangeException($"Position is not in range of category.")
: throw new ArgumentException("You can only modify with +X or -X. 0 is not valid.");
}
///
/// Updates the channel parent, moving the channel to the bottom of the new category.
///
/// New parent for channel. Use to remove from parent.
/// Sync permissions with parent. Defaults to null.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ModifyParentAsync(DiscordChannel newParent, bool? lockPermissions = null, string reason = null)
{
if (this.Guild == null)
throw new ArgumentException("Cannot modify parent of non-guild channels.");
if (!this.IsMovableInParent())
throw new NotSupportedException("You can't move this type of channel in categories.");
if (newParent.Type is not ChannelType.Category)
throw new ArgumentException("Only category type channels can be parents.");
var position = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type && xc.ParentId == newParent.Id) // gets list same type channels in parent
.Select(xc => xc.Position).DefaultIfEmpty(-1).Max() + 1; // returns highest position of list +1, default val: 0
var pmds = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type)
.OrderBy(xc => xc.Position)
.Select(x =>
{
var pmd = new RestGuildChannelNewParentPayload
{
ChannelId = x.Id,
Position = x.Position >= position ? x.Position + 1 : x.Position,
};
if (x.Id == this.Id)
{
pmd.Position = position;
pmd.ParentId = newParent?.Id;
pmd.LockPermissions = lockPermissions;
}
return pmd;
});
return this.Discord.ApiClient.ModifyGuildChannelParentAsync(this.Guild.Id, pmds, reason);
}
///
/// Moves the channel out of a category.
///
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RemoveParentAsync(string reason = null)
{
if (this.Guild == null)
throw new ArgumentException("Cannot modify parent of non-guild channels.");
if (!this.IsMovableInParent())
throw new NotSupportedException("You can't move this type of channel in categories.");
var pmds = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type)
.OrderBy(xc => xc.Position)
.Select(x =>
{
var pmd = new RestGuildChannelNoParentPayload { ChannelId = x.Id };
if (x.Id == this.Id)
{
pmd.Position = 1;
pmd.ParentId = null;
}
else
{
pmd.Position = x.Position < this.Position ? x.Position + 1 : x.Position;
}
return pmd;
});
return this.Discord.ApiClient.DetachGuildChannelParentAsync(this.Guild.Id, pmds, reason);
}
///
/// Returns a list of messages before a certain message.
/// The amount of messages to fetch.
/// Message to fetch before from.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetMessagesBeforeAsync(ulong before, int limit = 100)
=> this.GetMessagesInternalAsync(limit, before, null, null);
///
/// Returns a list of messages after a certain message.
/// The amount of messages to fetch.
/// Message to fetch after from.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetMessagesAfterAsync(ulong after, int limit = 100)
=> this.GetMessagesInternalAsync(limit, null, after, null);
///
/// Returns a list of messages around a certain message.
/// The amount of messages to fetch.
/// Message to fetch around from.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetMessagesAroundAsync(ulong around, int limit = 100)
=> this.GetMessagesInternalAsync(limit, null, null, around);
///
/// Returns a list of messages from the last message in the channel.
/// The amount of messages to fetch.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetMessagesAsync(int limit = 100) =>
this.GetMessagesInternalAsync(limit, null, null, null);
///
/// Returns a list of messages
///
/// How many messages should be returned.
/// Get messages before snowflake.
/// Get messages after snowflake.
/// Get messages around snowflake.
private async Task> GetMessagesInternalAsync(int limit = 100, ulong? before = null, ulong? after = null, ulong? around = null)
{
if (!this.IsWritable())
throw new ArgumentException("Cannot get the messages of a non-text channel.");
if (limit < 0)
throw new ArgumentException("Cannot get a negative number of messages.");
if (limit == 0)
return Array.Empty();
//return this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, limit, before, after, around);
if (limit > 100 && around != null)
throw new InvalidOperationException("Cannot get more than 100 messages around the specified ID.");
var msgs = new List(limit);
var remaining = limit;
ulong? last = null;
var isAfter = after != null;
int lastCount;
do
{
var fetchSize = remaining > 100 ? 100 : remaining;
var fetch = await this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, fetchSize, !isAfter ? last ?? before : null, isAfter ? last ?? after : null, around).ConfigureAwait(false);
lastCount = fetch.Count;
remaining -= lastCount;
if (!isAfter)
{
msgs.AddRange(fetch);
last = fetch.LastOrDefault()?.Id;
}
else
{
msgs.InsertRange(0, fetch);
last = fetch.FirstOrDefault()?.Id;
}
}
while (remaining > 0 && lastCount > 0);
return new ReadOnlyCollection(msgs);
}
///
/// Deletes multiple messages if they are less than 14 days old. If they are older, none of the messages will be deleted and you will receive a error.
///
/// A collection of messages to delete.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task DeleteMessagesAsync(IEnumerable messages, string reason = null)
{
// don't enumerate more than once
var msgs = messages.Where(x => x.Channel.Id == this.Id).Select(x => x.Id).ToArray();
if (messages == null || !msgs.Any())
throw new ArgumentException("You need to specify at least one message to delete.");
if (msgs.Length < 2)
{
await this.Discord.ApiClient.DeleteMessageAsync(this.Id, msgs.Single(), reason).ConfigureAwait(false);
return;
}
for (var i = 0; i < msgs.Length; i += 100)
await this.Discord.ApiClient.DeleteMessagesAsync(this.Id, msgs.Skip(i).Take(100), reason).ConfigureAwait(false);
}
///
/// Deletes a message
///
/// The message to be deleted.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteMessageAsync(DiscordMessage message, string reason = null)
=> this.Discord.ApiClient.DeleteMessageAsync(this.Id, message.Id, reason);
///
/// Returns a list of invite objects
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetInvitesAsync() =>
this.Guild == null
? throw new ArgumentException("Cannot get the invites of a channel that does not belong to a guild.")
: this.Discord.ApiClient.GetChannelInvitesAsync(this.Id);
///
/// Create a new invite object
///
/// Duration of invite in seconds before expiry, or 0 for never. Defaults to 86400.
/// Max number of uses or 0 for unlimited. Defaults to 0
/// Whether this invite should be temporary. Defaults to false.
/// Whether this invite should be unique. Defaults to false.
/// The target type. Defaults to null.
/// The target activity ID. Defaults to null.
/// The target user id. Defaults to null.
/// The audit log reason.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task CreateInviteAsync(int maxAge = 86400, int maxUses = 0, bool temporary = false, bool unique = false, TargetType? targetType = null, ulong? targetApplicationId = null, ulong? targetUser = null, string reason = null)
=> this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, maxAge, maxUses, targetType, targetApplicationId, targetUser, temporary, unique, reason);
#region Stage
///
/// Opens a stage.
///
/// Topic of the stage.
/// Whether @everyone should be notified.
/// Privacy level of the stage (Defaults to .
/// Audit log reason.
/// Stage instance
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task OpenStageAsync(string topic, bool sendStartNotification = false, StagePrivacyLevel privacyLevel = StagePrivacyLevel.GuildOnly, string reason = null)
=> await this.Discord.ApiClient.CreateStageInstanceAsync(this.Id, topic, sendStartNotification, privacyLevel, reason);
///
/// Modifies a stage topic.
///
/// New topic of the stage.
/// New privacy level of the stage.
/// Audit log reason.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ModifyStageAsync(Optional topic, Optional privacyLevel, string reason = null)
=> await this.Discord.ApiClient.ModifyStageInstanceAsync(this.Id, topic, privacyLevel, reason);
///
/// Closes a stage.
///
/// Audit log reason.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CloseStageAsync(string reason = null)
=> await this.Discord.ApiClient.DeleteStageInstanceAsync(this.Id, reason);
///
/// Gets a stage.
///
/// The requested stage.
/// 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.
public async Task GetStageAsync()
=> await this.Discord.ApiClient.GetStageInstanceAsync(this.Id);
#endregion
#region Scheduled Events
///
/// Creates a scheduled event based on the channel type.
///
/// The name.
/// The scheduled start time.
/// The description.
/// The cover image.
/// The reason.
/// A scheduled event.
/// Thrown when the resource does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CreateScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, string description = null, Optional coverImage = default, string reason = null)
{
if (!this.IsVoiceJoinable())
throw new NotSupportedException("Cannot create a scheduled event for this type of channel. Channel type must be either voice or stage.");
var type = this.Type == ChannelType.Voice ? ScheduledEventEntityType.Voice : ScheduledEventEntityType.StageInstance;
return await this.Guild.CreateScheduledEventAsync(name, scheduledStartTime, null, this, null, description, type, coverImage, reason);
}
#endregion
#region Threads
///
/// Creates a thread.
/// Depending on whether it is created inside an or an it is either an or an .
/// Depending on whether the is set to it is either an or an (default).
///
/// The name of the thread.
/// till it gets archived. Defaults to .
/// Can be either an , or an .
/// The per user ratelimit, aka slowdown.
/// Audit log reason.
/// The created thread.
/// Thrown when the client does not have the or or if creating a private thread the permission.
/// Thrown when the guild hasn't enabled threads atm.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
- /// Thrown when the cannot be modified. This happens, when the guild hasn't reached a certain boost . Or if is not enabled for guild. This happens, if the guild does not have
+ /// Thrown when the cannot be modified. This happens, when the guild hasn't reached a certain boost . Or if is not enabled for guild. This happens, if the guild does not have
public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration autoArchiveDuration = ThreadAutoArchiveDuration.OneHour, ChannelType type = ChannelType.PublicThread, int? rateLimitPerUser = null, string reason = null) =>
type != ChannelType.NewsThread && type != ChannelType.PublicThread && type != ChannelType.PrivateThread
? throw new NotSupportedException("Wrong thread type given.")
: !this.IsThreadHolder()
? throw new NotSupportedException("Parent channel can't have threads.")
: type == ChannelType.PrivateThread
? Utilities.CheckThreadPrivateFeature(this.Guild)
? Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, autoArchiveDuration)
? await this.Discord.ApiClient.CreateThreadAsync(this.Id, null, name, autoArchiveDuration, type, rateLimitPerUser, isForum: false, reason: reason)
: throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.")
: throw new NotSupportedException($"Cannot create a private thread. Guild needs to be boost tier two.")
: Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, autoArchiveDuration)
? await this.Discord.ApiClient.CreateThreadAsync(this.Id, null, name, autoArchiveDuration, this.Type == ChannelType.News ? ChannelType.NewsThread : ChannelType.PublicThread, rateLimitPerUser, isForum: false, reason: reason)
: throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.");
///
/// Creates a forum post.
///
/// The name of the post.
/// The message of the post.
/// The per user ratelimit, aka slowdown.
/// The tags to add on creation.
/// Audit log reason.
/// The created thread.
/// Thrown when the client does not have the permission.
/// Thrown when the guild hasn't enabled threads atm.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CreatePostAsync(string name, DiscordMessageBuilder builder, int? rateLimitPerUser = null, IEnumerable? tags = null, string reason = null)
=> this.Type != ChannelType.Forum ? throw new NotSupportedException("Parent channel must be forum.") : await this.Discord.ApiClient.CreateThreadAsync(this.Id, null, name, null, null, rateLimitPerUser, tags, builder, true, reason);
///
/// Gets joined archived private threads. Can contain more threads.
/// If the result's value 'HasMore' is true, you need to recall this function to get older threads.
///
/// Get threads created before this thread id.
/// Defines the limit of returned .
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetJoinedPrivateArchivedThreadsAsync(ulong? before, int? limit)
=> await this.Discord.ApiClient.GetJoinedPrivateArchivedThreadsAsync(this.Id, before, limit);
///
/// Gets archived public threads. Can contain more threads.
/// If the result's value 'HasMore' is true, you need to recall this function to get older threads.
///
/// Get threads created before this thread id.
/// Defines the limit of returned .
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task GetPublicArchivedThreadsAsync(ulong? before, int? limit)
=> await this.Discord.ApiClient.GetPublicArchivedThreadsAsync(this.Id, before, limit);
///
/// Gets archived private threads. Can contain more threads.
/// If the result's value 'HasMore' is true, you need to recall this function to get older threads.
///
/// Get threads created before this thread id.
/// Defines the limit of returned .
/// 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.
public async Task GetPrivateArchivedThreadsAsync(ulong? before, int? limit)
=> await this.Discord.ApiClient.GetPrivateArchivedThreadsAsync(this.Id, before, limit);
///
/// Gets a forum channel tag.
///
/// The id of the tag to get.
/// Thrown when the tag does not exist.
public ForumPostTag GetForumPostTag(ulong id)
{
var tag = this.InternalAvailableTags.First(x => x.Id == id);
tag.Discord = this.Discord;
tag.ChannelId = this.Id;
tag.Channel = this;
return tag;
}
///
/// Tries to get a forum channel tag.
///
/// The id of the tag to get or null if not found.
public ForumPostTag? TryGetForumPostTag(ulong id)
{
var tag = this.InternalAvailableTags.FirstOrDefault(x => x.Id == id);
if (tag is not null)
{
tag.Discord = this.Discord;
tag.ChannelId = this.Id;
}
return tag;
}
///
/// Creates a forum channel tag.
///
/// The name of the tag.
/// The emoji of the tag. Has to be either a of the current guild or a .
/// Whether only moderators should be able to apply this tag.
/// The audit log reason.
/// Thrown when the client does not have the permission.
/// Thrown when the tag does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CreateForumPostTagAsync(string name, DiscordEmoji emoji = null, bool moderated = false, string reason = null)
=> this.Type != ChannelType.Forum ? throw new NotSupportedException("Channel needs to be type of Forum") :
this.AvailableTags.Count == 20 ?
throw new NotSupportedException("Cannot have more than 20 tags in a forum channel.") :
await this.Discord.ApiClient.ModifyForumChannelAsync(this.Id, null, null, Optional.None, Optional.None, null, Optional.None, this.InternalAvailableTags.Append(new ForumPostTag()
{
Name = name,
EmojiId = emoji != null && emoji.Id != 0 ? emoji.Id : null,
UnicodeEmojiString = emoji?.Id == null || emoji?.Id == 0 ? emoji?.Name ?? null : null,
Moderated = moderated,
Id = null
}).ToList(), Optional.None, Optional.None, Optional.None, Optional.None, Optional.None, null, Optional.None, reason);
///
/// Deletes a forum channel tag.
///
/// The id of the tag to delete.
/// The audit log reason.
/// Thrown when the client does not have the permission.
/// Thrown when the tag does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task DeleteForumPostTag(ulong id, string reason = null)
=> this.Type != ChannelType.Forum ? throw new NotSupportedException("Channel needs to be type of Forum") : await this.Discord.ApiClient.ModifyForumChannelAsync(this.Id, null, null, Optional.None, Optional.None, null, Optional.None, this.InternalAvailableTags?.Where(x => x.Id != id)?.ToList(), Optional.None, Optional.None, Optional.None, Optional.None, Optional.None, null, Optional.None, reason);
#endregion
///
/// Adds a channel permission overwrite for specified role.
///
/// The role to have the permission added.
/// The permissions to allow.
/// The permissions to deny.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task AddOverwriteAsync(DiscordRole role, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null)
=> this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, role.Id, allow, deny, "role", reason);
///
/// Adds a channel permission overwrite for specified member.
///
/// The member to have the permission added.
/// The permissions to allow.
/// The permissions to deny.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task AddOverwriteAsync(DiscordMember member, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null)
=> this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, member.Id, allow, deny, "member", reason);
///
/// Deletes a channel permission overwrite for specified member.
///
/// The member to have the permission deleted.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteOverwriteAsync(DiscordMember member, string reason = null)
=> this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, member.Id, reason);
///
/// Deletes a channel permission overwrite for specified role.
///
/// The role to have the permission deleted.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task DeleteOverwriteAsync(DiscordRole role, string reason = null)
=> this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, role.Id, reason);
///
/// Post a typing indicator.
///
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task TriggerTypingAsync() =>
!this.IsWritable()
? throw new ArgumentException("Cannot start typing in a non-text channel.")
: this.Discord.ApiClient.TriggerTypingAsync(this.Id);
///
/// Returns all pinned messages.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task> GetPinnedMessagesAsync() =>
!this.IsWritable()
? throw new ArgumentException("A non-text channel does not have pinned messages.")
: this.Discord.ApiClient.GetPinnedMessagesAsync(this.Id);
///
/// Create a new webhook.
///
/// The name of the webhook.
/// The image for the default webhook avatar.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task CreateWebhookAsync(string name, Optional avatar = default, string reason = null)
=> await this.Discord.ApiClient.CreateWebhookAsync(this.IsThread() ? this.ParentId!.Value : this.Id, name,
ImageTool.Base64FromStream(avatar), reason).ConfigureAwait(false);
///
/// Returns a list of webhooks.
///
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exist.
/// Thrown when Discord is unable to process the request.
public Task> GetWebhooksAsync()
=> this.Discord.ApiClient.GetChannelWebhooksAsync(this.IsThread() ? this.ParentId!.Value : this.Id);
///
/// Moves a member to this voice channel.
///
/// The member to be moved.
/// Thrown when the client does not have the permission.
/// Thrown when the channel does not exists or if the Member does not exists.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task PlaceMemberAsync(DiscordMember member)
{
if (!this.IsVoiceJoinable())
throw new ArgumentException("Cannot place a member in a non-voice channel.");
await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, member.Id, default, default, default,
default, this.Id, null).ConfigureAwait(false);
}
///
/// Follows a news channel.
///
/// Channel to crosspost messages to.
/// Thrown when trying to follow a non-news channel.
/// Thrown when the current user doesn't have on the target channel.
public Task FollowAsync(DiscordChannel targetChannel) =>
this.Type != ChannelType.News
? throw new ArgumentException("Cannot follow a non-news channel.")
: this.Discord.ApiClient.FollowChannelAsync(this.Id, targetChannel.Id);
///
/// Publishes a message in a news channel to following channels.
///
/// Message to publish.
/// Thrown when the message has already been crossposted.
///
/// Thrown when the current user doesn't have and/or
///
public Task CrosspostMessageAsync(DiscordMessage message) =>
(message.Flags & MessageFlags.Crossposted) == MessageFlags.Crossposted
? throw new ArgumentException("Message is already crossposted.")
: this.Discord.ApiClient.CrosspostMessageAsync(this.Id, message.Id);
///
/// Updates the current user's suppress state in this channel, if stage channel.
///
/// Toggles the suppress state.
/// Sets the time the user requested to speak.
/// Thrown when the channel is not a stage channel.
public async Task UpdateCurrentUserVoiceStateAsync(bool? suppress, DateTimeOffset? requestToSpeakTimestamp = null)
{
if (this.Type != ChannelType.Stage)
throw new ArgumentException("Voice state can only be updated in a stage channel.");
await this.Discord.ApiClient.UpdateCurrentUserVoiceStateAsync(this.GuildId.Value, this.Id, suppress, requestToSpeakTimestamp).ConfigureAwait(false);
}
///
/// Calculates permissions for a given member.
///
/// Member to calculate permissions for.
/// Calculated permissions for a given member.
public Permissions PermissionsFor(DiscordMember mbr)
{
// user > role > everyone
// allow > deny > undefined
// =>
// user allow > user deny > role allow > role deny > everyone allow > everyone deny
if (this.IsPrivate || this.Guild == null)
return Permissions.None;
if (this.Guild.OwnerId == mbr.Id)
return PermissionMethods.FullPerms;
Permissions perms;
// assign @everyone permissions
var everyoneRole = this.Guild.EveryoneRole;
perms = everyoneRole.Permissions;
// roles that member is in
var mbRoles = mbr.Roles.Where(xr => xr.Id != everyoneRole.Id);
// assign permissions from member's roles (in order)
perms |= mbRoles.Aggregate(Permissions.None, (c, role) => c | role.Permissions);
// Administrator grants all permissions and cannot be overridden
if ((perms & Permissions.Administrator) == Permissions.Administrator)
return PermissionMethods.FullPerms;
// channel overrides for roles that member is in
var mbRoleOverrides = mbRoles
.Select(xr => this.PermissionOverwritesInternal.FirstOrDefault(xo => xo.Id == xr.Id))
.Where(xo => xo != null)
.ToList();
// assign channel permission overwrites for @everyone pseudo-role
var everyoneOverwrites = this.PermissionOverwritesInternal.FirstOrDefault(xo => xo.Id == everyoneRole.Id);
if (everyoneOverwrites != null)
{
perms &= ~everyoneOverwrites.Denied;
perms |= everyoneOverwrites.Allowed;
}
// assign channel permission overwrites for member's roles (explicit deny)
perms &= ~mbRoleOverrides.Aggregate(Permissions.None, (c, overs) => c | overs.Denied);
// assign channel permission overwrites for member's roles (explicit allow)
perms |= mbRoleOverrides.Aggregate(Permissions.None, (c, overs) => c | overs.Allowed);
// channel overrides for just this member
var mbOverrides = this.PermissionOverwritesInternal.FirstOrDefault(xo => xo.Id == mbr.Id);
if (mbOverrides == null) return perms;
// assign channel permission overwrites for just this member
perms &= ~mbOverrides.Denied;
perms |= mbOverrides.Allowed;
return perms;
}
///
/// Returns a string representation of this channel.
///
/// String representation of this channel.
public override string ToString() =>
this.Type == ChannelType.Category
? $"Channel Category {this.Name} ({this.Id})"
: this.Type == ChannelType.Text || this.Type == ChannelType.News || this.IsThread()
? $"Channel #{this.Name} ({this.Id})"
: this.IsVoiceJoinable()
? $"Channel #!{this.Name} ({this.Id})"
: !string.IsNullOrWhiteSpace(this.Name) ? $"Channel {this.Name} ({this.Id})" : $"Channel {this.Id}";
#endregion
///
/// Checks whether this is equal to another object.
///
/// Object to compare to.
/// Whether the object is equal to this .
public override bool Equals(object obj)
=> this.Equals(obj as DiscordChannel);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(DiscordChannel e)
=> e is not null && (ReferenceEquals(this, e) || this.Id == e.Id);
///
/// Gets the hash code for this .
///
/// The hash code for this .
public override int GetHashCode()
=> this.Id.GetHashCode();
///
/// Gets whether the two objects are equal.
///
/// First channel to compare.
/// Second channel to compare.
/// Whether the two channels are equal.
public static bool operator ==(DiscordChannel e1, DiscordChannel e2)
{
var o1 = e1 as object;
var o2 = e2 as object;
return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id);
}
///
/// Gets whether the two objects are not equal.
///
/// First channel to compare.
/// Second channel to compare.
/// Whether the two channels are not equal.
public static bool operator !=(DiscordChannel e1, DiscordChannel e2)
=> !(e1 == e2);
}
diff --git a/DisCatSharp/Entities/Color/DiscordColor.cs b/DisCatSharp/Entities/Color/DiscordColor.cs
index 0a0c4ef97..ff771c5bb 100644
--- a/DisCatSharp/Entities/Color/DiscordColor.cs
+++ b/DisCatSharp/Entities/Color/DiscordColor.cs
@@ -1,129 +1,129 @@
// 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.Globalization;
using System.Linq;
namespace DisCatSharp.Entities;
///
/// Represents a color used in Discord API.
///
-public partial struct DiscordColor
+public readonly partial struct DiscordColor
{
private static readonly char[] s_hexAlphabet = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
///
/// Gets the integer representation of this color.
///
public int Value { get; }
///
/// Gets the red component of this color as an 8-bit integer.
///
public byte R
=> (byte)((this.Value >> 16) & 0xFF);
///
/// Gets the green component of this color as an 8-bit integer.
///
public byte G
=> (byte)((this.Value >> 8) & 0xFF);
///
/// Gets the blue component of this color as an 8-bit integer.
///
public byte B
=> (byte)(this.Value & 0xFF);
///
/// Creates a new color with specified value.
///
/// Value of the color.
public DiscordColor(int color)
{
this.Value = color;
}
///
/// Creates a new color with specified values for red, green, and blue components.
///
/// Value of the red component.
/// Value of the green component.
/// Value of the blue component.
public DiscordColor(byte r, byte g, byte b)
{
this.Value = (r << 16) | (g << 8) | b;
}
///
/// Creates a new color with specified values for red, green, and blue components.
///
/// Value of the red component.
/// Value of the green component.
/// Value of the blue component.
public DiscordColor(float r, float g, float b)
{
if (r < 0 || r > 1 || g < 0 || g > 1 || b < 0 || b > 1)
throw new ArgumentOutOfRangeException(null, "Each component must be between 0.0 and 1.0 inclusive.");
var rb = (byte)(r * 255);
var gb = (byte)(g * 255);
var bb = (byte)(b * 255);
this.Value = (rb << 16) | (gb << 8) | bb;
}
///
/// Creates a new color from specified string representation.
///
/// String representation of the color. Must be 6 hexadecimal characters, optionally with # prefix.
public DiscordColor(string color)
{
if (string.IsNullOrWhiteSpace(color))
throw new ArgumentNullException(nameof(color), "Null or empty values are not allowed!");
if (color.Length != 6 && color.Length != 7)
throw new ArgumentException("Color must be 6 or 7 characters in length.", nameof(color));
color = color.ToUpper();
if (color.Length == 7 && color[0] != '#')
throw new ArgumentException("7-character colors must begin with #.", nameof(color));
else if (color.Length == 7)
color = color[1..];
if (color.Any(xc => !s_hexAlphabet.Contains(xc)))
throw new ArgumentException("Colors must consist of hexadecimal characters only.", nameof(color));
this.Value = int.Parse(color, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
}
///
/// Gets a string representation of this color.
///
/// String representation of this color.
public override string ToString() => $"#{this.Value:X6}";
public static implicit operator DiscordColor(int value)
=> new(value);
}
diff --git a/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs b/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs
index 4a94b5515..f21bf3c5b 100644
--- a/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs
+++ b/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs
@@ -1,1322 +1,1322 @@
// 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.Exceptions;
using DisCatSharp.Net;
using DisCatSharp.Net.Abstractions;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
namespace DisCatSharp.Entities;
public partial class DiscordGuild
{
// TODO: Rework audit logs!
///
/// Gets audit log entries for this guild.
///
/// Maximum number of entries to fetch.
/// Filter by member responsible.
/// Filter by action type.
/// A collection of requested audit log entries.
/// Thrown when the client does not have the permission.
/// Thrown when Discord is unable to process the request.
public async Task> GetAuditLogsAsync(int? limit = null, DiscordMember byMember = null, AuditLogActionType? actionType = null)
{
var alrs = new List();
int ac = 1, tc = 0, rmn = 100;
var last = 0ul;
while (ac > 0)
{
rmn = limit != null ? limit.Value - tc : 100;
rmn = Math.Min(100, rmn);
if (rmn <= 0) break;
var alr = await this.Discord.ApiClient.GetAuditLogsAsync(this.Id, rmn, null, last == 0 ? null : last, byMember?.Id, (int?)actionType).ConfigureAwait(false);
ac = alr.Entries.Count;
tc += ac;
if (ac > 0)
{
last = alr.Entries[alr.Entries.Count - 1].Id;
alrs.Add(alr);
}
}
var amr = alrs.SelectMany(xa => xa.Users)
.GroupBy(xu => xu.Id)
.Select(xgu => xgu.First());
foreach (var xau in amr)
{
if (this.Discord.UserCache.ContainsKey(xau.Id))
continue;
var xtu = new TransportUser
{
Id = xau.Id,
Username = xau.Username,
Discriminator = xau.Discriminator,
AvatarHash = xau.AvatarHash
};
var xu = new DiscordUser(xtu) { Discord = this.Discord };
xu = this.Discord.UserCache.AddOrUpdate(xu.Id, xu, (id, old) =>
{
old.Username = xu.Username;
old.Discriminator = xu.Discriminator;
old.AvatarHash = xu.AvatarHash;
return old;
});
}
var atgse = alrs.SelectMany(xa => xa.ScheduledEvents)
.GroupBy(xse => xse.Id)
.Select(xgse => xgse.First());
var ath = alrs.SelectMany(xa => xa.Threads)
.GroupBy(xt => xt.Id)
.Select(xgt => xgt.First());
var aig = alrs.SelectMany(xa => xa.Integrations)
.GroupBy(xi => xi.Id)
.Select(xgi => xgi.First());
var ahr = alrs.SelectMany(xa => xa.Webhooks)
.GroupBy(xh => xh.Id)
.Select(xgh => xgh.First());
var ams = amr.Select(xau => this.MembersInternal != null && this.MembersInternal.TryGetValue(xau.Id, out var member) ? member : new DiscordMember { Discord = this.Discord, Id = xau.Id, GuildId = this.Id });
var amd = ams.ToDictionary(xm => xm.Id, xm => xm);
#pragma warning disable CS0219
Dictionary dtc = null;
Dictionary di = null;
Dictionary dse = null;
#pragma warning restore
Dictionary ahd = null;
if (ahr.Any())
{
var whr = await this.GetWebhooksAsync().ConfigureAwait(false);
var whs = whr.ToDictionary(xh => xh.Id, xh => xh);
var amh = ahr.Select(xah => whs.TryGetValue(xah.Id, out var webhook) ? webhook : new DiscordWebhook { Discord = this.Discord, Name = xah.Name, Id = xah.Id, AvatarHash = xah.AvatarHash, ChannelId = xah.ChannelId, GuildId = xah.GuildId, Token = xah.Token });
ahd = amh.ToDictionary(xh => xh.Id, xh => xh);
}
var acs = alrs.SelectMany(xa => xa.Entries).OrderByDescending(xa => xa.Id);
var entries = new List();
foreach (var xac in acs)
{
DiscordAuditLogEntry entry = null;
ulong t1, t2;
int t3, t4;
long t5, t6;
bool p1, p2;
switch (xac.ActionType)
{
case AuditLogActionType.Invalid:
break;
case AuditLogActionType.GuildUpdate:
entry = new DiscordAuditLogGuildEntry
{
Target = this
};
var entrygld = entry as DiscordAuditLogGuildEntry;
foreach (var xc in xac.Changes)
{
PropertyChange GetChannelChange()
{
ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1);
ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2);
return new PropertyChange
{
Before = this.GetChannel(t1) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id },
After = this.GetChannel(t2) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id }
};
}
switch (xc.Key.ToLowerInvariant())
{
case "name":
entrygld.NameChange = new PropertyChange
{
Before = xc.OldValueString,
After = xc.NewValueString
};
break;
case "owner_id":
entrygld.OwnerChange = new PropertyChange
{
Before = this.MembersInternal != null && this.MembersInternal.TryGetValue(xc.OldValueUlong, out var oldMember) ? oldMember : await this.GetMemberAsync(xc.OldValueUlong).ConfigureAwait(false),
After = this.MembersInternal != null && this.MembersInternal.TryGetValue(xc.NewValueUlong, out var newMember) ? newMember : await this.GetMemberAsync(xc.NewValueUlong).ConfigureAwait(false)
};
break;
case "icon_hash":
entrygld.IconChange = new PropertyChange
{
Before = xc.OldValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id}/{xc.OldValueString}.webp" : null,
After = xc.OldValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id}/{xc.NewValueString}.webp" : null
};
break;
case "verification_level":
entrygld.VerificationLevelChange = new PropertyChange
{
Before = (VerificationLevel)(long)xc.OldValue,
After = (VerificationLevel)(long)xc.NewValue
};
break;
case "afk_channel_id":
entrygld.AfkChannelChange = GetChannelChange();
break;
case "system_channel_flags":
entrygld.SystemChannelFlagsChange = new PropertyChange()
{
Before = (SystemChannelFlags)(long)xc.OldValue,
After = (SystemChannelFlags)(long)xc.NewValue
};
break;
case "widget_channel_id":
entrygld.WidgetChannelChange = GetChannelChange();
break;
case "rules_channel_id":
entrygld.RulesChannelChange = GetChannelChange();
break;
case "public_updates_channel_id":
entrygld.PublicUpdatesChannelChange = GetChannelChange();
break;
case "splash_hash":
entrygld.SplashChange = new PropertyChange
{
Before = xc.OldValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id}/{xc.OldValueString}.webp?size=2048" : null,
After = xc.NewValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id}/{xc.NewValueString}.webp?size=2048" : null
};
break;
case "default_message_notifications":
entrygld.NotificationSettingsChange = new PropertyChange
{
Before = (DefaultMessageNotifications)(long)xc.OldValue,
After = (DefaultMessageNotifications)(long)xc.NewValue
};
break;
case "system_channel_id":
entrygld.SystemChannelChange = GetChannelChange();
break;
case "explicit_content_filter":
entrygld.ExplicitContentFilterChange = new PropertyChange
{
Before = (ExplicitContentFilter)(long)xc.OldValue,
After = (ExplicitContentFilter)(long)xc.NewValue
};
break;
case "mfa_level":
entrygld.MfaLevelChange = new PropertyChange
{
Before = (MfaLevel)(long)xc.OldValue,
After = (MfaLevel)(long)xc.NewValue
};
break;
case "region":
entrygld.RegionChange = new PropertyChange
{
Before = xc.OldValueString,
After = xc.NewValueString
};
break;
case "vanity_url_code":
entrygld.VanityUrlCodeChange = new PropertyChange
{
Before = xc.OldValueString,
After = xc.NewValueString
};
break;
case "premium_progress_bar_enabled":
entrygld.PremiumProgressBarChange = new PropertyChange
{
Before = (bool)xc.OldValue,
After = (bool)xc.NewValue
};
break;
default:
- this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in guild update: {0} - this should be reported to library developers", xc.Key);
+ this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in guild update: {key} - this should be reported to library developers", xc.Key);
break;
}
}
break;
case AuditLogActionType.ChannelCreate:
case AuditLogActionType.ChannelDelete:
case AuditLogActionType.ChannelUpdate:
entry = new DiscordAuditLogChannelEntry
{
Target = this.GetChannel(xac.TargetId.Value) ?? new DiscordChannel { Id = xac.TargetId.Value, Discord = this.Discord, GuildId = this.Id }
};
var entrychn = entry as DiscordAuditLogChannelEntry;
foreach (var xc in xac.Changes)
{
switch (xc.Key.ToLowerInvariant())
{
case "name":
entrychn.NameChange = new PropertyChange
{
Before = xc.OldValueString,
After = xc.NewValueString
};
break;
case "type":
p1 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1);
p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2);
entrychn.TypeChange = new PropertyChange
{
Before = p1 ? (ChannelType?)t1 : null,
After = p2 ? (ChannelType?)t2 : null
};
break;
case "flags":
entrychn.ChannelFlagsChange = new PropertyChange()
{
Before = (ChannelFlags)(long)(xc.OldValue ?? 0L),
After = (ChannelFlags)(long)(xc.NewValue ?? 0L)
};
break;
case "permission_overwrites":
var olds = xc.OldValues?.OfType()
?.Select(xjo => xjo.ToObject())
?.Select(xo => { xo.Discord = this.Discord; return xo; });
var news = xc.NewValues?.OfType()
?.Select(xjo => xjo.ToObject())
?.Select(xo => { xo.Discord = this.Discord; return xo; });
entrychn.OverwriteChange = new PropertyChange>
{
Before = olds != null ? new ReadOnlyCollection(new List(olds)) : null,
After = news != null ? new ReadOnlyCollection(new List(news)) : null
};
break;
case "topic":
entrychn.TopicChange = new PropertyChange
{
Before = xc.OldValueString,
After = xc.NewValueString
};
break;
case "nsfw":
entrychn.NsfwChange = new PropertyChange
{
Before = (bool?)xc.OldValue,
After = (bool?)xc.NewValue
};
break;
case "rtc_region":
entrychn.RtcRegionIdChange = new PropertyChange
{
Before = xc.OldValueString,
After = xc.NewValueString
};
break;
case "bitrate":
entrychn.BitrateChange = new PropertyChange
{
Before = (int?)(long?)xc.OldValue,
After = (int?)(long?)xc.NewValue
};
break;
case "user_limit":
entrychn.UserLimitChange = new PropertyChange
{
Before = (int?)(long?)xc.OldValue,
After = (int?)(long?)xc.NewValue
};
break;
case "rate_limit_per_user":
entrychn.PerUserRateLimitChange = new PropertyChange
{
Before = (int?)(long?)xc.OldValue,
After = (int?)(long?)xc.NewValue
};
break;
case "default_auto_archive_duration":
entrychn.DefaultAutoArchiveDurationChange = new PropertyChange
{
Before = (ThreadAutoArchiveDuration?)(long?)xc.OldValue,
After = (ThreadAutoArchiveDuration?)(long?)xc.NewValue
};
break;
case "available_tags":
var oldTags = xc.OldValues?.OfType()
?.Select(xjo => xjo.ToObject())
?.Select(xo => { xo.Discord = this.Discord; return xo; });
var newTags = xc.NewValues?.OfType()
?.Select(xjo => xjo.ToObject