diff --git a/DisCatSharp.CommandsNext/CommandsNextExtension.cs b/DisCatSharp.CommandsNext/CommandsNextExtension.cs index e9447807a..717168ad7 100644 --- a/DisCatSharp.CommandsNext/CommandsNextExtension.cs +++ b/DisCatSharp.CommandsNext/CommandsNextExtension.cs @@ -1,1086 +1,1086 @@ // 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); if (this._userFriendlyTypeNames.ContainsKey(t)) this._userFriendlyTypeNames.Remove(t); if (!ti.IsValueType) return; var nullableType = typeof(Nullable<>).MakeGenericType(t); if (!this.ArgumentConverters.ContainsKey(nullableType)) return; this.ArgumentConverters.Remove(nullableType); this._userFriendlyTypeNames.Remove(nullableType); } /// /// Registers a user-friendly type name. /// /// Type to register the name for. /// Name to register. public void RegisterUserFriendlyTypeName(string value) { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value), "Name cannot be null or empty."); var t = typeof(T); var ti = t.GetTypeInfo(); if (!this.ArgumentConverters.ContainsKey(t)) throw new InvalidOperationException("Cannot register a friendly name for a type which has no associated converter."); this._userFriendlyTypeNames[t] = value; if (!ti.IsValueType) return; var nullableType = typeof(Nullable<>).MakeGenericType(t); this._userFriendlyTypeNames[nullableType] = value; } /// /// Converts a type into user-friendly type name. /// /// Type to convert. /// User-friendly type name. public string GetUserFriendlyTypeName(Type t) { if (this._userFriendlyTypeNames.ContainsKey(t)) return this._userFriendlyTypeNames[t]; var ti = t.GetTypeInfo(); if (ti.IsGenericTypeDefinition && t.GetGenericTypeDefinition() == typeof(Nullable<>)) { var tn = ti.GenericTypeArguments[0]; return this._userFriendlyTypeNames.ContainsKey(tn) ? this._userFriendlyTypeNames[tn] : tn.Name; } return t.Name; } #endregion #region Helpers /// /// Allows easier interoperability with reflection by turning the returned by /// into a task containing , using the provided generic type information. /// private async Task ConvertArgumentToObj(string value, CommandContext ctx) => await this.ConvertArgument(value, ctx).ConfigureAwait(false); /// /// Gets the configuration-specific string comparer. This returns or , /// depending on whether is set to or . /// /// A string comparer. internal IEqualityComparer GetStringComparer() => this._config.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; #endregion #region Events /// /// Triggered whenever a command executes successfully. /// public event AsyncEventHandler CommandExecuted { add => this._executed.Register(value); remove => this._executed.Unregister(value); } private AsyncEvent _executed; /// /// Triggered whenever a command throws an exception during execution. /// public event AsyncEventHandler CommandErrored { add => this._error.Register(value); remove => this._error.Unregister(value); } private AsyncEvent _error; /// /// Fires when a command gets executed. /// /// The command execution event arguments. private Task OnCommandExecuted(CommandExecutionEventArgs e) => this._executed.InvokeAsync(this, e); /// /// Fires when a command fails. /// /// The command error event arguments. private Task OnCommandErrored(CommandErrorEventArgs e) => this._error.InvokeAsync(this, e); #endregion } diff --git a/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs b/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs index c2c2232d6..adb5c3447 100644 --- a/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs +++ b/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs @@ -1,132 +1,132 @@ // 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; // ReSharper disable InconsistentNaming namespace DisCatSharp.Common.Serialization; /// -/// Defines the format for string-serialized and objects. +/// Defines the format for string-serialized and objects. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public sealed class DateTimeFormatAttribute : SerializationAttribute { /// /// Gets the ISO 8601 format string of "yyyy-MM-ddTHH:mm:ss.fffzzz". /// public const string FORMAT_ISO_8601 = "yyyy-MM-ddTHH:mm:ss.fffzzz"; /// /// Gets the RFC 1123 format string of "R". /// public const string FORMAT_RFC_1123 = "R"; /// /// Gets the general long format. /// public const string FORMAT_LONG = "G"; /// /// Gets the general short format. /// public const string FORMAT_SHORT = "g"; /// /// Gets the custom datetime format string to use. /// public string Format { get; } /// /// Gets the predefined datetime format kind. /// public DateTimeFormatKind Kind { get; } /// /// Specifies a predefined format to use. /// /// Predefined format kind to use. public DateTimeFormatAttribute(DateTimeFormatKind kind) { if (kind < 0 || kind > DateTimeFormatKind.InvariantLocaleShort) throw new ArgumentOutOfRangeException(nameof(kind), "Specified format kind is not legal or supported."); this.Kind = kind; this.Format = null; } /// /// Specifies a custom format to use. /// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings for more details. /// /// Custom format string to use. public DateTimeFormatAttribute(string format) { if (string.IsNullOrWhiteSpace(format)) throw new ArgumentNullException(nameof(format), "Specified format cannot be null or empty."); this.Kind = DateTimeFormatKind.Custom; this.Format = format; } } /// -/// Defines which built-in format to use for for and serialization. +/// Defines which built-in format to use for for and serialization. /// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings and https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings for more details. /// public enum DateTimeFormatKind : int { /// /// Specifies ISO 8601 format, which is equivalent to .NET format string of "yyyy-MM-ddTHH:mm:ss.fffzzz". /// ISO8601 = 0, /// /// Specifies RFC 1123 format, which is equivalent to .NET format string of "R". /// RFC1123 = 1, /// /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons. /// CurrentLocaleLong = 2, /// /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons. /// CurrentLocaleShort = 3, /// /// Specifies a format defined by , with a format string of "G". /// InvariantLocaleLong = 4, /// /// Specifies a format defined by , with a format string of "g". /// InvariantLocaleShort = 5, /// /// Specifies a custom format. This value is not usable directly. /// Custom = int.MaxValue } diff --git a/DisCatSharp.Common/Attributes/TimeSpanAttributes.cs b/DisCatSharp.Common/Attributes/TimeSpanAttributes.cs index af1e257ed..b5b123fba 100644 --- a/DisCatSharp.Common/Attributes/TimeSpanAttributes.cs +++ b/DisCatSharp.Common/Attributes/TimeSpanAttributes.cs @@ -1,41 +1,41 @@ // 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; namespace DisCatSharp.Common.Serialization; /// -/// Specifies that this will be serialized as a number of whole seconds. +/// Specifies that this will be serialized as a number of whole seconds. /// This value will always be serialized as a number. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public sealed class TimeSpanSecondsAttribute : SerializationAttribute { } /// -/// Specifies that this will be serialized as a number of whole milliseconds. +/// Specifies that this will be serialized as a number of whole milliseconds. /// This value will always be serialized as a number. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public sealed class TimeSpanMillisecondsAttribute : SerializationAttribute { } diff --git a/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs b/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs index af570ce41..850614912 100644 --- a/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs +++ b/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs @@ -1,132 +1,132 @@ // 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; // ReSharper disable InconsistentNaming namespace DisCatSharp.Common.Serialization; /// -/// Defines the format for string-serialized objects. +/// Defines the format for string-serialized objects. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public sealed class TimeSpanFormatAttribute : SerializationAttribute { /// /// Gets the ISO 8601 format string of @"ddThh\:mm\:ss\.fff". /// public const string FORMAT_ISO_8601 = @"ddThh\:mm\:ss\.fff"; /// /// Gets the constant format. /// public const string FORMAT_CONSTANT = "c"; /// /// Gets the general long format. /// public const string FORMAT_LONG = "G"; /// /// Gets the general short format. /// public const string FORMAT_SHORT = "g"; /// /// Gets the custom datetime format string to use. /// public string Format { get; } /// /// Gets the predefined datetime format kind. /// public TimeSpanFormatKind Kind { get; } /// /// Specifies a predefined format to use. /// /// Predefined format kind to use. public TimeSpanFormatAttribute(TimeSpanFormatKind kind) { if (kind < 0 || kind > TimeSpanFormatKind.InvariantLocaleShort) throw new ArgumentOutOfRangeException(nameof(kind), "Specified format kind is not legal or supported."); this.Kind = kind; this.Format = null; } /// /// Specifies a custom format to use. /// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-timespan-format-strings for more details. /// /// Custom format string to use. public TimeSpanFormatAttribute(string format) { if (string.IsNullOrWhiteSpace(format)) throw new ArgumentNullException(nameof(format), "Specified format cannot be null or empty."); this.Kind = TimeSpanFormatKind.Custom; this.Format = format; } } /// -/// Defines which built-in format to use for serialization. +/// Defines which built-in format to use for serialization. /// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings and https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-timespan-format-strings for more details. /// public enum TimeSpanFormatKind : int { /// /// Specifies ISO 8601-like time format, which is equivalent to .NET format string of @"ddThh\:mm\:ss\.fff". /// ISO8601 = 0, /// /// Specifies a format defined by , with a format string of "c". /// InvariantConstant = 1, /// /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons. /// CurrentLocaleLong = 2, /// /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons. /// CurrentLocaleShort = 3, /// /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons. /// InvariantLocaleLong = 4, /// /// Specifies a format defined by , with a format string of "g". This format is not recommended for portability reasons. /// InvariantLocaleShort = 5, /// /// Specifies a custom format. This value is not usable directly. /// Custom = int.MaxValue } diff --git a/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs b/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs index cf3b9aed8..32144740a 100644 --- a/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs +++ b/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs @@ -1,41 +1,41 @@ // 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; namespace DisCatSharp.Common.Serialization; /// -/// Specifies that this or will be serialized as Unix timestamp seconds. +/// Specifies that this or will be serialized as Unix timestamp seconds. /// This value will always be serialized as a number. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public sealed class UnixSecondsAttribute : SerializationAttribute { } /// -/// Specifies that this or will be serialized as Unix timestamp milliseconds. +/// Specifies that this or will be serialized as Unix timestamp milliseconds. /// This value will always be serialized as a number. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public sealed class UnixMillisecondsAttribute : SerializationAttribute { } diff --git a/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs b/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs index 3ace497bc..75e2141ea 100644 --- a/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs +++ b/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs @@ -1,813 +1,813 @@ // 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; using System.Collections.Generic; using System.Collections.Immutable; namespace DisCatSharp.Common; /// -/// Represents collection of string keys and values, allowing the use of for dictionary operations. +/// Represents collection of string keys and values, allowing the use of for dictionary operations. /// /// Type of items in this dictionary. public sealed class CharSpanLookupDictionary : IDictionary, IReadOnlyDictionary, IDictionary { /// /// Gets the collection of all keys present in this dictionary. /// public IEnumerable Keys => this.GetKeysInternal(); /// /// Gets the keys. /// ICollection IDictionary.Keys => this.GetKeysInternal(); /// /// Gets the keys. /// ICollection IDictionary.Keys => this.GetKeysInternal(); /// /// Gets the collection of all values present in this dictionary. /// public IEnumerable Values => this.GetValuesInternal(); /// /// Gets the values. /// ICollection IDictionary.Values => this.GetValuesInternal(); /// /// Gets the values. /// ICollection IDictionary.Values => this.GetValuesInternal(); /// /// Gets the total number of items in this dictionary. /// public int Count { get; private set; } /// /// Gets whether this dictionary is read-only. /// public bool IsReadOnly => false; /// /// Gets whether this dictionary has a fixed size. /// public bool IsFixedSize => false; /// /// Gets whether this dictionary is considered thread-safe. /// public bool IsSynchronized => false; /// /// Gets the object which allows synchronizing access to this dictionary. /// public object SyncRoot { get; } = new(); /// /// Gets or sets a value corresponding to given key in this dictionary. /// /// Key to get or set the value for. /// Value matching the supplied key, if applicable. public TValue this[string key] { get { if (key == null) throw new ArgumentNullException(nameof(key)); if (!this.TryRetrieveInternal(key.AsSpan(), out var value)) throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary."); return value; } set { if (key == null) throw new ArgumentNullException(nameof(key)); this.TryInsertInternal(key, value, true); } } /// /// Gets or sets a value corresponding to given key in this dictionary. /// /// Key to get or set the value for. /// Value matching the supplied key, if applicable. public TValue this[ReadOnlySpan key] { get { if (!this.TryRetrieveInternal(key, out var value)) throw new KeyNotFoundException($"The given key was not present in the dictionary."); return value; } set { unsafe { fixed (char* chars = &key.GetPinnableReference()) this.TryInsertInternal(new string(chars, 0, key.Length), value, true); } } } object IDictionary.this[object key] { get { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); if (!this.TryRetrieveInternal(tkey.AsSpan(), out var value)) throw new KeyNotFoundException($"The given key '{tkey}' was not present in the dictionary."); return value; } set { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); if (!(value is TValue tvalue)) { tvalue = default; if (tvalue != null) throw new ArgumentException($"Value needs to be an instance of {typeof(TValue)}."); } this.TryInsertInternal(tkey, tvalue, true); } } /// /// Gets the internal buckets. /// private readonly Dictionary _internalBuckets; /// /// Creates a new, empty with string keys and items of type . /// public CharSpanLookupDictionary() { this._internalBuckets = new Dictionary(); } /// /// Creates a new, empty with string keys and items of type and sets its initial capacity to specified value. /// /// Initial capacity of the dictionary. public CharSpanLookupDictionary(int initialCapacity) { this._internalBuckets = new Dictionary(initialCapacity); } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied dictionary. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupDictionary(IDictionary values) : this(values.Count) { foreach (var (k, v) in values) this.Add(k, v); } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied dictionary. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupDictionary(IReadOnlyDictionary values) : this(values.Count) { foreach (var (k, v) in values) this.Add(k, v); } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied key-value collection. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupDictionary(IEnumerable> values) : this() { foreach (var (k, v) in values) this.Add(k, v); } /// /// Inserts a specific key and corresponding value into this dictionary. /// /// Key to insert. /// Value corresponding to this key. public void Add(string key, TValue value) { if (!this.TryInsertInternal(key, value, false)) throw new ArgumentException("Given key is already present in the dictionary.", nameof(key)); } /// /// Inserts a specific key and corresponding value into this dictionary. /// /// Key to insert. /// Value corresponding to this key. public void Add(ReadOnlySpan key, TValue value) { unsafe { fixed (char* chars = &key.GetPinnableReference()) if (!this.TryInsertInternal(new string(chars, 0, key.Length), value, false)) throw new ArgumentException("Given key is already present in the dictionary.", nameof(key)); } } /// /// Attempts to insert a specific key and corresponding value into this dictionary. /// /// Key to insert. /// Value corresponding to this key. /// Whether the operation was successful. public bool TryAdd(string key, TValue value) => this.TryInsertInternal(key, value, false); /// /// Attempts to insert a specific key and corresponding value into this dictionary. /// /// Key to insert. /// Value corresponding to this key. /// Whether the operation was successful. public bool TryAdd(ReadOnlySpan key, TValue value) { unsafe { fixed (char* chars = &key.GetPinnableReference()) return this.TryInsertInternal(new string(chars, 0, key.Length), value, false); } } /// /// Attempts to retrieve a value corresponding to the supplied key from this dictionary. /// /// Key to retrieve the value for. /// Retrieved value. /// Whether the operation was successful. public bool TryGetValue(string key, out TValue value) { if (key == null) throw new ArgumentNullException(nameof(key)); return this.TryRetrieveInternal(key.AsSpan(), out value); } /// /// Attempts to retrieve a value corresponding to the supplied key from this dictionary. /// /// Key to retrieve the value for. /// Retrieved value. /// Whether the operation was successful. public bool TryGetValue(ReadOnlySpan key, out TValue value) => this.TryRetrieveInternal(key, out value); /// /// Attempts to remove a value corresponding to the supplied key from this dictionary. /// /// Key to remove the value for. /// Removed value. /// Whether the operation was successful. public bool TryRemove(string key, out TValue value) { if (key == null) throw new ArgumentNullException(nameof(key)); return this.TryRemoveInternal(key.AsSpan(), out value); } /// /// Attempts to remove a value corresponding to the supplied key from this dictionary. /// /// Key to remove the value for. /// Removed value. /// Whether the operation was successful. public bool TryRemove(ReadOnlySpan key, out TValue value) => this.TryRemoveInternal(key, out value); /// /// Checks whether this dictionary contains the specified key. /// /// Key to check for in this dictionary. /// Whether the key was present in the dictionary. public bool ContainsKey(string key) => this.ContainsKeyInternal(key.AsSpan()); /// /// Checks whether this dictionary contains the specified key. /// /// Key to check for in this dictionary. /// Whether the key was present in the dictionary. public bool ContainsKey(ReadOnlySpan key) => this.ContainsKeyInternal(key); /// /// Removes all items from this dictionary. /// public void Clear() { this._internalBuckets.Clear(); this.Count = 0; } /// /// Gets an enumerator over key-value pairs in this dictionary. /// /// public IEnumerator> GetEnumerator() => new Enumerator(this); /// /// Removes the. /// /// The key. /// A bool. bool IDictionary.Remove(string key) => this.TryRemove(key.AsSpan(), out _); /// /// Adds the. /// /// The key. /// The value. void IDictionary.Add(object key, object value) { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); if (!(value is TValue tvalue)) { tvalue = default; if (tvalue != null) throw new ArgumentException($"Value needs to be an instance of {typeof(TValue)}."); } this.Add(tkey, tvalue); } /// /// Removes the. /// /// The key. void IDictionary.Remove(object key) { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); this.TryRemove(tkey, out _); } /// /// Contains the. /// /// The key. /// A bool. bool IDictionary.Contains(object key) { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); return this.ContainsKey(tkey); } /// /// Gets the enumerator. /// /// An IDictionaryEnumerator. IDictionaryEnumerator IDictionary.GetEnumerator() => new Enumerator(this); /// /// Adds the. /// /// The item. void ICollection>.Add(KeyValuePair item) => this.Add(item.Key, item.Value); /// /// Removes the. /// /// The item. /// A bool. bool ICollection>.Remove(KeyValuePair item) => this.TryRemove(item.Key, out _); /// /// Contains the. /// /// The item. /// A bool. bool ICollection>.Contains(KeyValuePair item) => this.TryGetValue(item.Key, out var value) && EqualityComparer.Default.Equals(value, item.Value); /// /// Copies the to. /// /// The array. /// The array index. void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { if (array.Length - arrayIndex < this.Count) throw new ArgumentException("Target array is too small.", nameof(array)); var i = arrayIndex; foreach (var (k, v) in this._internalBuckets) { var kdv = v; while (kdv != null) { array[i++] = new KeyValuePair(kdv.Key, kdv.Value); kdv = kdv.Next; } } } /// /// Copies the to. /// /// The array. /// The array index. void ICollection.CopyTo(Array array, int arrayIndex) { if (array is KeyValuePair[] tarray) { (this as ICollection>).CopyTo(tarray, arrayIndex); return; } if (array is not object[]) throw new ArgumentException($"Array needs to be an instance of {typeof(TValue[])} or object[]."); var i = arrayIndex; foreach (var (k, v) in this._internalBuckets) { var kdv = v; while (kdv != null) { array.SetValue(new KeyValuePair(kdv.Key, kdv.Value), i++); kdv = kdv.Next; } } } /// /// Gets the enumerator. /// /// An IEnumerator. IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); /// /// Tries the insert internal. /// /// The key. /// The value. /// If true, replace. /// A bool. private bool TryInsertInternal(string key, TValue value, bool replace) { if (key == null) throw new ArgumentNullException(nameof(key), "Key cannot be null."); var hash = key.CalculateKnuthHash(); if (!this._internalBuckets.ContainsKey(hash)) { this._internalBuckets.Add(hash, new KeyedValue(key, hash, value)); this.Count++; return true; } var kdv = this._internalBuckets[hash]; var kdvLast = kdv; while (kdv != null) { if (kdv.Key == key) { if (!replace) return false; kdv.Value = value; return true; } kdvLast = kdv; kdv = kdv.Next; } kdvLast.Next = new KeyedValue(key, hash, value); this.Count++; return true; } /// /// Tries the retrieve internal. /// /// The key. /// The value. /// A bool. private bool TryRetrieveInternal(ReadOnlySpan key, out TValue value) { value = default; var hash = key.CalculateKnuthHash(); if (!this._internalBuckets.TryGetValue(hash, out var kdv)) return false; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) { value = kdv.Value; return true; } } return false; } /// /// Tries the remove internal. /// /// The key. /// The value. /// A bool. private bool TryRemoveInternal(ReadOnlySpan key, out TValue value) { value = default; var hash = key.CalculateKnuthHash(); if (!this._internalBuckets.TryGetValue(hash, out var kdv)) return false; if (kdv.Next == null && key.SequenceEqual(kdv.Key.AsSpan())) { // Only bucket under this hash and key matches, pop the entire bucket value = kdv.Value; this._internalBuckets.Remove(hash); this.Count--; return true; } else if (kdv.Next == null) { // Only bucket under this hash and key does not match, cannot remove return false; } else if (key.SequenceEqual(kdv.Key.AsSpan())) { // First key in the bucket matches, pop it and set its child as current bucket value = kdv.Value; this._internalBuckets[hash] = kdv.Next; this.Count--; return true; } var kdvLast = kdv; kdv = kdv.Next; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) { // Key matched, remove this bucket from the chain value = kdv.Value; kdvLast.Next = kdv.Next; this.Count--; return true; } kdvLast = kdv; kdv = kdv.Next; } return false; } /// /// Contains the key internal. /// /// The key. /// A bool. private bool ContainsKeyInternal(ReadOnlySpan key) { var hash = key.CalculateKnuthHash(); if (!this._internalBuckets.TryGetValue(hash, out var kdv)) return false; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) return true; kdv = kdv.Next; } return false; } /// /// Gets the keys internal. /// /// An ImmutableArray. private ImmutableArray GetKeysInternal() { var builder = ImmutableArray.CreateBuilder(this.Count); foreach (var value in this._internalBuckets.Values) { var kdv = value; while (kdv != null) { builder.Add(kdv.Key); kdv = kdv.Next; } } return builder.MoveToImmutable(); } /// /// Gets the values internal. /// /// An ImmutableArray. private ImmutableArray GetValuesInternal() { var builder = ImmutableArray.CreateBuilder(this.Count); foreach (var value in this._internalBuckets.Values) { var kdv = value; while (kdv != null) { builder.Add(kdv.Value); kdv = kdv.Next; } } return builder.MoveToImmutable(); } /// /// The keyed value. /// private class KeyedValue { /// /// Gets the key hash. /// public ulong KeyHash { get; } /// /// Gets the key. /// public string Key { get; } /// /// Gets or sets the value. /// public TValue Value { get; set; } /// /// Gets or sets the next. /// public KeyedValue Next { get; set; } /// /// Initializes a new instance of the class. /// /// The key. /// The key hash. /// The value. public KeyedValue(string key, ulong keyHash, TValue value) { this.KeyHash = keyHash; this.Key = key; this.Value = value; } } /// /// The enumerator. /// private class Enumerator : IEnumerator>, IDictionaryEnumerator { /// /// Gets the current. /// public KeyValuePair Current { get; private set; } /// /// Gets the current. /// object IEnumerator.Current => this.Current; /// /// Gets the key. /// object IDictionaryEnumerator.Key => this.Current.Key; /// /// Gets the value. /// object IDictionaryEnumerator.Value => this.Current.Value; /// /// Gets the entry. /// DictionaryEntry IDictionaryEnumerator.Entry => new(this.Current.Key, this.Current.Value); /// /// Gets the internal dictionary. /// private readonly CharSpanLookupDictionary _internalDictionary; /// /// Gets the internal enumerator. /// private readonly IEnumerator> _internalEnumerator; /// /// Gets or sets the current value. /// private KeyedValue _currentValue; /// /// Initializes a new instance of the class. /// /// The sp dict. public Enumerator(CharSpanLookupDictionary spDict) { this._internalDictionary = spDict; this._internalEnumerator = this._internalDictionary._internalBuckets.GetEnumerator(); } /// /// Moves the next. /// /// A bool. public bool MoveNext() { var kdv = this._currentValue; if (kdv == null) { if (!this._internalEnumerator.MoveNext()) return false; kdv = this._internalEnumerator.Current.Value; this.Current = new KeyValuePair(kdv.Key, kdv.Value); this._currentValue = kdv.Next; return true; } this.Current = new KeyValuePair(kdv.Key, kdv.Value); this._currentValue = kdv.Next; return true; } /// /// Resets the. /// public void Reset() { this._internalEnumerator.Reset(); this.Current = default; this._currentValue = null; } /// /// Disposes the. /// public void Dispose() => this.Reset(); } } diff --git a/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs b/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs index b8c3b2a1d..e10dd97ff 100644 --- a/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs +++ b/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs @@ -1,415 +1,415 @@ // 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; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; namespace DisCatSharp.Common; /// -/// Represents collection of string keys and values, allowing the use of for dictionary operations. +/// Represents collection of string keys and values, allowing the use of for dictionary operations. /// /// Type of items in this dictionary. public sealed class CharSpanLookupReadOnlyDictionary : IReadOnlyDictionary { /// /// Gets the collection of all keys present in this dictionary. /// public IEnumerable Keys => this.GetKeysInternal(); /// /// Gets the collection of all values present in this dictionary. /// public IEnumerable Values => this.GetValuesInternal(); /// /// Gets the total number of items in this dictionary. /// public int Count { get; } /// /// Gets a value corresponding to given key in this dictionary. /// /// Key to get or set the value for. /// Value matching the supplied key, if applicable. public TValue this[string key] { get { if (key == null) throw new ArgumentNullException(nameof(key)); if (!this.TryRetrieveInternal(key.AsSpan(), out var value)) throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary."); return value; } } /// /// Gets a value corresponding to given key in this dictionary. /// /// Key to get or set the value for. /// Value matching the supplied key, if applicable. public TValue this[ReadOnlySpan key] { get { if (!this.TryRetrieveInternal(key, out var value)) throw new KeyNotFoundException($"The given key was not present in the dictionary."); return value; } } /// /// Gets the internal buckets. /// private readonly IReadOnlyDictionary _internalBuckets; /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied dictionary. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupReadOnlyDictionary(IDictionary values) : this(values as IEnumerable>) { } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied dictionary. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupReadOnlyDictionary(IReadOnlyDictionary values) : this(values as IEnumerable>) { } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied key-value collection. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupReadOnlyDictionary(IEnumerable> values) { this._internalBuckets = PrepareItems(values, out var count); this.Count = count; } /// /// Attempts to retrieve a value corresponding to the supplied key from this dictionary. /// /// Key to retrieve the value for. /// Retrieved value. /// Whether the operation was successful. public bool TryGetValue(string key, out TValue value) { if (key == null) throw new ArgumentNullException(nameof(key)); return this.TryRetrieveInternal(key.AsSpan(), out value); } /// /// Attempts to retrieve a value corresponding to the supplied key from this dictionary. /// /// Key to retrieve the value for. /// Retrieved value. /// Whether the operation was successful. public bool TryGetValue(ReadOnlySpan key, out TValue value) => this.TryRetrieveInternal(key, out value); /// /// Checks whether this dictionary contains the specified key. /// /// Key to check for in this dictionary. /// Whether the key was present in the dictionary. public bool ContainsKey(string key) => this.ContainsKeyInternal(key.AsSpan()); /// /// Checks whether this dictionary contains the specified key. /// /// Key to check for in this dictionary. /// Whether the key was present in the dictionary. public bool ContainsKey(ReadOnlySpan key) => this.ContainsKeyInternal(key); /// /// Gets an enumerator over key-value pairs in this dictionary. /// /// public IEnumerator> GetEnumerator() => new Enumerator(this); /// /// Gets the enumerator. /// /// An IEnumerator. IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); /// /// Tries the retrieve internal. /// /// The key. /// The value. /// A bool. private bool TryRetrieveInternal(ReadOnlySpan key, out TValue value) { value = default; var hash = key.CalculateKnuthHash(); if (!this._internalBuckets.TryGetValue(hash, out var kdv)) return false; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) { value = kdv.Value; return true; } } return false; } /// /// Contains the key internal. /// /// The key. /// A bool. private bool ContainsKeyInternal(ReadOnlySpan key) { var hash = key.CalculateKnuthHash(); if (!this._internalBuckets.TryGetValue(hash, out var kdv)) return false; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) return true; kdv = kdv.Next; } return false; } /// /// Gets the keys internal. /// /// An ImmutableArray. private ImmutableArray GetKeysInternal() { var builder = ImmutableArray.CreateBuilder(this.Count); foreach (var value in this._internalBuckets.Values) { var kdv = value; while (kdv != null) { builder.Add(kdv.Key); kdv = kdv.Next; } } return builder.MoveToImmutable(); } /// /// Gets the values internal. /// /// An ImmutableArray. private ImmutableArray GetValuesInternal() { var builder = ImmutableArray.CreateBuilder(this.Count); foreach (var value in this._internalBuckets.Values) { var kdv = value; while (kdv != null) { builder.Add(kdv.Value); kdv = kdv.Next; } } return builder.MoveToImmutable(); } /// /// Prepares the items. /// /// The items. /// The count. /// An IReadOnlyDictionary. private static IReadOnlyDictionary PrepareItems(IEnumerable> items, out int count) { count = 0; var dict = new Dictionary(); foreach (var (k, v) in items) { if (k == null) throw new ArgumentException("Keys cannot be null.", nameof(items)); var hash = k.CalculateKnuthHash(); if (!dict.ContainsKey(hash)) { dict.Add(hash, new KeyedValue(k, hash, v)); count++; continue; } var kdv = dict[hash]; var kdvLast = kdv; while (kdv != null) { if (kdv.Key == k) throw new ArgumentException("Given key is already present in the dictionary.", nameof(items)); kdvLast = kdv; kdv = kdv.Next; } kdvLast.Next = new KeyedValue(k, hash, v); count++; } return new ReadOnlyDictionary(dict); } /// /// The keyed value. /// private class KeyedValue { /// /// Gets the key hash. /// public ulong KeyHash { get; } /// /// Gets the key. /// public string Key { get; } /// /// Gets or sets the value. /// public TValue Value { get; set; } /// /// Gets or sets the next. /// public KeyedValue Next { get; set; } /// /// Initializes a new instance of the class. /// /// The key. /// The key hash. /// The value. public KeyedValue(string key, ulong keyHash, TValue value) { this.KeyHash = keyHash; this.Key = key; this.Value = value; } } /// /// The enumerator. /// private class Enumerator : IEnumerator> { /// /// Gets the current. /// public KeyValuePair Current { get; private set; } /// /// Gets the current. /// object IEnumerator.Current => this.Current; /// /// Gets the internal dictionary. /// private readonly CharSpanLookupReadOnlyDictionary _internalDictionary; /// /// Gets the internal enumerator. /// private readonly IEnumerator> _internalEnumerator; /// /// Gets or sets the current value. /// private KeyedValue _currentValue; /// /// Initializes a new instance of the class. /// /// The sp dict. public Enumerator(CharSpanLookupReadOnlyDictionary spDict) { this._internalDictionary = spDict; this._internalEnumerator = this._internalDictionary._internalBuckets.GetEnumerator(); } /// /// Moves the next. /// /// A bool. public bool MoveNext() { var kdv = this._currentValue; if (kdv == null) { if (!this._internalEnumerator.MoveNext()) return false; kdv = this._internalEnumerator.Current.Value; this.Current = new KeyValuePair(kdv.Key, kdv.Value); this._currentValue = kdv.Next; return true; } this.Current = new KeyValuePair(kdv.Key, kdv.Value); this._currentValue = kdv.Next; return true; } /// /// Resets the. /// public void Reset() { this._internalEnumerator.Reset(); this.Current = default; this._currentValue = null; } /// /// Disposes the. /// public void Dispose() => this.Reset(); } } diff --git a/DisCatSharp.Common/Types/ContinuousMemoryBuffer.cs b/DisCatSharp.Common/Types/ContinuousMemoryBuffer.cs index 0368bb08d..95937b8fb 100644 --- a/DisCatSharp.Common/Types/ContinuousMemoryBuffer.cs +++ b/DisCatSharp.Common/Types/ContinuousMemoryBuffer.cs @@ -1,254 +1,254 @@ // 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.Buffers; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace DisCatSharp.Common.Types; /// /// Provides a resizable memory buffer analogous to , using a single continuous memory region instead. /// /// Type of item to hold in the buffer. public sealed class ContinuousMemoryBuffer : IMemoryBuffer where T : unmanaged { /// public ulong Capacity => (ulong)this._buff.Length; /// public ulong Length => (ulong)this._pos; /// public ulong Count => (ulong)(this._pos / this._itemSize); private readonly MemoryPool _pool; private IMemoryOwner _buffOwner; private Memory _buff; private readonly bool _clear; private int _pos; private readonly int _itemSize; private bool _isDisposed; /// /// Creates a new buffer with a specified segment size, specified number of initially-allocated segments, and supplied memory pool. /// /// Initial size of the buffer in bytes. Defaults to 64KiB. - /// Memory pool to use for renting buffers. Defaults to . + /// Memory pool to use for renting buffers. Defaults to . /// Determines whether the underlying buffers should be cleared on exit. If dealing with sensitive data, it might be a good idea to set this option to true. public ContinuousMemoryBuffer(int initialSize = 65536, MemoryPool memPool = default, bool clearOnDispose = false) { this._itemSize = Unsafe.SizeOf(); this._pool = memPool ?? MemoryPool.Shared; this._clear = clearOnDispose; this._buffOwner = this._pool.Rent(initialSize); this._buff = this._buffOwner.Memory; this._isDisposed = false; } /// public void Write(ReadOnlySpan data) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); var bytes = MemoryMarshal.AsBytes(data); this.EnsureSize(this._pos + bytes.Length); bytes.CopyTo(this._buff[this._pos..].Span); this._pos += bytes.Length; } /// public void Write(T[] data, int start, int count) => this.Write(data.AsSpan(start, count)); /// public void Write(ArraySegment data) => this.Write(data.AsSpan()); /// public void Write(Stream stream) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); if (stream.CanSeek) this.WriteStreamSeekable(stream); else this.WriteStreamUnseekable(stream); } /// /// Writes the stream seekable. /// /// The stream. private void WriteStreamSeekable(Stream stream) { if (stream.Length > int.MaxValue) throw new ArgumentException("Stream is too long.", nameof(stream)); this.EnsureSize(this._pos + (int)stream.Length); var memo = ArrayPool.Shared.Rent((int)stream.Length); try { var br = stream.Read(memo, 0, memo.Length); memo.AsSpan(0, br).CopyTo(this._buff[this._pos..].Span); } finally { ArrayPool.Shared.Return(memo); } this._pos += (int)stream.Length; } /// /// Writes the stream unseekable. /// /// The stream. private void WriteStreamUnseekable(Stream stream) { var memo = ArrayPool.Shared.Rent(4096); try { var br = 0; while ((br = stream.Read(memo, 0, memo.Length)) != 0) { this.EnsureSize(this._pos + br); memo.AsSpan(0, br).CopyTo(this._buff[this._pos..].Span); this._pos += br; } } finally { ArrayPool.Shared.Return(memo); } } /// public bool Read(Span destination, ulong source, out int itemsWritten) { itemsWritten = 0; if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); source *= (ulong)this._itemSize; if (source > this.Count) throw new ArgumentOutOfRangeException(nameof(source), "Cannot copy data from beyond the buffer."); var start = (int)source; var sbuff = this._buff[start..this._pos ].Span; var dbuff = MemoryMarshal.AsBytes(destination); if (sbuff.Length > dbuff.Length) sbuff = sbuff[..dbuff.Length]; itemsWritten = sbuff.Length / this._itemSize; sbuff.CopyTo(dbuff); return this.Length - source != (ulong)itemsWritten; } /// public bool Read(T[] data, int start, int count, ulong source, out int itemsWritten) => this.Read(data.AsSpan(start, count), source, out itemsWritten); /// public bool Read(ArraySegment data, ulong source, out int itemsWritten) => this.Read(data.AsSpan(), source, out itemsWritten); /// public T[] ToArray() { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); return MemoryMarshal.Cast(this._buff[..this._pos].Span).ToArray(); } /// public void CopyTo(Stream destination) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); var buff = this._buff[..this._pos].ToArray(); destination.Write(buff, 0, buff.Length); } /// public void Clear() { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); this._pos = 0; } /// /// Disposes of any resources claimed by this buffer. /// public void Dispose() { if (this._isDisposed) return; this._isDisposed = true; if (this._clear) this._buff.Span.Clear(); this._buffOwner.Dispose(); this._buff = default; } /// /// Ensures the size. /// /// The new capacity. private void EnsureSize(int newCapacity) { var cap = this._buff.Length; if (cap >= newCapacity) return; var factor = newCapacity / cap; if (newCapacity % cap != 0) ++factor; var newActualCapacity = cap * factor; var newBuffOwner = this._pool.Rent(newActualCapacity); var newBuff = newBuffOwner.Memory; this._buff.Span.CopyTo(newBuff.Span); if (this._clear) this._buff.Span.Clear(); this._buffOwner.Dispose(); this._buffOwner = newBuffOwner; this._buff = newBuff; } } diff --git a/DisCatSharp.Common/Types/MemoryBuffer.cs b/DisCatSharp.Common/Types/MemoryBuffer.cs index bc90b25aa..fd91cdd1f 100644 --- a/DisCatSharp.Common/Types/MemoryBuffer.cs +++ b/DisCatSharp.Common/Types/MemoryBuffer.cs @@ -1,339 +1,339 @@ // 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.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace DisCatSharp.Common.Types; /// /// Provides a resizable memory buffer, which can be read from and written to. It will automatically resize whenever required. /// /// Type of item to hold in the buffer. public sealed class MemoryBuffer : IMemoryBuffer where T : unmanaged { /// public ulong Capacity => this._segments.Aggregate(0UL, (a, x) => a + (ulong)x.Memory.Length); // .Sum() does only int /// public ulong Length { get; private set; } /// public ulong Count => this.Length / (ulong)this._itemSize; private readonly MemoryPool _pool; private readonly int _segmentSize; private int _lastSegmentLength; private int _segNo; private readonly bool _clear; private readonly List> _segments; private readonly int _itemSize; private bool _isDisposed; /// /// Creates a new buffer with a specified segment size, specified number of initially-allocated segments, and supplied memory pool. /// /// Byte size of an individual segment. Defaults to 64KiB. /// Number of segments to allocate. Defaults to 0. - /// Memory pool to use for renting buffers. Defaults to . + /// Memory pool to use for renting buffers. Defaults to . /// Determines whether the underlying buffers should be cleared on exit. If dealing with sensitive data, it might be a good idea to set this option to true. public MemoryBuffer(int segmentSize = 65536, int initialSegmentCount = 0, MemoryPool memPool = default, bool clearOnDispose = false) { this._itemSize = Unsafe.SizeOf(); if (segmentSize % this._itemSize != 0) throw new ArgumentException("Segment size must match size of individual item."); this._pool = memPool ?? MemoryPool.Shared; this._segmentSize = segmentSize; this._segNo = 0; this._lastSegmentLength = 0; this._clear = clearOnDispose; this._segments = Enumerable.Range(0, initialSegmentCount) .Select(x => this._pool.Rent(this._segmentSize)) .ToList(); this.Length = 0; this._isDisposed = false; } /// public void Write(ReadOnlySpan data) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); var src = MemoryMarshal.AsBytes(data); this.Grow(src.Length); while (this._segNo < this._segments.Count && src.Length > 0) { var seg = this._segments[this._segNo]; var mem = seg.Memory; var avs = mem.Length - this._lastSegmentLength; avs = avs > src.Length ? src.Length : avs; var dmem = mem[this._lastSegmentLength..]; src[..avs].CopyTo(dmem.Span); src = src[avs..]; this.Length += (ulong)avs; this._lastSegmentLength += avs; if (this._lastSegmentLength == mem.Length) { this._segNo++; this._lastSegmentLength = 0; } } } /// public void Write(T[] data, int start, int count) => this.Write(data.AsSpan(start, count)); /// public void Write(ArraySegment data) => this.Write(data.AsSpan()); /// public void Write(Stream stream) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); if (stream.CanSeek) this.WriteStreamSeekable(stream); else this.WriteStreamUnseekable(stream); } /// /// Writes the stream seekable. /// /// The stream. private void WriteStreamSeekable(Stream stream) { var len = (int)(stream.Length - stream.Position); this.Grow(len); var buff = new byte[this._segmentSize]; while (this._segNo < this._segments.Count && len > 0) { var seg = this._segments[this._segNo]; var mem = seg.Memory; var avs = mem.Length - this._lastSegmentLength; avs = avs > len ? len : avs; var dmem = mem[this._lastSegmentLength..]; var lsl = this._lastSegmentLength; var slen = dmem.Span.Length - lsl; stream.Read(buff, 0, slen); buff.AsSpan(0, slen).CopyTo(dmem.Span); len -= dmem.Span.Length; this.Length += (ulong)avs; this._lastSegmentLength += avs; if (this._lastSegmentLength == mem.Length) { this._segNo++; this._lastSegmentLength = 0; } } } /// /// Writes the stream unseekable. /// /// The stream. private void WriteStreamUnseekable(Stream stream) { var read = 0; var buff = new byte[this._segmentSize]; var buffs = buff.AsSpan(); while ((read = stream.Read(buff, 0, buff.Length - this._lastSegmentLength)) != 0) this.Write(MemoryMarshal.Cast(buffs[..read])); } /// public bool Read(Span destination, ulong source, out int itemsWritten) { itemsWritten = 0; if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); source *= (ulong)this._itemSize; if (source > this.Count) throw new ArgumentOutOfRangeException(nameof(source), "Cannot copy data from beyond the buffer."); // Find where to begin var i = 0; for (; i < this._segments.Count; i++) { var seg = this._segments[i]; var mem = seg.Memory; if ((ulong)mem.Length > source) break; source -= (ulong)mem.Length; } // Do actual copy var dl = (int)(this.Length - source); var sri = (int)source; var dst = MemoryMarshal.AsBytes(destination); for (; i < this._segments.Count && dst.Length > 0; i++) { var seg = this._segments[i]; var mem = seg.Memory; var src = mem.Span; if (sri != 0) { src = src[sri..]; sri = 0; } if (itemsWritten + src.Length > dl) src = src[..(dl - itemsWritten)]; if (src.Length > dst.Length) src = src[..dst.Length]; src.CopyTo(dst); dst = dst[src.Length..]; itemsWritten += src.Length; } itemsWritten /= this._itemSize; return this.Length - source != (ulong)itemsWritten; } /// public bool Read(T[] data, int start, int count, ulong source, out int itemsWritten) => this.Read(data.AsSpan(start, count), source, out itemsWritten); /// public bool Read(ArraySegment data, ulong source, out int itemsWritten) => this.Read(data.AsSpan(), source, out itemsWritten); /// public T[] ToArray() { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); var bytes = new T[this.Count]; this.Read(bytes, 0, out _); return bytes; } /// public void CopyTo(Stream destination) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); var longest = this._segments.Max(x => x.Memory.Length); var buff = new byte[longest]; foreach (var seg in this._segments) { var mem = seg.Memory.Span; var spn = buff.AsSpan(0, mem.Length); mem.CopyTo(spn); destination.Write(buff, 0, spn.Length); } } /// public void Clear() { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); this._segNo = 0; this._lastSegmentLength = 0; this.Length = 0; } /// /// Disposes of any resources claimed by this buffer. /// public void Dispose() { if (this._isDisposed) return; this._isDisposed = true; foreach (var segment in this._segments) { if (this._clear) segment.Memory.Span.Clear(); segment.Dispose(); } } /// /// Grows the. /// /// The min amount. private void Grow(int minAmount) { var capacity = this.Capacity; var length = this.Length; var totalAmt = length + (ulong)minAmount; if (capacity >= totalAmt) return; // we're good var amt = (int)(totalAmt - capacity); var segCount = amt / this._segmentSize; if (amt % this._segmentSize != 0) segCount++; // Basically List.EnsureCapacity // Default grow behaviour is minimum current*2 var segCap = this._segments.Count + segCount; if (segCap > this._segments.Capacity) this._segments.Capacity = segCap < this._segments.Capacity * 2 ? this._segments.Capacity * 2 : segCap; for (var i = 0; i < segCount; i++) this._segments.Add(this._pool.Rent(this._segmentSize)); } } diff --git a/DisCatSharp.Common/Types/SecureRandom.cs b/DisCatSharp.Common/Types/SecureRandom.cs index 917747b7c..c7a800252 100644 --- a/DisCatSharp.Common/Types/SecureRandom.cs +++ b/DisCatSharp.Common/Types/SecureRandom.cs @@ -1,331 +1,331 @@ // 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.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.Cryptography; namespace DisCatSharp.Common; /// -/// Provides a cryptographically-secure pseudorandom number generator (CSPRNG) implementation compatible with . +/// Provides a cryptographically-secure pseudorandom number generator (CSPRNG) implementation compatible with . /// public sealed class SecureRandom : Random, IDisposable { /// /// Gets the r n g. /// private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); private volatile bool _isDisposed; /// /// Creates a new instance of . /// public SecureRandom() { } /// /// Finalizes this instance by disposing it. /// ~SecureRandom() { this.Dispose(); } /// /// Fills a supplied buffer with random bytes. /// /// Buffer to fill with random bytes. public void GetBytes(byte[] buffer) => this._rng.GetBytes(buffer); /// /// Fills a supplied buffer with random nonzero bytes. /// /// Buffer to fill with random nonzero bytes. public void GetNonZeroBytes(byte[] buffer) => this._rng.GetNonZeroBytes(buffer); /// /// Fills a supplied memory region with random bytes. /// /// Memory region to fill with random bytes. public void GetBytes(Span buffer) { var buff = ArrayPool.Shared.Rent(buffer.Length); try { var buffSpan = buff.AsSpan(0, buffer.Length); this._rng.GetBytes(buff); buffSpan.CopyTo(buffer); } finally { ArrayPool.Shared.Return(buff); } } /// /// Fills a supplied memory region with random nonzero bytes. /// /// Memory region to fill with random nonzero bytes. public void GetNonZeroBytes(Span buffer) { var buff = ArrayPool.Shared.Rent(buffer.Length); try { var buffSpan = buff.AsSpan(0, buffer.Length); this._rng.GetNonZeroBytes(buff); buffSpan.CopyTo(buffer); } finally { ArrayPool.Shared.Return(buff); } } /// /// Generates a signed 8-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public sbyte GetInt8(sbyte min = 0, sbyte max = sbyte.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); var offset = (sbyte)(min < 0 ? -min : 0); min += offset; max += offset; return (sbyte)(Math.Abs(this.Generate()) % (max - min) + min - offset); } /// /// Generates a unsigned 8-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public byte GetUInt8(byte min = 0, byte max = byte.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); return (byte)(this.Generate() % (max - min) + min); } /// /// Generates a signed 16-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public short GetInt16(short min = 0, short max = short.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); var offset = (short)(min < 0 ? -min : 0); min += offset; max += offset; return (short)(Math.Abs(this.Generate()) % (max - min) + min - offset); } /// /// Generates a unsigned 16-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public ushort GetUInt16(ushort min = 0, ushort max = ushort.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); return (ushort)(this.Generate() % (max - min) + min); } /// /// Generates a signed 32-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public int GetInt32(int min = 0, int max = int.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); var offset = min < 0 ? -min : 0; min += offset; max += offset; return Math.Abs(this.Generate()) % (max - min) + min - offset; } /// /// Generates a unsigned 32-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public uint GetUInt32(uint min = 0, uint max = uint.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); return this.Generate() % (max - min) + min; } /// /// Generates a signed 64-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public long GetInt64(long min = 0, long max = long.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); var offset = min < 0 ? -min : 0; min += offset; max += offset; return Math.Abs(this.Generate()) % (max - min) + min - offset; } /// /// Generates a unsigned 64-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public ulong GetUInt64(ulong min = 0, ulong max = ulong.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); return this.Generate() % (max - min) + min; } /// /// Generates a 32-bit floating-point number between 0.0 and 1.0. /// /// Generated 32-bit floating-point number. public float GetSingle() { var (i1, i2) = ((float)this.GetInt32(), (float)this.GetInt32()); return i1 / i2 % 1.0F; } /// /// Generates a 64-bit floating-point number between 0.0 and 1.0. /// /// Generated 64-bit floating-point number. public double GetDouble() { var (i1, i2) = ((double)this.GetInt64(), (double)this.GetInt64()); return i1 / i2 % 1.0; } /// /// Generates a 32-bit integer between 0 and . Upper end exclusive. /// /// Generated 32-bit integer. public override int Next() => this.GetInt32(); /// /// Generates a 32-bit integer between 0 and . Upper end exclusive. /// /// Maximum value of the generated integer. /// Generated 32-bit integer. public override int Next(int maxValue) => this.GetInt32(0, maxValue); /// /// Generates a 32-bit integer between and . Upper end exclusive. /// /// Minimum value of the generate integer. /// Maximum value of the generated integer. /// Generated 32-bit integer. public override int Next(int minValue, int maxValue) => this.GetInt32(minValue, maxValue); /// /// Generates a 64-bit floating-point number between 0.0 and 1.0. Upper end exclusive. /// /// Generated 64-bit floating-point number. public override double NextDouble() => this.GetDouble(); /// /// Fills specified buffer with random bytes. /// /// Buffer to fill with bytes. public override void NextBytes(byte[] buffer) => this.GetBytes(buffer); /// /// Fills specified memory region with random bytes. /// /// Memory region to fill with bytes. public new void NextBytes(Span buffer) => this.GetBytes(buffer); /// /// Disposes this instance and its resources. /// public void Dispose() { if (this._isDisposed) return; this._isDisposed = true; this._rng.Dispose(); GC.SuppressFinalize(this); } /// /// Generates a random 64-bit floating-point number between 0.0 and 1.0. Upper end exclusive. /// /// Generated 64-bit floating-point number. protected override double Sample() => this.GetDouble(); /// /// Generates the. /// /// A T. private T Generate() where T : struct { var size = Unsafe.SizeOf(); Span buff = stackalloc byte[size]; this.GetBytes(buff); return MemoryMarshal.Read(buff); } } diff --git a/DisCatSharp.Common/Types/Serialization/ComplexDecomposer.cs b/DisCatSharp.Common/Types/Serialization/ComplexDecomposer.cs index dc7f30976..b6c5e5e39 100644 --- a/DisCatSharp.Common/Types/Serialization/ComplexDecomposer.cs +++ b/DisCatSharp.Common/Types/Serialization/ComplexDecomposer.cs @@ -1,114 +1,114 @@ // 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.Numerics; namespace DisCatSharp.Common.Serialization; /// -/// Decomposes numbers into tuples (arrays of 2). +/// Decomposes numbers into tuples (arrays of 2). /// public sealed class ComplexDecomposer : IDecomposer { /// /// Gets the t complex. /// private static Type s_complex { get; } = typeof(Complex); /// /// Gets the t double array. /// private static Type s_doubleArray { get; } = typeof(double[]); /// /// Gets the t double enumerable. /// private static Type s_doubleEnumerable { get; } = typeof(IEnumerable); /// /// Gets the t object array. /// private static Type s_objectArray { get; } = typeof(object[]); /// /// Gets the t object enumerable. /// private static Type s_objectEnumerable { get; } = typeof(IEnumerable); /// public bool CanDecompose(Type t) => t == s_complex; /// public bool CanRecompose(Type t) => t == s_doubleArray || t == s_objectArray || s_doubleEnumerable.IsAssignableFrom(t) || s_objectEnumerable.IsAssignableFrom(t); /// public bool TryDecompose(object obj, Type tobj, out object decomposed, out Type tdecomposed) { decomposed = null; tdecomposed = s_doubleArray; if (tobj != s_complex || obj is not Complex c) return false; decomposed = new[] { c.Real, c.Imaginary }; return true; } /// public bool TryRecompose(object obj, Type tobj, Type trecomposed, out object recomposed) { recomposed = null; if (trecomposed != s_complex) return false; // ie if (s_doubleEnumerable.IsAssignableFrom(tobj) && obj is IEnumerable ied) { if (!ied.TryFirstTwo(out var values)) return false; var (real, imag) = values; recomposed = new Complex(real, imag); return true; } // ie if (s_objectEnumerable.IsAssignableFrom(tobj) && obj is IEnumerable ieo) { if (!ieo.TryFirstTwo(out var values)) return false; var (real, imag) = values; if (real is not double dreal || imag is not double dimag) return false; recomposed = new Complex(dreal, dimag); return true; } return false; } } diff --git a/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs b/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs index 55496f243..d838d23af 100644 --- a/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs +++ b/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs @@ -1,199 +1,199 @@ // 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.Immutable; using System.Threading.Tasks; namespace DisCatSharp.Common.Utilities; /// /// ABC for , allowing for using instances thereof without knowing the underlying instance's type parameters. /// public abstract class AsyncEvent { /// /// Gets the name of this event. /// public string Name { get; } /// /// Prevents a default instance of the class from being created. /// /// The name. private protected AsyncEvent(string name) { this.Name = name; } } /// /// Implementation of asynchronous event. The handlers of such events are executed asynchronously, but sequentially. /// /// Type of the object that dispatches this event. /// Type of event argument object passed to this event's handlers. public sealed class AsyncEvent : AsyncEvent where TArgs : AsyncEventArgs { /// - /// Gets the maximum allotted execution time for all handlers. Any event which causes the handler to time out + /// Gets the maximum allotted execution time for all handlers. Any event which causes the handler to time out /// will raise a non-fatal . /// public TimeSpan MaximumExecutionTime { get; } private readonly object _lock = new(); private ImmutableArray> _handlers; private readonly AsyncEventExceptionHandler _exceptionHandler; /// /// Creates a new asynchronous event with specified name and exception handler. /// /// Name of this event. - /// Maximum handler execution time. A value of means infinite. + /// Maximum handler execution time. A value of means infinite. /// Delegate which handles exceptions caused by this event. public AsyncEvent(string name, TimeSpan maxExecutionTime, AsyncEventExceptionHandler exceptionHandler) : base(name) { this._handlers = ImmutableArray>.Empty; this._exceptionHandler = exceptionHandler; this.MaximumExecutionTime = maxExecutionTime; } /// /// Registers a new handler for this event. /// /// Handler to register for this event. public void Register(AsyncEventHandler handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); lock (this._lock) this._handlers = this._handlers.Add(handler); } /// /// Unregisters an existing handler from this event. /// /// Handler to unregister from the event. public void Unregister(AsyncEventHandler handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); lock (this._lock) this._handlers = this._handlers.Remove(handler); } /// /// Unregisters all existing handlers from this event. /// public void UnregisterAll() => this._handlers = ImmutableArray>.Empty; /// /// Raises this event by invoking all of its registered handlers, in order of registration. /// All exceptions throw during invocation will be handled by the event's registered exception handler. /// /// Object which raised this event. /// Arguments for this event. /// Defines what to do with exceptions caught from handlers. /// public async Task InvokeAsync(TSender sender, TArgs e, AsyncEventExceptionMode exceptionMode = AsyncEventExceptionMode.Default) { var handlers = this._handlers; if (handlers.Length == 0) return; // Collect exceptions List exceptions = null; if ((exceptionMode & AsyncEventExceptionMode.ThrowAll) != 0) exceptions = new List(handlers.Length * 2 /* timeout + regular */); // If we have a timeout configured, start the timeout task var timeout = this.MaximumExecutionTime > TimeSpan.Zero ? Task.Delay(this.MaximumExecutionTime) : null; foreach (var handler in handlers) { try { // Start the handler execution var handlerTask = handler(sender, e); if (handlerTask != null && timeout != null) { // If timeout is configured, wait for any task to finish // If the timeout task finishes first, the handler is causing a timeout var result = await Task.WhenAny(timeout, handlerTask).ConfigureAwait(false); if (result == timeout) { timeout = null; var timeoutEx = new AsyncEventTimeoutException(this, handler); // Notify about the timeout and complete execution if ((exceptionMode & AsyncEventExceptionMode.HandleNonFatal) == AsyncEventExceptionMode.HandleNonFatal) this.HandleException(timeoutEx, handler, sender, e); if ((exceptionMode & AsyncEventExceptionMode.ThrowNonFatal) == AsyncEventExceptionMode.ThrowNonFatal) exceptions.Add(timeoutEx); await handlerTask.ConfigureAwait(false); } } else if (handlerTask != null) { // No timeout is configured, or timeout already expired, proceed as usual await handlerTask.ConfigureAwait(false); } if (e.Handled) break; } catch (Exception ex) { e.Handled = false; if ((exceptionMode & AsyncEventExceptionMode.HandleFatal) == AsyncEventExceptionMode.HandleFatal) this.HandleException(ex, handler, sender, e); if ((exceptionMode & AsyncEventExceptionMode.ThrowFatal) == AsyncEventExceptionMode.ThrowFatal) exceptions.Add(ex); } } if ((exceptionMode & AsyncEventExceptionMode.ThrowAll) != 0 && exceptions.Count > 0) throw new AggregateException("Exceptions were thrown during execution of the event's handlers.", exceptions); } /// /// Handles the exception. /// /// The ex. /// The handler. /// The sender. /// The args. private void HandleException(Exception ex, AsyncEventHandler handler, TSender sender, TArgs args) { if (this._exceptionHandler != null) this._exceptionHandler(this, ex, handler, sender, args); } } diff --git a/DisCatSharp.Common/Utilities/AsyncManualResetEvent.cs b/DisCatSharp.Common/Utilities/AsyncManualResetEvent.cs index 1dd49f83d..129a9fdb1 100644 --- a/DisCatSharp.Common/Utilities/AsyncManualResetEvent.cs +++ b/DisCatSharp.Common/Utilities/AsyncManualResetEvent.cs @@ -1,80 +1,80 @@ // 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.Threading; using System.Threading.Tasks; namespace DisCatSharp.Common.Utilities; /// -/// Represents a thread synchronization event that, when signaled, must be reset manually. Unlike , this event is asynchronous. +/// Represents a thread synchronization event that, when signaled, must be reset manually. Unlike , this event is asynchronous. /// public sealed class AsyncManualResetEvent { /// /// Gets whether this event has been signaled. /// public bool IsSet => this._resetTcs?.Task?.IsCompleted == true; private volatile TaskCompletionSource _resetTcs; /// /// Creates a new asynchronous synchronization event with initial state. /// /// Initial state of this event. public AsyncManualResetEvent(bool initialState) { this._resetTcs = new TaskCompletionSource(); if (initialState) this._resetTcs.TrySetResult(initialState); } // Spawn a threadpool thread instead of making a task // Maybe overkill, but I am less unsure of this than awaits and // potentially cross-scheduler interactions /// /// Asynchronously signal this event. /// /// public Task SetAsync() => Task.Run(() => this._resetTcs.TrySetResult(true)); /// /// Asynchronously wait for this event to be signaled. /// /// public Task WaitAsync() => this._resetTcs.Task; /// /// Reset this event's signal state to unsignaled. /// public void Reset() { while (true) { var tcs = this._resetTcs; if (!tcs.Task.IsCompleted || Interlocked.CompareExchange(ref this._resetTcs, new TaskCompletionSource(), tcs) == tcs) return; } } } diff --git a/DisCatSharp.Common/Utilities/Extensions.cs b/DisCatSharp.Common/Utilities/Extensions.cs index 7777aa83f..ce4dee7a5 100644 --- a/DisCatSharp.Common/Utilities/Extensions.cs +++ b/DisCatSharp.Common/Utilities/Extensions.cs @@ -1,530 +1,530 @@ // 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.Runtime.CompilerServices; namespace DisCatSharp.Common; /// /// Assortment of various extension and utility methods, designed to make working with various types a little easier. /// public static class Extensions { /// - /// Deconstructs a key-value pair item () into 2 separate variables. - /// This allows for enumerating over dictionaries in foreach blocks by using a (k, v) tuple as the enumerator variable, instead of having to use a directly. + /// Deconstructs a key-value pair item () into 2 separate variables. + /// This allows for enumerating over dictionaries in foreach blocks by using a (k, v) tuple as the enumerator variable, instead of having to use a directly. /// /// Type of dictionary item key. /// Type of dictionary item value. /// Key-value pair to deconstruct. /// Deconstructed key. /// Deconstructed value. public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) { key = kvp.Key; value = kvp.Value; } /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this sbyte num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs(num == sbyte.MinValue ? num + 1 : num))) + (num < 0 ? 2 /* include sign */ : 1); /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this byte num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(num)) + 1; /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this short num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs(num == short.MinValue ? num + 1 : num))) + (num < 0 ? 2 /* include sign */ : 1); /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this ushort num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(num)) + 1; /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this int num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs(num == int.MinValue ? num + 1 : num))) + (num < 0 ? 2 /* include sign */ : 1); /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this uint num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(num)) + 1; /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this long num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs(num == long.MinValue ? num + 1 : num))) + (num < 0 ? 2 /* include sign */ : 1); /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this ulong num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(num)) + 1; /// /// Tests whether given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this sbyte num, sbyte min, sbyte max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests whether given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this byte num, byte min, byte max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests whether given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this short num, short min, short max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests whether given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this ushort num, ushort min, ushort max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests whether given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this int num, int min, int max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests whether given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this uint num, uint min, uint max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests whether given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this long num, long min, long max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests whether given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this ulong num, ulong min, ulong max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests whether given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this float num, float min, float max, bool inclusive = true) { if (min > max) return false; return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests whether given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this double num, double min, double max, bool inclusive = true) { if (min > max) return false; return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Returns whether supplied character is in any of the following ranges: a-z, A-Z, 0-9. /// /// Character to test. /// Whether the character is in basic alphanumeric character range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsBasicAlphanumeric(this char c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); /// /// Returns whether supplied character is in the 0-9 range. /// /// Character to test. /// Whether the character is in basic numeric digit character range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsBasicDigit(this char c) => c >= '0' && c <= '9'; /// /// Returns whether supplied character is in the a-z or A-Z range. /// /// Character to test. /// Whether the character is in basic letter character range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsBasicLetter(this char c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); /// /// Tests whether given string ends with given character. /// /// String to test. /// Character to test for. /// Whether the supplied string ends with supplied character. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EndsWithCharacter(this string s, char c) => s.Length >= 1 && s[^1] == c; /// /// Tests whether given string starts with given character. /// /// String to test. /// Character to test for. /// Whether the supplied string starts with supplied character. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool StartsWithCharacter(this string s, char c) => s.Length >= 1 && s[0] == c; // https://stackoverflow.com/questions/9545619/a-fast-hash-function-for-string-in-c-sharp // Calls are inlined to call the underlying method directly /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this ReadOnlySpan chars) => Knuth(chars); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this Span chars) => Knuth(chars); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this ReadOnlyMemory chars) => Knuth(chars.Span); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this Memory chars) => Knuth(chars.Span); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this ArraySegment chars) => Knuth(chars.AsSpan()); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this char[] chars) => Knuth(chars.AsSpan()); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Offset in the array to start calculating from. /// Number of characters to compute the hash from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this char[] chars, int start, int count) => Knuth(chars.AsSpan(start, count)); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this string chars) => Knuth(chars.AsSpan()); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Offset in the array to start calculating from. /// Number of characters to compute the hash from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this string chars, int start, int count) => Knuth(chars.AsSpan(start, count)); /// /// Gets the two first elements of the , if they exist. /// /// The enumerable. /// The output values. Undefined if false is returned. /// Whether the contained enough elements. internal static bool TryFirstTwo(this IEnumerable enumerable, out (T first, T second) values) { values = default; using var enumerator = enumerable.GetEnumerator(); if (!enumerator.MoveNext()) return false; var first = enumerator.Current; if (!enumerator.MoveNext()) return false; values = (first, enumerator.Current); return true; } /// /// Knuths the. /// /// The chars. /// An ulong. private static ulong Knuth(ReadOnlySpan chars) { var hash = 3074457345618258791ul; foreach (var ch in chars) hash = (hash + ch) * 3074457345618258799ul; return hash; } /// /// Removes the first item matching the predicate from the list. /// /// /// /// /// Whether an item was removed. internal static bool RemoveFirst(this IList list, Predicate predicate) { for (var i = 0; i < list.Count; i++) { if (predicate(list[i])) { list.RemoveAt(i); return true; } } return false; } /// /// Populates an array with the given value. /// /// /// /// internal static void Populate(this T[] arr, T value) { for (var i = 0; i < arr.Length; i++) { arr[i] = value; } } } diff --git a/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs b/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs index dd2856620..5cc48ff5a 100644 --- a/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs +++ b/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs @@ -1,320 +1,320 @@ // 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; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Interactivity.Enums; namespace DisCatSharp.Interactivity.EventHandling { /// /// The pagination request. /// internal class PaginationRequest : IPaginationRequest { private TaskCompletionSource _tcs; private readonly CancellationTokenSource _ct; private readonly TimeSpan _timeout; private readonly List _pages; private readonly PaginationBehaviour _behaviour; private readonly DiscordMessage _message; private readonly PaginationEmojis _emojis; private readonly DiscordUser _user; private int _index; /// /// Creates a new Pagination request /// /// Message to paginate /// User to allow control for /// Behaviour during pagination /// Behavior on pagination end /// Emojis for this pagination object /// Timeout time /// Pagination pages internal PaginationRequest(DiscordMessage message, DiscordUser user, PaginationBehaviour behaviour, PaginationDeletion deletion, PaginationEmojis emojis, TimeSpan timeout, IEnumerable pages) { this._tcs = new TaskCompletionSource(); this._ct = new CancellationTokenSource(timeout); this._ct.Token.Register(() => this._tcs.TrySetResult(true)); this._timeout = timeout; this._message = message; this._user = user; this.PaginationDeletion = deletion; this._behaviour = behaviour; this._emojis = emojis; this._pages = new List(); foreach (var p in pages) { this._pages.Add(p); } } /// /// Gets the page count. /// public int PageCount => this._pages.Count; /// /// Gets the pagination deletion. /// public PaginationDeletion PaginationDeletion { get; } /// /// Gets the page async. /// /// A Task. public async Task GetPageAsync() { await Task.Yield(); return this._pages[this._index]; } /// /// Skips the left async. /// /// A Task. public async Task SkipLeftAsync() { await Task.Yield(); this._index = 0; } /// /// Skips the right async. /// /// A Task. public async Task SkipRightAsync() { await Task.Yield(); this._index = this._pages.Count - 1; } /// /// Nexts the page async. /// /// A Task. public async Task NextPageAsync() { await Task.Yield(); switch (this._behaviour) { case PaginationBehaviour.Ignore: if (this._index == this._pages.Count - 1) break; else this._index++; break; case PaginationBehaviour.WrapAround: if (this._index == this._pages.Count - 1) this._index = 0; else this._index++; break; } } /// /// Previous the page async. /// /// A Task. public async Task PreviousPageAsync() { await Task.Yield(); switch (this._behaviour) { case PaginationBehaviour.Ignore: if (this._index == 0) break; else this._index--; break; case PaginationBehaviour.WrapAround: if (this._index == 0) this._index = this._pages.Count - 1; else this._index--; break; } } /// /// Gets the buttons async. /// - /// + /// #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously public async Task> GetButtonsAsync() #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously => throw new NotSupportedException("This request does not support buttons."); /// /// Gets the emojis async. /// /// A Task. public async Task GetEmojisAsync() { await Task.Yield(); return this._emojis; } /// /// Gets the message async. /// /// A Task. public async Task GetMessageAsync() { await Task.Yield(); return this._message; } /// /// Gets the user async. /// /// A Task. public async Task GetUserAsync() { await Task.Yield(); return this._user; } /// /// Dos the cleanup async. /// /// A Task. public async Task DoCleanupAsync() { switch (this.PaginationDeletion) { case PaginationDeletion.DeleteEmojis: await this._message.DeleteAllReactionsAsync().ConfigureAwait(false); break; case PaginationDeletion.DeleteMessage: await this._message.DeleteAsync().ConfigureAwait(false); break; case PaginationDeletion.KeepEmojis: break; } } /// /// Gets the task completion source async. /// /// A Task. public async Task> GetTaskCompletionSourceAsync() { await Task.Yield(); return this._tcs; } ~PaginationRequest() { this.Dispose(); } /// /// Disposes this PaginationRequest. /// public void Dispose() { this._ct.Dispose(); this._tcs = null; } } } namespace DisCatSharp.Interactivity { /// /// The pagination emojis. /// public class PaginationEmojis { public DiscordEmoji SkipLeft; public DiscordEmoji SkipRight; public DiscordEmoji Left; public DiscordEmoji Right; public DiscordEmoji Stop; /// /// Initializes a new instance of the class. /// public PaginationEmojis() { this.Left = DiscordEmoji.FromUnicode("◀"); this.Right = DiscordEmoji.FromUnicode("▶"); this.SkipLeft = DiscordEmoji.FromUnicode("⏮"); this.SkipRight = DiscordEmoji.FromUnicode("⏭"); this.Stop = DiscordEmoji.FromUnicode("⏹"); } } /// /// The page. /// public class Page { /// /// Gets or sets the content. /// public string Content { get; set; } /// /// Gets or sets the embed. /// public DiscordEmbed Embed { get; set; } /// /// Initializes a new instance of the class. /// /// The content. /// The embed. public Page(string content = "", DiscordEmbedBuilder embed = null) { this.Content = content; this.Embed = embed?.Build(); } } } diff --git a/DisCatSharp.Interactivity/Extensions/ChannelExtensions.cs b/DisCatSharp.Interactivity/Extensions/ChannelExtensions.cs index 2f9e96daa..5710cd186 100644 --- a/DisCatSharp.Interactivity/Extensions/ChannelExtensions.cs +++ b/DisCatSharp.Interactivity/Extensions/ChannelExtensions.cs @@ -1,150 +1,150 @@ // This file is part of the DisCatSharp project, 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; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using DisCatSharp.Interactivity.EventHandling; namespace DisCatSharp.Interactivity.Extensions; /// /// Interactivity extension methods for . /// public static class ChannelExtensions { /// /// Waits for the next message sent in this channel that satisfies the predicate. /// /// The channel to monitor. /// A predicate that should return if a message matches. /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the channel. + /// Thrown if interactivity is not enabled for the client associated with the channel. public static Task> GetNextMessageAsync(this DiscordChannel channel, Func predicate, TimeSpan? timeoutOverride = null) => GetInteractivity(channel).WaitForMessageAsync(msg => msg.ChannelId == channel.Id && predicate(msg), timeoutOverride); /// /// Waits for the next message sent in this channel. /// /// The channel to monitor. /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the channel. + /// Thrown if interactivity is not enabled for the client associated with the channel. public static Task> GetNextMessageAsync(this DiscordChannel channel, TimeSpan? timeoutOverride = null) => channel.GetNextMessageAsync(msg => true, timeoutOverride); /// /// Waits for the next message sent in this channel from a specific user. /// /// The channel to monitor. /// The target user. /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the channel. + /// Thrown if interactivity is not enabled for the client associated with the channel. public static Task> GetNextMessageAsync(this DiscordChannel channel, DiscordUser user, TimeSpan? timeoutOverride = null) => channel.GetNextMessageAsync(msg => msg.Author.Id == user.Id, timeoutOverride); /// /// Waits for a specific user to start typing in this channel. /// /// The target channel. /// The target user. /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the channel. + /// Thrown if interactivity is not enabled for the client associated with the channel. public static Task> WaitForUserTypingAsync(this DiscordChannel channel, DiscordUser user, TimeSpan? timeoutOverride = null) => GetInteractivity(channel).WaitForUserTypingAsync(user, channel, timeoutOverride); /// /// Sends a new paginated message. /// /// Target channel. /// The user that will be able to control the pages. /// A collection of to display. /// Pagination emojis. /// Pagination behaviour (when hitting max and min indices). /// Deletion behaviour. /// Override timeout period. - /// Thrown if interactivity is not enabled for the client associated with the channel. + /// Thrown if interactivity is not enabled for the client associated with the channel. public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis, PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutOverride = null) => GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, emojis, behaviour, deletion, timeoutOverride); /// /// Sends a new paginated message with buttons. /// /// Target channel. /// The user that will be able to control the pages. /// A collection of to display. /// Pagination buttons (leave null to default to ones on configuration). /// Pagination behaviour. /// Deletion behaviour /// A custom cancellation token that can be cancelled at any point. - /// Thrown if interactivity is not enabled for the client associated with the channel. + /// Thrown if interactivity is not enabled for the client associated with the channel. public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) => GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, token); /// public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) => channel.SendPaginatedMessageAsync(user, pages, default, behaviour, deletion, token); /// /// Sends a new paginated message with buttons. /// /// Target channel. /// The user that will be able to control the pages. /// A collection of to display. /// Pagination buttons (leave null to default to ones on configuration). /// Pagination behaviour. /// Deletion behaviour. /// Override timeout period. - /// Thrown if interactivity is not enabled for the client associated with the channel. + /// Thrown if interactivity is not enabled for the client associated with the channel. public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutOverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) => GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, buttons, timeoutOverride, behaviour, deletion); /// /// Sends the paginated message async. /// /// The channel. /// The user. /// The pages. /// Override timeout period. /// The behaviour. /// The deletion. /// A Task. public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutOverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) => channel.SendPaginatedMessageAsync(user, pages, default, timeoutOverride, behaviour, deletion); /// /// Retrieves an interactivity instance from a channel instance. /// private static InteractivityExtension GetInteractivity(DiscordChannel channel) { var client = (DiscordClient)channel.Discord; var interactivity = client.GetInteractivity(); return interactivity ?? throw new InvalidOperationException($"Interactivity is not enabled for this {(client.IsShard ? "shard" : "client")}."); } } diff --git a/DisCatSharp.Interactivity/Extensions/ClientExtensions.cs b/DisCatSharp.Interactivity/Extensions/ClientExtensions.cs index 036fe3757..80279d85e 100644 --- a/DisCatSharp.Interactivity/Extensions/ClientExtensions.cs +++ b/DisCatSharp.Interactivity/Extensions/ClientExtensions.cs @@ -1,99 +1,99 @@ // 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.Threading.Tasks; namespace DisCatSharp.Interactivity.Extensions; /// /// Interactivity extension methods for and . /// public static class ClientExtensions { /// /// Enables interactivity for this instance. /// /// The client to enable interactivity for. /// A configuration instance. Default configuration values will be used if none is provided. /// A brand new instance. - /// Thrown if interactivity has already been enabled for the client instance. + /// Thrown if interactivity has already been enabled for the client instance. public static InteractivityExtension UseInteractivity(this DiscordClient client, InteractivityConfiguration configuration = null) { if (client.GetExtension() != null) throw new InvalidOperationException($"Interactivity is already enabled for this {(client.IsShard ? "shard" : "client")}."); configuration ??= new InteractivityConfiguration(); var extension = new InteractivityExtension(configuration); client.AddExtension(extension); return extension; } /// /// Enables interactivity for each shard. /// /// The shard client to enable interactivity for. /// Configuration to use for all shards. If one isn't provided, default configuration values will be used. /// A dictionary containing new instances for each shard. public static async Task> UseInteractivityAsync(this DiscordShardedClient client, InteractivityConfiguration configuration = null) { var extensions = new Dictionary(); await client.InitializeShardsAsync().ConfigureAwait(false); foreach (var shard in client.ShardClients.Select(xkvp => xkvp.Value)) { var extension = shard.GetExtension() ?? shard.UseInteractivity(configuration); extensions.Add(shard.ShardId, extension); } return new ReadOnlyDictionary(extensions); } /// /// Retrieves the registered instance for this client. /// /// The client to retrieve an instance from. /// An existing instance, or if interactivity is not enabled for the instance. public static InteractivityExtension GetInteractivity(this DiscordClient client) => client.GetExtension(); /// /// Retrieves a instance for each shard. /// /// The shard client to retrieve interactivity instances from. /// A dictionary containing instances for each shard. public static async Task> GetInteractivityAsync(this DiscordShardedClient client) { await client.InitializeShardsAsync().ConfigureAwait(false); var extensions = new Dictionary(); foreach (var shard in client.ShardClients.Select(xkvp => xkvp.Value)) { extensions.Add(shard.ShardId, shard.GetExtension()); } return new ReadOnlyDictionary(extensions); } } diff --git a/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs b/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs index 215cb40a3..735170684 100644 --- a/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs +++ b/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs @@ -1,243 +1,243 @@ // This file is part of the DisCatSharp project, 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.Threading; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using DisCatSharp.Interactivity.EventHandling; namespace DisCatSharp.Interactivity.Extensions; /// /// Interactivity extension methods for . /// public static class MessageExtensions { /// /// Waits for the next message that has the same author and channel as this message. /// /// Original message. /// Overrides the timeout set in public static Task> GetNextMessageAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null) => message.Channel.GetNextMessageAsync(message.Author, timeoutOverride); /// /// Waits for the next message with the same author and channel as this message, which also satisfies a predicate. /// /// Original message. /// A predicate that should return if a message matches. /// Overrides the timeout set in public static Task> GetNextMessageAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => message.Channel.GetNextMessageAsync(msg => msg.Author.Id == message.Author.Id && message.ChannelId == msg.ChannelId && predicate(msg), timeoutOverride); /// /// Waits for any button to be pressed on the specified message. /// /// The message to wait on. public static Task> WaitForButtonAsync(this DiscordMessage message) => GetInteractivity(message).WaitForButtonAsync(message); /// /// Waits for any button to be pressed on the specified message. /// /// The message to wait on. /// Overrides the timeout set in public static Task> WaitForButtonAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForButtonAsync(message, timeoutOverride); /// /// Waits for any button to be pressed on the specified message. /// /// The message to wait on. /// A custom cancellation token that can be cancelled at any point. public static Task> WaitForButtonAsync(this DiscordMessage message, CancellationToken token) => GetInteractivity(message).WaitForButtonAsync(message, token); /// /// Waits for a button with the specified Id to be pressed on the specified message. /// /// The message to wait on. /// The Id of the button to wait for. /// Overrides the timeout set in public static Task> WaitForButtonAsync(this DiscordMessage message, string id, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForButtonAsync(message, id, timeoutOverride); /// /// Waits for a button with the specified Id to be pressed on the specified message. /// /// The message to wait on. /// The Id of the button to wait for. /// A custom cancellation token that can be cancelled at any point. public static Task> WaitForButtonAsync(this DiscordMessage message, string id, CancellationToken token) => GetInteractivity(message).WaitForButtonAsync(message, id, token); /// /// Waits for any button to be pressed on the specified message by the specified user. /// /// The message to wait on. /// The user to wait for button input from. /// Overrides the timeout set in public static Task> WaitForButtonAsync(this DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForButtonAsync(message, user, timeoutOverride); /// /// Waits for any button to be pressed on the specified message by the specified user. /// /// The message to wait on. /// The user to wait for button input from. /// A custom cancellation token that can be cancelled at any point. public static Task> WaitForButtonAsync(this DiscordMessage message, DiscordUser user, CancellationToken token) => GetInteractivity(message).WaitForButtonAsync(message, user, token); /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. /// Override the timeout specified in public static Task> WaitForButtonAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForButtonAsync(message, predicate, timeoutOverride); /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. - /// A token to cancel interactivity with at any time. Pass to wait indefinitely. + /// A token to cancel interactivity with at any time. Pass to wait indefinitely. public static Task> WaitForButtonAsync(this DiscordMessage message, Func predicate, CancellationToken token) => GetInteractivity(message).WaitForButtonAsync(message, predicate, token); /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. /// Override the timeout period specified in . - /// Thrown when the message doesn't contain any dropdowns + /// Thrown when the message doesn't contain any dropdowns public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForSelectAsync(message, predicate, timeoutOverride); /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. - /// A token that can be used to cancel interactivity. Pass to wait indefinitely. - /// Thrown when the message doesn't contain any dropdowns + /// A token that can be used to cancel interactivity. Pass to wait indefinitely. + /// Thrown when the message doesn't contain any dropdowns public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, CancellationToken token) => GetInteractivity(message).WaitForSelectAsync(message, predicate, token); /// /// Waits for a dropdown to be interacted with. /// /// The message to wait on. /// The Id of the dropdown to wait for. /// Overrides the timeout set in public static Task> WaitForSelectAsync(this DiscordMessage message, string id, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForSelectAsync(message, id, timeoutOverride); /// /// Waits for a dropdown to be interacted with. /// /// The message to wait on. /// The Id of the dropdown to wait for. /// A custom cancellation token that can be cancelled at any point. public static Task> WaitForSelectAsync(this DiscordMessage message, string id, CancellationToken token) => GetInteractivity(message).WaitForSelectAsync(message, id, token); /// /// Waits for a dropdown to be interacted with by the specified user. /// /// The message to wait on. /// The user to wait for. /// The Id of the dropdown to wait for. /// public static Task> WaitForSelectAsync(this DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForSelectAsync(message, user, id, timeoutOverride); /// /// Waits for a dropdown to be interacted with by the specified user. /// /// The message to wait on. /// The user to wait for. /// The Id of the dropdown to wait for. /// A custom cancellation token that can be cancelled at any point. public static Task> WaitForSelectAsync(this DiscordMessage message, DiscordUser user, string id, CancellationToken token) => GetInteractivity(message).WaitForSelectAsync(message, user, id, token); /// /// Waits for a reaction on this message from a specific user. /// /// Target message. /// The target user. /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the message. + /// Thrown if interactivity is not enabled for the client associated with the message. public static Task> WaitForReactionAsync(this DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForReactionAsync(message, user, timeoutOverride); /// /// Waits for a specific reaction on this message from the specified user. /// /// Target message. /// The target user. /// The target emoji. /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the message. + /// Thrown if interactivity is not enabled for the client associated with the message. public static Task> WaitForReactionAsync(this DiscordMessage message, DiscordUser user, DiscordEmoji emoji, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForReactionAsync(e => e.Emoji == emoji, message, user, timeoutOverride); /// /// Collects all reactions on this message within the timeout duration. /// /// The message to collect reactions from. /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the message. + /// Thrown if interactivity is not enabled for the client associated with the message. public static Task> CollectReactionsAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null) => GetInteractivity(message).CollectReactionsAsync(message, timeoutOverride); /// /// Begins a poll using this message. /// /// Target message. /// Options for this poll. /// Overrides the action set in /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the message. + /// Thrown if interactivity is not enabled for the client associated with the message. public static Task> DoPollAsync(this DiscordMessage message, IEnumerable emojis, PollBehaviour? behaviorOverride = null, TimeSpan? timeoutOverride = null) => GetInteractivity(message).DoPollAsync(message, emojis, behaviorOverride, timeoutOverride); /// /// Retrieves an interactivity instance from a message instance. /// internal static InteractivityExtension GetInteractivity(DiscordMessage message) { var client = (DiscordClient)message.Discord; var interactivity = client.GetInteractivity(); return interactivity ?? throw new InvalidOperationException($"Interactivity is not enabled for this {(client.IsShard ? "shard" : "client")}."); } } diff --git a/DisCatSharp.Interactivity/InteractivityExtension.cs b/DisCatSharp.Interactivity/InteractivityExtension.cs index 42cfda735..c0fa81fed 100644 --- a/DisCatSharp.Interactivity/InteractivityExtension.cs +++ b/DisCatSharp.Interactivity/InteractivityExtension.cs @@ -1,971 +1,971 @@ // 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.Threading; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using DisCatSharp.Interactivity.EventHandling; namespace DisCatSharp.Interactivity; /// /// Extension class for DisCatSharp.Interactivity /// public class InteractivityExtension : BaseExtension { /// /// Gets the config. /// internal InteractivityConfiguration Config { get; } private EventWaiter _messageCreatedWaiter; private EventWaiter _messageReactionAddWaiter; private EventWaiter _typingStartWaiter; private EventWaiter _modalInteractionWaiter; private EventWaiter _componentInteractionWaiter; private ComponentEventWaiter _componentEventWaiter; private ModalEventWaiter _modalEventWaiter; private ReactionCollector _reactionCollector; private Poller _poller; private Paginator _paginator; private ComponentPaginator _compPaginator; /// /// Initializes a new instance of the class. /// /// The configuration. internal InteractivityExtension(InteractivityConfiguration cfg) { this.Config = new InteractivityConfiguration(cfg); } /// /// Setups the Interactivity Extension. /// /// Discord client. protected internal override void Setup(DiscordClient client) { this.Client = client; this._messageCreatedWaiter = new EventWaiter(this.Client); this._messageReactionAddWaiter = new EventWaiter(this.Client); this._componentInteractionWaiter = new EventWaiter(this.Client); this._modalInteractionWaiter = new EventWaiter(this.Client); this._typingStartWaiter = new EventWaiter(this.Client); this._poller = new Poller(this.Client); this._reactionCollector = new ReactionCollector(this.Client); this._paginator = new Paginator(this.Client); this._compPaginator = new ComponentPaginator(this.Client, this.Config); this._componentEventWaiter = new ComponentEventWaiter(this.Client, this.Config); this._modalEventWaiter = new ModalEventWaiter(this.Client, this.Config); } /// /// Makes a poll and returns poll results. /// /// Message to create poll on. /// Emojis to use for this poll. /// What to do when the poll ends. /// Override timeout period. /// public async Task> DoPollAsync(DiscordMessage m, IEnumerable emojis, PollBehaviour? behaviour = default, TimeSpan? timeout = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); if (!emojis.Any()) throw new ArgumentException("You need to provide at least one emoji for a poll!"); foreach (var em in emojis) await m.CreateReactionAsync(em).ConfigureAwait(false); var res = await this._poller.DoPollAsync(new PollRequest(m, timeout ?? this.Config.Timeout, emojis)).ConfigureAwait(false); var pollBehaviour = behaviour ?? this.Config.PollBehaviour; var thisMember = await m.Channel.Guild.GetMemberAsync(this.Client.CurrentUser.Id).ConfigureAwait(false); if (pollBehaviour == PollBehaviour.DeleteEmojis && m.Channel.PermissionsFor(thisMember).HasPermission(Permissions.ManageMessages)) await m.DeleteAllReactionsAsync().ConfigureAwait(false); return new ReadOnlyCollection(res.ToList()); } /// /// Waits for any button in the specified collection to be pressed. /// /// The message to wait on. /// A collection of buttons to listen for. /// Override the timeout period in . /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, buttons, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button in the specified collection to be pressed. /// /// The message to wait on. /// A collection of buttons to listen for. /// A custom cancellation token that can be cancelled at any point. /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!buttons.Any()) throw new ArgumentException("You must specify at least one button to listen for."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Provided Message does not contain any button components."); var res = await this._componentEventWaiter .WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && buttons.Any(b => b.CustomId == c.Id), token)).ConfigureAwait(false); return new InteractivityResult(res is null, res); } /// /// Waits for a user modal submit. /// /// The custom id of the modal to wait for. /// Override the timeout period specified in . /// A with the result of the modal. public Task> WaitForModalAsync(string customId, TimeSpan? timeoutOverride = null) => this.WaitForModalAsync(customId, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a user modal submit. /// /// The custom id of the modal to wait for. /// A custom cancellation token that can be cancelled at any point. /// A with the result of the modal. public async Task> WaitForModalAsync(string customId, CancellationToken token) { var result = await this ._modalEventWaiter .WaitForModalMatchAsync(new ModalMatchRequest(customId, c => c.Interaction.Type == InteractionType.ModalSubmit, token)) .ConfigureAwait(false); return new InteractivityResult(result is null, result); } /// /// Waits for any button on the specified message to be pressed. /// /// The message to wait for the button on. /// Override the timeout period specified in . /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button on the specified message to be pressed. /// /// The message to wait for the button on. /// A custom cancellation token that can be cancelled at any point. /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var ids = message.Components.SelectMany(m => m.Components).Select(c => c.CustomId); var result = await this ._componentEventWaiter .WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && ids.Contains(c.Id), token)) .ConfigureAwait(false); return new InteractivityResult(result is null, result); } /// /// Waits for any button on the specified message to be pressed by the specified user. /// /// The message to wait for the button on. /// The user to wait for the button press from. /// Override the timeout period specified in . /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, user, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button on the specified message to be pressed by the specified user. /// /// The message to wait for the button on. /// The user to wait for the button press from. /// A custom cancellation token that can be cancelled at any point. /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var result = await this ._componentEventWaiter .WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.User == user, token)) .ConfigureAwait(false); return new InteractivityResult(result is null, result); } /// /// Waits for a button with the specified Id to be pressed. /// /// The message to wait for the button on. /// The Id of the button to wait for. /// Override the timeout period specified in . /// A with the result of the operation. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, id, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a button with the specified Id to be pressed. /// /// The message to wait for the button on. /// The Id of the button to wait for. /// Override the timeout period specified in . /// A with the result of the operation. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); if (!message.Components.SelectMany(c => c.Components).OfType().Any(c => c.CustomId == id)) throw new ArgumentException($"Message does not contain button with Id of '{id}'."); var result = await this ._componentEventWaiter .WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.Id == id, token)) .ConfigureAwait(false); return new InteractivityResult(result is null, result); } /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. /// Override the timeout specified in public Task> WaitForButtonAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, predicate, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. - /// A token to cancel interactivity with at any time. Pass to wait indefinitely. + /// A token to cancel interactivity with at any time. Pass to wait indefinitely. public async Task> WaitForButtonAsync(DiscordMessage message, Func predicate, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var result = await this ._componentEventWaiter .WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType is ComponentType.Button && predicate(c), token)) .ConfigureAwait(false); return new InteractivityResult(result is null, result); } /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. /// Override the timeout period specified in . - /// Thrown when the Provided message does not contain any dropdowns + /// Thrown when the Provided message does not contain any dropdowns public Task> WaitForSelectAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, predicate, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. - /// A token that can be used to cancel interactivity. Pass to wait indefinitely. - /// Thrown when the Provided message does not contain any dropdowns + /// A token that can be used to cancel interactivity. Pass to wait indefinitely. + /// Thrown when the Provided message does not contain any dropdowns public async Task> WaitForSelectAsync(DiscordMessage message, Func predicate, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); var result = await this ._componentEventWaiter .WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType is ComponentType.Select && predicate(c), token)) .ConfigureAwait(false); return new InteractivityResult(result is null, result); } /// /// Waits for a dropdown to be interacted with. /// /// This is here for backwards-compatibility and will internally create a cancellation token. /// The message to wait on. /// The Id of the dropdown to wait on. /// Override the timeout period specified in . - /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. + /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public Task> WaitForSelectAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, id, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a dropdown to be interacted with. /// /// The message to wait on. /// The Id of the dropdown to wait on. /// A custom cancellation token that can be cancelled at any point. - /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. + /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public async Task> WaitForSelectAsync(DiscordMessage message, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id)) throw new ArgumentException($"Message does not contain select component with Id of '{id}'."); var result = await this ._componentEventWaiter .WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Select && c.Id == id, token)) .ConfigureAwait(false); return new InteractivityResult(result is null, result); } /// /// Waits for a dropdown to be interacted with by a specific user. /// /// The message to wait on. /// The user to wait on. /// The Id of the dropdown to wait on. /// Override the timeout period specified in . - /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. + /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, user, id, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a dropdown to be interacted with by a specific user. /// /// The message to wait on. /// The user to wait on. /// The Id of the dropdown to wait on. /// A custom cancellation token that can be cancelled at any point. - /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. + /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public async Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id)) throw new ArgumentException($"Message does not contain select with Id of '{id}'."); var result = await this ._componentEventWaiter .WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Id == id && c.User == user, token)).ConfigureAwait(false); return new InteractivityResult(result is null, result); } /// /// Waits for a specific message. /// /// Predicate to match. /// Override timeout period. public async Task> WaitForMessageAsync(Func predicate, TimeSpan? timeoutOverride = null) { if (!Utilities.HasMessageIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No message intents are enabled."); var timeout = timeoutOverride ?? this.Config.Timeout; var returns = await this._messageCreatedWaiter.WaitForMatchAsync(new MatchRequest(x => predicate(x.Message), timeout)).ConfigureAwait(false); return new InteractivityResult(returns == null, returns?.Message); } /// /// Wait for a specific reaction. /// /// Predicate to match. /// Override timeout period. public async Task> WaitForReactionAsync(Func predicate, TimeSpan? timeoutOverride = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); var timeout = timeoutOverride ?? this.Config.Timeout; var returns = await this._messageReactionAddWaiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Wait for a specific reaction. /// For this Event you need the intent specified in /// /// Message reaction was added to. /// User that made the reaction. /// Override timeout period. public async Task> WaitForReactionAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) => await this.WaitForReactionAsync(x => x.User.Id == user.Id && x.Message.Id == message.Id, timeoutOverride).ConfigureAwait(false); /// /// Waits for a specific reaction. /// For this Event you need the intent specified in /// /// Predicate to match. /// Message reaction was added to. /// User that made the reaction. /// Override timeout period. public async Task> WaitForReactionAsync(Func predicate, DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) => await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id && x.Message.Id == message.Id, timeoutOverride).ConfigureAwait(false); /// /// Waits for a specific reaction. /// For this Event you need the intent specified in /// /// predicate to match. /// User that made the reaction. /// Override timeout period. public async Task> WaitForReactionAsync(Func predicate, DiscordUser user, TimeSpan? timeoutOverride = null) => await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id, timeoutOverride).ConfigureAwait(false); /// /// Waits for a user to start typing. /// /// User that starts typing. /// Channel the user is typing in. /// Override timeout period. public async Task> WaitForUserTypingAsync(DiscordUser user, DiscordChannel channel, TimeSpan? timeoutOverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutOverride ?? this.Config.Timeout; var returns = await this._typingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.User.Id == user.Id && x.Channel.Id == channel.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Waits for a user to start typing. /// /// User that starts typing. /// Override timeout period. public async Task> WaitForUserTypingAsync(DiscordUser user, TimeSpan? timeoutOverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutOverride ?? this.Config.Timeout; var returns = await this._typingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.User.Id == user.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Waits for any user to start typing. /// /// Channel to type in. /// Override timeout period. public async Task> WaitForTypingAsync(DiscordChannel channel, TimeSpan? timeoutOverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutOverride ?? this.Config.Timeout; var returns = await this._typingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.Channel.Id == channel.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Collects reactions on a specific message. /// /// Message to collect reactions on. /// Override timeout period. public async Task> CollectReactionsAsync(DiscordMessage m, TimeSpan? timeoutOverride = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); var timeout = timeoutOverride ?? this.Config.Timeout; var collection = await this._reactionCollector.CollectAsync(new ReactionCollectRequest(m, timeout)).ConfigureAwait(false); return collection; } /// /// Waits for specific event args to be received. Make sure the appropriate are registered, if needed. /// /// /// The predicate. /// Override timeout period. public async Task> WaitForEventArgsAsync(Func predicate, TimeSpan? timeoutOverride = null) where T : AsyncEventArgs { var timeout = timeoutOverride ?? this.Config.Timeout; using var waiter = new EventWaiter(this.Client); var res = await waiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false); return new InteractivityResult(res == null, res); } /// /// Collects the event arguments. /// /// The predicate. /// Override timeout period. public async Task> CollectEventArgsAsync(Func predicate, TimeSpan? timeoutOverride = null) where T : AsyncEventArgs { var timeout = timeoutOverride ?? this.Config.Timeout; using var waiter = new EventWaiter(this.Client); var res = await waiter.CollectMatchesAsync(new CollectRequest(predicate, timeout)).ConfigureAwait(false); return res; } /// /// Sends a paginated message with buttons. /// /// The channel to send it on. /// User to give control. /// The pages. /// Pagination buttons (pass null to use buttons defined in ). /// Pagination behaviour. /// Deletion behaviour. /// A custom cancellation token that can be cancelled at any point. public async Task SendPaginatedMessageAsync( DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) { var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.ButtonBehavior; var bts = buttons ?? this.Config.PaginationButtons; bts = new PaginationButtons(bts); if (bhv is PaginationBehaviour.Ignore) { bts.SkipLeft.Disable(); bts.Left.Disable(); } var builder = new DiscordMessageBuilder() .WithContent(pages.First().Content) .WithEmbed(pages.First().Embed) .AddComponents(bts.ButtonArray); var message = await builder.SendAsync(channel).ConfigureAwait(false); var req = new ButtonPaginationRequest(message, user, bhv, del, bts, pages, token == default ? this.GetCancellationToken() : token); await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false); } /// /// Sends a paginated message with buttons. /// /// The channel to send it on. /// User to give control. /// The pages. /// Pagination buttons (pass null to use buttons defined in ). /// Pagination behaviour. /// Deletion behaviour. /// Override timeout period. public Task SendPaginatedMessageAsync( DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutOverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) => this.SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, this.GetCancellationToken(timeoutOverride)); /// /// Sends the paginated message. /// /// The channel. /// The user. /// The pages. /// The behaviour. /// The deletion. /// The token. /// A Task. public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) => this.SendPaginatedMessageAsync(channel, user, pages, default, behaviour, deletion, token); /// /// Sends the paginated message. /// /// The channel. /// The user. /// The pages. /// Override timeout period. /// The behaviour. /// The deletion. /// A Task. public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutOverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) => this.SendPaginatedMessageAsync(channel, user, pages, timeoutOverride, behaviour, deletion); /// /// Sends a paginated message. /// For this Event you need the intent specified in /// /// Channel to send paginated message in. /// User to give control. /// Pages. /// Pagination emojis. /// Pagination behaviour (when hitting max and min indices). /// Deletion behaviour. /// Override timeout period. public async Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis, PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutOverride = null) { var builder = new DiscordMessageBuilder() .WithContent(pages.First().Content) .WithEmbed(pages.First().Embed); var m = await builder.SendAsync(channel).ConfigureAwait(false); var timeout = timeoutOverride ?? this.Config.Timeout; var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.PaginationDeletion; var ems = emojis ?? this.Config.PaginationEmojis; var pRequest = new PaginationRequest(m, user, bhv, del, ems, timeout, pages); await this._paginator.DoPaginationAsync(pRequest).ConfigureAwait(false); } /// /// Sends a paginated message in response to an interaction. /// /// Pass the interaction directly. Interactivity will ACK it. /// /// /// The interaction to create a response to. /// Whether the interaction was deferred. /// Whether the response should be ephemeral. /// The user to listen for button presses from. /// The pages to paginate. /// Optional: custom buttons. /// Pagination behaviour. /// Deletion behaviour. /// A custom cancellation token that can be cancelled at any point. public async Task SendPaginatedResponseAsync(DiscordInteraction interaction, bool deferred, bool ephemeral, DiscordUser user, IEnumerable pages, PaginationButtons buttons = null, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) { var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.ButtonBehavior; var bts = buttons ?? this.Config.PaginationButtons; bts = new PaginationButtons(bts); if (bhv is PaginationBehaviour.Ignore) { bts.SkipLeft.Disable(); bts.Left.Disable(); } DiscordMessage message; if (deferred) { var builder = new DiscordWebhookBuilder() .WithContent(pages.First().Content) .AddEmbed(pages.First().Embed) .AddComponents(bts.ButtonArray); message = await interaction.EditOriginalResponseAsync(builder).ConfigureAwait(false); } else { var builder = new DiscordInteractionResponseBuilder() .WithContent(pages.First().Content) .AddEmbed(pages.First().Embed) .AsEphemeral(ephemeral) .AddComponents(bts.ButtonArray); await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder).ConfigureAwait(false); message = await interaction.GetOriginalResponseAsync().ConfigureAwait(false); } var req = new InteractionPaginationRequest(interaction, message, user, bhv, del, bts, pages, token); await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false); } /// /// Waits for a custom pagination request to finish. /// This does NOT handle removing emojis after finishing for you. /// /// /// public async Task WaitForCustomPaginationAsync(IPaginationRequest request) => await this._paginator.DoPaginationAsync(request).ConfigureAwait(false); /// /// Waits for custom button-based pagination request to finish. ///
/// This does not invoke . ///
/// The request to wait for. public async Task WaitForCustomComponentPaginationAsync(IPaginationRequest request) => await this._compPaginator.DoPaginationAsync(request).ConfigureAwait(false); /// /// Generates pages from a string, and puts them in message content. /// /// Input string. /// How to split input string. /// public IEnumerable GeneratePagesInContent(string input, SplitType splitType = SplitType.Character) { if (string.IsNullOrEmpty(input)) throw new ArgumentException("You must provide a string that is not null or empty!"); var result = new List(); List split; switch (splitType) { default: case SplitType.Character: split = this.SplitString(input, 500).ToList(); break; case SplitType.Line: var subsplit = input.Split('\n'); split = new List(); var s = ""; for (var i = 0; i < subsplit.Length; i++) { s += subsplit[i]; if (i >= 15 && i % 15 == 0) { split.Add(s); s = ""; } } if (split.All(x => x != s)) split.Add(s); break; } var page = 1; foreach (var s in split) { result.Add(new Page($"Page {page}:\n{s}")); page++; } return result; } /// /// Generates pages from a string, and puts them in message embeds. /// /// Input string. /// How to split input string. /// Base embed for output embeds. /// public IEnumerable GeneratePagesInEmbed(string input, SplitType splitType = SplitType.Character, DiscordEmbedBuilder embedBase = null) { if (string.IsNullOrEmpty(input)) throw new ArgumentException("You must provide a string that is not null or empty!"); var embed = embedBase ?? new DiscordEmbedBuilder(); var result = new List(); List split; switch (splitType) { default: case SplitType.Character: split = this.SplitString(input, 500).ToList(); break; case SplitType.Line: var subsplit = input.Split('\n'); split = new List(); var s = ""; for (var i = 0; i < subsplit.Length; i++) { s += $"{subsplit[i]}\n"; if (i % 15 == 0 && i != 0) { split.Add(s); s = ""; } } if (!split.Any(x => x == s)) split.Add(s); break; } var page = 1; foreach (var s in split) { result.Add(new Page("", new DiscordEmbedBuilder(embed).WithDescription(s).WithFooter($"Page {page}/{split.Count}"))); page++; } return result; } /// /// Splits the string. /// /// The string. /// The chunk size. private List SplitString(string str, int chunkSize) { var res = new List(); var len = str.Length; var i = 0; while (i < len) { var size = Math.Min(len - i, chunkSize); res.Add(str.Substring(i, size)); i += size; } return res; } /// /// Gets the cancellation token. /// /// The timeout. private CancellationToken GetCancellationToken(TimeSpan? timeout = null) => new CancellationTokenSource(timeout ?? this.Config.Timeout).Token; /// /// Handles an invalid interaction. /// /// The interaction. private async Task HandleInvalidInteraction(DiscordInteraction interaction) { var at = this.Config.ResponseBehavior switch { InteractionResponseBehavior.Ack => interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate), InteractionResponseBehavior.Respond => interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder { Content = this.Config.ResponseMessage, IsEphemeral = true}), InteractionResponseBehavior.Ignore => Task.CompletedTask, _ => throw new ArgumentException("Unknown enum value.") }; await at; } } diff --git a/DisCatSharp.Lavalink/LavalinkExtension.cs b/DisCatSharp.Lavalink/LavalinkExtension.cs index 9aa3231dd..7f2159754 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]; var con = new LavalinkNodeConnection(this.Client, this, config); con.NodeDisconnected += this.Con_NodeDisconnected; con.Disconnected += this.Con_Disconnected; this._connectedNodes[con.NodeEndpoint] = con; try { await con.StartAsync().ConfigureAwait(false); } catch { this.Con_NodeDisconnected(con); throw; } return con; } /// /// Gets the Lavalink node connection for the specified endpoint. /// /// Endpoint at which the node resides. /// Lavalink node connection. public LavalinkNodeConnection GetNodeConnection(ConnectionEndpoint endpoint) => this._connectedNodes.ContainsKey(endpoint) ? this._connectedNodes[endpoint] : null; /// /// Gets a Lavalink node connection based on load balancing and an optional voice region. /// /// The region to compare with the node's , if any. /// The least load affected node connection, or null if no nodes are present. public LavalinkNodeConnection GetIdealNodeConnection(DiscordVoiceRegion region = null) { if (this._connectedNodes.Count <= 1) return this._connectedNodes.Values.FirstOrDefault(); var nodes = this._connectedNodes.Values.ToArray(); if (region != null) { var regionPredicate = new Func(x => x.Region == region); if (nodes.Any(regionPredicate)) nodes = nodes.Where(regionPredicate).ToArray(); if (nodes.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.VoiceNext/StreamExtensions.cs b/DisCatSharp.VoiceNext/StreamExtensions.cs index 7f7357bc2..7c0c62b11 100644 --- a/DisCatSharp.VoiceNext/StreamExtensions.cs +++ b/DisCatSharp.VoiceNext/StreamExtensions.cs @@ -1,71 +1,71 @@ // 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.Buffers; using System.IO; using System.Threading; using System.Threading.Tasks; namespace DisCatSharp.VoiceNext; /// /// The stream extensions. /// public static class StreamExtensions { /// /// Asynchronously reads the bytes from the current stream and writes them to the specified . /// - /// The source + /// The source /// The target /// The size, in bytes, of the buffer. This value must be greater than zero. If , defaults to the packet size specified by . /// The token to monitor for cancellation requests. /// public static async Task CopyToAsync(this Stream source, VoiceTransmitSink destination, int? bufferSize = null, CancellationToken cancellationToken = default) { // adapted from CoreFX // https://source.dot.net/#System.Private.CoreLib/Stream.cs,8048a9680abdd13b if (source is null) throw new ArgumentNullException(nameof(source)); if (destination is null) throw new ArgumentNullException(nameof(destination)); if (bufferSize != null && bufferSize <= 0) throw new ArgumentOutOfRangeException(nameof(bufferSize), bufferSize, "bufferSize cannot be less than or equal to zero"); var bufferLength = bufferSize ?? destination.SampleLength; var buffer = ArrayPool.Shared.Rent(bufferLength); try { int bytesRead; while ((bytesRead = await source.ReadAsync(buffer.AsMemory(0, bufferLength), cancellationToken).ConfigureAwait(false)) != 0) { await destination.WriteAsync(new ReadOnlyMemory(buffer, 0, bytesRead), cancellationToken).ConfigureAwait(false); } } finally { ArrayPool.Shared.Return(buffer); } } } diff --git a/DisCatSharp.VoiceNext/VoiceNextExtension.cs b/DisCatSharp.VoiceNext/VoiceNextExtension.cs index fc7f47b3f..3ab318245 100644 --- a/DisCatSharp.VoiceNext/VoiceNextExtension.cs +++ b/DisCatSharp.VoiceNext/VoiceNextExtension.cs @@ -1,265 +1,265 @@ // 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; /// /// Vnc_S the voice disconnected. /// /// The guild. /// A Task. private async Task Vnc_VoiceDisconnected(DiscordGuild guild) { VoiceNextConnection vnc = null; if (this._activeConnections.ContainsKey(guild.Id)) this._activeConnections.TryRemove(guild.Id, out vnc); var vsd = new VoiceDispatch { OpCode = 4, Payload = new VoiceStateUpdatePayload { GuildId = guild.Id, ChannelId = null } }; var vsj = JsonConvert.SerializeObject(vsd, Formatting.None); await (guild.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false); } /// /// Client_S the voice state update. /// /// The client. /// The e. /// A Task. private Task Client_VoiceStateUpdate(DiscordClient client, VoiceStateUpdateEventArgs e) { var gld = e.Guild; if (gld == null) return Task.CompletedTask; if (e.User == null) return Task.CompletedTask; if (e.User.Id == this.Client.CurrentUser.Id) { if (e.After.Channel == null && this._activeConnections.TryRemove(gld.Id, out var ac)) ac.Disconnect(); if (this._activeConnections.TryGetValue(e.Guild.Id, out var vnc)) vnc.TargetChannel = e.Channel; if (!string.IsNullOrWhiteSpace(e.SessionId) && e.Channel != null && this._voiceStateUpdates.TryRemove(gld.Id, out var xe)) xe.SetResult(e); } return Task.CompletedTask; } /// /// Client_S the voice server update. /// /// The client. /// The e. /// A Task. private async Task Client_VoiceServerUpdate(DiscordClient client, VoiceServerUpdateEventArgs e) { var gld = e.Guild; if (gld == null) return; if (this._activeConnections.TryGetValue(e.Guild.Id, out var vnc)) { vnc.ServerData = new VoiceServerUpdatePayload { Endpoint = e.Endpoint, GuildId = e.Guild.Id, Token = e.VoiceToken }; var eps = e.Endpoint; var epi = eps.LastIndexOf(':'); var eph = string.Empty; var epp = 443; if (epi != -1) { eph = eps[..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/Clients/BaseDiscordClient.cs b/DisCatSharp/Clients/BaseDiscordClient.cs index d223c1b6e..77a35bc46 100644 --- a/DisCatSharp/Clients/BaseDiscordClient.cs +++ b/DisCatSharp/Clients/BaseDiscordClient.cs @@ -1,321 +1,321 @@ // 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. #pragma warning disable CS0618 using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Net.Http; using System.Reflection; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Net; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace DisCatSharp; /// /// Represents a common base for various Discord Client implementations. /// public abstract class BaseDiscordClient : IDisposable { /// /// Gets the api client. /// internal protected DiscordApiClient ApiClient { get; } /// /// Gets the configuration. /// internal protected DiscordConfiguration Configuration { get; } /// /// Gets the instance of the logger for this client. /// public ILogger Logger { get; internal set; } /// /// Gets the string representing the version of bot lib. /// public string VersionString { get; } /// /// Gets the bot library name. /// public string BotLibrary { get; } [Obsolete("Use GetLibraryDeveloperTeamAsync")] public DisCatSharpTeam LibraryDeveloperTeamAsync => this.GetLibraryDevelopmentTeamAsync().Result; /// /// Gets the current user. /// public DiscordUser CurrentUser { get; internal set; } /// /// Gets the current application. /// public DiscordApplication CurrentApplication { get; internal set; } /// - /// Exposes a Http Client for custom operations. + /// Exposes a Http Client for custom operations. /// public HttpClient RestClient { get; internal set; } /// /// Gets the cached guilds for this client. /// public abstract IReadOnlyDictionary Guilds { get; } /// /// Gets the cached users for this client. /// public ConcurrentDictionary UserCache { get; internal set; } /// /// Gets the service provider. /// This allows passing data around without resorting to static members. /// Defaults to null. /// internal IServiceProvider ServiceProvider { get; set; } /// /// Gets the list of available voice regions. Note that this property will not contain VIP voice regions. /// public IReadOnlyDictionary VoiceRegions => this.VoiceRegionsLazy.Value; /// /// Gets the list of available voice regions. This property is meant as a way to modify . /// protected internal ConcurrentDictionary InternalVoiceRegions { get; set; } internal Lazy> VoiceRegionsLazy; /// /// Initializes this Discord API client. /// /// Configuration for this client. protected BaseDiscordClient(DiscordConfiguration config) { this.Configuration = new DiscordConfiguration(config); this.ServiceProvider = config.ServiceProvider; if (this.ServiceProvider != null) { this.Configuration.LoggerFactory ??= config.ServiceProvider.GetService(); this.Logger = config.ServiceProvider.GetService>(); } if (this.Configuration.LoggerFactory == null) { this.Configuration.LoggerFactory = new DefaultLoggerFactory(); this.Configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this)); } this.Logger ??= this.Configuration.LoggerFactory.CreateLogger(); this.ApiClient = new DiscordApiClient(this); this.UserCache = new ConcurrentDictionary(); this.InternalVoiceRegions = new ConcurrentDictionary(); this.VoiceRegionsLazy = new Lazy>(() => new ReadOnlyDictionary(this.InternalVoiceRegions)); this.RestClient = new(); this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent()); this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("X-Discord-Locale", this.Configuration.Locale); var a = typeof(DiscordClient).GetTypeInfo().Assembly; var iv = a.GetCustomAttribute(); if (iv != null) { this.VersionString = iv.InformationalVersion; } else { var v = a.GetName().Version; var vs = v.ToString(3); if (v.Revision > 0) this.VersionString = $"{vs}, CI build {v.Revision}"; } this.BotLibrary = "DisCatSharp"; } /// /// Gets the current API application. /// public async Task GetCurrentApplicationAsync() { var tapp = await this.ApiClient.GetCurrentApplicationInfoAsync().ConfigureAwait(false); var app = new DiscordApplication { Discord = this, Id = tapp.Id, Name = tapp.Name, Description = tapp.Description, Summary = tapp.Summary, IconHash = tapp.IconHash, RpcOrigins = tapp.RpcOrigins != null ? new ReadOnlyCollection(tapp.RpcOrigins) : null, Flags = tapp.Flags, RequiresCodeGrant = tapp.BotRequiresCodeGrant, IsPublic = tapp.IsPublicBot, IsHook = tapp.IsHook, Type = tapp.Type, PrivacyPolicyUrl = tapp.PrivacyPolicyUrl, TermsOfServiceUrl = tapp.TermsOfServiceUrl, CustomInstallUrl = tapp.CustomInstallUrl, InstallParams = tapp.InstallParams, Tags = (tapp.Tags ?? Enumerable.Empty()).ToArray() }; if (tapp.Team == null) { app.Owners = new ReadOnlyCollection(new[] { new DiscordUser(tapp.Owner) }); app.Team = null; app.TeamName = null; } else { app.Team = new DiscordTeam(tapp.Team); var members = tapp.Team.Members .Select(x => new DiscordTeamMember(x) { TeamId = app.Team.Id, TeamName = app.Team.Name, User = new DiscordUser(x.User) }) .ToArray(); var owners = members .Where(x => x.MembershipStatus == DiscordTeamMembershipStatus.Accepted) .Select(x => x.User) .ToArray(); app.Owners = new ReadOnlyCollection(owners); app.Team.Owner = owners.FirstOrDefault(x => x.Id == tapp.Team.OwnerId); app.Team.Members = new ReadOnlyCollection(members); app.TeamName = app.Team.Name; } app.GuildId = tapp.GuildId.ValueOrDefault(); app.Slug = tapp.Slug.ValueOrDefault(); app.PrimarySkuId = tapp.PrimarySkuId.ValueOrDefault(); app.VerifyKey = tapp.VerifyKey.ValueOrDefault(); app.CoverImageHash = tapp.CoverImageHash.ValueOrDefault(); return app; } /// /// Gets a list of voice regions. /// - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public Task> ListVoiceRegionsAsync() => this.ApiClient.ListVoiceRegionsAsync(); /// /// Initializes this client. This method fetches information about current user, application, and voice regions. /// public virtual async Task InitializeAsync() { if (this.CurrentUser == null) { this.CurrentUser = await this.ApiClient.GetCurrentUserAsync().ConfigureAwait(false); this.UserCache.AddOrUpdate(this.CurrentUser.Id, this.CurrentUser, (id, xu) => this.CurrentUser); } if (this.Configuration.TokenType == TokenType.Bot && this.CurrentApplication == null) this.CurrentApplication = await this.GetCurrentApplicationAsync().ConfigureAwait(false); if (this.Configuration.TokenType != TokenType.Bearer && this.InternalVoiceRegions.IsEmpty) { var vrs = await this.ListVoiceRegionsAsync().ConfigureAwait(false); foreach (var xvr in vrs) this.InternalVoiceRegions.TryAdd(xvr.Id, xvr); } } /// /// Gets the current gateway info for the provided token. /// If no value is provided, the configuration value will be used instead. /// /// A gateway info object. public async Task GetGatewayInfoAsync(string token = null) { if (this.Configuration.TokenType != TokenType.Bot) throw new InvalidOperationException("Only bot tokens can access this info."); if (string.IsNullOrEmpty(this.Configuration.Token)) { if (string.IsNullOrEmpty(token)) throw new InvalidOperationException("Could not locate a valid token."); this.Configuration.Token = token; var res = await this.ApiClient.GetGatewayInfoAsync().ConfigureAwait(false); this.Configuration.Token = null; return res; } return await this.ApiClient.GetGatewayInfoAsync().ConfigureAwait(false); } /// /// Gets some information about the development team behind DisCatSharp. /// Can be used for crediting etc. /// Note: This call contacts servers managed by the DCS team, no information is collected. /// The team, or null with errors being logged on failure. /// public async Task GetLibraryDevelopmentTeamAsync() => await DisCatSharpTeam.Get(this.RestClient, this.Logger, this.ApiClient).ConfigureAwait(false); /// /// Gets a cached user. /// /// The user id. internal DiscordUser GetCachedOrEmptyUserInternal(ulong userId) { this.TryGetCachedUserInternal(userId, out var user); return user; } /// /// Tries the get a cached user. /// /// The user id. /// The user. internal bool TryGetCachedUserInternal(ulong userId, out DiscordUser user) { if (this.UserCache.TryGetValue(userId, out user)) return true; user = new DiscordUser { Id = userId, Discord = this }; return false; } /// /// Disposes this client. /// public abstract void Dispose(); } diff --git a/DisCatSharp/Clients/DiscordClient.cs b/DisCatSharp/Clients/DiscordClient.cs index c6ab4654d..739119e5b 100644 --- a/DisCatSharp/Clients/DiscordClient.cs +++ b/DisCatSharp/Clients/DiscordClient.cs @@ -1,1333 +1,1333 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.IO; 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.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; namespace DisCatSharp; /// /// A Discord API wrapper. /// public sealed partial class DiscordClient : BaseDiscordClient { #region Internal Fields/Properties internal bool IsShard = false; /// /// Gets the message cache. /// internal RingBuffer MessageCache { get; } private List _extensions = new(); private StatusUpdate _status; /// /// Gets the connection lock. /// private readonly ManualResetEventSlim _connectionLock = new(true); #endregion #region Public Fields/Properties /// /// Gets the gateway protocol version. /// public int GatewayVersion { get; internal set; } /// /// Gets the gateway session information for this client. /// public GatewayInfo GatewayInfo { get; internal set; } /// /// Gets the gateway URL. /// public Uri GatewayUri { get; internal set; } /// /// Gets the total number of shards the bot is connected to. /// public int ShardCount => this.GatewayInfo != null ? this.GatewayInfo.ShardCount : this.Configuration.ShardCount; /// /// Gets the currently connected shard ID. /// public int ShardId => this.Configuration.ShardId; /// /// Gets the intents configured for this client. /// public DiscordIntents Intents => this.Configuration.Intents; /// /// Gets a dictionary of guilds that this client is in. The dictionary's key is the guild ID. Note that the /// guild objects in this dictionary will not be filled in if the specific guilds aren't available (the /// or events haven't been fired yet) /// public override IReadOnlyDictionary Guilds { get; } internal ConcurrentDictionary GuildsInternal = new(); /// /// Gets the websocket latency for this client. /// public int Ping => Volatile.Read(ref this._ping); private int _ping; /// /// Gets the collection of presences held by this client. /// public IReadOnlyDictionary Presences => this._presencesLazy.Value; internal Dictionary PresencesInternal = new(); private Lazy> _presencesLazy; /// /// Gets the collection of presences held by this client. /// public IReadOnlyDictionary EmbeddedActivities => this._embeddedActivitiesLazy.Value; internal Dictionary EmbeddedActivitiesInternal = new(); private Lazy> _embeddedActivitiesLazy; #endregion #region Constructor/Internal Setup /// /// Initializes a new instance of . /// /// Specifies configuration parameters. public DiscordClient(DiscordConfiguration config) : base(config) { if (this.Configuration.MessageCacheSize > 0) { var intents = this.Configuration.Intents; this.MessageCache = intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages) ? new RingBuffer(this.Configuration.MessageCacheSize) : null; } this.InternalSetup(); this.Guilds = new ReadOnlyConcurrentDictionary(this.GuildsInternal); } /// /// Internal setup of the Client. /// internal void InternalSetup() { this._clientErrored = new AsyncEvent("CLIENT_ERRORED", EventExecutionLimit, this.Goof); this._socketErrored = new AsyncEvent("SOCKET_ERRORED", EventExecutionLimit, this.Goof); this._socketOpened = new AsyncEvent("SOCKET_OPENED", EventExecutionLimit, this.EventErrorHandler); this._socketClosed = new AsyncEvent("SOCKET_CLOSED", EventExecutionLimit, this.EventErrorHandler); this._ready = new AsyncEvent("READY", EventExecutionLimit, this.EventErrorHandler); this._resumed = new AsyncEvent("RESUMED", EventExecutionLimit, this.EventErrorHandler); this._channelCreated = new AsyncEvent("CHANNEL_CREATED", EventExecutionLimit, this.EventErrorHandler); this._channelUpdated = new AsyncEvent("CHANNEL_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._channelDeleted = new AsyncEvent("CHANNEL_DELETED", EventExecutionLimit, this.EventErrorHandler); this._dmChannelDeleted = new AsyncEvent("DM_CHANNEL_DELETED", EventExecutionLimit, this.EventErrorHandler); this._channelPinsUpdated = new AsyncEvent("CHANNEL_PINS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildCreated = new AsyncEvent("GUILD_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildAvailable = new AsyncEvent("GUILD_AVAILABLE", EventExecutionLimit, this.EventErrorHandler); this._guildUpdated = new AsyncEvent("GUILD_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildDeleted = new AsyncEvent("GUILD_DELETED", EventExecutionLimit, this.EventErrorHandler); this._guildUnavailable = new AsyncEvent("GUILD_UNAVAILABLE", EventExecutionLimit, this.EventErrorHandler); this._guildDownloadCompletedEv = new AsyncEvent("GUILD_DOWNLOAD_COMPLETED", EventExecutionLimit, this.EventErrorHandler); this._inviteCreated = new AsyncEvent("INVITE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._inviteDeleted = new AsyncEvent("INVITE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._messageCreated = new AsyncEvent("MESSAGE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._presenceUpdated = new AsyncEvent("PRESENCE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildBanAdded = new AsyncEvent("GUILD_BAN_ADD", EventExecutionLimit, this.EventErrorHandler); this._guildBanRemoved = new AsyncEvent("GUILD_BAN_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._guildEmojisUpdated = new AsyncEvent("GUILD_EMOJI_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildStickersUpdated = new AsyncEvent("GUILD_STICKER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationsUpdated = new AsyncEvent("GUILD_INTEGRATIONS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberAdded = new AsyncEvent("GUILD_MEMBER_ADD", EventExecutionLimit, this.EventErrorHandler); this._guildMemberRemoved = new AsyncEvent("GUILD_MEMBER_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberUpdated = new AsyncEvent("GUILD_MEMBER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildRoleCreated = new AsyncEvent("GUILD_ROLE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildRoleUpdated = new AsyncEvent("GUILD_ROLE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildRoleDeleted = new AsyncEvent("GUILD_ROLE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._messageAcknowledged = new AsyncEvent("MESSAGE_ACKNOWLEDGED", EventExecutionLimit, this.EventErrorHandler); this._messageUpdated = new AsyncEvent("MESSAGE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._messageDeleted = new AsyncEvent("MESSAGE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._messagesBulkDeleted = new AsyncEvent("MESSAGE_BULK_DELETED", EventExecutionLimit, this.EventErrorHandler); this._interactionCreated = new AsyncEvent("INTERACTION_CREATED", EventExecutionLimit, this.EventErrorHandler); this._componentInteractionCreated = new AsyncEvent("COMPONENT_INTERACTED", EventExecutionLimit, this.EventErrorHandler); this._contextMenuInteractionCreated = new AsyncEvent("CONTEXT_MENU_INTERACTED", EventExecutionLimit, this.EventErrorHandler); this._typingStarted = new AsyncEvent("TYPING_STARTED", EventExecutionLimit, this.EventErrorHandler); this._userSettingsUpdated = new AsyncEvent("USER_SETTINGS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._userUpdated = new AsyncEvent("USER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._voiceStateUpdated = new AsyncEvent("VOICE_STATE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._voiceServerUpdated = new AsyncEvent("VOICE_SERVER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildMembersChunked = new AsyncEvent("GUILD_MEMBERS_CHUNKED", EventExecutionLimit, this.EventErrorHandler); this._unknownEvent = new AsyncEvent("UNKNOWN_EVENT", EventExecutionLimit, this.EventErrorHandler); this._messageReactionAdded = new AsyncEvent("MESSAGE_REACTION_ADDED", EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemoved = new AsyncEvent("MESSAGE_REACTION_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._messageReactionsCleared = new AsyncEvent("MESSAGE_REACTIONS_CLEARED", EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemovedEmoji = new AsyncEvent("MESSAGE_REACTION_REMOVED_EMOJI", EventExecutionLimit, this.EventErrorHandler); this._webhooksUpdated = new AsyncEvent("WEBHOOKS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._heartbeated = new AsyncEvent("HEARTBEATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandCreated = new AsyncEvent("APPLICATION_COMMAND_CREATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandUpdated = new AsyncEvent("APPLICATION_COMMAND_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandDeleted = new AsyncEvent("APPLICATION_COMMAND_DELETED", EventExecutionLimit, this.EventErrorHandler); this._guildApplicationCommandCountUpdated = new AsyncEvent("GUILD_APPLICATION_COMMAND_COUNTS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandPermissionsUpdated = new AsyncEvent("APPLICATION_COMMAND_PERMISSIONS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationCreated = new AsyncEvent("INTEGRATION_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationUpdated = new AsyncEvent("INTEGRATION_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationDeleted = new AsyncEvent("INTEGRATION_DELETED", EventExecutionLimit, this.EventErrorHandler); this._stageInstanceCreated = new AsyncEvent("STAGE_INSTANCE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._stageInstanceUpdated = new AsyncEvent("STAGE_INSTANCE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._stageInstanceDeleted = new AsyncEvent("STAGE_INSTANCE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._threadCreated = new AsyncEvent("THREAD_CREATED", EventExecutionLimit, this.EventErrorHandler); this._threadUpdated = new AsyncEvent("THREAD_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._threadDeleted = new AsyncEvent("THREAD_DELETED", EventExecutionLimit, this.EventErrorHandler); this._threadListSynced = new AsyncEvent("THREAD_LIST_SYNCED", EventExecutionLimit, this.EventErrorHandler); this._threadMemberUpdated = new AsyncEvent("THREAD_MEMBER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._threadMembersUpdated = new AsyncEvent("THREAD_MEMBERS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._zombied = new AsyncEvent("ZOMBIED", EventExecutionLimit, this.EventErrorHandler); this._payloadReceived = new AsyncEvent("PAYLOAD_RECEIVED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventCreated = new AsyncEvent("GUILD_SCHEDULED_EVENT_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUpdated = new AsyncEvent("GUILD_SCHEDULED_EVENT_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventDeleted = new AsyncEvent("GUILD_SCHEDULED_EVENT_DELETED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUserAdded = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_ADDED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUserRemoved = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._embeddedActivityUpdated = new AsyncEvent("EMBEDDED_ACTIVITY_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberTimeoutAdded = new AsyncEvent("GUILD_MEMBER_TIMEOUT_ADDED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberTimeoutChanged = new AsyncEvent("GUILD_MEMBER_TIMEOUT_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberTimeoutRemoved = new AsyncEvent("GUILD_MEMBER_TIMEOUT_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._rateLimitHit = new AsyncEvent("RATELIMIT_HIT", EventExecutionLimit, this.EventErrorHandler); this.GuildsInternal.Clear(); this._presencesLazy = new Lazy>(() => new ReadOnlyDictionary(this.PresencesInternal)); this._embeddedActivitiesLazy = new Lazy>(() => new ReadOnlyDictionary(this.EmbeddedActivitiesInternal)); } #endregion #region Client Extension Methods /// /// Registers an extension with this client. /// /// Extension to register. public void AddExtension(BaseExtension ext) { ext.Setup(this); this._extensions.Add(ext); } /// /// Retrieves a previously registered extension from this client. /// /// The type of extension to retrieve. /// The requested extension. public T GetExtension() where T : BaseExtension => this._extensions.FirstOrDefault(x => x.GetType() == typeof(T)) as T; #endregion #region Public Connection Methods /// /// Connects to the gateway. /// /// The activity to set. Defaults to null. /// The optional status to set. Defaults to null. /// Since when is the client performing the specified activity. Defaults to null. - /// Thrown when an invalid token was provided. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when an invalid token was provided. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task ConnectAsync(DiscordActivity activity = null, UserStatus? status = null, DateTimeOffset? idlesince = null) { // Check if connection lock is already set, and set it if it isn't if (!this._connectionLock.Wait(0)) throw new InvalidOperationException("This client is already connected."); this._connectionLock.Set(); var w = 7500; var i = 5; var s = false; Exception cex = null; if (activity == null && status == null && idlesince == null) this._status = null; else { var sinceUnix = idlesince != null ? (long?)Utilities.GetUnixTime(idlesince.Value) : null; this._status = new StatusUpdate() { Activity = new TransportActivity(activity), Status = status ?? UserStatus.Online, IdleSince = sinceUnix, IsAfk = idlesince != null, ActivityInternal = activity }; } if (!this.IsShard) { if (this.Configuration.TokenType != TokenType.Bot) this.Logger.LogWarning(LoggerEvents.Misc, "You are logging in with a token that is not a bot token. This is not officially supported by Discord, and can result in your account being terminated if you aren't careful."); this.Logger.LogInformation(LoggerEvents.Startup, "Lib {0}, version {1}", this.BotLibrary, this.VersionString); } while (i-- > 0 || this.Configuration.ReconnectIndefinitely) { try { await this.InternalConnectAsync().ConfigureAwait(false); s = true; break; } catch (UnauthorizedException e) { FailConnection(this._connectionLock); throw new Exception("Authentication failed. Check your token and try again.", e); } catch (PlatformNotSupportedException) { FailConnection(this._connectionLock); throw; } catch (NotImplementedException) { FailConnection(this._connectionLock); throw; } catch (Exception ex) { FailConnection(null); cex = ex; if (i <= 0 && !this.Configuration.ReconnectIndefinitely) break; this.Logger.LogError(LoggerEvents.ConnectionFailure, ex, "Connection attempt failed, retrying in {0}s", w / 1000); await Task.Delay(w).ConfigureAwait(false); if (i > 0) w *= 2; } } if (!s && cex != null) { this._connectionLock.Set(); throw new Exception("Could not connect to Discord.", cex); } // non-closure, hence args static void FailConnection(ManualResetEventSlim cl) => // unlock this (if applicable) so we can let others attempt to connect cl?.Set(); } /// /// Reconnects to the gateway. /// /// Whether to start a new session. public Task ReconnectAsync(bool startNewSession = true) => this.InternalReconnectAsync(startNewSession, code: startNewSession ? 1000 : 4002); /// /// Disconnects from the gateway. /// public async Task DisconnectAsync() { this.Configuration.AutoReconnect = false; if (this.WebSocketClient != null) await this.WebSocketClient.DisconnectAsync().ConfigureAwait(false); } #endregion #region Public REST Methods /// /// Gets a user. /// /// Id of the user /// Whether to fetch the user again. Defaults to true. /// The requested user. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetUserAsync(ulong userId, bool fetch = true) { if (!fetch) { return this.TryGetCachedUserInternal(userId, out var usr) ? usr : new DiscordUser { Id = userId, Discord = this }; } else { var usr = await this.ApiClient.GetUserAsync(userId).ConfigureAwait(false); usr = this.UserCache.AddOrUpdate(userId, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; old.BannerHash = usr.BannerHash; old.BannerColorInternal = usr.BannerColorInternal; old.AvatarDecorationHash = usr.AvatarDecorationHash; old.ThemeColorsInternal = usr.ThemeColorsInternal; old.Pronouns = usr.Pronouns; return old; }); return usr; } } /// /// Removes all global application commands. /// public async Task RemoveGlobalApplicationCommandsAsync() => await this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, Array.Empty()); /// /// Removes all global application commands for a specific guild id. /// - /// The target guild id. + /// The target guild id. public async Task RemoveGuildApplicationCommandsAsync(ulong guildId) => await this.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, Array.Empty()); /// /// Removes all global application commands for a specific guild. /// - /// The target guild. + /// The target guild. public async Task RemoveGuildApplicationCommandsAsync(DiscordGuild guild) => await this.RemoveGuildApplicationCommandsAsync(guild.Id); /// /// Gets a channel. /// /// The id of the channel to get. /// The requested channel. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetChannelAsync(ulong id) => this.InternalGetCachedChannel(id) ?? await this.ApiClient.GetChannelAsync(id).ConfigureAwait(false); /// /// Gets a thread. /// /// The id of the thread to get. /// The requested thread. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetThreadAsync(ulong id) => this.InternalGetCachedThread(id) ?? await this.ApiClient.GetThreadAsync(id).ConfigureAwait(false); /// /// Sends a normal message. /// /// The channel to send to. /// The message content to send. /// The message that was sent. - /// 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 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 SendMessageAsync(DiscordChannel channel, string content) => this.ApiClient.CreateMessageAsync(channel.Id, content, embeds: null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message with an embed. /// /// The channel to send to. /// The embed to attach to the message. /// The message that was sent. - /// 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 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 SendMessageAsync(DiscordChannel channel, DiscordEmbed embed) => this.ApiClient.CreateMessageAsync(channel.Id, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message with content and an embed. /// /// Channel to send to. /// The message content to send. /// The embed to attach to the message. /// The message that was sent. - /// 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 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 SendMessageAsync(DiscordChannel channel, string content, DiscordEmbed embed) => this.ApiClient.CreateMessageAsync(channel.Id, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message with the . /// /// The channel to send the message to. /// The message builder. /// The message that was sent. - /// Thrown when the client does not have the permission if TTS is false 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. + /// Thrown when the client does not have the permission if TTS is false 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(DiscordChannel channel, DiscordMessageBuilder builder) => this.ApiClient.CreateMessageAsync(channel.Id, builder); /// - /// Sends a message with an . + /// Sends a message with an . /// /// The channel to send the message to. /// The message builder. /// The message that was sent. - /// Thrown when the client does not have the permission if TTS is false 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. + /// Thrown when the client does not have the permission if TTS is false 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(DiscordChannel channel, Action action) { var builder = new DiscordMessageBuilder(); action(builder); return this.ApiClient.CreateMessageAsync(channel.Id, builder); } /// /// Creates a guild. This requires the bot to be in less than 10 guilds total. /// /// Name of the guild. /// Voice region of the guild. /// Stream containing the icon for the guild. /// Verification level for the guild. /// Default message notification settings for the guild. /// System channel flags for the guild. /// The created guild. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateGuildAsync(string name, string region = null, Optional icon = default, VerificationLevel? verificationLevel = null, DefaultMessageNotifications? defaultMessageNotifications = null, SystemChannelFlags? systemChannelFlags = null) { var iconb64 = ImageTool.Base64FromStream(icon); return this.ApiClient.CreateGuildAsync(name, region, iconb64, verificationLevel, defaultMessageNotifications, systemChannelFlags); } /// /// Creates a guild from a template. This requires the bot to be in less than 10 guilds total. /// /// The template code. /// Name of the guild. /// Stream containing the icon for the guild. /// The created guild. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateGuildFromTemplateAsync(string code, string name, Optional icon = default) { var iconb64 = ImageTool.Base64FromStream(icon); return this.ApiClient.CreateGuildFromTemplateAsync(code, name, iconb64); } /// /// Executes a raw request. /// /// /// /// var request = await Client.ExecuteRawRequestAsync(RestRequestMethod.GET, $"{Endpoints.CHANNELS}/243184972190742178964/{Endpoints.INVITES}"); /// List<DiscordInvite> invites = DiscordJson.ToDiscordObject<List<DiscordInvite>>(request.Response); /// /// /// The method. /// The route. /// The route parameters. /// The json body. /// The additional headers. /// The ratelimit wait override. - /// Thrown when the resource does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the resource does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. /// A awaitable RestResponse [Obsolete("This is no longer needed. Use DiscordClient.RestClient instead.", false)] public async Task ExecuteRawRequestAsync(RestRequestMethod method, string route, object routeParams, string jsonBody = null, Dictionary additionalHeaders = null, double? ratelimitWaitOverride = null) { var bucket = this.ApiClient.Rest.GetBucket(method, route, routeParams, out var path); var url = Utilities.GetApiUriFor(path, this.Configuration); var res = await this.ApiClient.DoRequestAsync(this, bucket, url, method, route, additionalHeaders, DiscordJson.SerializeObject(jsonBody), ratelimitWaitOverride); return res; } /// /// Gets a guild. /// Setting to true will make a REST request. /// /// The guild ID to search for. /// Whether to include approximate presence and member counts in the returned guild. /// The requested Guild. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetGuildAsync(ulong id, bool? withCounts = null) { if (this.GuildsInternal.TryGetValue(id, out var guild) && (!withCounts.HasValue || !withCounts.Value)) return guild; guild = await this.ApiClient.GetGuildAsync(id, withCounts).ConfigureAwait(false); var channels = await this.ApiClient.GetGuildChannelsAsync(guild.Id).ConfigureAwait(false); foreach (var channel in channels) guild.ChannelsInternal[channel.Id] = channel; return guild; } /// /// Gets a guild preview. /// /// The guild ID. /// - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetGuildPreviewAsync(ulong id) => this.ApiClient.GetGuildPreviewAsync(id); /// /// Gets an invite. /// /// The invite code. /// Whether to include presence and total member counts in the returned invite. /// Whether to include the expiration date in the returned invite. /// The scheduled event id. /// The requested Invite. - /// Thrown when the invite does not exists. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the invite does not exists. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null, ulong? scheduledEventId = null) => this.ApiClient.GetInviteAsync(code, withCounts, withExpiration, scheduledEventId); /// /// Gets a list of user connections. /// - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task> GetConnectionsAsync() => this.ApiClient.GetUserConnectionsAsync(); /// /// Gets a sticker. /// /// The requested sticker. /// The id of the sticker. - /// Thrown when the sticker does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the sticker does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetStickerAsync(ulong id) => this.ApiClient.GetStickerAsync(id); /// /// Gets all nitro sticker packs. /// /// List of sticker packs. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task> GetStickerPacksAsync() => this.ApiClient.GetStickerPacksAsync(); /// /// Gets the In-App OAuth Url. /// /// Defaults to . /// Redirect Uri. /// Defaults to . /// The OAuth Url public Uri GetInAppOAuth(Permissions permissions = Permissions.None, OAuthScopes scopes = OAuthScopes.BOT_DEFAULT, string redir = null) { permissions &= PermissionMethods.FullPerms; // hey look, it's not all annoying and blue :P return new Uri(new QueryUriBuilder($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.OAUTH2}{Endpoints.AUTHORIZE}") .AddParameter("client_id", this.CurrentApplication.Id.ToString(CultureInfo.InvariantCulture)) .AddParameter("scope", OAuth.ResolveScopes(scopes)) .AddParameter("permissions", ((long)permissions).ToString(CultureInfo.InvariantCulture)) .AddParameter("state", "") .AddParameter("redirect_uri", redir ?? "") .ToString()); } /// /// Gets a webhook. /// /// The target webhook id. /// The requested webhook. - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetWebhookAsync(ulong id) => this.ApiClient.GetWebhookAsync(id); /// /// Gets a webhook. /// /// The target webhook id. /// The target webhook token. /// The requested webhook. - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetWebhookWithTokenAsync(ulong id, string token) => this.ApiClient.GetWebhookWithTokenAsync(id, token); /// /// Updates current user's activity and status. /// /// Activity to set. /// Status of the user. /// Since when is the client performing the specified activity. /// public Task UpdateStatusAsync(DiscordActivity activity = null, UserStatus? userStatus = null, DateTimeOffset? idleSince = null) => this.InternalUpdateStatusAsync(activity, userStatus, idleSince); /// /// Edits current user. /// /// New username. /// New avatar. /// The modified user. - /// Thrown when the user does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the user does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task UpdateCurrentUserAsync(string username = null, Optional avatar = default) { var av64 = ImageTool.Base64FromStream(avatar); var usr = await this.ApiClient.ModifyCurrentUserAsync(username, av64).ConfigureAwait(false); this.CurrentUser.Username = usr.Username; this.CurrentUser.Discriminator = usr.Discriminator; this.CurrentUser.AvatarHash = usr.AvatarHash; return this.CurrentUser; } /// /// Gets a guild template by the code. /// /// The code of the template. /// The guild template for the code. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetTemplateAsync(string code) => this.ApiClient.GetTemplateAsync(code); /// /// Gets all the global application commands for this application. /// /// Whether to get the full localization dict. /// A list of global application commands. public Task> GetGlobalApplicationCommandsAsync(bool withLocalizations = false) => this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id, withLocalizations); /// /// Overwrites the existing global application commands. New commands are automatically created and missing commands are automatically deleted. /// /// The list of commands to overwrite with. /// The list of global commands. public Task> BulkOverwriteGlobalApplicationCommandsAsync(IEnumerable commands) => this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, commands); /// /// Creates or overwrites a global application command. /// /// The command to create. /// The created command. public Task CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) => this.ApiClient.CreateGlobalApplicationCommandAsync(this.CurrentApplication.Id, command); /// /// Gets a global application command by its id. /// /// The id of the command to get. /// The command with the id. public Task GetGlobalApplicationCommandAsync(ulong commandId) => this.ApiClient.GetGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); /// /// Edits a global application command. /// /// The id of the command to edit. /// Action to perform. /// The edited command. public async Task EditGlobalApplicationCommandAsync(ulong commandId, Action action) { var mdl = new ApplicationCommandEditModel(); action(mdl); var applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync().ConfigureAwait(false)).Id; return await this.ApiClient.EditGlobalApplicationCommandAsync(applicationId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.NameLocalizations, mdl.DescriptionLocalizations, mdl.DefaultMemberPermissions, mdl.DmPermission, mdl.IsNsfw).ConfigureAwait(false); } /// /// Deletes a global application command. /// /// The id of the command to delete. public Task DeleteGlobalApplicationCommandAsync(ulong commandId) => this.ApiClient.DeleteGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); /// /// Gets all the application commands for a guild. /// /// The id of the guild to get application commands for. /// Whether to get the full localization dict. /// A list of application commands in the guild. public Task> GetGuildApplicationCommandsAsync(ulong guildId, bool withLocalizations = false) => this.ApiClient.GetGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, withLocalizations); /// /// Overwrites the existing application commands in a guild. New commands are automatically created and missing commands are automatically deleted. /// /// The id of the guild. /// The list of commands to overwrite with. /// The list of guild commands. public Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable commands) => this.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, commands); /// /// Creates or overwrites a guild application command. /// /// The id of the guild to create the application command in. /// The command to create. /// The created command. public Task CreateGuildApplicationCommandAsync(ulong guildId, DiscordApplicationCommand command) => this.ApiClient.CreateGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, command); /// /// Gets a application command in a guild by its id. /// /// The id of the guild the application command is in. /// The id of the command to get. /// The command with the id. public Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId) => this.ApiClient.GetGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); /// /// Edits a application command in a guild. /// /// The id of the guild the application command is in. /// The id of the command to edit. /// Action to perform. /// The edited command. public async Task EditGuildApplicationCommandAsync(ulong guildId, ulong commandId, Action action) { var mdl = new ApplicationCommandEditModel(); action(mdl); var applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync().ConfigureAwait(false)).Id; return await this.ApiClient.EditGuildApplicationCommandAsync(applicationId, guildId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.NameLocalizations, mdl.DescriptionLocalizations, mdl.DefaultMemberPermissions, mdl.DmPermission, mdl.IsNsfw).ConfigureAwait(false); } /// /// Deletes a application command in a guild. /// /// The id of the guild to delete the application command in. /// The id of the command. public Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) => this.ApiClient.DeleteGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); /// /// Gets all command permissions for a guild. /// /// The target guild. public Task> GetGuildApplicationCommandPermissionsAsync(ulong guildId) => this.ApiClient.GetGuildApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId); /// /// Gets the permissions for a guild command. /// /// The target guild. /// The target command id. public Task GetApplicationCommandPermissionAsync(ulong guildId, ulong commandId) => this.ApiClient.GetGuildApplicationCommandPermissionAsync(this.CurrentApplication.Id, guildId, commandId); #endregion #region Internal Caching Methods /// /// Gets the internal cached threads. /// /// The target thread id. /// The requested thread. internal DiscordThreadChannel InternalGetCachedThread(ulong threadId) { if (this.Guilds == null) return null; foreach (var guild in this.Guilds.Values) if (guild.Threads.TryGetValue(threadId, out var foundThread)) return foundThread; return null; } /// /// Gets the internal cached scheduled event. /// /// The target scheduled event id. /// The requested scheduled event. internal DiscordScheduledEvent InternalGetCachedScheduledEvent(ulong scheduledEventId) { if (this.Guilds == null) return null; foreach (var guild in this.Guilds.Values) if (guild.ScheduledEvents.TryGetValue(scheduledEventId, out var foundScheduledEvent)) return foundScheduledEvent; return null; } /// /// Gets the internal cached channel. /// /// The target channel id. /// The requested channel. internal DiscordChannel InternalGetCachedChannel(ulong channelId) { if (this.Guilds == null) return null; foreach (var guild in this.Guilds.Values) if (guild.Channels.TryGetValue(channelId, out var foundChannel)) return foundChannel; return null; } /// /// Gets the internal cached guild. /// /// The target guild id. /// The requested guild. internal DiscordGuild InternalGetCachedGuild(ulong? guildId) { if (this.GuildsInternal != null && guildId.HasValue) { if (this.GuildsInternal.TryGetValue(guildId.Value, out var guild)) return guild; } return null; } /// /// Updates a message. /// /// The message to update. /// The author to update. /// The guild to update. /// The member to update. private void UpdateMessage(DiscordMessage message, TransportUser author, DiscordGuild guild, TransportMember member) { if (author != null) { var usr = new DiscordUser(author) { Discord = this }; if (member != null) member.User = author; message.Author = this.UpdateUser(usr, guild?.Id, guild, member); } var channel = this.InternalGetCachedChannel(message.ChannelId); if (channel != null) return; channel = !message.GuildId.HasValue ? new DiscordDmChannel { Id = message.ChannelId, Discord = this, Type = ChannelType.Private } : new DiscordChannel { Id = message.ChannelId, Discord = this }; message.Channel = channel; } /// /// Updates a scheduled event. /// /// The scheduled event to update. /// The guild to update. /// The updated scheduled event. private DiscordScheduledEvent UpdateScheduledEvent(DiscordScheduledEvent scheduledEvent, DiscordGuild guild) { if (scheduledEvent != null) { _ = guild.ScheduledEventsInternal.AddOrUpdate(scheduledEvent.Id, scheduledEvent, (id, old) => { old.Discord = this; old.Description = scheduledEvent.Description; old.ChannelId = scheduledEvent.ChannelId; old.EntityId = scheduledEvent.EntityId; old.EntityType = scheduledEvent.EntityType; old.EntityMetadata = scheduledEvent.EntityMetadata; old.PrivacyLevel = scheduledEvent.PrivacyLevel; old.Name = scheduledEvent.Name; old.Status = scheduledEvent.Status; old.UserCount = scheduledEvent.UserCount; old.ScheduledStartTimeRaw = scheduledEvent.ScheduledStartTimeRaw; old.ScheduledEndTimeRaw = scheduledEvent.ScheduledEndTimeRaw; return old; }); } return scheduledEvent; } /// /// Updates a user. /// /// The user to update. /// The guild id to update. /// The guild to update. /// The member to update. /// The updated user. private DiscordUser UpdateUser(DiscordUser usr, ulong? guildId, DiscordGuild guild, TransportMember mbr) { if (mbr != null) { if (mbr.User != null) { usr = new DiscordUser(mbr.User) { Discord = this }; _ = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; old.BannerHash = usr.BannerHash; old.BannerColorInternal = usr.BannerColorInternal; old.AvatarDecorationHash = usr.AvatarDecorationHash; old.ThemeColorsInternal = usr.ThemeColorsInternal; old.Pronouns = usr.Pronouns; return old; }); usr = new DiscordMember(mbr) { Discord = this, GuildId = guildId.Value }; } var intents = this.Configuration.Intents; DiscordMember member = default; if (!intents.HasAllPrivilegedIntents() || guild.IsLarge) // we have the necessary privileged intents, no need to worry about caching here unless guild is large. { if (guild?.MembersInternal.TryGetValue(usr.Id, out member) == false) { if (intents.HasIntent(DiscordIntents.GuildMembers) || this.Configuration.AlwaysCacheMembers) // member can be updated by events, so cache it { guild.MembersInternal.TryAdd(usr.Id, (DiscordMember)usr); } } else if (intents.HasIntent(DiscordIntents.GuildPresences) || this.Configuration.AlwaysCacheMembers) // we can attempt to update it if it's already in cache. { if (!intents.HasIntent(DiscordIntents.GuildMembers)) // no need to update if we already have the member events { _ = guild.MembersInternal.TryUpdate(usr.Id, (DiscordMember)usr, member); } } } } else if (usr.Username != null) // check if not a skeleton user { _ = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; old.BannerHash = usr.BannerHash; old.BannerColorInternal = usr.BannerColorInternal; old.AvatarDecorationHash = usr.AvatarDecorationHash; old.ThemeColorsInternal = usr.ThemeColorsInternal; old.Pronouns = usr.Pronouns; return old; }); } return usr; } /// /// Updates the cached scheduled events in a guild. /// /// The guild. /// The raw events. private void UpdateCachedScheduledEvent(DiscordGuild guild, JArray rawEvents) { if (this._disposed) return; if (rawEvents != null) { guild.ScheduledEventsInternal.Clear(); foreach (var xj in rawEvents) { var xtm = xj.ToDiscordObject(); xtm.Discord = this; guild.ScheduledEventsInternal[xtm.Id] = xtm; } } } /// /// Updates the cached guild. /// /// The new guild. /// The raw members. private void UpdateCachedGuild(DiscordGuild newGuild, JArray rawMembers) { if (this._disposed) return; if (!this.GuildsInternal.ContainsKey(newGuild.Id)) this.GuildsInternal[newGuild.Id] = newGuild; var guild = this.GuildsInternal[newGuild.Id]; if (newGuild.ChannelsInternal != null && !newGuild.ChannelsInternal.IsEmpty) { foreach (var channel in newGuild.ChannelsInternal.Values) { if (guild.ChannelsInternal.TryGetValue(channel.Id, out _)) continue; channel.Initialize(this); guild.ChannelsInternal[channel.Id] = channel; } } if (newGuild.ThreadsInternal != null && !newGuild.ThreadsInternal.IsEmpty) { foreach (var thread in newGuild.ThreadsInternal.Values) { if (guild.ThreadsInternal.TryGetValue(thread.Id, out _)) continue; guild.ThreadsInternal[thread.Id] = thread; } } if (newGuild.ScheduledEventsInternal != null && !newGuild.ScheduledEventsInternal.IsEmpty) { foreach (var @event in newGuild.ScheduledEventsInternal.Values) { if (guild.ScheduledEventsInternal.TryGetValue(@event.Id, out _)) continue; guild.ScheduledEventsInternal[@event.Id] = @event; } } foreach (var newEmoji in newGuild.EmojisInternal.Values) _ = guild.EmojisInternal.GetOrAdd(newEmoji.Id, _ => newEmoji); foreach (var newSticker in newGuild.StickersInternal.Values) _ = guild.StickersInternal.GetOrAdd(newSticker.Id, _ => newSticker); foreach (var newStageInstance in newGuild.StageInstancesInternal.Values) _ = guild.StageInstancesInternal.GetOrAdd(newStageInstance.Id, _ => newStageInstance); if (rawMembers != null) { guild.MembersInternal.Clear(); foreach (var xj in rawMembers) { var xtm = xj.ToDiscordObject(); var xu = new DiscordUser(xtm.User) { Discord = this }; _ = this.UserCache.AddOrUpdate(xtm.User.Id, xu, (id, old) => { old.Username = xu.Username; old.Discriminator = xu.Discriminator; old.AvatarHash = xu.AvatarHash; old.PremiumType = xu.PremiumType; return old; }); guild.MembersInternal[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, GuildId = guild.Id }; } } foreach (var role in newGuild.RolesInternal.Values) { if (guild.RolesInternal.TryGetValue(role.Id, out _)) continue; role.GuildId = guild.Id; guild.RolesInternal[role.Id] = role; } guild.Name = newGuild.Name; guild.AfkChannelId = newGuild.AfkChannelId; guild.AfkTimeout = newGuild.AfkTimeout; guild.DefaultMessageNotifications = newGuild.DefaultMessageNotifications; guild.RawFeatures = newGuild.RawFeatures; guild.IconHash = newGuild.IconHash; guild.MfaLevel = newGuild.MfaLevel; guild.OwnerId = newGuild.OwnerId; guild.VoiceRegionId = newGuild.VoiceRegionId; guild.SplashHash = newGuild.SplashHash; guild.VerificationLevel = newGuild.VerificationLevel; guild.WidgetEnabled = newGuild.WidgetEnabled; guild.WidgetChannelId = newGuild.WidgetChannelId; guild.ExplicitContentFilter = newGuild.ExplicitContentFilter; guild.PremiumTier = newGuild.PremiumTier; guild.PremiumSubscriptionCount = newGuild.PremiumSubscriptionCount; guild.PremiumProgressBarEnabled = newGuild.PremiumProgressBarEnabled; guild.BannerHash = newGuild.BannerHash; guild.Description = newGuild.Description; guild.VanityUrlCode = newGuild.VanityUrlCode; guild.SystemChannelId = newGuild.SystemChannelId; guild.SystemChannelFlags = newGuild.SystemChannelFlags; guild.DiscoverySplashHash = newGuild.DiscoverySplashHash; guild.MaxMembers = newGuild.MaxMembers; guild.MaxPresences = newGuild.MaxPresences; guild.ApproximateMemberCount = newGuild.ApproximateMemberCount; guild.ApproximatePresenceCount = newGuild.ApproximatePresenceCount; guild.MaxVideoChannelUsers = newGuild.MaxVideoChannelUsers; guild.PreferredLocale = newGuild.PreferredLocale; guild.RulesChannelId = newGuild.RulesChannelId; guild.PublicUpdatesChannelId = newGuild.PublicUpdatesChannelId; guild.ApplicationId = newGuild.ApplicationId; // fields not sent for update: // - guild.Channels // - voice states // - guild.JoinedAt = new_guild.JoinedAt; // - guild.Large = new_guild.Large; // - guild.MemberCount = Math.Max(new_guild.MemberCount, guild._members.Count); // - guild.Unavailable = new_guild.Unavailable; } /// /// Populates the message reactions and cache. /// /// The message. /// The author. /// The member. private void PopulateMessageReactionsAndCache(DiscordMessage message, TransportUser author, TransportMember member) { var guild = message.Channel?.Guild ?? this.InternalGetCachedGuild(message.GuildId); this.UpdateMessage(message, author, guild, member); message.ReactionsInternal ??= new List(); foreach (var xr in message.ReactionsInternal) xr.Emoji.Discord = this; if (this.Configuration.MessageCacheSize > 0 && message.Channel != null) this.MessageCache?.Add(message); } #endregion #region Disposal ~DiscordClient() { this.Dispose(); } /// /// Whether the client is disposed. /// private bool _disposed; /// /// Disposes the client. /// public override void Dispose() { if (this._disposed) return; this._disposed = true; GC.SuppressFinalize(this); this.DisconnectAsync().ConfigureAwait(false).GetAwaiter().GetResult(); this.ApiClient.Rest.Dispose(); this.CurrentUser = null; var extensions = this._extensions; // prevent _extensions being modified during dispose this._extensions = null; foreach (var extension in extensions) if (extension is IDisposable disposable) disposable.Dispose(); try { this._cancelTokenSource?.Cancel(); this._cancelTokenSource?.Dispose(); } catch { } this.GuildsInternal = null; this._heartbeatTask = null; } #endregion } diff --git a/DisCatSharp/Clients/DiscordShardedClient.cs b/DisCatSharp/Clients/DiscordShardedClient.cs index c45a22082..5fd5084e7 100644 --- a/DisCatSharp/Clients/DiscordShardedClient.cs +++ b/DisCatSharp/Clients/DiscordShardedClient.cs @@ -1,781 +1,781 @@ // 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. #pragma warning disable CS0618 using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Net.Http; using System.Reflection; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Net; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; namespace DisCatSharp; /// /// A Discord client that shards automatically. /// public sealed partial class DiscordShardedClient { #region Public Properties /// /// Gets the logger for this client. /// public ILogger Logger { get; } /// /// Gets all client shards. /// public IReadOnlyDictionary ShardClients { get; } /// /// Gets the gateway info for the client's session. /// public GatewayInfo GatewayInfo { get; private set; } /// /// Gets the current user. /// public DiscordUser CurrentUser { get; private set; } /// /// Gets the current application. /// public DiscordApplication CurrentApplication { get; private set; } [Obsolete("Use GetLibraryDevelopmentTeamAsync")] public DisCatSharpTeam LibraryDeveloperTeam => this.GetLibraryDevelopmentTeamAsync().Result; /// /// Gets the list of available voice regions. Note that this property will not contain VIP voice regions. /// public IReadOnlyDictionary VoiceRegions => this._voiceRegionsLazy?.Value; #endregion #region Private Properties/Fields /// /// Gets the configuration. /// private readonly DiscordConfiguration _configuration; /// /// Gets the list of available voice regions. This property is meant as a way to modify . /// private ConcurrentDictionary _internalVoiceRegions; /// /// Gets a list of shards. /// private readonly ConcurrentDictionary _shards = new(); /// /// Gets a lazy list of voice regions. /// private Lazy> _voiceRegionsLazy; /// /// Whether the shard client is started. /// private bool _isStarted; /// /// Whether manual sharding is enabled. /// private readonly bool _manuallySharding; #endregion #region Constructor /// /// Initializes a new auto-sharding Discord client. /// /// The configuration to use. public DiscordShardedClient(DiscordConfiguration config) { this.InternalSetup(); if (config.ShardCount > 1) this._manuallySharding = true; this._configuration = config; this.ShardClients = new ReadOnlyConcurrentDictionary(this._shards); if (this._configuration.LoggerFactory == null) { this._configuration.LoggerFactory = new DefaultLoggerFactory(); this._configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this._configuration.MinimumLogLevel, this._configuration.LogTimestampFormat)); } this.Logger = this._configuration.LoggerFactory.CreateLogger(); } #endregion #region Public Methods /// /// Initializes and connects all shards. /// - /// - /// + /// + /// public async Task StartAsync() { if (this._isStarted) throw new InvalidOperationException("This client has already been started."); this._isStarted = true; try { if (this._configuration.TokenType != TokenType.Bot) this.Logger.LogWarning(LoggerEvents.Misc, "You are logging in with a token that is not a bot token. This is not officially supported by Discord, and can result in your account being terminated if you aren't careful."); this.Logger.LogInformation(LoggerEvents.Startup, "Lib {0}, version {1}", this._botLibrary, this._versionString.Value); var shardc = await this.InitializeShardsAsync().ConfigureAwait(false); var connectTasks = new List(); this.Logger.LogInformation(LoggerEvents.ShardStartup, "Booting {0} shards.", shardc); for (var i = 0; i < shardc; i++) { //This should never happen, but in case it does... if (this.GatewayInfo.SessionBucket.MaxConcurrency < 1) this.GatewayInfo.SessionBucket.MaxConcurrency = 1; if (this.GatewayInfo.SessionBucket.MaxConcurrency == 1) await this.ConnectShardAsync(i).ConfigureAwait(false); else { //Concurrent login. connectTasks.Add(this.ConnectShardAsync(i)); if (connectTasks.Count == this.GatewayInfo.SessionBucket.MaxConcurrency) { await Task.WhenAll(connectTasks).ConfigureAwait(false); connectTasks.Clear(); } } } } catch (Exception ex) { await this.InternalStopAsync(false).ConfigureAwait(false); var message = $"Shard initialization failed, check inner exceptions for details: "; this.Logger.LogCritical(LoggerEvents.ShardClientError, $"{message}\n{ex}"); throw new AggregateException(message, ex); } } /// /// Disconnects and disposes all shards. /// - /// + /// public Task StopAsync() => this.InternalStopAsync(); /// /// Gets a shard from a guild id. /// /// If automatically sharding, this will use the method. /// Otherwise if manually sharding, it will instead iterate through each shard's guild caches. /// /// /// The guild ID for the shard. /// The found shard. Otherwise null if the shard was not found for the guild id. public DiscordClient GetShard(ulong guildId) { var index = this._manuallySharding ? this.GetShardIdFromGuilds(guildId) : Utilities.GetShardId(guildId, this.ShardClients.Count); return index != -1 ? this._shards[index] : null; } /// /// Gets a shard from a guild. /// /// If automatically sharding, this will use the method. /// Otherwise if manually sharding, it will instead iterate through each shard's guild caches. /// /// /// The guild for the shard. /// The found shard. Otherwise null if the shard was not found for the guild. public DiscordClient GetShard(DiscordGuild guild) => this.GetShard(guild.Id); /// /// Updates the status on all shards. /// /// The activity to set. Defaults to null. /// The optional status to set. Defaults to null. /// Since when is the client performing the specified activity. Defaults to null. /// Asynchronous operation. public async Task UpdateStatusAsync(DiscordActivity activity = null, UserStatus? userStatus = null, DateTimeOffset? idleSince = null) { var tasks = new List(); foreach (var client in this._shards.Values) tasks.Add(client.UpdateStatusAsync(activity, userStatus, idleSince)); await Task.WhenAll(tasks).ConfigureAwait(false); } /// /// /// public async Task GetLibraryDevelopmentTeamAsync() => await this.GetShard(0).GetLibraryDevelopmentTeamAsync().ConfigureAwait(false); #endregion #region Internal Methods /// /// Initializes the shards. /// /// The count of initialized shards. internal async Task InitializeShardsAsync() { if (!this._shards.IsEmpty) return this._shards.Count; this.GatewayInfo = await this.GetGatewayInfoAsync().ConfigureAwait(false); var shardCount = this._configuration.ShardCount == 1 ? this.GatewayInfo.ShardCount : this._configuration.ShardCount; var lf = new ShardedLoggerFactory(this.Logger); for (var i = 0; i < shardCount; i++) { var cfg = new DiscordConfiguration(this._configuration) { ShardId = i, ShardCount = shardCount, LoggerFactory = lf }; var client = new DiscordClient(cfg); if (!this._shards.TryAdd(i, client)) throw new InvalidOperationException("Could not initialize shards."); } return shardCount; } #endregion #region Private Methods & Version Property /// /// Gets the gateway info. /// private async Task GetGatewayInfoAsync() { var url = $"{Utilities.GetApiBaseUri(this._configuration)}{Endpoints.GATEWAY}{Endpoints.BOT}"; var http = new HttpClient(); http.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent()); http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(this._configuration)); http.DefaultRequestHeaders.TryAddWithoutValidation("X-Discord-Locale", this._configuration.Locale); if (this._configuration != null && this._configuration.Override != null) { http.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", this._configuration.Override); } this.Logger.LogDebug(LoggerEvents.ShardRest, $"Obtaining gateway information from GET {Endpoints.GATEWAY}{Endpoints.BOT}..."); var resp = await http.GetAsync(url).ConfigureAwait(false); http.Dispose(); if (!resp.IsSuccessStatusCode) { var ratelimited = await HandleHttpError(url, resp).ConfigureAwait(false); if (ratelimited) return await this.GetGatewayInfoAsync().ConfigureAwait(false); } var timer = new Stopwatch(); timer.Start(); var jo = JObject.Parse(await resp.Content.ReadAsStringAsync().ConfigureAwait(false)); var info = jo.ToObject(); //There is a delay from parsing here. timer.Stop(); info.SessionBucket.ResetAfterInternal -= (int)timer.ElapsedMilliseconds; info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.ResetAfterInternal); return info; async Task HandleHttpError(string reqUrl, HttpResponseMessage msg) { var code = (int)msg.StatusCode; if (code == 401 || code == 403) { throw new Exception($"Authentication failed, check your token and try again: {code} {msg.ReasonPhrase}"); } else if (code == 429) { this.Logger.LogError(LoggerEvents.ShardClientError, $"Ratelimit hit, requeuing request to {reqUrl}"); var hs = msg.Headers.ToDictionary(xh => xh.Key, xh => string.Join("\n", xh.Value), StringComparer.OrdinalIgnoreCase); var waitInterval = 0; if (hs.TryGetValue("Retry-After", out var retryAfterRaw)) waitInterval = int.Parse(retryAfterRaw, CultureInfo.InvariantCulture); await Task.Delay(waitInterval).ConfigureAwait(false); return true; } else if (code >= 500) { throw new Exception($"Internal Server Error: {code} {msg.ReasonPhrase}"); } else { throw new Exception($"An unsuccessful HTTP status code was encountered: {code} {msg.ReasonPhrase}"); } } } /// /// Gets the version string. /// private readonly Lazy _versionString = new(() => { var a = typeof(DiscordShardedClient).GetTypeInfo().Assembly; var iv = a.GetCustomAttribute(); if (iv != null) return iv.InformationalVersion; var v = a.GetName().Version; var vs = v.ToString(3); if (v.Revision > 0) vs = $"{vs}, CI build {v.Revision}"; return vs; }); /// /// Gets the name of the used bot library. /// private readonly string _botLibrary = "DisCatSharp"; #endregion #region Private Connection Methods /// /// Connects a shard. /// /// The shard id. private async Task ConnectShardAsync(int i) { if (!this._shards.TryGetValue(i, out var client)) throw new Exception($"Could not initialize shard {i}."); client.IsShard = true; if (this.GatewayInfo != null) { client.GatewayInfo = this.GatewayInfo; client.GatewayUri = new Uri(client.GatewayInfo.Url); } if (this.CurrentUser != null) client.CurrentUser = this.CurrentUser; if (this.CurrentApplication != null) client.CurrentApplication = this.CurrentApplication; if (this._internalVoiceRegions != null) { client.InternalVoiceRegions = this._internalVoiceRegions; client.VoiceRegionsLazy = new Lazy>(() => new ReadOnlyDictionary(client.InternalVoiceRegions)); } this.HookEventHandlers(client); await client.ConnectAsync(); this.Logger.LogInformation(LoggerEvents.ShardStartup, "Booted shard {0}.", i); this.GatewayInfo ??= client.GatewayInfo; if (this.CurrentUser == null) this.CurrentUser = client.CurrentUser; if (this.CurrentApplication == null) this.CurrentApplication = client.CurrentApplication; if (this._internalVoiceRegions == null) { this._internalVoiceRegions = client.InternalVoiceRegions; this._voiceRegionsLazy = new Lazy>(() => new ReadOnlyDictionary(this._internalVoiceRegions)); } } /// /// Stops all shards. /// /// Whether to enable the logger. private Task InternalStopAsync(bool enableLogger = true) { if (!this._isStarted) throw new InvalidOperationException("This client has not been started."); if (enableLogger) this.Logger.LogInformation(LoggerEvents.ShardShutdown, "Disposing {0} shards.", this._shards.Count); this._isStarted = false; this._voiceRegionsLazy = null; this.GatewayInfo = null; this.CurrentUser = null; this.CurrentApplication = null; for (var i = 0; i < this._shards.Count; i++) { if (this._shards.TryGetValue(i, out var client)) { this.UnhookEventHandlers(client); client.Dispose(); if (enableLogger) this.Logger.LogInformation(LoggerEvents.ShardShutdown, "Disconnected shard {0}.", i); } } this._shards.Clear(); return Task.CompletedTask; } #endregion #region Event Handler Initialization/Registering /// /// Sets the shard client up internally.. /// private void InternalSetup() { this._clientErrored = new AsyncEvent("CLIENT_ERRORED", DiscordClient.EventExecutionLimit, this.Goof); this._socketErrored = new AsyncEvent("SOCKET_ERRORED", DiscordClient.EventExecutionLimit, this.Goof); this._socketOpened = new AsyncEvent("SOCKET_OPENED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._socketClosed = new AsyncEvent("SOCKET_CLOSED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._ready = new AsyncEvent("READY", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._resumed = new AsyncEvent("RESUMED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelCreated = new AsyncEvent("CHANNEL_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelUpdated = new AsyncEvent("CHANNEL_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelDeleted = new AsyncEvent("CHANNEL_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._dmChannelDeleted = new AsyncEvent("DM_CHANNEL_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelPinsUpdated = new AsyncEvent("CHANNEL_PINS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildCreated = new AsyncEvent("GUILD_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildAvailable = new AsyncEvent("GUILD_AVAILABLE", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildUpdated = new AsyncEvent("GUILD_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildDeleted = new AsyncEvent("GUILD_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildUnavailable = new AsyncEvent("GUILD_UNAVAILABLE", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildDownloadCompleted = new AsyncEvent("GUILD_DOWNLOAD_COMPLETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._inviteCreated = new AsyncEvent("INVITE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._inviteDeleted = new AsyncEvent("INVITE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageCreated = new AsyncEvent("MESSAGE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._presenceUpdated = new AsyncEvent("PRESENCE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildBanAdded = new AsyncEvent("GUILD_BAN_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildBanRemoved = new AsyncEvent("GUILD_BAN_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildEmojisUpdated = new AsyncEvent("GUILD_EMOJI_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildStickersUpdated = new AsyncEvent("GUILD_STICKER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationsUpdated = new AsyncEvent("GUILD_INTEGRATIONS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberAdded = new AsyncEvent("GUILD_MEMBER_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberRemoved = new AsyncEvent("GUILD_MEMBER_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberUpdated = new AsyncEvent("GUILD_MEMBER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildRoleCreated = new AsyncEvent("GUILD_ROLE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildRoleUpdated = new AsyncEvent("GUILD_ROLE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildRoleDeleted = new AsyncEvent("GUILD_ROLE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageUpdated = new AsyncEvent("MESSAGE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageDeleted = new AsyncEvent("MESSAGE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageBulkDeleted = new AsyncEvent("MESSAGE_BULK_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._interactionCreated = new AsyncEvent("INTERACTION_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._componentInteractionCreated = new AsyncEvent("COMPONENT_INTERACTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._contextMenuInteractionCreated = new AsyncEvent("CONTEXT_MENU_INTERACTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._typingStarted = new AsyncEvent("TYPING_STARTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._userSettingsUpdated = new AsyncEvent("USER_SETTINGS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._userUpdated = new AsyncEvent("USER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._voiceStateUpdated = new AsyncEvent("VOICE_STATE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._voiceServerUpdated = new AsyncEvent("VOICE_SERVER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMembersChunk = new AsyncEvent("GUILD_MEMBERS_CHUNKED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._unknownEvent = new AsyncEvent("UNKNOWN_EVENT", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionAdded = new AsyncEvent("MESSAGE_REACTION_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemoved = new AsyncEvent("MESSAGE_REACTION_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionsCleared = new AsyncEvent("MESSAGE_REACTIONS_CLEARED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemovedEmoji = new AsyncEvent("MESSAGE_REACTION_REMOVED_EMOJI", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._webhooksUpdated = new AsyncEvent("WEBHOOKS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._heartbeated = new AsyncEvent("HEARTBEATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandCreated = new AsyncEvent("APPLICATION_COMMAND_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandUpdated = new AsyncEvent("APPLICATION_COMMAND_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandDeleted = new AsyncEvent("APPLICATION_COMMAND_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildApplicationCommandCountUpdated = new AsyncEvent("GUILD_APPLICATION_COMMAND_COUNTS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandPermissionsUpdated = new AsyncEvent("APPLICATION_COMMAND_PERMISSIONS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationCreated = new AsyncEvent("INTEGRATION_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationUpdated = new AsyncEvent("INTEGRATION_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationDeleted = new AsyncEvent("INTEGRATION_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._stageInstanceCreated = new AsyncEvent("STAGE_INSTANCE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._stageInstanceUpdated = new AsyncEvent("STAGE_INSTANCE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._stageInstanceDeleted = new AsyncEvent("STAGE_INSTANCE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadCreated = new AsyncEvent("THREAD_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadUpdated = new AsyncEvent("THREAD_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadDeleted = new AsyncEvent("THREAD_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadListSynced = new AsyncEvent("THREAD_LIST_SYNCED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadMemberUpdated = new AsyncEvent("THREAD_MEMBER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadMembersUpdated = new AsyncEvent("THREAD_MEMBERS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._zombied = new AsyncEvent("ZOMBIED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._payloadReceived = new AsyncEvent("PAYLOAD_RECEIVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventCreated = new AsyncEvent("GUILD_SCHEDULED_EVENT_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUpdated = new AsyncEvent("GUILD_SCHEDULED_EVENT_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventDeleted = new AsyncEvent("GUILD_SCHEDULED_EVENT_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUserAdded = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUserRemoved = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._embeddedActivityUpdated = new AsyncEvent("EMBEDDED_ACTIVITY_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberTimeoutAdded = new AsyncEvent("GUILD_MEMBER_TIMEOUT_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberTimeoutChanged = new AsyncEvent("GUILD_MEMBER_TIMEOUT_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberTimeoutRemoved = new AsyncEvent("GUILD_MEMBER_TIMEOUT_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); } /// /// Hooks the event handlers. /// /// The client. private void HookEventHandlers(DiscordClient client) { client.ClientErrored += this.Client_ClientError; client.SocketErrored += this.Client_SocketError; client.SocketOpened += this.Client_SocketOpened; client.SocketClosed += this.Client_SocketClosed; client.Ready += this.Client_Ready; client.Resumed += this.Client_Resumed; client.ChannelCreated += this.Client_ChannelCreated; client.ChannelUpdated += this.Client_ChannelUpdated; client.ChannelDeleted += this.Client_ChannelDeleted; client.DmChannelDeleted += this.Client_DMChannelDeleted; client.ChannelPinsUpdated += this.Client_ChannelPinsUpdated; client.GuildCreated += this.Client_GuildCreated; client.GuildAvailable += this.Client_GuildAvailable; client.GuildUpdated += this.Client_GuildUpdated; client.GuildDeleted += this.Client_GuildDeleted; client.GuildUnavailable += this.Client_GuildUnavailable; client.GuildDownloadCompleted += this.Client_GuildDownloadCompleted; client.InviteCreated += this.Client_InviteCreated; client.InviteDeleted += this.Client_InviteDeleted; client.MessageCreated += this.Client_MessageCreated; client.PresenceUpdated += this.Client_PresenceUpdate; client.GuildBanAdded += this.Client_GuildBanAdd; client.GuildBanRemoved += this.Client_GuildBanRemove; client.GuildEmojisUpdated += this.Client_GuildEmojisUpdate; client.GuildStickersUpdated += this.Client_GuildStickersUpdate; client.GuildIntegrationsUpdated += this.Client_GuildIntegrationsUpdate; client.GuildMemberAdded += this.Client_GuildMemberAdd; client.GuildMemberRemoved += this.Client_GuildMemberRemove; client.GuildMemberUpdated += this.Client_GuildMemberUpdate; client.GuildRoleCreated += this.Client_GuildRoleCreate; client.GuildRoleUpdated += this.Client_GuildRoleUpdate; client.GuildRoleDeleted += this.Client_GuildRoleDelete; client.MessageUpdated += this.Client_MessageUpdate; client.MessageDeleted += this.Client_MessageDelete; client.MessagesBulkDeleted += this.Client_MessageBulkDelete; client.InteractionCreated += this.Client_InteractionCreate; client.ComponentInteractionCreated += this.Client_ComponentInteractionCreate; client.ContextMenuInteractionCreated += this.Client_ContextMenuInteractionCreate; client.TypingStarted += this.Client_TypingStart; client.UserSettingsUpdated += this.Client_UserSettingsUpdate; client.UserUpdated += this.Client_UserUpdate; client.VoiceStateUpdated += this.Client_VoiceStateUpdate; client.VoiceServerUpdated += this.Client_VoiceServerUpdate; client.GuildMembersChunked += this.Client_GuildMembersChunk; client.UnknownEvent += this.Client_UnknownEvent; client.MessageReactionAdded += this.Client_MessageReactionAdd; client.MessageReactionRemoved += this.Client_MessageReactionRemove; client.MessageReactionsCleared += this.Client_MessageReactionRemoveAll; client.MessageReactionRemovedEmoji += this.Client_MessageReactionRemovedEmoji; client.WebhooksUpdated += this.Client_WebhooksUpdate; client.Heartbeated += this.Client_HeartBeated; client.ApplicationCommandCreated += this.Client_ApplicationCommandCreated; client.ApplicationCommandUpdated += this.Client_ApplicationCommandUpdated; client.ApplicationCommandDeleted += this.Client_ApplicationCommandDeleted; client.GuildApplicationCommandCountUpdated += this.Client_GuildApplicationCommandCountUpdated; client.ApplicationCommandPermissionsUpdated += this.Client_ApplicationCommandPermissionsUpdated; client.GuildIntegrationCreated += this.Client_GuildIntegrationCreated; client.GuildIntegrationUpdated += this.Client_GuildIntegrationUpdated; client.GuildIntegrationDeleted += this.Client_GuildIntegrationDeleted; client.StageInstanceCreated += this.Client_StageInstanceCreated; client.StageInstanceUpdated += this.Client_StageInstanceUpdated; client.StageInstanceDeleted += this.Client_StageInstanceDeleted; client.ThreadCreated += this.Client_ThreadCreated; client.ThreadUpdated += this.Client_ThreadUpdated; client.ThreadDeleted += this.Client_ThreadDeleted; client.ThreadListSynced += this.Client_ThreadListSynced; client.ThreadMemberUpdated += this.Client_ThreadMemberUpdated; client.ThreadMembersUpdated += this.Client_ThreadMembersUpdated; client.Zombied += this.Client_Zombied; client.PayloadReceived += this.Client_PayloadReceived; client.GuildScheduledEventCreated += this.Client_GuildScheduledEventCreated; client.GuildScheduledEventUpdated += this.Client_GuildScheduledEventUpdated; client.GuildScheduledEventDeleted += this.Client_GuildScheduledEventDeleted; client.GuildScheduledEventUserAdded += this.Client_GuildScheduledEventUserAdded; ; client.GuildScheduledEventUserRemoved += this.Client_GuildScheduledEventUserRemoved; client.EmbeddedActivityUpdated += this.Client_EmbeddedActivityUpdated; client.GuildMemberTimeoutAdded += this.Client_GuildMemberTimeoutAdded; client.GuildMemberTimeoutChanged += this.Client_GuildMemberTimeoutChanged; client.GuildMemberTimeoutRemoved += this.Client_GuildMemberTimeoutRemoved; } /// /// Unhooks the event handlers. /// /// The client. private void UnhookEventHandlers(DiscordClient client) { client.ClientErrored -= this.Client_ClientError; client.SocketErrored -= this.Client_SocketError; client.SocketOpened -= this.Client_SocketOpened; client.SocketClosed -= this.Client_SocketClosed; client.Ready -= this.Client_Ready; client.Resumed -= this.Client_Resumed; client.ChannelCreated -= this.Client_ChannelCreated; client.ChannelUpdated -= this.Client_ChannelUpdated; client.ChannelDeleted -= this.Client_ChannelDeleted; client.DmChannelDeleted -= this.Client_DMChannelDeleted; client.ChannelPinsUpdated -= this.Client_ChannelPinsUpdated; client.GuildCreated -= this.Client_GuildCreated; client.GuildAvailable -= this.Client_GuildAvailable; client.GuildUpdated -= this.Client_GuildUpdated; client.GuildDeleted -= this.Client_GuildDeleted; client.GuildUnavailable -= this.Client_GuildUnavailable; client.GuildDownloadCompleted -= this.Client_GuildDownloadCompleted; client.InviteCreated -= this.Client_InviteCreated; client.InviteDeleted -= this.Client_InviteDeleted; client.MessageCreated -= this.Client_MessageCreated; client.PresenceUpdated -= this.Client_PresenceUpdate; client.GuildBanAdded -= this.Client_GuildBanAdd; client.GuildBanRemoved -= this.Client_GuildBanRemove; client.GuildEmojisUpdated -= this.Client_GuildEmojisUpdate; client.GuildStickersUpdated -= this.Client_GuildStickersUpdate; client.GuildIntegrationsUpdated -= this.Client_GuildIntegrationsUpdate; client.GuildMemberAdded -= this.Client_GuildMemberAdd; client.GuildMemberRemoved -= this.Client_GuildMemberRemove; client.GuildMemberUpdated -= this.Client_GuildMemberUpdate; client.GuildRoleCreated -= this.Client_GuildRoleCreate; client.GuildRoleUpdated -= this.Client_GuildRoleUpdate; client.GuildRoleDeleted -= this.Client_GuildRoleDelete; client.MessageUpdated -= this.Client_MessageUpdate; client.MessageDeleted -= this.Client_MessageDelete; client.MessagesBulkDeleted -= this.Client_MessageBulkDelete; client.InteractionCreated -= this.Client_InteractionCreate; client.ComponentInteractionCreated -= this.Client_ComponentInteractionCreate; client.ContextMenuInteractionCreated -= this.Client_ContextMenuInteractionCreate; client.TypingStarted -= this.Client_TypingStart; client.UserSettingsUpdated -= this.Client_UserSettingsUpdate; client.UserUpdated -= this.Client_UserUpdate; client.VoiceStateUpdated -= this.Client_VoiceStateUpdate; client.VoiceServerUpdated -= this.Client_VoiceServerUpdate; client.GuildMembersChunked -= this.Client_GuildMembersChunk; client.UnknownEvent -= this.Client_UnknownEvent; client.MessageReactionAdded -= this.Client_MessageReactionAdd; client.MessageReactionRemoved -= this.Client_MessageReactionRemove; client.MessageReactionsCleared -= this.Client_MessageReactionRemoveAll; client.MessageReactionRemovedEmoji -= this.Client_MessageReactionRemovedEmoji; client.WebhooksUpdated -= this.Client_WebhooksUpdate; client.Heartbeated -= this.Client_HeartBeated; client.ApplicationCommandCreated -= this.Client_ApplicationCommandCreated; client.ApplicationCommandUpdated -= this.Client_ApplicationCommandUpdated; client.ApplicationCommandDeleted -= this.Client_ApplicationCommandDeleted; client.GuildApplicationCommandCountUpdated -= this.Client_GuildApplicationCommandCountUpdated; client.ApplicationCommandPermissionsUpdated -= this.Client_ApplicationCommandPermissionsUpdated; client.GuildIntegrationCreated -= this.Client_GuildIntegrationCreated; client.GuildIntegrationUpdated -= this.Client_GuildIntegrationUpdated; client.GuildIntegrationDeleted -= this.Client_GuildIntegrationDeleted; client.StageInstanceCreated -= this.Client_StageInstanceCreated; client.StageInstanceUpdated -= this.Client_StageInstanceUpdated; client.StageInstanceDeleted -= this.Client_StageInstanceDeleted; client.ThreadCreated -= this.Client_ThreadCreated; client.ThreadUpdated -= this.Client_ThreadUpdated; client.ThreadDeleted -= this.Client_ThreadDeleted; client.ThreadListSynced -= this.Client_ThreadListSynced; client.ThreadMemberUpdated -= this.Client_ThreadMemberUpdated; client.ThreadMembersUpdated -= this.Client_ThreadMembersUpdated; client.Zombied -= this.Client_Zombied; client.PayloadReceived -= this.Client_PayloadReceived; client.GuildScheduledEventCreated -= this.Client_GuildScheduledEventCreated; client.GuildScheduledEventUpdated -= this.Client_GuildScheduledEventUpdated; client.GuildScheduledEventDeleted -= this.Client_GuildScheduledEventDeleted; client.GuildScheduledEventUserAdded -= this.Client_GuildScheduledEventUserAdded; ; client.GuildScheduledEventUserRemoved -= this.Client_GuildScheduledEventUserRemoved; client.EmbeddedActivityUpdated -= this.Client_EmbeddedActivityUpdated; client.GuildMemberTimeoutAdded -= this.Client_GuildMemberTimeoutAdded; client.GuildMemberTimeoutChanged -= this.Client_GuildMemberTimeoutChanged; client.GuildMemberTimeoutRemoved -= this.Client_GuildMemberTimeoutRemoved; } /// /// Gets the shard id from guilds. /// /// The id. /// An int. private int GetShardIdFromGuilds(ulong id) { foreach (var s in this._shards.Values) { if (s.GuildsInternal.TryGetValue(id, out _)) { return s.ShardId; } } return -1; } #endregion #region Destructor ~DiscordShardedClient() { this.InternalStopAsync(false).GetAwaiter().GetResult(); } #endregion } diff --git a/DisCatSharp/DiscordConfiguration.cs b/DisCatSharp/DiscordConfiguration.cs index 9f28db6f0..58c670f55 100644 --- a/DisCatSharp/DiscordConfiguration.cs +++ b/DisCatSharp/DiscordConfiguration.cs @@ -1,290 +1,290 @@ // 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.Net; using DisCatSharp.Entities; using DisCatSharp.Net.Udp; using DisCatSharp.Net.WebSocket; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace DisCatSharp; /// /// Represents configuration for and . /// public sealed class DiscordConfiguration { /// /// Sets the token used to identify the client. /// public string Token { internal get => this._token; set { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value), "Token cannot be null, empty, or all whitespace."); this._token = value.Trim(); } } private string _token = ""; /// /// Sets the type of the token used to identify the client. /// Defaults to . /// public TokenType TokenType { internal get; set; } = TokenType.Bot; /// /// Sets the minimum logging level for messages. - /// Typically, the default value of is ok for most uses. + /// Typically, the default value of is ok for most uses. /// public LogLevel MinimumLogLevel { internal get; set; } = LogLevel.Information; /// /// Overwrites the api version. /// Defaults to 10. /// public string ApiVersion { internal get; set; } = "10"; /// /// Sets whether to rely on Discord for NTP (Network Time Protocol) synchronization with the "X-Ratelimit-Reset-After" header. /// If the system clock is unsynced, setting this to true will ensure ratelimits are synced with Discord and reduce the risk of hitting one. /// This should only be set to false if the system clock is synced with NTP. /// Defaults to true. /// public bool UseRelativeRatelimit { internal get; set; } = true; /// /// Allows you to overwrite the time format used by the internal debug logger. /// Only applicable when is set left at default value. Defaults to ISO 8601-like format. /// public string LogTimestampFormat { internal get; set; } = "yyyy-MM-dd HH:mm:ss zzz"; /// /// Sets the member count threshold at which guilds are considered large. /// Defaults to 250. /// public int LargeThreshold { internal get; set; } = 250; /// /// Sets whether to automatically reconnect in case a connection is lost. /// Defaults to true. /// public bool AutoReconnect { internal get; set; } = true; /// /// Sets the ID of the shard to connect to. /// If not sharding, or sharding automatically, this value should be left with the default value of 0. /// public int ShardId { internal get; set; } /// /// Sets the total number of shards the bot is on. If not sharding, this value should be left with a default value of 1. /// If sharding automatically, this value will indicate how many shards to boot. If left default for automatic sharding, the client will determine the shard count automatically. /// public int ShardCount { internal get; set; } = 1; /// /// Sets the level of compression for WebSocket traffic. /// Disabling this option will increase the amount of traffic sent via WebSocket. Setting will enable compression for READY and GUILD_CREATE payloads. Setting will enable compression for the entire WebSocket stream, drastically reducing amount of traffic. /// Defaults to . /// public GatewayCompressionLevel GatewayCompressionLevel { internal get; set; } = GatewayCompressionLevel.Stream; /// /// Sets the size of the global message cache. /// Setting this to 0 will disable message caching entirely. Defaults to 1024. /// public int MessageCacheSize { internal get; set; } = 1024; /// /// Sets the proxy to use for HTTP and WebSocket connections to Discord. /// Defaults to null. /// public IWebProxy Proxy { internal get; set; } /// /// Sets the timeout for HTTP requests. /// Set to to disable timeouts. /// Defaults to 20 seconds. /// public TimeSpan HttpTimeout { internal get; set; } = TimeSpan.FromSeconds(20); /// /// Defines that the client should attempt to reconnect indefinitely. /// This is typically a very bad idea to set to true, as it will swallow all connection errors. /// Defaults to false. /// public bool ReconnectIndefinitely { internal get; set; } /// /// Sets whether the client should attempt to cache members if exclusively using unprivileged intents. /// /// This will only take effect if there are no or /// intents specified. Otherwise, this will always be overwritten to true. /// /// Defaults to true. /// public bool AlwaysCacheMembers { internal get; set; } = true; /// /// Sets the gateway intents for this client. /// If set, the client will only receive events that they specify with intents. /// Defaults to . /// public DiscordIntents Intents { internal get; set; } = DiscordIntents.AllUnprivileged; /// /// Sets the factory method used to create instances of WebSocket clients. - /// Use and equivalents on other implementations to switch out client implementations. - /// Defaults to . + /// Use and equivalents on other implementations to switch out client implementations. + /// Defaults to . /// public WebSocketClientFactoryDelegate WebSocketClientFactory { internal get => this._webSocketClientFactory; set { if (value == null) throw new InvalidOperationException("You need to supply a valid WebSocket client factory method."); this._webSocketClientFactory = value; } } private WebSocketClientFactoryDelegate _webSocketClientFactory = WebSocketClient.CreateNew; /// /// Sets the factory method used to create instances of UDP clients. /// Use and equivalents on other implementations to switch out client implementations. /// Defaults to . /// public UdpClientFactoryDelegate UdpClientFactory { internal get => this._udpClientFactory; set => this._udpClientFactory = value ?? throw new InvalidOperationException("You need to supply a valid UDP client factory method."); } private UdpClientFactoryDelegate _udpClientFactory = DcsUdpClient.CreateNew; /// /// Sets the logger implementation to use. /// To create your own logger, implement the instance. /// Defaults to built-in implementation. /// public ILoggerFactory LoggerFactory { internal get; set; } /// /// Sets if the bot's status should show the mobile icon. /// Defaults to false. /// public bool MobileStatus { internal get; set; } /// /// Whether to use canary. has to be false. /// Defaults to false. /// public bool UseCanary { internal get; set; } /// /// Whether to use ptb. has to be false. /// Defaults to false. /// public bool UsePtb { internal get; set; } /// /// Refresh full guild channel cache. /// Defaults to false. /// public bool AutoRefreshChannelCache { internal get; set; } /// /// Do not use, this is meant for DisCatSharp Devs. /// Defaults to null. /// public string Override { internal get; set; } /// /// Sets your preferred API language. See for valid locales. /// public string Locale { internal get; set; } = DiscordLocales.AMERICAN_ENGLISH; /// /// Sets the service provider. /// This allows passing data around without resorting to static members. /// Defaults to an empty service provider. /// public IServiceProvider ServiceProvider { internal get; set; } = new ServiceCollection().BuildServiceProvider(true); /// /// Creates a new configuration with default values. /// public DiscordConfiguration() { } /// /// Utilized via Dependency Injection Pipeline /// /// [ActivatorUtilitiesConstructor] public DiscordConfiguration(IServiceProvider provider) { this.ServiceProvider = provider; } /// /// Creates a clone of another discord configuration. /// /// Client configuration to clone. public DiscordConfiguration(DiscordConfiguration other) { this.Token = other.Token; this.TokenType = other.TokenType; this.MinimumLogLevel = other.MinimumLogLevel; this.UseRelativeRatelimit = other.UseRelativeRatelimit; this.LogTimestampFormat = other.LogTimestampFormat; this.LargeThreshold = other.LargeThreshold; this.AutoReconnect = other.AutoReconnect; this.ShardId = other.ShardId; this.ShardCount = other.ShardCount; this.GatewayCompressionLevel = other.GatewayCompressionLevel; this.MessageCacheSize = other.MessageCacheSize; this.WebSocketClientFactory = other.WebSocketClientFactory; this.UdpClientFactory = other.UdpClientFactory; this.Proxy = other.Proxy; this.HttpTimeout = other.HttpTimeout; this.ReconnectIndefinitely = other.ReconnectIndefinitely; this.Intents = other.Intents; this.LoggerFactory = other.LoggerFactory; this.MobileStatus = other.MobileStatus; this.UseCanary = other.UseCanary; this.UsePtb = other.UsePtb; this.AutoRefreshChannelCache = other.AutoRefreshChannelCache; this.ApiVersion = other.ApiVersion; this.ServiceProvider = other.ServiceProvider; this.Override = other.Override; this.Locale = other.Locale; } } diff --git a/DisCatSharp/Entities/Channel/DiscordChannel.cs b/DisCatSharp/Entities/Channel/DiscordChannel.cs index b01b11743..aea3d05d5 100644 --- a/DisCatSharp/Entities/Channel/DiscordChannel.cs +++ b/DisCatSharp/Entities/Channel/DiscordChannel.cs @@ -1,1420 +1,1420 @@ // 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.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; } /// /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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, reason).ConfigureAwait(false); } /// /// Returns a specific message /// /// The id of the message /// Whether to bypass the message cache - /// 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 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 bypassCache = false) => this.Discord.Configuration.MessageCacheSize > 0 && !bypassCache && 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); /// /// 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. + /// 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.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. + /// 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) 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.ModifyForumChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Template, mdl.Nsfw, mdl.Parent.Map(p => p?.Id), mdl.DefaultReactionEmoji, mdl.PerUserRateLimit, mdl.PostCreateUserRateLimit, mdl.DefaultAutoArchiveDuration, mdl.PermissionOverwrites, 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. + /// 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. + /// 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; } } } /// /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. 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. + /// 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, TargetActivity? targetApplication = null, ulong? targetUser = null, string reason = null) => this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, maxAge, maxUses, targetType, targetApplication, 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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 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 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. + /// 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) { if (this.Type != ChannelType.Forum) throw new NotSupportedException("Parent channel must be forum."); else return 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. + /// 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. + /// 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. + /// 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. public ForumPostTag GetForumPostTag(ulong id) { var tag = this.InternalAvailableTags.First(x => x.Id == id); 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. + /// 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, bool moderated = false, string reason = null) => await this.Discord.ApiClient.CreateForumTagAsync(this.Id, name, emoji, moderated, 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. + /// 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 Task DeleteForumPostTag(ulong id, string reason = null) => this.Discord.ApiClient.DeleteForumTagAsync(id, this.Id, 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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. + /// 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 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. + /// 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/Channel/DiscordDmChannel.cs b/DisCatSharp/Entities/Channel/DiscordDmChannel.cs index 24e6b9e77..b81343aa2 100644 --- a/DisCatSharp/Entities/Channel/DiscordDmChannel.cs +++ b/DisCatSharp/Entities/Channel/DiscordDmChannel.cs @@ -1,92 +1,92 @@ // 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.Collections.Generic; using System.Globalization; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a direct message channel. /// public class DiscordDmChannel : DiscordChannel { /// /// Gets the recipients of this direct message. /// [JsonProperty("recipients", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyList Recipients { get; internal set; } /// /// Gets the hash of this channel's icon. /// [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] public string IconHash { get; internal set; } /// /// Gets the id of this direct message's creator. /// [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] public ulong OwnerId { get; internal set; } /// /// Gets the application id of the direct message's creator if it a bot. /// [JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)] public ulong ApplicationId { get; internal set; } /// /// Gets the URL of this channel's icon. /// [JsonIgnore] public string IconUrl => !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.CHANNEL_ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.png" : null; /// /// Only use for Group DMs! Whitelisted bots only. Requires user's oauth2 access token. /// /// The id of the user to add. /// The OAuth2 access token. /// The nickname to give to the user. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task AddDmRecipientAsync(ulong userId, string accessToken, string nickname) => this.Discord.ApiClient.AddGroupDmRecipientAsync(this.Id, userId, accessToken, nickname); /// /// Only use for Group DMs! Whitelisted bots only. Requires user's oauth2 access token. /// /// The id of the User to remove. /// The OAuth2 access token. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RemoveDmRecipientAsync(ulong userId, string accessToken) => this.Discord.ApiClient.RemoveGroupDmRecipientAsync(this.Id, userId); } diff --git a/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwrite.cs b/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwrite.cs index 71c61516d..f847dea21 100644 --- a/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwrite.cs +++ b/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwrite.cs @@ -1,123 +1,123 @@ // 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.Threading.Tasks; using DisCatSharp.Enums; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a permission overwrite for a channel. /// public class DiscordOverwrite : SnowflakeObject { /// /// Gets the type of the overwrite. Either "role" or "member". /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public OverwriteType Type { get; internal set; } /// /// Gets the allowed permission set. /// [JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)] public Permissions Allowed { get; internal set; } /// /// Gets the denied permission set. /// [JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)] public Permissions Denied { get; internal set; } [JsonIgnore] internal ulong ChannelId; #region Methods /// /// Deletes this channel overwrite. /// /// Reason as to why this overwrite gets deleted. - /// Thrown when the client does not have the permission. - /// Thrown when the overwrite does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the overwrite 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.DeleteChannelPermissionAsync(this.ChannelId, this.Id, reason); /// /// Updates this channel overwrite. /// /// Permissions that are allowed. /// Permissions that are denied. /// Reason as to why you made this change. - /// Thrown when the client does not have the permission. - /// Thrown when the overwrite does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the overwrite does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task UpdateAsync(Permissions? allow = null, Permissions? deny = null, string reason = null) => this.Discord.ApiClient.EditChannelPermissionsAsync(this.ChannelId, this.Id, allow ?? this.Allowed, deny ?? this.Denied, this.Type.ToString().ToLowerInvariant(), reason); /// /// Gets the DiscordMember that is affected by this overwrite. /// /// The DiscordMember that is affected by this overwrite - /// Thrown when the client does not have the permission. - /// Thrown when the overwrite does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the overwrite does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetMemberAsync() => this.Type != OverwriteType.Member ? throw new ArgumentException(nameof(this.Type), "This overwrite is for a role, not a member.") : await (await this.Discord.ApiClient.GetChannelAsync(this.ChannelId).ConfigureAwait(false)).Guild.GetMemberAsync(this.Id).ConfigureAwait(false); /// /// Gets the DiscordRole that is affected by this overwrite. /// /// The DiscordRole that is affected by this overwrite - /// Thrown when the role does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the role does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetRoleAsync() => this.Type != OverwriteType.Role ? throw new ArgumentException(nameof(this.Type), "This overwrite is for a member, not a role.") : (await this.Discord.ApiClient.GetChannelAsync(this.ChannelId).ConfigureAwait(false)).Guild.GetRole(this.Id); #endregion /// /// Initializes a new instance of the class. /// internal DiscordOverwrite() { } /// /// Checks whether given permissions are allowed, denied, or not set. /// /// Permissions to check. /// Whether given permissions are allowed, denied, or not set. public PermissionLevel CheckPermission(Permissions permission) => (this.Allowed & permission) != 0 ? PermissionLevel.Allowed : (this.Denied & permission) != 0 ? PermissionLevel.Denied : PermissionLevel.Unset; } diff --git a/DisCatSharp/Entities/DiscordUri.cs b/DisCatSharp/Entities/DiscordUri.cs index 95eefc038..3ef455f2d 100644 --- a/DisCatSharp/Entities/DiscordUri.cs +++ b/DisCatSharp/Entities/DiscordUri.cs @@ -1,161 +1,161 @@ // 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.CompilerServices; using Newtonsoft.Json; namespace DisCatSharp.Net; /// /// An URI in a Discord embed doesn't necessarily conform to the RFC 3986. If it uses the attachment:// /// protocol, it mustn't contain a trailing slash to be interpreted correctly as an embed attachment reference by /// Discord. /// [JsonConverter(typeof(DiscordUriJsonConverter))] public class DiscordUri { private readonly object _value; /// /// The type of this URI. /// public DiscordUriType Type { get; } /// /// Initializes a new instance of the class. /// /// The value. internal DiscordUri(Uri value) { this._value = value ?? throw new ArgumentNullException(nameof(value)); this.Type = DiscordUriType.Standard; } /// /// Initializes a new instance of the class. /// /// The value. internal DiscordUri(string value) { if (value == null) throw new ArgumentNullException(nameof(value)); if (IsStandard(value)) { this._value = new Uri(value); this.Type = DiscordUriType.Standard; } else { this._value = value; this.Type = DiscordUriType.NonStandard; } } /// /// Whether the uri is a standard uri /// /// Uri string [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsStandard(string value) => !value.StartsWith("attachment://"); /// /// Returns a string representation of this DiscordUri. /// /// This DiscordUri, as a string. public override string ToString() => this._value.ToString(); /// - /// Converts this DiscordUri into a canonical representation of a if it can be represented as + /// Converts this DiscordUri into a canonical representation of a if it can be represented as /// such, throwing an exception otherwise. /// /// A canonical representation of this DiscordUri. - /// If is not , as + /// If is not , as /// that would mean creating an invalid Uri, which would result in loss of data. public Uri ToUri() => this.Type == DiscordUriType.Standard ? this._value as Uri : throw new UriFormatException( $@"DiscordUri ""{this._value}"" would be invalid as a regular URI, please the {nameof(this.Type)} property first."); /// /// Represents a uri json converter. /// internal sealed class DiscordUriJsonConverter : JsonConverter { /// /// Writes the json. /// /// The writer. /// The value. /// The serializer. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => writer.WriteValue((value as DiscordUri)._value); /// /// Reads the json. /// /// The reader. /// The object type. /// The existing value. /// The serializer. public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var val = reader.Value; return val == null ? null : val is not string s ? throw new JsonReaderException("DiscordUri value invalid format! This is a bug in DisCatSharp. " + $"Include the type in your bug report: [[{reader.TokenType}]]") : IsStandard(s) ? new DiscordUri(new Uri(s)) : new DiscordUri(s); } /// /// Whether it can be converted. /// /// The object type. /// A bool. public override bool CanConvert(Type objectType) => objectType == typeof(DiscordUri); } } /// /// Represents a uri type. /// public enum DiscordUriType : byte { /// - /// Represents a URI that conforms to RFC 3986, meaning it's stored internally as a and will + /// Represents a URI that conforms to RFC 3986, meaning it's stored internally as a and will /// contain a trailing slash after the domain name. /// Standard, /// /// Represents a URI that does not conform to RFC 3986, meaning it's stored internally as a plain string and /// should be treated as one. /// NonStandard } diff --git a/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs b/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs index 2fb98ea37..d1c3ee389 100644 --- a/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs +++ b/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs @@ -1,1321 +1,1321 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; 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. + /// 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 : (ulong?)last, byMember?.Id, (int?)actionType).ConfigureAwait(false); ac = alr.Entries.Count(); tc += ac; if (ac > 0) { last = alr.Entries.Last().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); 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 old_tags = xc.OldValues?.OfType() ?.Select(xjo => xjo.ToObject()) ?.Select(xo => { xo.Discord = this.Discord; return xo; }); var new_tags = xc.NewValues?.OfType() ?.Select(xjo => xjo.ToObject()) ?.Select(xo => { xo.Discord = this.Discord; return xo; }); entrychn.AvailableTagsChange = new PropertyChange> { Before = old_tags != null ? new List(new List(old_tags)) : null, After = new_tags != null ? new List(new List(new_tags)) : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in channel update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.OverwriteCreate: case AuditLogActionType.OverwriteDelete: case AuditLogActionType.OverwriteUpdate: entry = new DiscordAuditLogOverwriteEntry { Target = this.GetChannel(xac.TargetId.Value)?.PermissionOverwrites.FirstOrDefault(xo => xo.Id == xac.Options.Id), Channel = this.GetChannel(xac.TargetId.Value) }; var entryovr = entry as DiscordAuditLogOverwriteEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "deny": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryovr.DenyChange = new PropertyChange { Before = p1 ? (Permissions?)t1 : null, After = p2 ? (Permissions?)t2 : null }; break; case "allow": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryovr.AllowChange = new PropertyChange { Before = p1 ? (Permissions?)t1 : null, After = p2 ? (Permissions?)t2 : null }; break; case "type": entryovr.TypeChange = new PropertyChange { Before = xc.OldValue != null ? (OverwriteType)(long)xc.OldValue : null, After = xc.NewValue != null ? (OverwriteType)(long)xc.NewValue : null }; break; case "id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryovr.TargetIdChange = new PropertyChange { Before = p1 ? (ulong?)t1 : null, After = p2 ? (ulong?)t2 : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in overwrite update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.Kick: entry = new DiscordAuditLogKickEntry { Target = amd.TryGetValue(xac.TargetId.Value, out var kickMember) ? kickMember : new DiscordMember { Id = xac.TargetId.Value, Discord = this.Discord, GuildId = this.Id } }; break; case AuditLogActionType.Prune: entry = new DiscordAuditLogPruneEntry { Days = xac.Options.DeleteMemberDays, Toll = xac.Options.MembersRemoved }; break; case AuditLogActionType.Ban: case AuditLogActionType.Unban: entry = new DiscordAuditLogBanEntry { Target = amd.TryGetValue(xac.TargetId.Value, out var unbanMember) ? unbanMember : new DiscordMember { Id = xac.TargetId.Value, Discord = this.Discord, GuildId = this.Id } }; break; case AuditLogActionType.MemberUpdate: case AuditLogActionType.MemberRoleUpdate: entry = new DiscordAuditLogMemberUpdateEntry { Target = amd.TryGetValue(xac.TargetId.Value, out var roleUpdMember) ? roleUpdMember : new DiscordMember { Id = xac.TargetId.Value, Discord = this.Discord, GuildId = this.Id } }; var entrymbu = entry as DiscordAuditLogMemberUpdateEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "nick": entrymbu.NicknameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "deaf": entrymbu.DeafenChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "mute": entrymbu.MuteChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "communication_disabled_until": entrymbu.CommunicationDisabledUntilChange = new PropertyChange { Before = (DateTime?)xc.OldValue, After = (DateTime?)xc.NewValue }; break; case "$add": entrymbu.AddedRoles = new ReadOnlyCollection(xc.NewValues.Select(xo => (ulong)xo["id"]).Select(this.GetRole).ToList()); break; case "$remove": entrymbu.RemovedRoles = new ReadOnlyCollection(xc.NewValues.Select(xo => (ulong)xo["id"]).Select(this.GetRole).ToList()); break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in member update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.RoleCreate: case AuditLogActionType.RoleDelete: case AuditLogActionType.RoleUpdate: entry = new DiscordAuditLogRoleUpdateEntry { Target = this.GetRole(xac.TargetId.Value) ?? new DiscordRole { Id = xac.TargetId.Value, Discord = this.Discord } }; var entryrol = entry as DiscordAuditLogRoleUpdateEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entryrol.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "color": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryrol.ColorChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; case "permissions": entryrol.PermissionChange = new PropertyChange { Before = xc.OldValue != null ? (Permissions?)long.Parse((string)xc.OldValue) : null, After = xc.NewValue != null ? (Permissions?)long.Parse((string)xc.NewValue) : null }; break; case "position": entryrol.PositionChange = new PropertyChange { Before = xc.OldValue != null ? (int?)(long)xc.OldValue : null, After = xc.NewValue != null ? (int?)(long)xc.NewValue : null, }; break; case "mentionable": entryrol.MentionableChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "hoist": entryrol.HoistChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "icon_hash": entryrol.IconHashChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in role update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.InviteCreate: case AuditLogActionType.InviteDelete: case AuditLogActionType.InviteUpdate: entry = new DiscordAuditLogInviteEntry(); var inv = new DiscordInvite { Discord = this.Discord, Guild = new DiscordInviteGuild { Discord = this.Discord, Id = this.Id, Name = this.Name, SplashHash = this.SplashHash } }; var entryinv = entry as DiscordAuditLogInviteEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "max_age": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryinv.MaxAgeChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; case "code": inv.Code = xc.OldValueString ?? xc.NewValueString; entryinv.CodeChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "temporary": entryinv.TemporaryChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "inviter_id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryinv.InviterChange = new PropertyChange { Before = amd.TryGetValue(t1, out var propBeforeMember) ? propBeforeMember : new DiscordMember { Id = t1, Discord = this.Discord, GuildId = this.Id }, After = amd.TryGetValue(t2, out var propAfterMember) ? propAfterMember : new DiscordMember { Id = t1, Discord = this.Discord, GuildId = this.Id }, }; break; case "channel_id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryinv.ChannelChange = new PropertyChange { Before = p1 ? this.GetChannel(t1) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null, After = p2 ? this.GetChannel(t2) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null }; var ch = entryinv.ChannelChange.Before ?? entryinv.ChannelChange.After; var cht = ch?.Type; inv.Channel = new DiscordInviteChannel { Discord = this.Discord, Id = p1 ? t1 : t2, Name = ch?.Name, Type = cht != null ? cht.Value : ChannelType.Unknown }; break; case "uses": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryinv.UsesChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; case "max_uses": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryinv.MaxUsesChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; // TODO: Add changes for target application default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in invite update: {0} - this should be reported to library developers", xc.Key); break; } } entryinv.Target = inv; break; case AuditLogActionType.WebhookCreate: case AuditLogActionType.WebhookDelete: case AuditLogActionType.WebhookUpdate: entry = new DiscordAuditLogWebhookEntry { Target = ahd.TryGetValue(xac.TargetId.Value, out var webhook) ? webhook : new DiscordWebhook { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrywhk = entry as DiscordAuditLogWebhookEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "application_id": // ??? p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrywhk.IdChange = new PropertyChange { Before = p1 ? t1 : null, After = p2 ? t2 : null }; break; case "name": entrywhk.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "channel_id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrywhk.ChannelChange = new PropertyChange { Before = p1 ? this.GetChannel(t1) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null, After = p2 ? this.GetChannel(t2) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null }; break; case "type": // ??? p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entrywhk.TypeChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; case "avatar_hash": entrywhk.AvatarHashChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in webhook update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.EmojiCreate: case AuditLogActionType.EmojiDelete: case AuditLogActionType.EmojiUpdate: entry = new DiscordAuditLogEmojiEntry { Target = this.EmojisInternal.TryGetValue(xac.TargetId.Value, out var target) ? target : new DiscordEmoji { Id = xac.TargetId.Value, Discord = this.Discord } }; var entryemo = entry as DiscordAuditLogEmojiEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entryemo.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in emote update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.StageInstanceCreate: case AuditLogActionType.StageInstanceDelete: case AuditLogActionType.StageInstanceUpdate: entry = new DiscordAuditLogStageEntry { Target = this.StageInstancesInternal.TryGetValue(xac.TargetId.Value, out var stage) ? stage : new DiscordStageInstance { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrysta = entry as DiscordAuditLogStageEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "topic": entrysta.TopicChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "privacy_level": entrysta.PrivacyLevelChange = new PropertyChange { Before = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5) ? (StagePrivacyLevel?)t5 : null, After = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6) ? (StagePrivacyLevel?)t6 : null, }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in stage instance update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.StickerCreate: case AuditLogActionType.StickerDelete: case AuditLogActionType.StickerUpdate: entry = new DiscordAuditLogStickerEntry { Target = this.StickersInternal.TryGetValue(xac.TargetId.Value, out var sticker) ? sticker : new DiscordSticker { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrysti = entry as DiscordAuditLogStickerEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entrysti.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "description": entrysti.DescriptionChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "tags": entrysti.TagsChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "guild_id": entrysti.GuildIdChange = new PropertyChange { Before = ulong.TryParse(xc.OldValueString, out var ogid) ? ogid : null, After = ulong.TryParse(xc.NewValueString, out var ngid) ? ngid : null }; break; case "available": entrysti.AvailabilityChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue, }; break; case "asset": entrysti.AssetChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "id": entrysti.IdChange = new PropertyChange { Before = ulong.TryParse(xc.OldValueString, out var oid) ? oid : null, After = ulong.TryParse(xc.NewValueString, out var nid) ? nid : null }; break; case "type": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entrysti.TypeChange = new PropertyChange { Before = p1 ? (StickerType?)t5 : null, After = p2 ? (StickerType?)t6 : null }; break; case "format_type": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entrysti.FormatChange = new PropertyChange { Before = p1 ? (StickerFormat?)t5 : null, After = p2 ? (StickerFormat?)t6 : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in sticker update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.MessageDelete: case AuditLogActionType.MessageBulkDelete: { entry = new DiscordAuditLogMessageEntry(); var entrymsg = entry as DiscordAuditLogMessageEntry; if (xac.Options != null) { entrymsg.Channel = this.GetChannel(xac.Options.ChannelId) ?? new DiscordChannel { Id = xac.Options.ChannelId, Discord = this.Discord, GuildId = this.Id }; entrymsg.MessageCount = xac.Options.Count; } if (entrymsg.Channel != null) { entrymsg.Target = this.Discord is DiscordClient dc && dc.MessageCache != null && dc.MessageCache.TryGet(xm => xm.Id == xac.TargetId.Value && xm.ChannelId == entrymsg.Channel.Id, out var msg) ? msg : new DiscordMessage { Discord = this.Discord, Id = xac.TargetId.Value }; } break; } case AuditLogActionType.MessagePin: case AuditLogActionType.MessageUnpin: { entry = new DiscordAuditLogMessagePinEntry(); var entrypin = entry as DiscordAuditLogMessagePinEntry; if (this.Discord is not DiscordClient dc) { break; } if (xac.Options != null) { DiscordMessage message = default; dc.MessageCache?.TryGet(x => x.Id == xac.Options.MessageId && x.ChannelId == xac.Options.ChannelId, out message); entrypin.Channel = this.GetChannel(xac.Options.ChannelId) ?? new DiscordChannel { Id = xac.Options.ChannelId, Discord = this.Discord, GuildId = this.Id }; entrypin.Message = message ?? new DiscordMessage { Id = xac.Options.MessageId, Discord = this.Discord }; } if (xac.TargetId.HasValue) { dc.UserCache.TryGetValue(xac.TargetId.Value, out var user); entrypin.Target = user ?? new DiscordUser { Id = user.Id, Discord = this.Discord }; } break; } case AuditLogActionType.BotAdd: { entry = new DiscordAuditLogBotAddEntry(); if (!(this.Discord is DiscordClient dc && xac.TargetId.HasValue)) { break; } dc.UserCache.TryGetValue(xac.TargetId.Value, out var bot); (entry as DiscordAuditLogBotAddEntry).TargetBot = bot ?? new DiscordUser { Id = xac.TargetId.Value, Discord = this.Discord }; break; } case AuditLogActionType.MemberMove: entry = new DiscordAuditLogMemberMoveEntry(); if (xac.Options == null) { break; } var moveentry = entry as DiscordAuditLogMemberMoveEntry; moveentry.UserCount = xac.Options.Count; moveentry.Channel = this.GetChannel(xac.Options.ChannelId) ?? new DiscordChannel { Id = xac.Options.ChannelId, Discord = this.Discord, GuildId = this.Id }; break; case AuditLogActionType.MemberDisconnect: entry = new DiscordAuditLogMemberDisconnectEntry { UserCount = xac.Options?.Count ?? 0 }; break; case AuditLogActionType.IntegrationCreate: case AuditLogActionType.IntegrationDelete: case AuditLogActionType.IntegrationUpdate: entry = new DiscordAuditLogIntegrationEntry(); var integentry = entry as DiscordAuditLogIntegrationEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "type": integentry.Type = new PropertyChange() { Before = xc.OldValueString, After = xc.NewValueString }; break; case "enable_emoticons": integentry.EnableEmoticons = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "expire_behavior": integentry.ExpireBehavior = new PropertyChange { Before = (int?)xc.OldValue, After = (int?)xc.NewValue }; break; case "expire_grace_period": integentry.ExpireBehavior = new PropertyChange { Before = (int?)xc.OldValue, After = (int?)xc.NewValue }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in integration update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.ThreadCreate: case AuditLogActionType.ThreadDelete: case AuditLogActionType.ThreadUpdate: entry = new DiscordAuditLogThreadEntry { Target = this.ThreadsInternal.TryGetValue(xac.TargetId.Value, out var thread) ? thread : new DiscordThreadChannel { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrythr = entry as DiscordAuditLogThreadEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entrythr.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); entrythr.TypeChange = new PropertyChange { Before = p1 ? (ChannelType?)t1 : null, After = p2 ? (ChannelType?)t2 : null }; break; case "archived": entrythr.ArchivedChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "locked": entrythr.LockedChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "invitable": entrythr.InvitableChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "auto_archive_duration": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entrythr.AutoArchiveDurationChange = new PropertyChange { Before = p1 ? (ThreadAutoArchiveDuration?)t5 : null, After = p2 ? (ThreadAutoArchiveDuration?)t6 : null }; break; case "rate_limit_per_user": entrythr.PerUserRateLimitChange = new PropertyChange { Before = (int?)(long?)xc.OldValue, After = (int?)(long?)xc.NewValue }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in thread update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.GuildScheduledEventCreate: case AuditLogActionType.GuildScheduledEventDelete: case AuditLogActionType.GuildScheduledEventUpdate: entry = new DiscordAuditLogGuildScheduledEventEntry { Target = this.ScheduledEventsInternal.TryGetValue(xac.TargetId.Value, out var scheduledEvent) ? scheduledEvent : new DiscordScheduledEvent { Id = xac.TargetId.Value, Discord = this.Discord } }; var entryse = entry as DiscordAuditLogGuildScheduledEventEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "channel_id": entryse.ChannelIdChange = new PropertyChange { Before = ulong.TryParse(xc.OldValueString, out var ogid) ? ogid : null, After = ulong.TryParse(xc.NewValueString, out var ngid) ? ngid : null }; break; case "name": entryse.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "description": entryse.DescriptionChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "location": entryse.LocationChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "privacy_level": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entryse.PrivacyLevelChange = new PropertyChange { Before = p1 ? (ScheduledEventPrivacyLevel?)t5 : null, After = p2 ? (ScheduledEventPrivacyLevel?)t6 : null }; break; case "entity_type": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entryse.EntityTypeChange = new PropertyChange { Before = p1 ? (ScheduledEventEntityType?)t5 : null, After = p2 ? (ScheduledEventEntityType?)t6 : null }; break; case "status": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entryse.StatusChange = new PropertyChange { Before = p1 ? (ScheduledEventStatus?)t5 : null, After = p2 ? (ScheduledEventStatus?)t6 : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in scheduled event update: {0} - this should be reported to library developers", xc.Key); break; } } break; // TODO: Handle ApplicationCommandPermissionUpdate case AuditLogActionType.ApplicationCommandPermissionUpdate: break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown audit log action type: {0} - this should be reported to library developers", (int)xac.ActionType); break; } if (entry == null) continue; entry.ActionCategory = xac.ActionType switch { AuditLogActionType.ChannelCreate or AuditLogActionType.EmojiCreate or AuditLogActionType.InviteCreate or AuditLogActionType.OverwriteCreate or AuditLogActionType.RoleCreate or AuditLogActionType.WebhookCreate or AuditLogActionType.IntegrationCreate or AuditLogActionType.StickerCreate or AuditLogActionType.StageInstanceCreate or AuditLogActionType.ThreadCreate or AuditLogActionType.GuildScheduledEventCreate => AuditLogActionCategory.Create, AuditLogActionType.ChannelDelete or AuditLogActionType.EmojiDelete or AuditLogActionType.InviteDelete or AuditLogActionType.MessageDelete or AuditLogActionType.MessageBulkDelete or AuditLogActionType.OverwriteDelete or AuditLogActionType.RoleDelete or AuditLogActionType.WebhookDelete or AuditLogActionType.IntegrationDelete or AuditLogActionType.StickerDelete or AuditLogActionType.StageInstanceDelete or AuditLogActionType.ThreadDelete or AuditLogActionType.GuildScheduledEventDelete => AuditLogActionCategory.Delete, AuditLogActionType.ChannelUpdate or AuditLogActionType.EmojiUpdate or AuditLogActionType.InviteUpdate or AuditLogActionType.MemberRoleUpdate or AuditLogActionType.MemberUpdate or AuditLogActionType.OverwriteUpdate or AuditLogActionType.RoleUpdate or AuditLogActionType.WebhookUpdate or AuditLogActionType.IntegrationUpdate or AuditLogActionType.StickerUpdate or AuditLogActionType.StageInstanceUpdate or AuditLogActionType.ThreadUpdate or AuditLogActionType.GuildScheduledEventUpdate => AuditLogActionCategory.Update, _ => AuditLogActionCategory.Other, }; entry.Discord = this.Discord; entry.ActionType = xac.ActionType; entry.Id = xac.Id; entry.Reason = xac.Reason; entry.UserResponsible = amd[xac.UserId]; entries.Add(entry); } return new ReadOnlyCollection(entries); } } diff --git a/DisCatSharp/Entities/Guild/DiscordGuild.cs b/DisCatSharp/Entities/Guild/DiscordGuild.cs index 2f4cd19f1..4f7c0173e 100644 --- a/DisCatSharp/Entities/Guild/DiscordGuild.cs +++ b/DisCatSharp/Entities/Guild/DiscordGuild.cs @@ -1,2027 +1,2027 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using DisCatSharp.Net.Serialization; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord guild. /// public partial class DiscordGuild : SnowflakeObject, IEquatable { /// /// Gets the guild's name. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets the guild icon's hash. /// [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] public string IconHash { get; internal set; } /// /// Gets the guild icon's url. /// [JsonIgnore] public string IconUrl => !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.{(this.IconHash.StartsWith("a_") ? "gif" : "png")}?size=1024" : null; /// /// Gets the guild splash's hash. /// [JsonProperty("splash", NullValueHandling = NullValueHandling.Ignore)] public string SplashHash { get; internal set; } /// /// Gets the guild splash's url. /// [JsonIgnore] public string SplashUrl => !string.IsNullOrWhiteSpace(this.SplashHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.SplashHash}.png?size=1024" : null; /// /// Gets the guild discovery splash's hash. /// [JsonProperty("discovery_splash", NullValueHandling = NullValueHandling.Ignore)] public string DiscoverySplashHash { get; internal set; } /// /// Gets the guild discovery splash's url. /// [JsonIgnore] public string DiscoverySplashUrl => !string.IsNullOrWhiteSpace(this.DiscoverySplashHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.GUILD_DISCOVERY_SPLASHES}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.DiscoverySplashHash}.png?size=1024" : null; /// /// Gets the preferred locale of this guild. /// This is used for server discovery, interactions and notices from Discord. Defaults to en-US. /// [JsonProperty("preferred_locale", NullValueHandling = NullValueHandling.Ignore)] public string PreferredLocale { get; internal set; } /// /// Gets the ID of the guild's owner. /// [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] public ulong OwnerId { get; internal set; } /// /// Gets the guild's owner. /// [JsonIgnore] public DiscordMember Owner => this.Members.TryGetValue(this.OwnerId, out var owner) ? owner : this.Discord.ApiClient.GetGuildMemberAsync(this.Id, this.OwnerId).ConfigureAwait(false).GetAwaiter().GetResult(); /// /// Gets permissions for the user in the guild (does not include channel overrides) /// [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] public Permissions? Permissions { get; set; } /// /// Gets the guild's voice region ID. /// [JsonProperty("region", NullValueHandling = NullValueHandling.Ignore)] internal string VoiceRegionId { get; set; } /// /// Gets the guild's voice region. /// [JsonIgnore] public DiscordVoiceRegion VoiceRegion => this.Discord.VoiceRegions[this.VoiceRegionId]; /// /// Gets the guild's AFK voice channel ID. /// [JsonProperty("afk_channel_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong AfkChannelId { get; set; } /// /// Gets the guild's AFK voice channel. /// [JsonIgnore] public DiscordChannel AfkChannel => this.GetChannel(this.AfkChannelId); /// /// List of . /// Null if DisCatSharp.ApplicationCommands is not used or no guild commands are registered. /// [JsonIgnore] public ReadOnlyCollection RegisteredApplicationCommands => new(this.InternalRegisteredApplicationCommands); [JsonIgnore] internal List InternalRegisteredApplicationCommands { get; set; } = null; /// /// Gets the guild's AFK timeout. /// [JsonProperty("afk_timeout", NullValueHandling = NullValueHandling.Ignore)] public int AfkTimeout { get; internal set; } /// /// Gets the guild's verification level. /// [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] public VerificationLevel VerificationLevel { get; internal set; } /// /// Gets the guild's default notification settings. /// [JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)] public DefaultMessageNotifications DefaultMessageNotifications { get; internal set; } /// /// Gets the guild's explicit content filter settings. /// [JsonProperty("explicit_content_filter")] public ExplicitContentFilter ExplicitContentFilter { get; internal set; } /// /// Gets the guild's nsfw level. /// [JsonProperty("nsfw_level")] public NsfwLevel NsfwLevel { get; internal set; } /// /// Gets the system channel id. /// [JsonProperty("system_channel_id", NullValueHandling = NullValueHandling.Include)] internal ulong? SystemChannelId { get; set; } /// /// Gets the channel where system messages (such as boost and welcome messages) are sent. /// [JsonIgnore] public DiscordChannel SystemChannel => this.SystemChannelId.HasValue ? this.GetChannel(this.SystemChannelId.Value) : null; /// /// Gets the settings for this guild's system channel. /// [JsonProperty("system_channel_flags")] public SystemChannelFlags SystemChannelFlags { get; internal set; } /// /// Gets whether this guild's widget is enabled. /// [JsonProperty("widget_enabled", NullValueHandling = NullValueHandling.Ignore)] public bool? WidgetEnabled { get; internal set; } /// /// Gets the widget channel id. /// [JsonProperty("widget_channel_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong? WidgetChannelId { get; set; } /// /// Gets the widget channel for this guild. /// [JsonIgnore] public DiscordChannel WidgetChannel => this.WidgetChannelId.HasValue ? this.GetChannel(this.WidgetChannelId.Value) : null; /// /// Gets the rules channel id. /// [JsonProperty("rules_channel_id")] internal ulong? RulesChannelId { get; set; } /// /// Gets the rules channel for this guild. /// This is only available if the guild is considered "discoverable". /// [JsonIgnore] public DiscordChannel RulesChannel => this.RulesChannelId.HasValue ? this.GetChannel(this.RulesChannelId.Value) : null; /// /// Gets the public updates channel id. /// [JsonProperty("public_updates_channel_id")] internal ulong? PublicUpdatesChannelId { get; set; } /// /// Gets the public updates channel (where admins and moderators receive messages from Discord) for this guild. /// This is only available if the guild is considered "discoverable". /// [JsonIgnore] public DiscordChannel PublicUpdatesChannel => this.PublicUpdatesChannelId.HasValue ? this.GetChannel(this.PublicUpdatesChannelId.Value) : null; /// /// Gets the application id of this guild if it is bot created. /// [JsonProperty("application_id")] public ulong? ApplicationId { get; internal set; } /// /// Gets a collection of this guild's roles. /// [JsonIgnore] public IReadOnlyDictionary Roles => new ReadOnlyConcurrentDictionary(this.RolesInternal); [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary RolesInternal; /// /// Gets a collection of this guild's stickers. /// [JsonIgnore] public IReadOnlyDictionary Stickers => new ReadOnlyConcurrentDictionary(this.StickersInternal); [JsonProperty("stickers", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary StickersInternal; /// /// Gets a collection of this guild's emojis. /// [JsonIgnore] public IReadOnlyDictionary Emojis => new ReadOnlyConcurrentDictionary(this.EmojisInternal); [JsonProperty("emojis", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary EmojisInternal; /// /// Gets a collection of this guild's features. /// [JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyList RawFeatures { get; internal set; } /// /// Gets the guild's features. /// [JsonIgnore] public GuildFeatures Features => new(this); /// /// Gets the required multi-factor authentication level for this guild. /// [JsonProperty("mfa_level", NullValueHandling = NullValueHandling.Ignore)] public MfaLevel MfaLevel { get; internal set; } /// /// Gets this guild's join date. /// [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset JoinedAt { get; internal set; } /// /// Gets whether this guild is considered to be a large guild. /// [JsonProperty("large", NullValueHandling = NullValueHandling.Ignore)] public bool IsLarge { get; internal set; } /// /// Gets whether this guild is unavailable. /// [JsonProperty("unavailable", NullValueHandling = NullValueHandling.Ignore)] public bool IsUnavailable { get; internal set; } /// /// Gets the total number of members in this guild. /// [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] public int MemberCount { get; internal set; } /// /// Gets the maximum amount of members allowed for this guild. /// [JsonProperty("max_members")] public int? MaxMembers { get; internal set; } /// /// Gets the maximum amount of presences allowed for this guild. /// [JsonProperty("max_presences")] public int? MaxPresences { get; internal set; } /// /// Gets the approximate number of members in this guild, when using and having withCounts set to true. /// [JsonProperty("approximate_member_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximateMemberCount { get; internal set; } /// /// Gets the approximate number of presences in this guild, when using and having withCounts set to true. /// [JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximatePresenceCount { get; internal set; } /// /// Gets the maximum amount of users allowed per video channel. /// [JsonProperty("max_video_channel_users", NullValueHandling = NullValueHandling.Ignore)] public int? MaxVideoChannelUsers { get; internal set; } /// /// Gets the maximum amount of users allowed per video stage channel. /// [JsonProperty("max_stage_video_channel_users", NullValueHandling = NullValueHandling.Ignore)] public int? MaxStageVideoChannelUsers { get; internal set; } /// /// Gets a dictionary of all the voice states for this guilds. The key for this dictionary is the ID of the user /// the voice state corresponds to. /// [JsonIgnore] public IReadOnlyDictionary VoiceStates => new ReadOnlyConcurrentDictionary(this.VoiceStatesInternal); [JsonProperty("voice_states", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary VoiceStatesInternal; /// /// Gets a dictionary of all the members that belong to this guild. The dictionary's key is the member ID. /// [JsonIgnore] public IReadOnlyDictionary Members => new ReadOnlyConcurrentDictionary(this.MembersInternal); [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary MembersInternal; /// /// Gets a dictionary of all the channels associated with this guild. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary Channels => new ReadOnlyConcurrentDictionary(this.ChannelsInternal); [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ChannelsInternal; internal ConcurrentDictionary Invites; /// /// Gets a dictionary of all the active threads associated with this guild the user has permission to view. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary Threads { get; internal set; } [JsonProperty("threads", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ThreadsInternal = new(); /// /// Gets a dictionary of all active stage instances. The dictionary's key is the stage ID. /// [JsonIgnore] public IReadOnlyDictionary StageInstances { get; internal set; } [JsonProperty("stage_instances", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary StageInstancesInternal = new(); /// /// Gets a dictionary of all scheduled events. /// [JsonIgnore] public IReadOnlyDictionary ScheduledEvents { get; internal set; } [JsonProperty("guild_scheduled_events", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ScheduledEventsInternal = new(); /// /// Gets the guild member for current user. /// [JsonIgnore] public DiscordMember CurrentMember => this._currentMemberLazy.Value; [JsonIgnore] private readonly Lazy _currentMemberLazy; /// /// Gets the @everyone role for this guild. /// [JsonIgnore] public DiscordRole EveryoneRole => this.GetRole(this.Id); [JsonIgnore] internal bool IsOwnerInternal; /// /// Gets whether the current user is the guild's owner. /// [JsonProperty("owner", NullValueHandling = NullValueHandling.Ignore)] public bool IsOwner { get => this.IsOwnerInternal || this.OwnerId == this.Discord.CurrentUser.Id; internal set => this.IsOwnerInternal = value; } /// /// Gets the vanity URL code for this guild, when applicable. /// [JsonProperty("vanity_url_code")] public string VanityUrlCode { get; internal set; } /// /// Gets the guild description, when applicable. /// [JsonProperty("description")] public string Description { get; internal set; } /// /// Gets this guild's banner hash, when applicable. /// [JsonProperty("banner")] public string BannerHash { get; internal set; } /// /// Gets this guild's banner in url form. /// [JsonIgnore] public string BannerUrl => !string.IsNullOrWhiteSpace(this.BannerHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}" : null; /// /// Whether this guild has the community feature enabled. /// [JsonIgnore] public bool IsCommunity => this.Features.HasCommunityEnabled; /// /// Whether this guild has enabled the welcome screen. /// [JsonIgnore] public bool HasWelcomeScreen => this.Features.HasWelcomeScreenEnabled; /// /// Whether this guild has enabled membership screening. /// [JsonIgnore] public bool HasMemberVerificationGate => this.Features.HasMembershipScreeningEnabled; /// /// Gets this guild's premium tier (Nitro boosting). /// [JsonProperty("premium_tier")] public PremiumTier PremiumTier { get; internal set; } /// /// Gets the amount of members that boosted this guild. /// [JsonProperty("premium_subscription_count", NullValueHandling = NullValueHandling.Ignore)] public int? PremiumSubscriptionCount { get; internal set; } /// /// Whether the premium progress bar is enabled. /// [JsonProperty("premium_progress_bar_enabled", NullValueHandling = NullValueHandling.Ignore)] public bool PremiumProgressBarEnabled { get; internal set; } /// /// Gets whether this guild is designated as NSFW. /// [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] public bool IsNsfw { get; internal set; } /// /// Gets this guild's hub type, if applicable. /// [JsonProperty("hub_type", NullValueHandling = NullValueHandling.Ignore)] public HubType HubType { get; internal set; } /// /// Gets a dictionary of all by position ordered channels associated with this guild. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary OrderedChannels => new ReadOnlyDictionary(this.InternalSortChannels()); /// /// Sorts the channels. /// private Dictionary InternalSortChannels() { Dictionary keyValuePairs = new(); var ochannels = this.GetOrderedChannels(); foreach (var ochan in ochannels) { if (ochan.Key != 0) keyValuePairs.Add(ochan.Key, this.GetChannel(ochan.Key)); foreach (var chan in ochan.Value) keyValuePairs.Add(chan.Id, chan); } return keyValuePairs; } /// /// Gets an ordered list out of the channel cache. /// Returns a Dictionary where the key is an ulong and can be mapped to s. /// Ignore the 0 key here, because that indicates that this is the "has no category" list. /// Each value contains a ordered list of text/news and voice/stage channels as . /// /// A ordered list of categories with its channels public Dictionary> GetOrderedChannels() { IReadOnlyList rawChannels = this.ChannelsInternal.Values.ToList(); Dictionary> orderedChannels = new() { { 0, new List() } }; foreach (var channel in rawChannels.Where(c => c.Type == ChannelType.Category).OrderBy(c => c.Position)) { orderedChannels.Add(channel.Id, new List()); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Text || c.Type == ChannelType.News || c.Type == ChannelType.Forum)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Text || c.Type == ChannelType.News || c.Type == ChannelType.Forum)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } return orderedChannels; } /// /// Gets an ordered list. /// Returns a Dictionary where the key is an ulong and can be mapped to s. /// Ignore the 0 key here, because that indicates that this is the "has no category" list. /// Each value contains a ordered list of text/news and voice/stage channels as . /// /// A ordered list of categories with its channels public async Task>> GetOrderedChannelsAsync() { var rawChannels = await this.Discord.ApiClient.GetGuildChannelsAsync(this.Id); Dictionary> orderedChannels = new() { { 0, new List() } }; foreach (var channel in rawChannels.Where(c => c.Type == ChannelType.Category).OrderBy(c => c.Position)) { orderedChannels.Add(channel.Id, new List()); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Text || c.Type == ChannelType.News || c.Type == ChannelType.Forum)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Text || c.Type == ChannelType.News || c.Type == ChannelType.Forum)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } return orderedChannels; } /// /// Whether it is synced. /// [JsonIgnore] internal bool IsSynced { get; set; } /// /// Initializes a new instance of the class. /// internal DiscordGuild() { this._currentMemberLazy = new Lazy(() => this.MembersInternal != null && this.MembersInternal.TryGetValue(this.Discord.CurrentUser.Id, out var member) ? member : null); this.Invites = new ConcurrentDictionary(); this.Threads = new ReadOnlyConcurrentDictionary(this.ThreadsInternal); this.StageInstances = new ReadOnlyConcurrentDictionary(this.StageInstancesInternal); this.ScheduledEvents = new ReadOnlyConcurrentDictionary(this.ScheduledEventsInternal); } #region Guild Methods /// /// Searches the current guild for members who's display name start with the specified name. /// /// The name to search for. /// The maximum amount of members to return. Max 1000. Defaults to 1. /// The members found, if any. public Task> SearchMembersAsync(string name, int? limit = 1) => this.Discord.ApiClient.SearchMembersAsync(this.Id, name, limit); /// /// Adds a new member to this guild /// /// User to add /// User's access token (OAuth2) /// new nickname /// new roles /// whether this user has to be muted /// whether this user has to be deafened - /// Thrown when the client does not have the permission. - /// Thrown when the or is not found. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the or is not found. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(DiscordUser user, string accessToken, string nickname = null, IEnumerable roles = null, bool muted = false, bool deaf = false) => this.Discord.ApiClient.AddGuildMemberAsync(this.Id, user.Id, accessToken, nickname, roles, muted, deaf); /// /// Deletes this guild. Requires the caller to be the owner of the guild. /// - /// Thrown when the client is not the owner of the guild. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client is not the owner of the guild. + /// Thrown when Discord is unable to process the request. public Task DeleteAsync() => this.Discord.ApiClient.DeleteGuildAsync(this.Id); /// /// Enables the mfa requirement for this guild. /// /// The audit log reason. - /// Thrown when the current user is not the guilds owner. - /// Thrown when the guild does not exist. - /// Thrown when Discord is unable to process the request. + /// Thrown when the current user is not the guilds owner. + /// Thrown when the guild does not exist. + /// Thrown when Discord is unable to process the request. public Task EnableMfaAsync(string reason = null) => this.IsOwner ? this.Discord.ApiClient.EnableGuildMfaAsync(this.Id, reason) : throw new Exception("The current user does not own the guild."); /// /// Disables the mfa requirement for this guild. /// /// The audit log reason. - /// Thrown when the current user is not the guilds owner. - /// Thrown when the guild does not exist. - /// Thrown when Discord is unable to process the request. + /// Thrown when the current user is not the guilds owner. + /// Thrown when the guild does not exist. + /// Thrown when Discord is unable to process the request. public Task DisableMfaAsync(string reason = null) => this.IsOwner ? this.Discord.ApiClient.DisableGuildMfaAsync(this.Id, reason) : throw new Exception("The current user does not own the guild."); /// /// Modifies this guild. /// /// Action to perform on this guild. /// The modified guild object. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task ModifyAsync(Action action) { var mdl = new GuildEditModel(); action(mdl); var afkChannelId = mdl.PublicUpdatesChannel .MapOrNull(c => c.Type != ChannelType.Voice ? throw new ArgumentException("AFK channel needs to be a text channel.") : c.Id); static Optional ChannelToId(Optional ch, string name) => ch.MapOrNull(c => c.Type != ChannelType.Text && c.Type != ChannelType.News ? throw new ArgumentException($"{name} channel needs to be a text channel.") : c.Id); var rulesChannelId = ChannelToId(mdl.RulesChannel, "Rules"); var publicUpdatesChannelId = ChannelToId(mdl.PublicUpdatesChannel, "Public updates"); var systemChannelId = ChannelToId(mdl.SystemChannel, "System"); var iconb64 = ImageTool.Base64FromStream(mdl.Icon); var splashb64 = ImageTool.Base64FromStream(mdl.Splash); var bannerb64 = ImageTool.Base64FromStream(mdl.Banner); var discoverySplash64 = ImageTool.Base64FromStream(mdl.DiscoverySplash); return await this.Discord.ApiClient.ModifyGuildAsync(this.Id, mdl.Name, mdl.VerificationLevel, mdl.DefaultMessageNotifications, mdl.MfaLevel, mdl.ExplicitContentFilter, afkChannelId, mdl.AfkTimeout, iconb64, mdl.Owner.Map(e => e.Id), splashb64, systemChannelId, mdl.SystemChannelFlags, publicUpdatesChannelId, rulesChannelId, mdl.Description, bannerb64, discoverySplash64, mdl.PreferredLocale, mdl.PremiumProgressBarEnabled, mdl.AuditLogReason).ConfigureAwait(false); } /// /// Modifies the community settings async. /// This sets if not highest and . /// /// If true, enable . /// The rules channel. /// The public updates channel. /// The preferred locale. Defaults to en-US. /// The description. /// The default message notifications. Defaults to /// The audit log reason. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task ModifyCommunitySettingsAsync(bool enabled, DiscordChannel rulesChannel = null, DiscordChannel publicUpdatesChannel = null, string preferredLocale = "en-US", string description = null, DefaultMessageNotifications defaultMessageNotifications = DefaultMessageNotifications.MentionsOnly, string reason = null) { var verificationLevel = this.VerificationLevel; if (this.VerificationLevel != VerificationLevel.Highest) { verificationLevel = VerificationLevel.High; } var explicitContentFilter = ExplicitContentFilter.AllMembers; static Optional ChannelToId(DiscordChannel ch, string name) => ch == null ? null : ch.Type != ChannelType.Text && ch.Type != ChannelType.News ? throw new ArgumentException($"{name} channel needs to be a text channel.") : ch.Id; var rulesChannelId = ChannelToId(rulesChannel, "Rules"); var publicUpdatesChannelId = ChannelToId(publicUpdatesChannel, "Public updates"); List features = new(); var rfeatures = this.RawFeatures.ToList(); if (!this.RawFeatures.Contains("COMMUNITY") && enabled) { rfeatures.Add("COMMUNITY"); } else if (this.RawFeatures.Contains("COMMUNITY") && !enabled) { rfeatures.Remove("COMMUNITY"); } features = rfeatures; return await this.Discord.ApiClient.ModifyGuildCommunitySettingsAsync(this.Id, features, rulesChannelId, publicUpdatesChannelId, preferredLocale, description, defaultMessageNotifications, explicitContentFilter, verificationLevel, reason).ConfigureAwait(false); } /// /// Enables invites for the guild. /// /// The audit log reason. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task EnableInvitesAsync(string reason = null) { List features = new(); var rfeatures = this.RawFeatures.ToList(); if (this.Features.InvitesDisabled) rfeatures.Remove("INVITES_DISABLED"); features = rfeatures; return await this.Discord.ApiClient.ModifyGuildFeaturesAsync(this.Id, features, reason); } /// /// Disables invites for the guild. /// /// /// - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task DisableInvitesAsync(string reason = null) { List features = new(); var rfeatures = this.RawFeatures.ToList(); if (!this.Features.InvitesDisabled) rfeatures.Add("INVITES_DISABLED"); features = rfeatures; return await this.Discord.ApiClient.ModifyGuildFeaturesAsync(this.Id, features, reason); } /// /// Timeout a specified member in this guild. /// /// Member to timeout. /// The datetime offset to time out the user. Up to 28 days. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(ulong memberId, DateTimeOffset until, string reason = null) => until.Subtract(DateTimeOffset.UtcNow).Days > 28 ? throw new ArgumentException("Timeout can not be longer than 28 days") : this.Discord.ApiClient.ModifyTimeoutAsync(this.Id, memberId, until, reason); /// /// Timeout a specified member in this guild. /// /// Member to timeout. /// The timespan to time out the user. Up to 28 days. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(ulong memberId, TimeSpan until, string reason = null) => this.TimeoutAsync(memberId, DateTimeOffset.UtcNow + until, reason); /// /// Timeout a specified member in this guild. /// /// Member to timeout. /// The datetime to time out the user. Up to 28 days. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(ulong memberId, DateTime until, string reason = null) => this.TimeoutAsync(memberId, until.ToUniversalTime() - DateTime.UtcNow, reason); /// /// Removes the timeout from a specified member in this guild. /// /// Member to remove the timeout from. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RemoveTimeoutAsync(ulong memberId, string reason = null) => this.Discord.ApiClient.ModifyTimeoutAsync(this.Id, memberId, null, reason); /// /// Bans a specified member from this guild. /// /// Member to ban. /// How many days to remove messages from. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task BanMemberAsync(DiscordMember member, int deleteMessageDays = 0, string reason = null) => this.Discord.ApiClient.CreateGuildBanAsync(this.Id, member.Id, deleteMessageDays, reason); /// /// Bans a specified user by ID. This doesn't require the user to be in this guild. /// /// ID of the user to ban. /// How many days to remove messages from. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task BanMemberAsync(ulong userId, int deleteMessageDays = 0, string reason = null) => this.Discord.ApiClient.CreateGuildBanAsync(this.Id, userId, deleteMessageDays, reason); /// /// Unbans a user from this guild. /// /// User to unban. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the user does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the user does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task UnbanMemberAsync(DiscordUser user, string reason = null) => this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, user.Id, reason); /// /// Unbans a user by ID. /// /// ID of the user to unban. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the user does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the user does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task UnbanMemberAsync(ulong userId, string reason = null) => this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, userId, reason); /// /// Leaves this guild. /// - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public Task LeaveAsync() => this.Discord.ApiClient.LeaveGuildAsync(this.Id); /// /// Gets the bans for this guild, allowing for pagination. /// /// Maximum number of bans to fetch. Max 1000. Defaults to 1000. /// The Id of the user before which to fetch the bans. Overrides if both are present. /// The Id of the user after which to fetch the bans. /// Collection of bans in this guild in ascending order by user id. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task> GetBansAsync(int? limit = null, ulong? before = null, ulong? after = null) => this.Discord.ApiClient.GetGuildBansAsync(this.Id, limit, before, after); /// /// Gets a ban for a specific user. /// /// The Id of the user to get the ban for. /// The requested ban object. - /// Thrown when the specified user is not banned. + /// Thrown when the specified user is not banned. public Task GetBanAsync(ulong userId) => this.Discord.ApiClient.GetGuildBanAsync(this.Id, userId); /// /// Gets a ban for a specific user. /// /// The user to get the ban for. /// The requested ban object. - /// Thrown when the specified user is not banned. + /// Thrown when the specified user is not banned. public Task GetBanAsync(DiscordUser user) => this.GetBanAsync(user.Id); #region Scheduled Events /// /// Creates a scheduled event. /// /// The name. /// The scheduled start time. /// The scheduled end time. /// The channel. /// The metadata. /// The description. /// The type. /// The cover image. /// The reason. /// A scheduled event. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the guild 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, DateTimeOffset? scheduledEndTime = null, DiscordChannel channel = null, DiscordScheduledEventEntityMetadata metadata = null, string description = null, ScheduledEventEntityType type = ScheduledEventEntityType.StageInstance, Optional coverImage = default, string reason = null) { var coverb64 = ImageTool.Base64FromStream(coverImage); return await this.Discord.ApiClient.CreateGuildScheduledEventAsync(this.Id, type == ScheduledEventEntityType.External ? null : channel?.Id, type == ScheduledEventEntityType.External ? metadata : null, name, scheduledStartTime, scheduledEndTime.HasValue && type == ScheduledEventEntityType.External ? scheduledEndTime.Value : null, description, type, coverb64, reason); } /// /// Creates a scheduled event with type . /// /// The name. /// The scheduled start time. /// The scheduled end time. /// The location of the external event. /// The description. /// The cover image. /// The reason. /// A scheduled event. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task CreateExternalScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, DateTimeOffset scheduledEndTime, string location, string description = null, Optional coverImage = default, string reason = null) { var coverb64 = ImageTool.Base64FromStream(coverImage); return await this.Discord.ApiClient.CreateGuildScheduledEventAsync(this.Id, null, new DiscordScheduledEventEntityMetadata(location), name, scheduledStartTime, scheduledEndTime, description, ScheduledEventEntityType.External, coverb64, reason); } /// /// Gets a specific scheduled events. /// /// The Id of the event to get. /// Whether to include user count. /// A scheduled event. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetScheduledEventAsync(ulong scheduledEventId, bool? withUserCount = null) => this.ScheduledEventsInternal.TryGetValue(scheduledEventId, out var ev) ? ev : await this.Discord.ApiClient.GetGuildScheduledEventAsync(this.Id, scheduledEventId, withUserCount); /// /// Gets a specific scheduled events. /// /// The event to get. /// Whether to include user count. /// A scheduled event. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetScheduledEventAsync(DiscordScheduledEvent scheduledEvent, bool? withUserCount = null) => await this.GetScheduledEventAsync(scheduledEvent.Id, withUserCount); /// /// Gets the guilds scheduled events. /// /// Whether to include user count. /// A list of the guilds scheduled events. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task> GetScheduledEventsAsync(bool? withUserCount = null) => await this.Discord.ApiClient.ListGuildScheduledEventsAsync(this.Id, withUserCount); #endregion /// /// Creates a new text channel in this guild. /// /// Name of the new channel. /// Category to put this channel in. /// Topic of the channel. /// Permission overwrites for this channel. /// Whether the channel is to be flagged as not safe for work. /// Slow mode timeout for users. /// The default auto archive duration for new threads. /// Reason for audit logs. /// The newly-created channel. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateTextChannelAsync(string name, DiscordChannel parent = null, Optional topic = default, IEnumerable overwrites = null, bool? nsfw = null, Optional perUserRateLimit = default, ThreadAutoArchiveDuration defaultAutoArchiveDuration = ThreadAutoArchiveDuration.OneDay, string reason = null) => this.CreateChannelAsync(name, ChannelType.Text, parent, topic, null, null, overwrites, nsfw, perUserRateLimit, null, defaultAutoArchiveDuration, reason); /// /// Creates a new forum channel in this guild. /// The field template is not yet released, so it won't applied. /// /// Name of the new channel. /// Category to put this channel in. /// Topic of the channel. /// Permission overwrites for this channel. /// Whether the channel is to be flagged as not safe for work. /// The default reaction emoji for posts. /// Slow mode timeout for users. /// Slow mode timeout for user post creations. /// The default auto archive duration for new threads. /// Reason for audit logs. /// The newly-created channel. - /// Thrown when the client does not have the permission or the guild does not have the forum channel feature. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission or the guild does not have the forum channel feature. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateForumChannelAsync(string name, DiscordChannel parent = null, Optional topic = default, IEnumerable overwrites = null, bool? nsfw = null, Optional defaultReactionEmoji = default, Optional perUserRateLimit = default, Optional postCreateUserRateLimit = default, ThreadAutoArchiveDuration defaultAutoArchiveDuration = ThreadAutoArchiveDuration.OneDay, string reason = null) => this.Discord.ApiClient.CreateForumChannelAsync(this.Id, name, parent?.Id, topic, null, nsfw, defaultReactionEmoji, perUserRateLimit, postCreateUserRateLimit, defaultAutoArchiveDuration, overwrites, reason); /// /// Creates a new channel category in this guild. /// /// Name of the new category. /// Permission overwrites for this category. /// Reason for audit logs. /// The newly-created channel category. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateChannelCategoryAsync(string name, IEnumerable overwrites = null, string reason = null) => this.CreateChannelAsync(name, ChannelType.Category, null, Optional.None, null, null, overwrites, null, Optional.None, null, null, reason); /// /// Creates a new stage channel in this guild. /// /// Name of the new stage channel. /// Permission overwrites for this stage channel. /// Reason for audit logs. /// The newly-created stage channel. - /// Thrown when the client does not have the . - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - /// Thrown when the guilds has not enabled community. + /// Thrown when the client does not have the . + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// Thrown when the guilds has not enabled community. public Task CreateStageChannelAsync(string name, IEnumerable overwrites = null, string reason = null) => this.Features.HasCommunityEnabled ? this.CreateChannelAsync(name, ChannelType.Stage, null, Optional.None, null, null, overwrites, null, Optional.None, null, null, reason) : throw new NotSupportedException("Guild has not enabled community. Can not create a stage channel."); /// /// Creates a new news channel in this guild. /// /// Name of the new news channel. /// Permission overwrites for this news channel. /// The default auto archive duration for new threads. /// Reason for audit logs. /// The newly-created news channel. - /// Thrown when the client does not have the . - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - /// Thrown when the guilds has not enabled community. + /// Thrown when the client does not have the . + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// Thrown when the guilds has not enabled community. public Task CreateNewsChannelAsync(string name, IEnumerable overwrites = null, string reason = null, ThreadAutoArchiveDuration defaultAutoArchiveDuration = ThreadAutoArchiveDuration.OneDay) => this.Features.HasCommunityEnabled ? this.CreateChannelAsync(name, ChannelType.News, null, Optional.None, null, null, overwrites, null, Optional.None, null, defaultAutoArchiveDuration, reason) : throw new NotSupportedException("Guild has not enabled community. Can not create a news channel."); /// /// Creates a new voice channel in this guild. /// /// Name of the new channel. /// Category to put this channel in. /// Bitrate of the channel. /// Maximum number of users in the channel. /// Permission overwrites for this channel. /// Video quality mode of the channel. /// Reason for audit logs. /// The newly-created channel. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateVoiceChannelAsync(string name, DiscordChannel parent = null, int? bitrate = null, int? userLimit = null, IEnumerable overwrites = null, VideoQualityMode? qualityMode = null, string reason = null) => this.CreateChannelAsync(name, ChannelType.Voice, parent, Optional.None, bitrate, userLimit, overwrites, null, Optional.None, qualityMode, null, reason); /// /// Creates a new channel in this guild. /// /// Name of the new channel. /// Type of the new channel. /// Category to put this channel in. /// Topic of the channel. /// Bitrate of the channel. Applies to voice only. /// Maximum number of users in the channel. Applies to voice only. /// Permission overwrites for this channel. /// Whether the channel is to be flagged as not safe for work. Applies to text only. /// Slow mode timeout for users. /// Video quality mode of the channel. Applies to voice only. /// The default auto archive duration for new threads. /// Reason for audit logs. /// The newly-created channel. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateChannelAsync(string name, ChannelType type, DiscordChannel parent = null, Optional topic = default, int? bitrate = null, int? userLimit = null, IEnumerable overwrites = null, bool? nsfw = null, Optional perUserRateLimit = default, VideoQualityMode? qualityMode = null, ThreadAutoArchiveDuration? defaultAutoArchiveDuration = null, string reason = null) => // technically you can create news/store channels but not always type != ChannelType.Text && type != ChannelType.Voice && type != ChannelType.Category && type != ChannelType.News && type != ChannelType.Store && type != ChannelType.Stage ? throw new ArgumentException("Channel type must be text, voice, stage, or category.", nameof(type)) : type == ChannelType.Category && parent != null ? throw new ArgumentException("Cannot specify parent of a channel category.", nameof(parent)) : this.Discord.ApiClient.CreateGuildChannelAsync(this.Id, name, type, parent?.Id, topic, bitrate, userLimit, overwrites, nsfw, perUserRateLimit, qualityMode, defaultAutoArchiveDuration, reason); /// /// Gets active threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetActiveThreadsAsync() => this.Discord.ApiClient.GetActiveThreadsAsync(this.Id); /// /// Deletes all channels in this guild. /// Note that this is irreversible. Use carefully! /// /// public Task DeleteAllChannelsAsync() { var tasks = this.Channels.Values.Select(xc => xc.DeleteAsync()); return Task.WhenAll(tasks); } /// /// Estimates the number of users to be pruned. /// /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. /// The roles to be included in the prune. /// Number of users that will be pruned. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetPruneCountAsync(int days = 7, IEnumerable includedRoles = null) { if (includedRoles != null) { includedRoles = includedRoles.Where(r => r != null); var rawRoleIds = includedRoles .Where(x => this.RolesInternal.ContainsKey(x.Id)) .Select(x => x.Id); return this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, rawRoleIds); } return this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, null); } /// /// Prunes inactive users from this guild. /// /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. /// Whether to return the prune count after this method completes. This is discouraged for larger guilds. /// The roles to be included in the prune. /// Reason for audit logs. /// Number of users pruned. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task PruneAsync(int days = 7, bool computePruneCount = true, IEnumerable includedRoles = null, string reason = null) { if (includedRoles != null) { includedRoles = includedRoles.Where(r => r != null); var rawRoleIds = includedRoles .Where(x => this.RolesInternal.ContainsKey(x.Id)) .Select(x => x.Id); return this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, rawRoleIds, reason); } return this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, null, reason); } /// /// Gets integrations attached to this guild. /// /// Collection of integrations attached to this guild. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task> GetIntegrationsAsync() => this.Discord.ApiClient.GetGuildIntegrationsAsync(this.Id); /// /// Attaches an integration from current user to this guild. /// /// Integration to attach. /// The integration after being attached to the guild. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task AttachUserIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.CreateGuildIntegrationAsync(this.Id, integration.Type, integration.Id); /// /// Modifies an integration in this guild. /// /// Integration to modify. /// Number of days after which the integration expires. /// Length of grace period which allows for renewing the integration. /// Whether emotes should be synced from this integration. /// The modified integration. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ModifyIntegrationAsync(DiscordIntegration integration, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) => this.Discord.ApiClient.ModifyGuildIntegrationAsync(this.Id, integration.Id, expireBehaviour, expireGracePeriod, enableEmoticons); /// /// Removes an integration from this guild. /// /// Integration to remove. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task DeleteIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.DeleteGuildIntegrationAsync(this.Id, integration); /// /// Forces re-synchronization of an integration for this guild. /// /// Integration to synchronize. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task SyncIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.SyncGuildIntegrationAsync(this.Id, integration.Id); /// /// Gets the voice regions for this guild. /// /// Voice regions available for this guild. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public async Task> ListVoiceRegionsAsync() { var vrs = await this.Discord.ApiClient.GetGuildVoiceRegionsAsync(this.Id).ConfigureAwait(false); foreach (var xvr in vrs) this.Discord.InternalVoiceRegions.TryAdd(xvr.Id, xvr); return vrs; } /// /// Gets an invite from this guild from an invite code. /// /// The invite code /// An invite, or null if not in cache. public DiscordInvite GetInvite(string code) => this.Invites.TryGetValue(code, out var invite) ? invite : null; /// /// Gets all the invites created for all the channels in this guild. /// /// A collection of invites. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public async Task> GetInvitesAsync() { var res = await this.Discord.ApiClient.GetGuildInvitesAsync(this.Id).ConfigureAwait(false); var intents = this.Discord.Configuration.Intents; if (!intents.HasIntent(DiscordIntents.GuildInvites)) { foreach (var r in res) this.Invites[r.Code] = r; } return res; } /// /// Gets the vanity invite for this guild. /// /// A partial vanity invite. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task GetVanityInviteAsync() => this.Discord.ApiClient.GetGuildVanityUrlAsync(this.Id); /// /// Gets all the webhooks created for all the channels in this guild. /// /// A collection of webhooks this guild has. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task> GetWebhooksAsync() => this.Discord.ApiClient.GetGuildWebhooksAsync(this.Id); /// /// Gets this guild's widget image. /// /// The format of the widget. /// The URL of the widget image. public string GetWidgetImage(WidgetType bannerType = WidgetType.Shield) { var param = bannerType switch { WidgetType.Banner1 => "banner1", WidgetType.Banner2 => "banner2", WidgetType.Banner3 => "banner3", WidgetType.Banner4 => "banner4", _ => "shield", }; return $"{Endpoints.BASE_URI}{Endpoints.GUILDS}/{this.Id}{Endpoints.WIDGET_PNG}?style={param}"; } /// /// Gets a member of this guild by their user ID. /// /// ID of the member to get. /// The requested member. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public async Task GetMemberAsync(ulong userId, bool fetch = false) { if (!fetch && this.MembersInternal != null && this.MembersInternal.TryGetValue(userId, out var mbr)) return mbr; mbr = await this.Discord.ApiClient.GetGuildMemberAsync(this.Id, userId).ConfigureAwait(false); var intents = this.Discord.Configuration.Intents; if (intents.HasIntent(DiscordIntents.GuildMembers)) { if (this.MembersInternal != null) { this.MembersInternal[userId] = mbr; } } return mbr; } /// /// Retrieves a full list of members from Discord. This method will bypass cache. /// /// A collection of all members in this guild. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public async Task> GetAllMembersAsync() { var recmbr = new HashSet(); var recd = 1000; var last = 0ul; while (recd > 0) { var tms = await this.Discord.ApiClient.ListGuildMembersAsync(this.Id, 1000, last == 0 ? null : (ulong?)last).ConfigureAwait(false); recd = tms.Count; foreach (var xtm in tms) { var usr = new DiscordUser(xtm.User) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(xtm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discord = usr.Discord; old.AvatarHash = usr.AvatarHash; return old; }); recmbr.Add(new DiscordMember(xtm) { Discord = this.Discord, GuildId = this.Id }); } var tm = tms.LastOrDefault(); last = tm?.User.Id ?? 0; } return new ReadOnlySet(recmbr); } /// /// Requests that Discord send a list of guild members based on the specified arguments. This method will fire the event. /// If no arguments aside from and are specified, this will request all guild members. /// /// Filters the returned members based on what the username starts with. Either this or must not be null. /// The must also be greater than 0 if this is specified. /// Total number of members to request. This must be greater than 0 if is specified. /// Whether to include the associated with the fetched members. /// Whether to limit the request to the specified user ids. Either this or must not be null. /// The unique string to identify the response. public async Task RequestMembersAsync(string query = "", int limit = 0, bool? presences = null, IEnumerable userIds = null, string nonce = null) { if (this.Discord is not DiscordClient client) throw new InvalidOperationException("This operation is only valid for regular Discord clients."); if (query == null && userIds == null) throw new ArgumentException("The query and user IDs cannot both be null."); if (query != null && userIds != null) query = null; var grgm = new GatewayRequestGuildMembers(this) { Query = query, Limit = limit >= 0 ? limit : 0, Presences = presences, UserIds = userIds, Nonce = nonce }; var payload = new GatewayPayload { OpCode = GatewayOpCode.RequestGuildMembers, Data = grgm }; var payloadStr = JsonConvert.SerializeObject(payload, Formatting.None); await client.WsSendAsync(payloadStr).ConfigureAwait(false); } /// /// Gets all the channels this guild has. /// /// A collection of this guild's channels. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public Task> GetChannelsAsync() => this.Discord.ApiClient.GetGuildChannelsAsync(this.Id); /// /// Creates a new role in this guild. /// /// Name of the role. /// Permissions for the role. /// Color for the role. /// Whether the role is to be hoisted. /// Whether the role is to be mentionable. /// Reason for audit logs. /// The newly-created role. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task CreateRoleAsync(string name = null, Permissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string reason = null) => this.Discord.ApiClient.CreateGuildRoleAsync(this.Id, name, permissions, color?.Value, hoist, mentionable, reason); /// /// Gets a role from this guild by its ID. /// /// ID of the role to get. /// Requested role. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public DiscordRole GetRole(ulong id) => this.RolesInternal.TryGetValue(id, out var role) ? role : null; /// /// Gets a channel from this guild by its ID. /// /// ID of the channel to get. /// Requested channel. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public DiscordChannel GetChannel(ulong id) => this.ChannelsInternal != null && this.ChannelsInternal.TryGetValue(id, out var channel) ? channel : null; /// /// Gets a thread from this guild by its ID. /// /// ID of the thread to get. /// Requested thread. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public DiscordThreadChannel GetThread(ulong id) => this.ThreadsInternal != null && this.ThreadsInternal.TryGetValue(id, out var thread) ? thread : null; /// /// Gets all of this guild's custom emojis. /// /// All of this guild's custom emojis. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public Task> GetEmojisAsync() => this.Discord.ApiClient.GetGuildEmojisAsync(this.Id); /// /// Gets this guild's specified custom emoji. /// /// ID of the emoji to get. /// The requested custom emoji. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public Task GetEmojiAsync(ulong id) => this.Discord.ApiClient.GetGuildEmojiAsync(this.Id, id); /// /// Creates a new custom emoji for this guild. /// /// Name of the new emoji. /// Image to use as the emoji. /// Roles for which the emoji will be available. This works only if your application is whitelisted as integration. /// Reason for audit log. /// The newly-created emoji. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task CreateEmojiAsync(string name, Stream image, IEnumerable roles = null, string reason = null) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); name = name.Trim(); if (name.Length < 2 || name.Length > 50) throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long."); if (image == null) throw new ArgumentNullException(nameof(image)); var image64 = ImageTool.Base64FromStream(image); return this.Discord.ApiClient.CreateGuildEmojiAsync(this.Id, name, image64, roles?.Select(xr => xr.Id), reason); } /// /// Modifies a this guild's custom emoji. /// /// Emoji to modify. /// New name for the emoji. /// Roles for which the emoji will be available. This works only if your application is whitelisted as integration. /// Reason for audit log. /// The modified emoji. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task ModifyEmojiAsync(DiscordGuildEmoji emoji, string name, IEnumerable roles = null, string reason = null) { if (emoji == null) throw new ArgumentNullException(nameof(emoji)); if (emoji.Guild.Id != this.Id) throw new ArgumentException("This emoji does not belong to this guild."); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); name = name.Trim(); return name.Length < 2 || name.Length > 50 ? throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long.") : this.Discord.ApiClient.ModifyGuildEmojiAsync(this.Id, emoji.Id, name, roles?.Select(xr => xr.Id), reason); } /// /// Deletes this guild's custom emoji. /// /// Emoji to delete. /// Reason for audit log. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task DeleteEmojiAsync(DiscordGuildEmoji emoji, string reason = null) => emoji == null ? throw new ArgumentNullException(nameof(emoji)) : emoji.Guild.Id != this.Id ? throw new ArgumentException("This emoji does not belong to this guild.") : this.Discord.ApiClient.DeleteGuildEmojiAsync(this.Id, emoji.Id, reason); /// /// Gets all of this guild's custom stickers. /// /// All of this guild's custom stickers. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public async Task> GetStickersAsync() { var stickers = await this.Discord.ApiClient.GetGuildStickersAsync(this.Id); foreach (var xstr in stickers) { this.StickersInternal.AddOrUpdate(xstr.Id, xstr, (id, old) => { old.Name = xstr.Name; old.Description = xstr.Description; old.InternalTags = xstr.InternalTags; return old; }); } return stickers; } /// /// Gets a sticker /// - /// Thrown when the sticker could not be found. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Sticker does not belong to a guild. + /// Thrown when the sticker could not be found. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Sticker does not belong to a guild. public Task GetStickerAsync(ulong stickerId) => this.Discord.ApiClient.GetGuildStickerAsync(this.Id, stickerId); /// /// Creates a sticker /// /// The name of the sticker. /// The optional description of the sticker. /// The emoji to associate the sticker with. /// The file format the sticker is written in. /// The sticker. /// Audit log reason - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task CreateStickerAsync(string name, string description, DiscordEmoji emoji, Stream file, StickerFormat format, string reason = null) { var fileExt = format switch { StickerFormat.Png => "png", StickerFormat.Apng => "png", StickerFormat.Lottie => "json", _ => throw new InvalidOperationException("This format is not supported.") }; var contentType = format switch { StickerFormat.Png => "image/png", StickerFormat.Apng => "image/png", StickerFormat.Lottie => "application/json", _ => throw new InvalidOperationException("This format is not supported.") }; return emoji.Id is not 0 ? throw new InvalidOperationException("Only unicode emoji can be used for stickers.") : name.Length < 2 || name.Length > 30 ? throw new ArgumentOutOfRangeException(nameof(name), "Sticker name needs to be between 2 and 30 characters long.") : description.Length < 1 || description.Length > 100 ? throw new ArgumentOutOfRangeException(nameof(description), "Sticker description needs to be between 1 and 100 characters long.") : this.Discord.ApiClient.CreateGuildStickerAsync(this.Id, name, description, emoji.GetDiscordName().Replace(":", ""), new DiscordMessageFile("sticker", file, null, fileExt, contentType), reason); } /// /// Modifies a sticker /// /// The id of the sticker to modify /// The name of the sticker /// The description of the sticker /// The emoji to associate with this sticker. /// Audit log reason /// A sticker object - /// Thrown when the sticker could not be found. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Sticker does not belong to a guild. + /// Thrown when the sticker could not be found. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Sticker does not belong to a guild. public async Task ModifyStickerAsync(ulong sticker, Optional name, Optional description, Optional emoji, string reason = null) { if (!this.StickersInternal.TryGetValue(sticker, out var stickerobj) || stickerobj.Guild.Id != this.Id) throw new ArgumentException("This sticker does not belong to this guild."); if (name.HasValue && (name.Value.Length < 2 || name.Value.Length > 30)) throw new ArgumentException("Sticker name needs to be between 2 and 30 characters long."); if (description.HasValue && (description.Value.Length < 1 || description.Value.Length > 100)) throw new ArgumentException("Sticker description needs to be between 1 and 100 characters long."); if (emoji.HasValue && emoji.Value.Id > 0) throw new ArgumentException("Only unicode emojis can be used with stickers."); string uemoji = null; if (emoji.HasValue) uemoji = emoji.Value.GetDiscordName().Replace(":", ""); var usticker = await this.Discord.ApiClient.ModifyGuildStickerAsync(this.Id, sticker, name, description, uemoji, reason).ConfigureAwait(false); if (this.StickersInternal.TryGetValue(usticker.Id, out var old)) this.StickersInternal.TryUpdate(usticker.Id, usticker, old); return usticker; } /// /// Modifies a sticker /// /// The sticker to modify /// The name of the sticker /// The description of the sticker /// The emoji to associate with this sticker. /// Audit log reason /// A sticker object - /// Thrown when the sticker could not be found. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Sticker does not belong to a guild. + /// Thrown when the sticker could not be found. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Sticker does not belong to a guild. public Task ModifyStickerAsync(DiscordSticker sticker, Optional name, Optional description, Optional emoji, string reason = null) => this.ModifyStickerAsync(sticker.Id, name, description, emoji, reason); /// /// Deletes a sticker /// /// Id of sticker to delete /// Audit log reason - /// Thrown when the sticker could not be found. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Sticker does not belong to a guild. + /// Thrown when the sticker could not be found. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Sticker does not belong to a guild. public Task DeleteStickerAsync(ulong sticker, string reason = null) => !this.StickersInternal.TryGetValue(sticker, out var stickerobj) ? throw new ArgumentNullException(nameof(sticker)) : stickerobj.Guild.Id != this.Id ? throw new ArgumentException("This sticker does not belong to this guild.") : this.Discord.ApiClient.DeleteGuildStickerAsync(this.Id, sticker, reason); /// /// Deletes a sticker /// /// Sticker to delete /// Audit log reason - /// Thrown when the sticker could not be found. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Sticker does not belong to a guild. + /// Thrown when the sticker could not be found. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Sticker does not belong to a guild. public Task DeleteStickerAsync(DiscordSticker sticker, string reason = null) => this.DeleteStickerAsync(sticker.Id, reason); /// /// Gets the default channel for this guild. /// Default channel is the first channel current member can see. /// /// This member's default guild. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public DiscordChannel GetDefaultChannel() => this.ChannelsInternal?.Values.Where(xc => xc.Type == ChannelType.Text) .OrderBy(xc => xc.Position) .FirstOrDefault(xc => (xc.PermissionsFor(this.CurrentMember) & DisCatSharp.Permissions.AccessChannels) == DisCatSharp.Permissions.AccessChannels); /// /// Gets the guild's widget /// /// The guild's widget public Task GetWidgetAsync() => this.Discord.ApiClient.GetGuildWidgetAsync(this.Id); /// /// Gets the guild's widget settings /// /// The guild's widget settings public Task GetWidgetSettingsAsync() => this.Discord.ApiClient.GetGuildWidgetSettingsAsync(this.Id); /// /// Modifies the guild's widget settings /// /// If the widget is enabled or not /// Widget channel /// Reason the widget settings were modified /// The newly modified widget settings public Task ModifyWidgetSettingsAsync(bool? isEnabled = null, DiscordChannel channel = null, string reason = null) => this.Discord.ApiClient.ModifyGuildWidgetSettingsAsync(this.Id, isEnabled, channel?.Id, reason); /// /// Gets all of this guild's templates. /// /// All of the guild's templates. - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task> GetTemplatesAsync() => this.Discord.ApiClient.GetGuildTemplatesAsync(this.Id); /// /// Creates a guild template. /// /// Name of the template. /// Description of the template. /// The template created. - /// Throws when a template already exists for the guild or a null parameter is provided for the name. - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Throws when a template already exists for the guild or a null parameter is provided for the name. + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task CreateTemplateAsync(string name, string description = null) => this.Discord.ApiClient.CreateGuildTemplateAsync(this.Id, name, description); /// /// Syncs the template to the current guild's state. /// /// The code of the template to sync. /// The template synced. - /// Throws when the template for the code cannot be found - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Throws when the template for the code cannot be found + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task SyncTemplateAsync(string code) => this.Discord.ApiClient.SyncGuildTemplateAsync(this.Id, code); /// /// Modifies the template's metadata. /// /// The template's code. /// Name of the template. /// Description of the template. /// The template modified. - /// Throws when the template for the code cannot be found - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Throws when the template for the code cannot be found + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task ModifyTemplateAsync(string code, string name = null, string description = null) => this.Discord.ApiClient.ModifyGuildTemplateAsync(this.Id, code, name, description); /// /// Deletes the template. /// /// The code of the template to delete. /// The deleted template. - /// Throws when the template for the code cannot be found - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. + /// Throws when the template for the code cannot be found + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. public Task DeleteTemplateAsync(string code) => this.Discord.ApiClient.DeleteGuildTemplateAsync(this.Id, code); /// /// Gets this guild's membership screening form. /// /// This guild's membership screening form. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public Task GetMembershipScreeningFormAsync() => this.Discord.ApiClient.GetGuildMembershipScreeningFormAsync(this.Id); /// /// Modifies this guild's membership screening form. /// /// Action to perform /// The modified screening form. - /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. + /// Thrown when Discord is unable to process the request. public async Task ModifyMembershipScreeningFormAsync(Action action) { var mdl = new MembershipScreeningEditModel(); action(mdl); return await this.Discord.ApiClient.ModifyGuildMembershipScreeningFormAsync(this.Id, mdl.Enabled, mdl.Fields, mdl.Description); } /// /// Gets all the application commands in this guild. /// /// A list of application commands in this guild. public Task> GetApplicationCommandsAsync() => this.Discord.ApiClient.GetGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id); /// /// Overwrites the existing application commands in this guild. New commands are automatically created and missing commands are automatically delete /// /// The list of commands to overwrite with. /// The list of guild commands public Task> BulkOverwriteApplicationCommandsAsync(IEnumerable commands) => this.Discord.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id, commands); /// /// Creates or overwrites a application command in this guild. /// /// The command to create. /// The created command. public Task CreateApplicationCommandAsync(DiscordApplicationCommand command) => this.Discord.ApiClient.CreateGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, command); /// /// Edits a application command in this guild. /// /// The id of the command to edit. /// Action to perform. /// The edit command. public async Task EditApplicationCommandAsync(ulong commandId, Action action) { var mdl = new ApplicationCommandEditModel(); action(mdl); return await this.Discord.ApiClient.EditGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.NameLocalizations, mdl.DescriptionLocalizations, mdl.DefaultMemberPermissions, mdl.DmPermission, mdl.IsNsfw).ConfigureAwait(false); } /// /// Gets this guild's welcome screen. /// /// This guild's welcome screen object. - /// Thrown when Discord is unable to process the request. + /// Thrown when Discord is unable to process the request. public Task GetWelcomeScreenAsync() => this.Discord.ApiClient.GetGuildWelcomeScreenAsync(this.Id); /// /// Modifies this guild's welcome screen. /// /// Action to perform. /// The modified welcome screen. - /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. + /// Thrown when Discord is unable to process the request. public async Task ModifyWelcomeScreenAsync(Action action) { var mdl = new WelcomeScreenEditModel(); action(mdl); return await this.Discord.ApiClient.ModifyGuildWelcomeScreenAsync(this.Id, mdl.Enabled, mdl.WelcomeChannels, mdl.Description).ConfigureAwait(false); } #endregion /// /// Returns a string representation of this guild. /// /// String representation of this guild. public override string ToString() => $"Guild {this.Id}; {this.Name}"; /// /// 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 DiscordGuild); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordGuild 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 guild to compare. /// Second guild to compare. /// Whether the two guilds are equal. public static bool operator ==(DiscordGuild e1, DiscordGuild 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 guild to compare. /// Second guild to compare. /// Whether the two guilds are not equal. public static bool operator !=(DiscordGuild e1, DiscordGuild e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/Guild/DiscordGuildEmoji.cs b/DisCatSharp/Entities/Guild/DiscordGuildEmoji.cs index cde206710..1e50822e3 100644 --- a/DisCatSharp/Entities/Guild/DiscordGuildEmoji.cs +++ b/DisCatSharp/Entities/Guild/DiscordGuildEmoji.cs @@ -1,77 +1,77 @@ // 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.Collections.Generic; using System.Threading.Tasks; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a guild emoji. /// public sealed class DiscordGuildEmoji : DiscordEmoji { /// /// Gets the user that created this emoji. /// [JsonIgnore] public DiscordUser User { get; internal set; } /// /// Gets the guild to which this emoji belongs. /// [JsonIgnore] public DiscordGuild Guild { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordGuildEmoji() { } /// /// Modifies this emoji. /// /// New name for this emoji. /// Roles for which this emoji will be available. This works only if your application is whitelisted as integration. /// Reason for audit log. /// The modified emoji. - /// Thrown when the client does not have the permission. - /// Thrown when the emoji does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ModifyAsync(string name, IEnumerable roles = null, string reason = null) => this.Guild.ModifyEmojiAsync(this, name, roles, reason); /// /// Deletes this emoji. /// /// Reason for audit log. - /// Thrown when the client does not have the permission. - /// Thrown when the emoji does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the emoji 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.Guild.DeleteEmojiAsync(this, reason); } diff --git a/DisCatSharp/Entities/Guild/DiscordMember.cs b/DisCatSharp/Entities/Guild/DiscordMember.cs index 0d20571d1..8166077d8 100644 --- a/DisCatSharp/Entities/Guild/DiscordMember.cs +++ b/DisCatSharp/Entities/Guild/DiscordMember.cs @@ -1,795 +1,795 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord guild member. /// public class DiscordMember : DiscordUser, IEquatable { /// /// Initializes a new instance of the class. /// internal DiscordMember() { this._roleIdsLazy = new Lazy>(() => new ReadOnlyCollection(this.RoleIdsInternal)); } /// /// Initializes a new instance of the class. /// /// The user. internal DiscordMember(DiscordUser user) { this.Discord = user.Discord; this.Id = user.Id; this.RoleIdsInternal = new List(); this._roleIdsLazy = new Lazy>(() => new ReadOnlyCollection(this.RoleIdsInternal)); } /// /// Initializes a new instance of the class. /// /// The mbr. internal DiscordMember(TransportMember mbr) { this.Id = mbr.User.Id; this.IsDeafened = mbr.IsDeafened; this.IsMuted = mbr.IsMuted; this.JoinedAt = mbr.JoinedAt; this.Nickname = mbr.Nickname; this.PremiumSince = mbr.PremiumSince; this.IsPending = mbr.IsPending; this.GuildAvatarHash = mbr.GuildAvatarHash; this.GuildBannerHash = mbr.GuildBannerHash; this.GuildBio = mbr.GuildBio; this.GuildPronouns = mbr.GuildPronouns; this.CommunicationDisabledUntil = mbr.CommunicationDisabledUntil; this.AvatarHashInternal = mbr.AvatarHash; this.RoleIdsInternal = mbr.Roles ?? new List(); this._roleIdsLazy = new Lazy>(() => new ReadOnlyCollection(this.RoleIdsInternal)); this.MemberFlags = mbr.MemberFlags; } /// /// Gets the members avatar hash. /// [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] public virtual string GuildAvatarHash { get; internal set; } /// /// Gets the members avatar URL. /// [JsonIgnore] public string GuildAvatarUrl => string.IsNullOrWhiteSpace(this.GuildAvatarHash) ? this.User.AvatarUrl : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.GUILDS}/{this.GuildId.ToString(CultureInfo.InvariantCulture)}{Endpoints.USERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}{Endpoints.AVATARS}/{this.GuildAvatarHash}.{(this.GuildAvatarHash.StartsWith("a_") ? "gif" : "png")}?size=1024"; /// /// Gets the members banner hash. /// [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] public virtual string GuildBannerHash { get; internal set; } /// /// Gets the members banner URL. /// [JsonIgnore] public string GuildBannerUrl => string.IsNullOrWhiteSpace(this.GuildBannerHash) ? this.User.BannerUrl : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.GUILDS}/{this.GuildId.ToString(CultureInfo.InvariantCulture)}{Endpoints.USERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}{Endpoints.BANNERS}/{this.GuildBannerHash}.{(this.GuildBannerHash.StartsWith("a_") ? "gif" : "png")}?size=1024"; /// /// The color of this member's banner. Mutually exclusive with . /// [JsonIgnore] public override DiscordColor? BannerColor => this.User.BannerColor; /// /// Gets this member's nickname. /// [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] public string Nickname { get; internal set; } /// /// Gets the members guild bio. /// This is not available to bots tho. /// [JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)] public string GuildBio { get; internal set; } /// /// Gets the members's pronouns. /// [JsonProperty("pronouns", NullValueHandling = NullValueHandling.Ignore)] public string GuildPronouns { get; internal set; } /// /// Gets the members flags. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public MemberFlags MemberFlags { get; internal set; } [JsonIgnore] internal string AvatarHashInternal; /// /// Gets this member's display name. /// [JsonIgnore] public string DisplayName => this.Nickname ?? this.Username; /// /// List of role ids /// [JsonIgnore] internal IReadOnlyList RoleIds => this._roleIdsLazy.Value; [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] internal List RoleIdsInternal; [JsonIgnore] private readonly Lazy> _roleIdsLazy; /// /// Gets the list of roles associated with this member. /// [JsonIgnore] public IEnumerable Roles => this.RoleIds.Select(id => this.Guild.GetRole(id)).Where(x => x != null); /// /// Gets the color associated with this user's top color-giving role, otherwise 0 (no color). /// [JsonIgnore] public DiscordColor Color { get { var role = this.Roles.OrderByDescending(xr => xr.Position).FirstOrDefault(xr => xr.Color.Value != 0); return role != null ? role.Color : new DiscordColor(); } } /// /// Date the user joined the guild /// [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset JoinedAt { get; internal set; } /// /// Date the user started boosting this server /// [JsonProperty("premium_since", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset? PremiumSince { get; internal set; } /// /// Date until the can communicate again. /// [JsonProperty("communication_disabled_until", NullValueHandling = NullValueHandling.Include)] public DateTime? CommunicationDisabledUntil { get; internal set; } /// /// If the user is deafened /// [JsonProperty("is_deafened", NullValueHandling = NullValueHandling.Ignore)] public bool IsDeafened { get; internal set; } /// /// If the user is muted /// [JsonProperty("is_muted", NullValueHandling = NullValueHandling.Ignore)] public bool IsMuted { get; internal set; } /// /// Whether the user has not passed the guild's Membership Screening requirements yet. /// [JsonProperty("pending", NullValueHandling = NullValueHandling.Ignore)] public bool? IsPending { get; internal set; } /// /// Gets this member's voice state. /// [JsonIgnore] public DiscordVoiceState VoiceState => this.Discord.Guilds[this.GuildId].VoiceStates.TryGetValue(this.Id, out var voiceState) ? voiceState : null; [JsonIgnore] internal ulong GuildId = 0; /// /// Gets the guild of which this member is a part of. /// [JsonIgnore] public DiscordGuild Guild => this.Discord.Guilds[this.GuildId]; /// /// Gets whether this member is the Guild owner. /// [JsonIgnore] public bool IsOwner => this.Id == this.Guild.OwnerId; /// /// Gets the member's position in the role hierarchy, which is the member's highest role's position. Returns for the guild's owner. /// [JsonIgnore] public int Hierarchy => this.IsOwner ? int.MaxValue : this.RoleIds.Count == 0 ? 0 : this.Roles.Max(x => x.Position); /// /// Gets the permissions for the current member. /// [JsonIgnore] public Permissions Permissions => this.GetPermissions(); #region Overridden user properties /// /// Gets the user. /// [JsonIgnore] internal DiscordUser User => this.Discord.UserCache[this.Id]; /// /// Gets this member's username. /// [JsonIgnore] public override string Username { get => this.User.Username; internal set => this.User.Username = value; } /// /// Gets the member's 4-digit discriminator. /// [JsonIgnore] public override string Discriminator { get => this.User.Discriminator; internal set => this.User.Discriminator = value; } /// /// Gets the member's avatar hash. /// [JsonIgnore] public override string AvatarHash { get => this.User.AvatarHash; internal set => this.User.AvatarHash = value; } /// /// Gets the member's banner hash. /// [JsonIgnore] public override string BannerHash { get => this.User.BannerHash; internal set => this.User.BannerHash = value; } /// /// Gets whether the member is a bot. /// [JsonIgnore] public override bool IsBot { get => this.User.IsBot; internal set => this.User.IsBot = value; } /// /// Gets the member's email address. /// This is only present in OAuth. /// [JsonIgnore] public override string Email { get => this.User.Email; internal set => this.User.Email = value; } /// /// Gets whether the member has multi-factor authentication enabled. /// [JsonIgnore] public override bool? MfaEnabled { get => this.User.MfaEnabled; internal set => this.User.MfaEnabled = value; } /// /// Gets whether the member is verified. /// This is only present in OAuth. /// [JsonIgnore] public override bool? Verified { get => this.User.Verified; internal set => this.User.Verified = value; } /// /// Gets the member's chosen language /// [JsonIgnore] public override string Locale { get => this.User.Locale; internal set => this.User.Locale = value; } /// /// Gets the user's flags. /// [JsonIgnore] public override UserFlags? OAuthFlags { get => this.User.OAuthFlags; internal set => this.User.OAuthFlags = value; } /// /// Gets the member's flags for OAuth. /// [JsonIgnore] public override UserFlags? Flags { get => this.User.Flags; internal set => this.User.Flags = value; } #endregion /// /// Creates a direct message channel to this member. /// /// Direct message channel to this member. - /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateDmChannelAsync() => this.Discord.ApiClient.CreateDmAsync(this.Id); /// /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. /// /// Content of the message to send. /// The sent message. - /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(string content) { if (this.IsBot && this.Discord.CurrentUser.IsBot) throw new ArgumentException("Bots cannot DM each other."); var chn = await this.CreateDmChannelAsync().ConfigureAwait(false); return await chn.SendMessageAsync(content).ConfigureAwait(false); } /// /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. /// /// Embed to attach to the message. /// The sent message. - /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(DiscordEmbed embed) { if (this.IsBot && this.Discord.CurrentUser.IsBot) throw new ArgumentException("Bots cannot DM each other."); var chn = await this.CreateDmChannelAsync().ConfigureAwait(false); return await chn.SendMessageAsync(embed).ConfigureAwait(false); } /// /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. /// /// Content of the message to send. /// Embed to attach to the message. /// The sent message. - /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(string content, DiscordEmbed embed) { if (this.IsBot && this.Discord.CurrentUser.IsBot) throw new ArgumentException("Bots cannot DM each other."); var chn = await this.CreateDmChannelAsync().ConfigureAwait(false); return await chn.SendMessageAsync(content, embed).ConfigureAwait(false); } /// /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. /// /// Builder to with the message. /// The sent message. - /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task SendMessageAsync(DiscordMessageBuilder message) { if (this.IsBot && this.Discord.CurrentUser.IsBot) throw new ArgumentException("Bots cannot DM each other."); var chn = await this.CreateDmChannelAsync().ConfigureAwait(false); return await chn.SendMessageAsync(message).ConfigureAwait(false); } /// /// Sets this member's voice mute status. /// /// Whether the member is to be muted. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task SetMuteAsync(bool mute, string reason = null) => this.Discord.ApiClient.ModifyGuildMemberAsync(this.GuildId, this.Id, default, default, mute, default, default, reason); /// /// Sets this member's voice deaf status. /// /// Whether the member is to be deafened. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task SetDeafAsync(bool deaf, string reason = null) => this.Discord.ApiClient.ModifyGuildMemberAsync(this.GuildId, this.Id, default, default, default, deaf, default, reason); /// /// Modifies this member. /// /// Action to perform on this member. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task ModifyAsync(Action action) { var mdl = new MemberEditModel(); action(mdl); if (mdl.VoiceChannel.HasValue && mdl.VoiceChannel.Value != null && mdl.VoiceChannel.Value.Type != ChannelType.Voice && mdl.VoiceChannel.Value.Type != ChannelType.Stage) throw new ArgumentException("Given channel is not a voice or stage channel.", nameof(action)); if (mdl.Nickname.HasValue && this.Discord.CurrentUser.Id == this.Id) { await this.Discord.ApiClient.ModifyCurrentMemberNicknameAsync(this.Guild.Id, mdl.Nickname.Value, mdl.AuditLogReason).ConfigureAwait(false); await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, Optional.None, mdl.Roles.Map(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, mdl.VoiceChannel.Map(e => e?.Id), mdl.AuditLogReason).ConfigureAwait(false); } else { await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, mdl.Nickname, mdl.Roles.Map(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, mdl.VoiceChannel.Map(e => e?.Id), mdl.AuditLogReason).ConfigureAwait(false); } } /// /// Disconnects the member from their current voice channel. /// public async Task DisconnectFromVoiceAsync() => await this.ModifyAsync(x => x.VoiceChannel = null); /// /// Adds a timeout to a member. /// /// The datetime offset to time out the user. Up to 28 days. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(DateTimeOffset until, string reason = null) => until.Subtract(DateTimeOffset.UtcNow).Days > 28 ? throw new ArgumentException("Timeout can not be longer than 28 days") : this.Discord.ApiClient.ModifyTimeoutAsync(this.Guild.Id, this.Id, until, reason); /// /// Adds a timeout to a member. /// /// The timespan to time out the user. Up to 28 days. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(TimeSpan until, string reason = null) => this.TimeoutAsync(DateTimeOffset.UtcNow + until, reason); /// /// Adds a timeout to a member. /// /// The datetime to time out the user. Up to 28 days. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(DateTime until, string reason = null) => this.TimeoutAsync(until.ToUniversalTime() - DateTime.UtcNow, reason); /// /// Removes the timeout from a member. /// /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RemoveTimeoutAsync(string reason = null) => this.Discord.ApiClient.ModifyTimeoutAsync(this.Guild.Id, this.Id, null, reason); /// /// Grants a role to the member. /// /// Role to grant. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GrantRoleAsync(DiscordRole role, string reason = null) => this.Discord.ApiClient.AddGuildMemberRoleAsync(this.Guild.Id, this.Id, role.Id, reason); /// /// Revokes a role from a member. /// /// Role to revoke. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RevokeRoleAsync(DiscordRole role, string reason = null) => this.Discord.ApiClient.RemoveGuildMemberRoleAsync(this.Guild.Id, this.Id, role.Id, reason); /// /// Sets the member's roles to ones specified. /// /// Roles to set. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ReplaceRolesAsync(IEnumerable roles, string reason = null) => this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, default, Optional.Some(roles.Select(xr => xr.Id)), default, default, default, reason); /// /// Bans this member from their guild. /// /// How many days to remove messages from. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task BanAsync(int deleteMessageDays = 0, string reason = null) => this.Guild.BanMemberAsync(this, deleteMessageDays, reason); /// /// Unbans this member from their guild. /// /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task UnbanAsync(string reason = null) => this.Guild.UnbanMemberAsync(this, reason); /// /// Kicks this member from their guild. /// /// Reason for audit logs. /// - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RemoveAsync(string reason = null) => this.Discord.ApiClient.RemoveGuildMemberAsync(this.GuildId, this.Id, reason); /// /// Moves this member to the specified voice channel /// /// - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task PlaceInAsync(DiscordChannel channel) => channel.PlaceMemberAsync(this); /// /// Updates the member's suppress state in a stage channel. /// /// The channel the member is currently in. /// Toggles the member's suppress state. - /// Thrown when the channel in not a voice channel. + /// Thrown when the channel in not a voice channel. public async Task UpdateVoiceStateAsync(DiscordChannel channel, bool? suppress) { if (channel.Type != ChannelType.Stage) throw new ArgumentException("Voice state can only be updated in a stage channel."); await this.Discord.ApiClient.UpdateUserVoiceStateAsync(this.Guild.Id, this.Id, channel.Id, suppress).ConfigureAwait(false); } /// /// Makes the user a speaker. /// - /// Thrown when the user is not inside an stage channel. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the user is not inside an stage channel. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task MakeSpeakerAsync() { var vs = this.VoiceState; if (vs == null || vs.Channel.Type != ChannelType.Stage) throw new ArgumentException("Voice state can only be updated when the user is inside an stage channel."); await this.Discord.ApiClient.UpdateUserVoiceStateAsync(this.Guild.Id, this.Id, vs.Channel.Id, false).ConfigureAwait(false); } /// /// Moves the user to audience. /// - /// Thrown when the user is not inside an stage channel. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the user is not inside an stage channel. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task MoveToAudienceAsync() { var vs = this.VoiceState; if (vs == null || vs.Channel.Type != ChannelType.Stage) throw new ArgumentException("Voice state can only be updated when the user is inside an stage channel."); await this.Discord.ApiClient.UpdateUserVoiceStateAsync(this.Guild.Id, this.Id, vs.Channel.Id, true).ConfigureAwait(false); } /// /// Calculates permissions in a given channel for this member. /// /// Channel to calculate permissions for. /// Calculated permissions for this member in the channel. public Permissions PermissionsIn(DiscordChannel channel) => channel.PermissionsFor(this); /// /// Get's the current member's roles based on the sum of the permissions of their given roles. /// private Permissions GetPermissions() { if (this.Guild.OwnerId == this.Id) return PermissionMethods.FullPerms; Permissions perms; // assign @everyone permissions var everyoneRole = this.Guild.EveryoneRole; perms = everyoneRole.Permissions; // assign permissions from member's roles (in order) perms |= this.Roles.Aggregate(Permissions.None, (c, role) => c | role.Permissions); // Administrator grants all permissions and cannot be overridden return (perms & Permissions.Administrator) == Permissions.Administrator ? PermissionMethods.FullPerms : perms; } /// /// Returns a string representation of this member. /// /// String representation of this member. public override string ToString() => $"Member {this.Id}; {this.Username}#{this.Discriminator} ({this.DisplayName})"; /// /// 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 DiscordMember); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordMember e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.GuildId == e.GuildId)); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() { var hash = 13; hash = (hash * 7) + this.Id.GetHashCode(); hash = (hash * 7) + this.GuildId.GetHashCode(); return hash; } /// /// Gets whether the two objects are equal. /// /// First member to compare. /// Second member to compare. /// Whether the two members are equal. public static bool operator ==(DiscordMember e1, DiscordMember 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 && e1.GuildId == e2.GuildId)); } /// /// Gets whether the two objects are not equal. /// /// First member to compare. /// Second member to compare. /// Whether the two members are not equal. public static bool operator !=(DiscordMember e1, DiscordMember e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/Guild/DiscordRole.cs b/DisCatSharp/Entities/Guild/DiscordRole.cs index 5bd61dc35..2ea000214 100644 --- a/DisCatSharp/Entities/Guild/DiscordRole.cs +++ b/DisCatSharp/Entities/Guild/DiscordRole.cs @@ -1,292 +1,292 @@ // 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; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a discord role, to which users can be assigned. /// public class DiscordRole : SnowflakeObject, IEquatable { /// /// Gets the name of this role. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets the color of this role. /// [JsonIgnore] public DiscordColor Color => new(this.ColorInternal); [JsonProperty("color", NullValueHandling = NullValueHandling.Ignore)] internal int ColorInternal; /// /// Gets whether this role is hoisted. /// [JsonProperty("hoist", NullValueHandling = NullValueHandling.Ignore)] public bool IsHoisted { get; internal set; } /// /// Gets the position of this role in the role hierarchy. /// [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] public int Position { get; internal set; } /// /// Gets the permissions set for this role. /// [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] public Permissions Permissions { get; internal set; } /// /// Gets whether this role is managed by an integration. /// [JsonProperty("managed", NullValueHandling = NullValueHandling.Ignore)] public bool IsManaged { get; internal set; } /// /// Gets whether this role is mentionable. /// [JsonProperty("mentionable", NullValueHandling = NullValueHandling.Ignore)] public bool IsMentionable { get; internal set; } /// /// Gets the tags this role has. /// [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] public DiscordRoleTags Tags { get; internal set; } /// /// Gets the role icon's hash. /// [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] public string IconHash { get; internal set; } /// /// Gets the role icon's url. /// [JsonIgnore] public string IconUrl => !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ROLE_ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.png?size=64" : null; /// /// Gets the role unicode_emoji. /// [JsonProperty("unicode_emoji", NullValueHandling = NullValueHandling.Ignore)] internal string UnicodeEmojiString; /// /// Gets the unicode emoji. /// public DiscordEmoji UnicodeEmoji => this.UnicodeEmojiString != null ? DiscordEmoji.FromName(this.Discord, $":{this.UnicodeEmojiString}:", false) : null; [JsonIgnore] internal ulong GuildId = 0; /// /// Gets the role flags. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public RoleFlags Flags { get; internal set; } /// /// Gets a mention string for this role. If the role is mentionable, this string will mention all the users that belong to this role. /// public string Mention => Formatter.Mention(this); #region Methods /// /// Modifies this role's position. /// /// New position /// Reason why we moved it /// - /// Thrown when the client does not have the permission. - /// Thrown when the role does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the role 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) { var roles = this.Discord.Guilds[this.GuildId].Roles.Values.OrderByDescending(xr => xr.Position) .Select(x => new RestGuildRoleReorderPayload { RoleId = x.Id, Position = x.Id == this.Id ? position : x.Position <= position ? x.Position - 1 : x.Position }); return this.Discord.ApiClient.ModifyGuildRolePositionAsync(this.GuildId, roles, reason); } /// /// Updates this role. /// /// New role name. /// New role permissions. /// New role color. /// New role hoist. /// Whether this role is mentionable. /// Audit log reason. - /// Thrown when the client does not have the permission. - /// Thrown when the role does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the role does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ModifyAsync(string name = null, Permissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string reason = null) => this.Discord.ApiClient.ModifyGuildRoleAsync(this.GuildId, this.Id, name, permissions, color?.Value, hoist, mentionable, null, null, reason); /// /// Updates this role. /// /// The action. /// Thrown when the client does not have the permission. - /// Thrown when the role does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the role does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Action action) { var mdl = new RoleEditModel(); action(mdl); var canContinue = true; if ((mdl.Icon.HasValue && mdl.Icon.Value != null) || (mdl.UnicodeEmoji.HasValue && mdl.UnicodeEmoji.Value != null)) canContinue = this.Discord.Guilds[this.GuildId].Features.CanSetRoleIcons; var iconb64 = Optional.FromNullable(null); if (mdl.Icon.HasValue && mdl.Icon.Value != null) iconb64 = ImageTool.Base64FromStream(mdl.Icon); else if (mdl.Icon.HasValue) iconb64 = Optional.Some(null); var emoji = Optional.FromNullable(null); if (mdl.UnicodeEmoji.HasValue && mdl.UnicodeEmoji.Value != null) emoji = mdl.UnicodeEmoji .MapOrNull(e => e.Id == 0 ? e.Name : throw new ArgumentException("Emoji must be unicode")); else if (mdl.UnicodeEmoji.HasValue) emoji = Optional.Some(null); return canContinue ? this.Discord.ApiClient.ModifyGuildRoleAsync(this.GuildId, this.Id, mdl.Name, mdl.Permissions, mdl.Color?.Value, mdl.Hoist, mdl.Mentionable, iconb64, emoji, mdl.AuditLogReason) : throw new NotSupportedException($"Cannot modify role icon. Guild needs boost tier two."); } /// /// Deletes this role. /// /// Reason as to why this role has been deleted. /// - /// Thrown when the client does not have the permission. - /// Thrown when the role does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the role 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.DeleteRoleAsync(this.GuildId, this.Id, reason); #endregion /// /// Initializes a new instance of the class. /// internal DiscordRole() { } /// /// Checks whether this role has specific permissions. /// /// Permissions to check for. /// Whether the permissions are allowed or not. public PermissionLevel CheckPermission(Permissions permission) => (this.Permissions & permission) != 0 ? PermissionLevel.Allowed : PermissionLevel.Unset; /// /// Returns a string representation of this role. /// /// String representation of this role. public override string ToString() => $"Role {this.Id}; {this.Name}"; /// /// 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 DiscordRole); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordRole e) => e switch { null => false, _ => 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 role to compare. /// Second role to compare. /// Whether the two roles are equal. public static bool operator ==(DiscordRole e1, DiscordRole e2) => e1 is null == e2 is null && ((e1 is null && e2 is null) || e1.Id == e2.Id); /// /// Gets whether the two objects are not equal. /// /// First role to compare. /// Second role to compare. /// Whether the two roles are not equal. public static bool operator !=(DiscordRole e1, DiscordRole e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEvent.cs b/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEvent.cs index 11486017b..c6d1bc259 100644 --- a/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEvent.cs +++ b/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEvent.cs @@ -1,327 +1,327 @@ // 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.Globalization; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using DisCatSharp.Net.Models; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents an scheduled event. /// public class DiscordScheduledEvent : SnowflakeObject, IEquatable { /// /// Gets the guild id of the associated scheduled event. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] public ulong GuildId { get; internal set; } /// /// Gets the guild to which this scheduled event belongs. /// [JsonIgnore] public DiscordGuild Guild => this.Discord.Guilds.TryGetValue(this.GuildId, out var guild) ? guild : null; /// /// Gets the associated channel. /// [JsonIgnore] public Task Channel => this.ChannelId.HasValue ? this.Discord.ApiClient.GetChannelAsync(this.ChannelId.Value) : null; /// /// Gets id of the associated channel id. /// [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? ChannelId { get; internal set; } /// /// Gets the ID of the user that created the scheduled event. /// [JsonProperty("creator_id")] public ulong CreatorId { get; internal set; } /// /// Gets the user that created the scheduled event. /// [JsonProperty("creator")] public DiscordUser Creator { get; internal set; } /// /// Gets the member that created the scheduled event. /// [JsonIgnore] public DiscordMember CreatorMember => this.Guild.MembersInternal.TryGetValue(this.CreatorId, out var owner) ? owner : this.Discord.ApiClient.GetGuildMemberAsync(this.GuildId, this.CreatorId).Result; /// /// Gets the name of the scheduled event. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets the description of the scheduled event. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; internal set; } /// /// Gets this event's cover hash, when applicable. /// [JsonProperty("image", NullValueHandling = NullValueHandling.Include)] public string CoverImageHash { get; internal set; } /// /// Gets this event's cover in url form. /// [JsonIgnore] public string CoverImageUrl => !string.IsNullOrWhiteSpace(this.CoverImageHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.GUILD_EVENTS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.CoverImageHash}.png" : null; /// /// Gets the scheduled start time of the scheduled event. /// [JsonIgnore] public DateTimeOffset? ScheduledStartTime => !string.IsNullOrWhiteSpace(this.ScheduledStartTimeRaw) && DateTimeOffset.TryParse(this.ScheduledStartTimeRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : null; /// /// Gets the scheduled start time of the scheduled event as raw string. /// [JsonProperty("scheduled_start_time", NullValueHandling = NullValueHandling.Ignore)] internal string ScheduledStartTimeRaw { get; set; } /// /// Gets the scheduled end time of the scheduled event. /// [JsonIgnore] public DateTimeOffset? ScheduledEndTime => !string.IsNullOrWhiteSpace(this.ScheduledEndTimeRaw) && DateTimeOffset.TryParse(this.ScheduledEndTimeRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : null; /// /// Gets the scheduled end time of the scheduled event as raw string. /// [JsonProperty("scheduled_end_time", NullValueHandling = NullValueHandling.Ignore)] internal string ScheduledEndTimeRaw { get; set; } /// /// Gets the privacy level of the scheduled event. /// [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] internal ScheduledEventPrivacyLevel PrivacyLevel { get; set; } /// /// Gets the status of the scheduled event. /// [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] public ScheduledEventStatus Status { get; internal set; } /// /// Gets the entity type. /// [JsonProperty("entity_type", NullValueHandling = NullValueHandling.Ignore)] public ScheduledEventEntityType EntityType { get; internal set; } /// /// Gets id of the entity. /// [JsonProperty("entity_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? EntityId { get; internal set; } /// /// Gets metadata of the entity. /// [JsonProperty("entity_metadata", NullValueHandling = NullValueHandling.Ignore)] public DiscordScheduledEventEntityMetadata EntityMetadata { get; internal set; } /* This isn't used. * See https://github.com/discord/discord-api-docs/pull/3586#issuecomment-969066061. * Was originally for paid stages. /// /// Gets the sku ids of the scheduled event. /// [JsonProperty("sku_ids", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyList SkuIds { get; internal set; }*/ /// /// Gets the total number of users subscribed to the scheduled event. /// [JsonProperty("user_count", NullValueHandling = NullValueHandling.Ignore)] public int UserCount { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordScheduledEvent() { } #region Methods /// /// Modifies the current scheduled event. /// /// Action to perform on this thread - /// Thrown when the client does not have the permission. - /// Thrown when the event does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the event does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task ModifyAsync(Action action) { var mdl = new ScheduledEventEditModel(); action(mdl); Optional channelId = null; if (this.EntityType == ScheduledEventEntityType.External || mdl.EntityType != ScheduledEventEntityType.External) channelId = mdl.Channel .MapOrNull(c => c.Type != ChannelType.Voice && c.Type != ChannelType.Stage ? throw new ArgumentException("Channel needs to be a voice or stage channel.") : c.Id); var coverb64 = ImageTool.Base64FromStream(mdl.CoverImage); var scheduledEndTime = Optional.None; if (mdl.ScheduledEndTime.HasValue && mdl.EntityType.HasValue ? mdl.EntityType == ScheduledEventEntityType.External : this.EntityType == ScheduledEventEntityType.External) scheduledEndTime = mdl.ScheduledEndTime.Value; await this.Discord.ApiClient.ModifyGuildScheduledEventAsync(this.GuildId, this.Id, channelId, this.EntityType == ScheduledEventEntityType.External ? new DiscordScheduledEventEntityMetadata(mdl.Location.Value) : null, mdl.Name, mdl.ScheduledStartTime, scheduledEndTime, mdl.Description, mdl.EntityType, mdl.Status, coverb64, mdl.AuditLogReason); } /// /// Starts the current scheduled event. /// - /// Thrown when the client does not have the permission. - /// Thrown when the event does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the event does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task StartAsync(string reason = null) => this.Status == ScheduledEventStatus.Scheduled ? await this.Discord.ApiClient.ModifyGuildScheduledEventStatusAsync(this.GuildId, this.Id, ScheduledEventStatus.Active, reason) : throw new InvalidOperationException("You can only start scheduled events"); /// /// Cancels the current scheduled event. /// /// The audit log reason. - /// Thrown when the client does not have the permission. - /// Thrown when the event does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the event does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task CancelAsync(string reason = null) => this.Status == ScheduledEventStatus.Scheduled ? await this.Discord.ApiClient.ModifyGuildScheduledEventStatusAsync(this.GuildId, this.Id, ScheduledEventStatus.Canceled, reason) : throw new InvalidOperationException("You can only cancel scheduled events"); /// /// Ends the current scheduled event. /// /// The audit log reason. - /// Thrown when the client does not have the permission. - /// Thrown when the event does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the event does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task EndAsync(string reason = null) => this.Status == ScheduledEventStatus.Active ? await this.Discord.ApiClient.ModifyGuildScheduledEventStatusAsync(this.GuildId, this.Id, ScheduledEventStatus.Completed, reason) : throw new InvalidOperationException("You can only stop active events"); /// /// Gets a list of users RSVP'd to the scheduled event. /// /// The limit how many users to receive from the event. Defaults to 100. Max 100. /// Get results of before the given snowflake. /// Get results of after the given snowflake. /// Whether to include guild member data. - /// Thrown when the client does not have the correct permissions. - /// Thrown when the event does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the correct permissions. + /// Thrown when the event does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task> GetUsersAsync(int? limit = null, ulong? before = null, ulong? after = null, bool? withMember = null) => await this.Discord.ApiClient.GetGuildScheduledEventRspvUsersAsync(this.GuildId, this.Id, limit, before, after, withMember); /// /// Deletes a scheduled event. /// /// The audit log reason. - /// Thrown when the client does not have the permission. - /// Thrown when the event does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the event does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task DeleteAsync(string reason = null) => await this.Discord.ApiClient.DeleteGuildScheduledEventAsync(this.GuildId, this.Id, reason); #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 DiscordScheduledEvent); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordScheduledEvent 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 event to compare. /// Second event to compare. /// Whether the two events are equal. public static bool operator ==(DiscordScheduledEvent e1, DiscordScheduledEvent 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 event to compare. /// Second event to compare. /// Whether the two events are not equal. public static bool operator !=(DiscordScheduledEvent e1, DiscordScheduledEvent e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs b/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs index f43e8a54c..925b7e2fc 100644 --- a/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs +++ b/DisCatSharp/Entities/Interaction/DiscordFollowupMessageBuilder.cs @@ -1,313 +1,313 @@ // 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.IO; using System.Linq; namespace DisCatSharp.Entities; /// /// Constructs a followup message to an interaction. /// public sealed class DiscordFollowupMessageBuilder { /// /// Whether this followup message is text-to-speech. /// public bool IsTts { get; set; } /// /// Whether this followup message should be ephemeral. /// public bool IsEphemeral { get; set; } /// /// Indicates this message is ephemeral. /// internal int? Flags => this.IsEphemeral ? 64 : null; /// /// Message to send on followup message. /// public string Content { get => this._content; set { if (value != null && value.Length > 2000) throw new ArgumentException("Content length cannot exceed 2000 characters.", nameof(value)); this._content = value; } } private string _content; /// /// Embeds to send on followup message. /// public IReadOnlyList Embeds => this._embeds; private readonly List _embeds = new(); /// /// Files to send on this followup message. /// public IReadOnlyList Files => this._files; private readonly List _files = new(); /// /// Components to send on this followup message. /// public IReadOnlyList Components => this._components; private readonly List _components = new(); /// /// Mentions to send on this followup message. /// public IReadOnlyList Mentions => this._mentions; private readonly List _mentions = new(); /// /// Appends a collection of components to the message. /// /// The collection of components to add. /// The builder to chain calls with. - /// contained more than 5 components. + /// contained more than 5 components. public DiscordFollowupMessageBuilder AddComponents(params DiscordComponent[] components) => this.AddComponents((IEnumerable)components); /// /// Appends several rows of components to the message /// /// The rows of components to add, holding up to five each. /// public DiscordFollowupMessageBuilder AddComponents(IEnumerable components) { var ara = components.ToArray(); if (ara.Length + this._components.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) this._components.Add(ar); return this; } /// /// Appends a collection of components to the message. /// /// The collection of components to add. /// The builder to chain calls with. - /// contained more than 5 components. + /// contained more than 5 components. public DiscordFollowupMessageBuilder AddComponents(IEnumerable components) { var compArr = components.ToArray(); var count = compArr.Length; if (count > 5) throw new ArgumentException("Cannot add more than 5 components per action row!"); var arc = new DiscordActionRowComponent(compArr); this._components.Add(arc); return this; } /// /// Indicates if the followup message must use text-to-speech. /// /// Text-to-speech /// The builder to chain calls with. public DiscordFollowupMessageBuilder WithTts(bool tts) { this.IsTts = tts; return this; } /// /// Sets the message to send with the followup message.. /// /// Message to send. /// The builder to chain calls with. public DiscordFollowupMessageBuilder WithContent(string content) { this.Content = content; return this; } /// /// Adds an embed to the followup message. /// /// Embed to add. /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddEmbed(DiscordEmbed embed) { this._embeds.Add(embed); return this; } /// /// Adds the given embeds to the followup message. /// /// Embeds to add. /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddEmbeds(IEnumerable embeds) { this._embeds.AddRange(embeds); return this; } /// /// Adds a file to the followup message. /// /// Name of the file. /// File data. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false, string description = null) { if (this.Files.Count >= 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == filename)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(filename, data, data.Position, description: description)); else this._files.Add(new DiscordMessageFile(filename, data, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddFile(FileStream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count >= 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description)); else this._files.Add(new DiscordMessageFile(stream.Name, stream, null, description: description)); return this; } /// /// Adds the given files to the followup message. /// /// Dictionary of file name and file data. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddFiles(Dictionary files, bool resetStreamPosition = false) { if (this.Files.Count + files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { if (this._files.Any(x => x.FileName == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(file.Key, file.Value, file.Value.Position)); else this._files.Add(new DiscordMessageFile(file.Key, file.Value, null)); } return this; } /// /// Adds the mention to the mentions to parse, etc. with the followup message. /// /// Mention to add. /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddMention(IMention mention) { this._mentions.Add(mention); return this; } /// /// Adds the mentions to the mentions to parse, etc. with the followup message. /// /// Mentions to add. /// The builder to chain calls with. public DiscordFollowupMessageBuilder AddMentions(IEnumerable mentions) { this._mentions.AddRange(mentions); return this; } /// /// Sets the followup message to be ephemeral. /// /// Whether the followup should be ephemeral. Defaults to true. public DiscordFollowupMessageBuilder AsEphemeral(bool ephemeral = true) { this.IsEphemeral = ephemeral; return this; } /// /// Clears all message components on this builder. /// public void ClearComponents() => this._components.Clear(); /// /// Allows for clearing the Followup Message builder so that it can be used again to send a new message. /// public void Clear() { this.Content = ""; this._embeds.Clear(); this.IsTts = false; this._mentions.Clear(); this._files.Clear(); this.IsEphemeral = false; this._components.Clear(); } /// /// Validates the builder. /// internal void Validate() { if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && !this.Embeds.Any()) throw new ArgumentException("You must specify content, an embed, or at least one file."); } } diff --git a/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs b/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs index 9e5ca590d..4e2263530 100644 --- a/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs +++ b/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs @@ -1,183 +1,183 @@ // 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.Linq; namespace DisCatSharp.Entities; /// /// Constructs an interaction modal response. /// public sealed class DiscordInteractionModalBuilder { /// /// Title of modal. /// public string Title { get => this._title; set { if (value != null && value.Length > 128) throw new ArgumentException("Title length cannot exceed 128 characters.", nameof(value)); this._title = value; } } private string _title; /// /// Custom id of modal. /// public string CustomId { get; set; } /// /// Components to send on this interaction response. /// public IReadOnlyList ModalComponents => this._components; private readonly List _components = new(); /// /// Constructs a new empty interaction modal builder. /// public DiscordInteractionModalBuilder(string title = null, string customId = null) { this.Title = title ?? "Title"; this.CustomId = customId ?? Guid.NewGuid().ToString(); } public DiscordInteractionModalBuilder WithTitle(string title) { this.Title = title; return this; } public DiscordInteractionModalBuilder WithCustomId(string customId) { this.CustomId = customId; return this; } /// /// Appends a collection of text components to the builder. Each call will append to a new row. /// /// The components to append. Up to five. /// The current builder to chain calls with. - /// Thrown when passing more than 5 components. + /// Thrown when passing more than 5 components. public DiscordInteractionModalBuilder AddTextComponents(params DiscordTextComponent[] components) => this.AddModalComponents(components); /// /// Appends a collection of select components to the builder. Each call will append to a new row. /// /// The components to append. Up to five. /// The current builder to chain calls with. - /// Thrown when passing more than 5 components. + /// Thrown when passing more than 5 components. public DiscordInteractionModalBuilder AddSelectComponents(params DiscordSelectComponent[] components) => this.AddModalComponents(components); /// /// Appends a text component to the builder. /// /// The component to append. /// The current builder to chain calls with. public DiscordInteractionModalBuilder AddTextComponent(DiscordTextComponent component) => this.AddModalComponents(component); /// /// Appends a select component to the builder. /// /// The component to append. /// The current builder to chain calls with. public DiscordInteractionModalBuilder AddSelectComponent(DiscordSelectComponent component) => this.AddModalComponents(component); /// /// Appends a collection of components to the builder. /// /// The components to append. Up to five. /// The current builder to chain calls with. - /// Thrown when passing more than 5 components. + /// Thrown when passing more than 5 components. public DiscordInteractionModalBuilder AddModalComponents(params DiscordComponent[] components) { var ara = components.ToArray(); if (ara.Length > 5) throw new ArgumentException("You can only add 5 components to modals."); if (this._components.Count + ara.Length > 5) throw new ArgumentException($"You try to add too many components. We already have {this._components.Count}."); foreach (var ar in ara) this._components.Add(new DiscordActionRowComponent(new List() { ar })); return this; } /// /// Appends several rows of components to the message /// /// The rows of components to add, holding up to five each. /// The current builder to chain calls with. - /// Thrown when passing more than 5 components. + /// Thrown when passing more than 5 components. public DiscordInteractionModalBuilder AddModalComponents(IEnumerable components) { var ara = components.ToArray(); if (ara.Length + this._components.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) this._components.Add(ar); return this; } /// /// Appends a collection of components to the builder. Each call will append to a new row. /// /// The component to append. /// The current builder to chain calls with. internal DiscordInteractionModalBuilder AddModalComponents(DiscordComponent component) { this._components.Add(new DiscordActionRowComponent(new List() { component })); return this; } /// /// Clears all message components on this builder. /// public void ClearComponents() => this._components.Clear(); /// /// Allows for clearing the Interaction Response Builder so that it can be used again to send a new response. /// public void Clear() { this._components.Clear(); this.Title = null; this.CustomId = null; } } diff --git a/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs b/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs index 23e377bcd..ee8f4586b 100644 --- a/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs +++ b/DisCatSharp/Entities/Interaction/DiscordInteractionResponseBuilder.cs @@ -1,352 +1,352 @@ // 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.IO; using System.Linq; namespace DisCatSharp.Entities; /// /// Constructs an interaction response. /// public sealed class DiscordInteractionResponseBuilder { /// /// Whether this interaction response is text-to-speech. /// public bool IsTts { get; set; } /// /// Whether this interaction response should be ephemeral. /// public bool IsEphemeral { get; set; } /// /// Content of the message to send. /// public string Content { get => this._content; set { if (value != null && value.Length > 2000) throw new ArgumentException("Content length cannot exceed 2000 characters.", nameof(value)); this._content = value; } } private string _content; /// /// Embeds to send on this interaction response. /// public IReadOnlyList Embeds => this._embeds; private readonly List _embeds = new(); /// /// Files to send on this interaction response. /// public IReadOnlyList Files => this._files; private readonly List _files = new(); /// /// Components to send on this interaction response. /// public IReadOnlyList Components => this._components; private readonly List _components = new(); /// /// The choices to send on this interaction response. /// Mutually exclusive with content, embed, and components. /// public IReadOnlyList Choices => this._choices; private readonly List _choices = new(); /// /// Mentions to send on this interaction response. /// public IEnumerable Mentions => this._mentions; private readonly List _mentions = new(); /// /// Constructs a new empty interaction response builder. /// public DiscordInteractionResponseBuilder() { } /// /// Constructs a new based on an existing . /// /// The builder to copy. public DiscordInteractionResponseBuilder(DiscordMessageBuilder builder) { this._content = builder.Content; this._mentions = builder.Mentions; this._embeds.AddRange(builder.Embeds); this._components.AddRange(builder.Components); } /// /// Appends a collection of components to the builder. Each call will append to a new row. /// /// The components to append. Up to five. /// The current builder to chain calls with. - /// Thrown when passing more than 5 components. + /// Thrown when passing more than 5 components. public DiscordInteractionResponseBuilder AddComponents(params DiscordComponent[] components) => this.AddComponents((IEnumerable)components); /// /// Appends several rows of components to the message /// /// The rows of components to add, holding up to five each. /// public DiscordInteractionResponseBuilder AddComponents(IEnumerable components) { var ara = components.ToArray(); if (ara.Length + this._components.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) this._components.Add(ar); return this; } /// /// Appends a collection of components to the builder. Each call will append to a new row. /// /// The components to append. Up to five. /// The current builder to chain calls with. - /// Thrown when passing more than 5 components. + /// Thrown when passing more than 5 components. public DiscordInteractionResponseBuilder AddComponents(IEnumerable components) { var compArr = components.ToArray(); var count = compArr.Length; if (count > 5) throw new ArgumentException("Cannot add more than 5 components per action row!"); var arc = new DiscordActionRowComponent(compArr); this._components.Add(arc); return this; } /// /// Indicates if the interaction response will be text-to-speech. /// /// Text-to-speech public DiscordInteractionResponseBuilder WithTts(bool tts) { this.IsTts = tts; return this; } /// /// Sets the interaction response to be ephemeral. /// /// Whether the response should be ephemeral. Defaults to true. public DiscordInteractionResponseBuilder AsEphemeral(bool ephemeral = true) { this.IsEphemeral = ephemeral; return this; } /// /// Sets the content of the message to send. /// /// Content to send. public DiscordInteractionResponseBuilder WithContent(string content) { this.Content = content; return this; } /// /// Adds an embed to send with the interaction response. /// /// Embed to add. public DiscordInteractionResponseBuilder AddEmbed(DiscordEmbed embed) { if (embed != null) this._embeds.Add(embed); // Interactions will 400 silently // return this; } /// /// Adds the given embeds to send with the interaction response. /// /// Embeds to add. public DiscordInteractionResponseBuilder AddEmbeds(IEnumerable embeds) { this._embeds.AddRange(embeds); return this; } /// /// Adds a file to the interaction response. /// /// Name of the file. /// File data. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. /// The builder to chain calls with. public DiscordInteractionResponseBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false, string description = null) { if (this.Files.Count >= 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == filename)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(filename, data, data.Position, description: description)); else this._files.Add(new DiscordMessageFile(filename, data, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. /// The builder to chain calls with. public DiscordInteractionResponseBuilder AddFile(FileStream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count >= 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description)); else this._files.Add(new DiscordMessageFile(stream.Name, stream, null, description: description)); return this; } /// /// Adds the given files to the interaction response builder. /// /// Dictionary of file name and file data. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// The builder to chain calls with. public DiscordInteractionResponseBuilder AddFiles(Dictionary files, bool resetStreamPosition = false) { if (this.Files.Count + files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { if (this._files.Any(x => x.FileName == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(file.Key, file.Value, file.Value.Position)); else this._files.Add(new DiscordMessageFile(file.Key, file.Value, null)); } return this; } /// /// Adds the mention to the mentions to parse, etc. with the interaction response. /// /// Mention to add. public DiscordInteractionResponseBuilder AddMention(IMention mention) { this._mentions.Add(mention); return this; } /// /// Adds the mentions to the mentions to parse, etc. with the interaction response. /// /// Mentions to add. public DiscordInteractionResponseBuilder AddMentions(IEnumerable mentions) { this._mentions.AddRange(mentions); return this; } /// /// Adds a single auto-complete choice to the builder. /// /// The choice to add. /// The current builder to chain calls with. public DiscordInteractionResponseBuilder AddAutoCompleteChoice(DiscordApplicationCommandAutocompleteChoice choice) { this._choices.Add(choice); return this; } /// /// Adds auto-complete choices to the builder. /// /// The choices to add. /// The current builder to chain calls with. public DiscordInteractionResponseBuilder AddAutoCompleteChoices(IEnumerable choices) { this._choices.AddRange(choices); return this; } /// /// Adds auto-complete choices to the builder. /// /// The choices to add. /// The current builder to chain calls with. public DiscordInteractionResponseBuilder AddAutoCompleteChoices(params DiscordApplicationCommandAutocompleteChoice[] choices) => this.AddAutoCompleteChoices((IEnumerable)choices); /// /// Clears all message components on this builder. /// public void ClearComponents() => this._components.Clear(); /// /// Allows for clearing the Interaction Response Builder so that it can be used again to send a new response. /// public void Clear() { this.Content = ""; this._embeds.Clear(); this.IsTts = false; this.IsEphemeral = false; this._mentions.Clear(); this._components.Clear(); this._choices.Clear(); this._files.Clear(); } } diff --git a/DisCatSharp/Entities/Invite/DiscordInvite.cs b/DisCatSharp/Entities/Invite/DiscordInvite.cs index 5c10f33f3..acc764fda 100644 --- a/DisCatSharp/Entities/Invite/DiscordInvite.cs +++ b/DisCatSharp/Entities/Invite/DiscordInvite.cs @@ -1,198 +1,198 @@ // 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.Threading.Tasks; using DisCatSharp.Enums; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord invite. /// public class DiscordInvite { /// /// Gets the base client. /// internal BaseDiscordClient Discord { get; set; } /// /// Gets the invite's code. /// [JsonProperty("code", NullValueHandling = NullValueHandling.Ignore)] public string Code { get; internal set; } /// /// Gets the invite's url. /// [JsonIgnore] public string Url => DiscordDomain.GetDomain(CoreDomain.DiscordShortlink).Url + "/" + this.Code; /// /// Gets the invite's url as Uri. /// [JsonIgnore] public Uri Uri => new(this.Url); /// /// Gets the guild this invite is for. /// [JsonProperty("guild", NullValueHandling = NullValueHandling.Ignore)] public DiscordInviteGuild Guild { get; internal set; } /// /// Gets the channel this invite is for. /// [JsonProperty("channel", NullValueHandling = NullValueHandling.Ignore)] public DiscordInviteChannel Channel { get; internal set; } /// /// Gets the target type for the voice channel this invite is for. /// [JsonProperty("target_type", NullValueHandling = NullValueHandling.Ignore)] public TargetType? TargetType { get; internal set; } /// /// Gets the type of this invite. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public InviteType Type { get; internal set; } /// /// Gets the user that is currently livestreaming. /// [JsonProperty("target_user", NullValueHandling = NullValueHandling.Ignore)] public DiscordUser TargetUser { get; internal set; } /// /// Gets the embedded partial application to open for this voice channel. /// [JsonProperty("target_application", NullValueHandling = NullValueHandling.Ignore)] public DiscordApplication TargetApplication { get; internal set; } /// /// Gets the approximate guild online member count for the invite. /// [JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximatePresenceCount { get; internal set; } /// /// Gets the approximate guild total member count for the invite. /// [JsonProperty("approximate_member_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximateMemberCount { get; internal set; } /// /// Gets the user who created the invite. /// [JsonProperty("inviter", NullValueHandling = NullValueHandling.Ignore)] public DiscordUser Inviter { get; internal set; } /// /// Gets the number of times this invite has been used. /// [JsonProperty("uses", NullValueHandling = NullValueHandling.Ignore)] public int Uses { get; internal set; } /// /// Gets the max number of times this invite can be used. /// [JsonProperty("max_uses", NullValueHandling = NullValueHandling.Ignore)] public int MaxUses { get; internal set; } /// /// Gets duration in seconds after which the invite expires. /// [JsonProperty("max_age", NullValueHandling = NullValueHandling.Ignore)] public int MaxAge { get; internal set; } /// /// Gets whether this invite only grants temporary membership. /// [JsonProperty("temporary", NullValueHandling = NullValueHandling.Ignore)] public bool IsTemporary { get; internal set; } /// /// Gets the date and time this invite was created. /// [JsonProperty("created_at", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset CreatedAt { get; internal set; } /// /// Gets the date and time when this invite expires. /// [JsonProperty("expires_at", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset ExpiresAt { get; internal set; } /// /// Gets the date and time when this invite got expired. /// [JsonProperty("expired_at", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset ExpiredAt { get; internal set; } /// /// Gets whether this invite is revoked. /// [JsonProperty("revoked", NullValueHandling = NullValueHandling.Ignore)] public bool IsRevoked { get; internal set; } /// /// Gets the stage instance this invite is for. /// [JsonProperty("stage_instance", NullValueHandling = NullValueHandling.Ignore)] public DiscordInviteStage Stage { get; internal set; } /// /// Gets the guild scheduled event data for the invite. /// [JsonProperty("guild_scheduled_event", NullValueHandling = NullValueHandling.Ignore)] public DiscordScheduledEvent GuildScheduledEvent { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordInvite() { } /// /// Deletes the invite. /// /// Reason for audit logs. /// - /// Thrown when the client does not have the permission or the permission. - /// Thrown when the emoji does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission or the permission. + /// Thrown when the emoji 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.DeleteInviteAsync(this.Code, reason); /// /// Converts this invite into an invite link. /// /// A discord.gg invite link. public override string ToString() => $"{DiscordDomain.GetDomain(CoreDomain.DiscordShortlink).Url}/{this.Code}"; } diff --git a/DisCatSharp/Entities/Message/DiscordMessage.cs b/DisCatSharp/Entities/Message/DiscordMessage.cs index c500200ea..1fbe6f3e0 100644 --- a/DisCatSharp/Entities/Message/DiscordMessage.cs +++ b/DisCatSharp/Entities/Message/DiscordMessage.cs @@ -1,878 +1,878 @@ // 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 Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord text message. /// public class DiscordMessage : SnowflakeObject, IEquatable { /// /// Initializes a new instance of the class. /// internal DiscordMessage() { this._attachmentsLazy = new Lazy>(() => new ReadOnlyCollection(this.AttachmentsInternal)); this._embedsLazy = new Lazy>(() => new ReadOnlyCollection(this.EmbedsInternal)); this._mentionedChannelsLazy = new Lazy>(() => this.MentionedChannelsInternal != null ? new ReadOnlyCollection(this.MentionedChannelsInternal) : Array.Empty()); this._mentionedRolesLazy = new Lazy>(() => this.MentionedRolesInternal != null ? new ReadOnlyCollection(this.MentionedRolesInternal) : Array.Empty()); this.MentionedUsersLazy = new Lazy>(() => new ReadOnlyCollection(this.MentionedUsersInternal)); this._reactionsLazy = new Lazy>(() => new ReadOnlyCollection(this.ReactionsInternal)); this._stickersLazy = new Lazy>(() => new ReadOnlyCollection(this.StickersInternal)); this._jumpLink = new Lazy(() => { string gid = null; if (this.Channel != null) gid = this.Channel is DiscordDmChannel ? "@me" : this.Channel is DiscordThreadChannel ? this.INTERNAL_THREAD.GuildId.Value.ToString(CultureInfo.InvariantCulture) : this.Channel.GuildId.Value.ToString(CultureInfo.InvariantCulture); var cid = this.ChannelId.ToString(CultureInfo.InvariantCulture); var mid = this.Id.ToString(CultureInfo.InvariantCulture); return new Uri($"https://{(this.Discord.Configuration.UseCanary ? "canary.discord.com" : this.Discord.Configuration.UsePtb ? "ptb.discord.com" : "discord.com")}/channels/{gid}/{cid}/{mid}"); }); } /// /// Initializes a new instance of the class. /// /// The other message. internal DiscordMessage(DiscordMessage other) : this() { this.Discord = other.Discord; this.AttachmentsInternal = other.AttachmentsInternal; // the attachments cannot change, thus no need to copy and reallocate. this.EmbedsInternal = new List(other.EmbedsInternal); if (other.MentionedChannelsInternal != null) this.MentionedChannelsInternal = new List(other.MentionedChannelsInternal); if (other.MentionedRolesInternal != null) this.MentionedRolesInternal = new List(other.MentionedRolesInternal); if (other.MentionedRoleIds != null) this.MentionedRoleIds = new List(other.MentionedRoleIds); this.MentionedUsersInternal = new List(other.MentionedUsersInternal); this.ReactionsInternal = new List(other.ReactionsInternal); this.StickersInternal = new List(other.StickersInternal); this.Author = other.Author; this.ChannelId = other.ChannelId; this.Content = other.Content; this.EditedTimestampRaw = other.EditedTimestampRaw; this.Id = other.Id; this.IsTts = other.IsTts; this.MessageType = other.MessageType; this.Pinned = other.Pinned; this.TimestampRaw = other.TimestampRaw; this.WebhookId = other.WebhookId; } /// /// Gets the channel in which the message was sent. /// [JsonIgnore] public DiscordChannel Channel { get => (this.Discord as DiscordClient)?.InternalGetCachedChannel(this.ChannelId) ?? this._channel; internal set => this._channel = value; } private DiscordChannel _channel; /// /// Gets the thread in which the message was sent. /// [JsonIgnore] private DiscordThreadChannel INTERNAL_THREAD { get => (this.Discord as DiscordClient)?.InternalGetCachedThread(this.ChannelId) ?? this._thread; set => this._thread = value; } private DiscordThreadChannel _thread; /// /// Gets the ID of the channel in which the message was sent. /// [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] public ulong ChannelId { get; internal set; } /// /// Gets the components this message was sent with. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyCollection Components { get; internal set; } /// /// Gets the user or member that sent the message. /// [JsonProperty("author", NullValueHandling = NullValueHandling.Ignore)] public DiscordUser Author { get; internal set; } /// /// Gets the message's content. /// [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] public string Content { get; internal set; } /// /// Gets the message's creation timestamp. /// [JsonIgnore] public DateTimeOffset Timestamp => !string.IsNullOrWhiteSpace(this.TimestampRaw) && DateTimeOffset.TryParse(this.TimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : this.CreationTimestamp; /// /// Gets the message's creation timestamp as raw string. /// [JsonProperty("timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string TimestampRaw { get; set; } /// /// Gets the message's edit timestamp. Will be null if the message was not edited. /// [JsonIgnore] public DateTimeOffset? EditedTimestamp => !string.IsNullOrWhiteSpace(this.EditedTimestampRaw) && DateTimeOffset.TryParse(this.EditedTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? (DateTimeOffset?)dto : null; /// /// Gets the message's edit timestamp as raw string. Will be null if the message was not edited. /// [JsonProperty("edited_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string EditedTimestampRaw { get; set; } /// /// Gets whether this message was edited. /// [JsonIgnore] public bool IsEdited => !string.IsNullOrWhiteSpace(this.EditedTimestampRaw); /// /// Gets whether the message is a text-to-speech message. /// [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] public bool IsTts { get; internal set; } /// /// Gets whether the message mentions everyone. /// [JsonProperty("mention_everyone", NullValueHandling = NullValueHandling.Ignore)] public bool MentionEveryone { get; internal set; } /// /// Gets users or members mentioned by this message. /// [JsonIgnore] public IReadOnlyList MentionedUsers => this.MentionedUsersLazy.Value; [JsonProperty("mentions", NullValueHandling = NullValueHandling.Ignore)] internal List MentionedUsersInternal; [JsonIgnore] internal readonly Lazy> MentionedUsersLazy; // TODO: this will probably throw an exception in DMs since it tries to wrap around a null List... // this is probably low priority but need to find out a clean way to solve it... /// /// Gets roles mentioned by this message. /// [JsonIgnore] public IReadOnlyList MentionedRoles => this._mentionedRolesLazy.Value; [JsonIgnore] internal List MentionedRolesInternal; [JsonProperty("mention_roles")] internal List MentionedRoleIds; [JsonIgnore] private readonly Lazy> _mentionedRolesLazy; /// /// Gets channels mentioned by this message. /// [JsonIgnore] public IReadOnlyList MentionedChannels => this._mentionedChannelsLazy.Value; [JsonIgnore] internal List MentionedChannelsInternal; [JsonIgnore] private readonly Lazy> _mentionedChannelsLazy; /// /// Gets files attached to this message. /// [JsonIgnore] public IReadOnlyList Attachments => this._attachmentsLazy.Value; [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] internal List AttachmentsInternal = new(); [JsonIgnore] private readonly Lazy> _attachmentsLazy; /// /// Gets embeds attached to this message. /// [JsonIgnore] public IReadOnlyList Embeds => this._embedsLazy.Value; [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] internal List EmbedsInternal = new(); [JsonIgnore] private readonly Lazy> _embedsLazy; /// /// Gets reactions used on this message. /// [JsonIgnore] public IReadOnlyList Reactions => this._reactionsLazy.Value; [JsonProperty("reactions", NullValueHandling = NullValueHandling.Ignore)] internal List ReactionsInternal = new(); [JsonIgnore] private readonly Lazy> _reactionsLazy; /// /// Gets the nonce sent with the message, if the message was sent by the client. /// [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] public string Nonce { get; internal set; } /// /// Gets whether the message is pinned. /// [JsonProperty("pinned", NullValueHandling = NullValueHandling.Ignore)] public bool Pinned { get; internal set; } /// /// Gets the id of the webhook that generated this message. /// [JsonProperty("webhook_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? WebhookId { get; internal set; } /// /// Gets the type of the message. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public MessageType? MessageType { get; internal set; } /// /// Gets the message activity in the Rich Presence embed. /// [JsonProperty("activity", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessageActivity Activity { get; internal set; } /// /// Gets the message application in the Rich Presence embed. /// [JsonProperty("application", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessageApplication Application { get; internal set; } /// /// Gets the message application id in the Rich Presence embed. /// [JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)] public ulong ApplicationId { get; internal set; } /// /// Gets the internal reference. /// [JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)] internal InternalDiscordMessageReference? InternalReference { get; set; } /// /// Gets the original message reference from the crossposted message. /// [JsonIgnore] public DiscordMessageReference Reference => this.InternalReference.HasValue ? this?.InternalBuildMessageReference() : null; /// /// Gets the bitwise flags for this message. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public MessageFlags? Flags { get; internal set; } /// /// Gets whether the message originated from a webhook. /// [JsonIgnore] public bool WebhookMessage => this.WebhookId != null; /// /// Gets the jump link to this message. /// [JsonIgnore] public Uri JumpLink => this._jumpLink.Value; private readonly Lazy _jumpLink; /// /// Gets stickers for this message. /// [JsonIgnore] public IReadOnlyList Stickers => this._stickersLazy.Value; [JsonProperty("sticker_items", NullValueHandling = NullValueHandling.Ignore)] internal List StickersInternal = new(); [JsonIgnore] private readonly Lazy> _stickersLazy; /// /// Gets the guild id. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? GuildId { get; internal set; } /// /// Gets the message object for the referenced message /// [JsonProperty("referenced_message", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessage ReferencedMessage { get; internal set; } /// /// Gets whether the message is a response to an interaction. /// [JsonProperty("interaction", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessageInteraction Interaction { get; internal set; } /// /// Gets the thread that was started from this message. /// [JsonProperty("thread", NullValueHandling = NullValueHandling.Ignore)] public DiscordThreadChannel Thread { get; internal set; } /// /// Build the message reference. /// internal DiscordMessageReference InternalBuildMessageReference() { var client = this.Discord as DiscordClient; var guildId = this.InternalReference.Value.GuildId; var channelId = this.InternalReference.Value.ChannelId; var messageId = this.InternalReference.Value.MessageId; var reference = new DiscordMessageReference(); if (guildId.HasValue) reference.Guild = client.GuildsInternal.TryGetValue(guildId.Value, out var g) ? g : new DiscordGuild { Id = guildId.Value, Discord = client }; var channel = client.InternalGetCachedChannel(channelId.Value); if (channel == null) { reference.Channel = new DiscordChannel { Id = channelId.Value, Discord = client }; if (guildId.HasValue) reference.Channel.GuildId = guildId.Value; } else reference.Channel = channel; if (client.MessageCache != null && client.MessageCache.TryGet(m => m.Id == messageId.Value && m.ChannelId == channelId, out var msg)) reference.Message = msg; else { reference.Message = new DiscordMessage { ChannelId = this.ChannelId, Discord = client }; if (messageId.HasValue) reference.Message.Id = messageId.Value; } return reference; } /// /// Gets the mentions. /// /// An array of IMentions. private List GetMentions() { var mentions = new List(); if (this.ReferencedMessage != null && this.MentionedUsersInternal.Contains(this.ReferencedMessage.Author)) mentions.Add(new RepliedUserMention()); if (this.MentionedUsersInternal.Any()) mentions.AddRange(this.MentionedUsersInternal.Select(m => (IMention)new UserMention(m))); if (this.MentionedRoleIds.Any()) mentions.AddRange(this.MentionedRoleIds.Select(r => (IMention)new RoleMention(r))); return mentions; } /// /// Populates the mentions. /// internal void PopulateMentions() { var guild = this.Channel?.Guild; this.MentionedUsersInternal ??= new List(); this.MentionedRolesInternal ??= new List(); this.MentionedChannelsInternal ??= new List(); var mentionedUsers = new HashSet(new DiscordUserComparer()); if (guild != null) { foreach (var usr in this.MentionedUsersInternal) { usr.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); mentionedUsers.Add(guild.MembersInternal.TryGetValue(usr.Id, out var member) ? member : usr); } } if (!string.IsNullOrWhiteSpace(this.Content)) { //mentionedUsers.UnionWith(Utilities.GetUserMentions(this).Select(this.Discord.GetCachedOrEmptyUserInternal)); if (guild != null) { //this._mentionedRoles = this._mentionedRoles.Union(Utilities.GetRoleMentions(this).Select(xid => guild.GetRole(xid))).ToList(); this.MentionedRolesInternal = this.MentionedRolesInternal.Union(this.MentionedRoleIds.Select(xid => guild.GetRole(xid))).ToList(); this.MentionedChannelsInternal = this.MentionedChannelsInternal.Union(Utilities.GetChannelMentions(this).Select(xid => guild.GetChannel(xid))).ToList(); } } this.MentionedUsersInternal = mentionedUsers.ToList(); } /// /// Edits the message. /// /// New content. - /// Thrown when the client tried to modify a message not sent by them. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client tried to modify a message not sent by them. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional content) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, default, this.GetMentions(), default, default, Array.Empty(), default); /// /// Edits the message. /// /// New embed. - /// Thrown when the client tried to modify a message not sent by them. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client tried to modify a message not sent by them. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional embed = default) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, embed.Map(v => new[] { v }).ValueOr(Array.Empty()), this.GetMentions(), default, default, Array.Empty(), default); /// /// Edits the message. /// /// New content. /// New embed. - /// Thrown when the client tried to modify a message not sent by them. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client tried to modify a message not sent by them. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional content, Optional embed = default) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embed.Map(v => new[] { v }).ValueOr(Array.Empty()), this.GetMentions(), default, default, Array.Empty(), default); /// /// Edits the message. /// /// New content. /// New embeds. - /// Thrown when the client tried to modify a message not sent by them. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client tried to modify a message not sent by them. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional content, Optional> embeds = default) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embeds, this.GetMentions(), default, default, Array.Empty(), default); /// /// Edits the message. /// /// The builder of the message to edit. - /// Thrown when the client tried to modify a message not sent by them. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client tried to modify a message not sent by them. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task ModifyAsync(DiscordMessageBuilder builder) { builder.Validate(true); return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, Optional.Some(builder.Embeds.AsEnumerable()), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? Optional.Some(builder.Attachments.AsEnumerable()) : builder.KeepAttachmentsInternal.HasValue ? builder.KeepAttachmentsInternal.Value ? Optional.Some(this.Attachments.AsEnumerable()) : Array.Empty() : null); } /// /// Edits the message embed suppression. /// /// Suppress embeds. - /// Thrown when the client tried to modify a message not sent by them. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client tried to modify a message not sent by them. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ModifySuppressionAsync(bool suppress = false) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, default, default, suppress, default, default); /// /// Clears all attachments from the message. /// /// public Task ClearAttachmentsAsync() => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, this.GetMentions(), default, default, default, Array.Empty()); /// /// Edits the message. /// /// The builder of the message to edit. - /// Thrown when the client tried to modify a message not sent by them. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client tried to modify a message not sent by them. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task ModifyAsync(Action action) { var builder = new DiscordMessageBuilder(); action(builder); builder.Validate(true); return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, Optional.Some(builder.Embeds.AsEnumerable()), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? Optional.Some(builder.Attachments.AsEnumerable()) : builder.KeepAttachmentsInternal.HasValue ? builder.KeepAttachmentsInternal.Value ? Optional.Some(this.Attachments.AsEnumerable()) : Array.Empty() : null); } /// /// Deletes the message. /// - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task DeleteAsync(string reason = null) => this.Discord.ApiClient.DeleteMessageAsync(this.ChannelId, this.Id, reason); /// /// Creates a thread. /// Depending on the of the parent channel it's either a or a . /// /// The name of the thread. /// till it gets archived. Defaults to /// The per user ratelimit, aka slowdown. /// The reason. - /// Thrown when the client does not have the or permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - /// Thrown when the cannot be modified. + /// Thrown when the client does not have the or permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// Thrown when the cannot be modified. public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration autoArchiveDuration = ThreadAutoArchiveDuration.OneHour, int? rateLimitPerUser = null, string reason = null) => Utilities.CheckThreadAutoArchiveDurationFeature(this.Channel.Guild, autoArchiveDuration) ? await this.Discord.ApiClient.CreateThreadAsync(this.ChannelId, this.Id, name, autoArchiveDuration, this.Channel.Type == ChannelType.News ? ChannelType.NewsThread : ChannelType.PublicThread, rateLimitPerUser, isForum: false, reason: reason) : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); /// /// Pins the message in its channel. /// - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task PinAsync() => this.Discord.ApiClient.PinMessageAsync(this.ChannelId, this.Id); /// /// Unpins the message in its channel. /// - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task UnpinAsync() => this.Discord.ApiClient.UnpinMessageAsync(this.ChannelId, this.Id); /// /// Responds to the message. This produces a reply. /// /// Message content to respond with. /// The sent message. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RespondAsync(string content) => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false); /// /// Responds to the message. This produces a reply. /// /// Embed to attach to the message. /// The sent message. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RespondAsync(DiscordEmbed embed) => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false); /// /// Responds to the message. This produces a reply. /// /// Message content to respond with. /// Embed to attach to the message. /// The sent message. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RespondAsync(string content, DiscordEmbed embed) => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false); /// /// Responds to the message. This produces a reply. /// /// The Discord message builder. /// The sent message. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RespondAsync(DiscordMessageBuilder builder) => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false)); /// /// Responds to the message. This produces a reply. /// /// The Discord message builder. /// The sent message. - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RespondAsync(Action action) { var builder = new DiscordMessageBuilder(); action(builder); return this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false)); } /// /// Creates a reaction to this message. /// /// The emoji you want to react with, either an emoji or name:id - /// Thrown when the client does not have the permission. - /// Thrown when the emoji does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task CreateReactionAsync(DiscordEmoji emoji) => this.Discord.ApiClient.CreateReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); /// /// Deletes your own reaction /// /// Emoji for the reaction you want to remove, either an emoji or name:id - /// Thrown when the emoji does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task DeleteOwnReactionAsync(DiscordEmoji emoji) => this.Discord.ApiClient.DeleteOwnReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); /// /// Deletes another user's reaction. /// /// Emoji for the reaction you want to remove, either an emoji or name:id. /// Member you want to remove the reaction for /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the emoji does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task DeleteReactionAsync(DiscordEmoji emoji, DiscordUser user, string reason = null) => this.Discord.ApiClient.DeleteUserReactionAsync(this.ChannelId, this.Id, user.Id, emoji.ToReactionString(), reason); /// /// Gets users that reacted with this emoji. /// /// Emoji to react with. /// Limit of users to fetch. /// Fetch users after this user's id. - /// Thrown when the emoji does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task> GetReactionsAsync(DiscordEmoji emoji, int limit = 25, ulong? after = null) => this.GetReactionsInternalAsync(emoji, limit, after); /// /// Deletes all reactions for this message. /// /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the emoji does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task DeleteAllReactionsAsync(string reason = null) => this.Discord.ApiClient.DeleteAllReactionsAsync(this.ChannelId, this.Id, reason); /// /// Deletes all reactions of a specific reaction for this message. /// /// The emoji to clear, either an emoji or name:id. - /// Thrown when the client does not have the permission. - /// Thrown when the emoji does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task DeleteReactionsEmojiAsync(DiscordEmoji emoji) => this.Discord.ApiClient.DeleteReactionsEmojiAsync(this.ChannelId, this.Id, emoji.ToReactionString()); /// /// Gets the reactions. /// /// The emoji to search for. /// The limit of results. /// Get the reasctions after snowflake. private async Task> GetReactionsInternalAsync(DiscordEmoji emoji, int limit = 25, ulong? after = null) { if (limit < 0) throw new ArgumentException("Cannot get a negative number of reactions' users."); if (limit == 0) return Array.Empty(); var users = new List(limit); var remaining = limit; var last = after; int lastCount; do { var fetchSize = remaining > 100 ? 100 : remaining; var fetch = await this.Discord.ApiClient.GetReactionsAsync(this.Channel.Id, this.Id, emoji.ToReactionString(), last, fetchSize).ConfigureAwait(false); lastCount = fetch.Count; remaining -= lastCount; users.AddRange(fetch); last = fetch.LastOrDefault()?.Id; } while (remaining > 0 && lastCount > 0); return new ReadOnlyCollection(users); } /// /// Returns a string representation of this message. /// /// String representation of this message. public override string ToString() => $"Message {this.Id}; Attachment count: {this.AttachmentsInternal.Count}; Embed count: {this.EmbedsInternal.Count}; Contents: {this.Content}"; /// /// 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 DiscordMessage); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordMessage e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.ChannelId == e.ChannelId)); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() { var hash = 13; hash = (hash * 7) + this.Id.GetHashCode(); hash = (hash * 7) + this.ChannelId.GetHashCode(); return hash; } /// /// Gets whether the two objects are equal. /// /// First message to compare. /// Second message to compare. /// Whether the two messages are equal. public static bool operator ==(DiscordMessage e1, DiscordMessage 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 && e1.ChannelId == e2.ChannelId)); } /// /// Gets whether the two objects are not equal. /// /// First message to compare. /// Second message to compare. /// Whether the two messages are not equal. public static bool operator !=(DiscordMessage e1, DiscordMessage e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs b/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs index 92c396e51..8de34e824 100644 --- a/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs +++ b/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs @@ -1,459 +1,459 @@ // 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.IO; using System.Linq; using System.Threading.Tasks; namespace DisCatSharp.Entities; /// /// Constructs a Message to be sent. /// public sealed class DiscordMessageBuilder { /// /// Gets or Sets the Message to be sent. /// public string Content { get => this._content; set { if (value != null && value.Length > 2000) throw new ArgumentException("Content cannot exceed 2000 characters.", nameof(value)); this._content = value; } } private string _content; /// /// Gets or sets the embed for the builder. This will always set the builder to have one embed. /// public DiscordEmbed Embed { get => this._embeds.Count > 0 ? this._embeds[0] : null; set { this._embeds.Clear(); this._embeds.Add(value); } } /// /// Gets the Sticker to be send. /// public DiscordSticker Sticker { get; set; } /// /// Gets the Embeds to be sent. /// public IReadOnlyList Embeds => this._embeds; private readonly List _embeds = new(); /// /// Gets or Sets if the message should be TTS. /// public bool IsTts { get; set; } /// /// Whether to keep previous attachments. /// internal bool? KeepAttachmentsInternal; /// /// Gets the Allowed Mentions for the message to be sent. /// public List Mentions { get; private set; } /// /// Gets the Files to be sent in the Message. /// public IReadOnlyCollection Files => this.FilesInternal; internal readonly List FilesInternal = new(); /// /// Gets the components that will be attached to the message. /// public IReadOnlyList Components => this.ComponentsInternal; internal readonly List ComponentsInternal = new(5); /// /// Gets the Attachments to be sent in the Message. /// public IReadOnlyList Attachments => this.AttachmentsInternal; internal readonly List AttachmentsInternal = new(); /// /// Gets the Reply Message ID. /// public ulong? ReplyId { get; private set; } /// /// Gets if the Reply should mention the user. /// public bool MentionOnReply { get; private set; } /// /// Gets if the embeds should be suppressed. /// public bool Suppressed { get; private set; } /// /// Gets if the Reply will error if the Reply Message Id does not reference a valid message. /// If set to false, invalid replies are send as a regular message. /// Defaults to false. /// public bool FailOnInvalidReply { get; set; } /// /// Sets the Content of the Message. /// /// The content to be set. /// The current builder to be chained. public DiscordMessageBuilder WithContent(string content) { this.Content = content; return this; } /// /// Adds a sticker to the message. Sticker must be from current guild. /// /// The sticker to add. /// The current builder to be chained. public DiscordMessageBuilder WithSticker(DiscordSticker sticker) { this.Sticker = sticker; return this; } /// /// Adds a row of components to a message, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the message. /// The current builder to be chained. - /// No components were passed. + /// No components were passed. public DiscordMessageBuilder AddComponents(params DiscordComponent[] components) => this.AddComponents((IEnumerable)components); /// /// Appends several rows of components to the message /// /// The rows of components to add, holding up to five each. /// public DiscordMessageBuilder AddComponents(IEnumerable components) { var ara = components.ToArray(); if (ara.Length + this.ComponentsInternal.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) this.ComponentsInternal.Add(ar); return this; } /// /// Adds a row of components to a message, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the message. /// The current builder to be chained. - /// No components were passed. + /// No components were passed. public DiscordMessageBuilder AddComponents(IEnumerable components) { var cmpArr = components.ToArray(); var count = cmpArr.Length; if (!cmpArr.Any()) throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); if (count > 5) throw new ArgumentException("Cannot add more than 5 components per action row!"); var comp = new DiscordActionRowComponent(cmpArr); this.ComponentsInternal.Add(comp); return this; } /// /// Sets if the message should be TTS. /// /// If TTS should be set. /// The current builder to be chained. public DiscordMessageBuilder HasTts(bool isTts) { this.IsTts = isTts; return this; } /// /// Sets the embed for the current builder. /// /// The embed that should be set. /// The current builder to be chained. public DiscordMessageBuilder WithEmbed(DiscordEmbed embed) { if (embed == null) return this; this.Embed = embed; return this; } /// /// Appends an embed to the current builder. /// /// The embed that should be appended. /// The current builder to be chained. public DiscordMessageBuilder AddEmbed(DiscordEmbed embed) { if (embed == null) return this; //Providing null embeds will produce a 400 response from Discord.// this._embeds.Add(embed); return this; } /// /// Appends several embeds to the current builder. /// /// The embeds that should be appended. /// The current builder to be chained. public DiscordMessageBuilder AddEmbeds(IEnumerable embeds) { this._embeds.AddRange(embeds); return this; } /// /// Sets if the message has allowed mentions. /// /// The allowed Mention that should be sent. /// The current builder to be chained. public DiscordMessageBuilder WithAllowedMention(IMention allowedMention) { if (this.Mentions != null) this.Mentions.Add(allowedMention); else this.Mentions = new List { allowedMention }; return this; } /// /// Sets if the message has allowed mentions. /// /// The allowed Mentions that should be sent. /// The current builder to be chained. public DiscordMessageBuilder WithAllowedMentions(IEnumerable allowedMentions) { if (this.Mentions != null) this.Mentions.AddRange(allowedMentions); else this.Mentions = allowedMentions.ToList(); return this; } /// /// Sets if the message has files to be sent. /// /// The fileName that the file should be sent as. /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. /// The current builder to be chained. public DiscordMessageBuilder WithFile(string fileName, Stream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this.FilesInternal.Any(x => x.FileName == fileName)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this.FilesInternal.Add(new DiscordMessageFile(fileName, stream, stream.Position, description: description)); else this.FilesInternal.Add(new DiscordMessageFile(fileName, stream, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. /// The current builder to be chained. public DiscordMessageBuilder WithFile(FileStream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this.FilesInternal.Any(x => x.FileName == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this.FilesInternal.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description)); else this.FilesInternal.Add(new DiscordMessageFile(stream.Name, stream, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Files that should be sent. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// The current builder to be chained. public DiscordMessageBuilder WithFiles(Dictionary files, bool resetStreamPosition = false) { if (this.Files.Count + files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { if (this.FilesInternal.Any(x => x.FileName == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this.FilesInternal.Add(new DiscordMessageFile(file.Key, file.Value, file.Value.Position)); else this.FilesInternal.Add(new DiscordMessageFile(file.Key, file.Value, null)); } return this; } /// /// Modifies the given attachments on edit. /// /// Attachments to edit. /// public DiscordMessageBuilder ModifyAttachments(IEnumerable attachments) { this.AttachmentsInternal.AddRange(attachments); return this; } /// /// Whether to keep the message attachments, if new ones are added. /// /// public DiscordMessageBuilder KeepAttachments(bool keep) { this.KeepAttachmentsInternal = keep; return this; } /// /// Sets if the message is a reply /// /// The ID of the message to reply to. /// If we should mention the user in the reply. /// Whether sending a reply that references an invalid message should be /// The current builder to be chained. public DiscordMessageBuilder WithReply(ulong messageId, bool mention = false, bool failOnInvalidReply = false) { this.ReplyId = messageId; this.MentionOnReply = mention; this.FailOnInvalidReply = failOnInvalidReply; if (mention) { this.Mentions ??= new List(); this.Mentions.Add(new RepliedUserMention()); } return this; } /// /// Sends the Message to a specific channel /// /// The channel the message should be sent to. /// The current builder to be chained. public Task SendAsync(DiscordChannel channel) => channel.SendMessageAsync(this); /// /// Sends the modified message. /// Note: Message replies cannot be modified. To clear the reply, simply pass to . /// /// The original Message to modify. /// The current builder to be chained. public Task ModifyAsync(DiscordMessage msg) => msg.ModifyAsync(this); /// /// Clears all message components on this builder. /// public void ClearComponents() => this.ComponentsInternal.Clear(); /// /// Allows for clearing the Message Builder so that it can be used again to send a new message. /// public void Clear() { this.Content = ""; this._embeds.Clear(); this.IsTts = false; this.Mentions = null; this.FilesInternal.Clear(); this.ReplyId = null; this.MentionOnReply = false; this.ComponentsInternal.Clear(); this.Suppressed = false; this.Sticker = null; this.AttachmentsInternal.Clear(); this.KeepAttachmentsInternal = false; } /// /// Does the validation before we send a the Create/Modify request. /// /// Tells the method to perform the Modify Validation or Create Validation. internal void Validate(bool isModify = false) { if (this._embeds.Count > 10) throw new ArgumentException("A message can only have up to 10 embeds."); if (!isModify) { if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && (!this.Embeds?.Any() ?? true) && this.Sticker is null && (!this.Embeds?.Any() ?? true)) throw new ArgumentException("You must specify content, an embed, a sticker, a component or at least one file."); if (this.Components.Count > 5) throw new InvalidOperationException("You can only have 5 action rows per message."); if (this.Components.Any(c => c.Components.Count > 5)) throw new InvalidOperationException("Action rows can only have 5 components"); } } } diff --git a/DisCatSharp/Entities/Optional.cs b/DisCatSharp/Entities/Optional.cs index 3ec352227..2809e2d8f 100644 --- a/DisCatSharp/Entities/Optional.cs +++ b/DisCatSharp/Entities/Optional.cs @@ -1,400 +1,400 @@ // 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.Linq; using System.Reflection; using DisCatSharp.Net.Serialization; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; namespace DisCatSharp.Entities; /// /// Helper methods for instantiating an . /// /// /// This class only serves to provide and /// as utility that supports type inference. /// public static class Optional { /// /// Provided for easy creation of empty s. /// public static readonly None None = new(); /// /// Creates a new with specified value and valid state. /// /// Value to populate the optional with. /// Type of the value. /// Created optional. public static Optional Some(T value) => value; /// - /// + /// /// /// /// /// public static Optional FromNullable(T value) => value == null ? None : value; /// /// Creates a new with specified value and valid state. /// /// Value to populate the optional with. /// Type of the value. /// Created optional. [Obsolete("Renamed to Some.")] public static Optional FromValue(T value) => value; /// /// Creates a new empty with no value and invalid state. /// /// The type that the created instance is wrapping around. /// Created optional. [Obsolete("Use None.")] public static Optional FromNoValue() => default; } /// /// Unit type for creating an empty s. /// public struct None { } /// /// Used internally to make serialization more convenient, do NOT change this, do NOT implement this yourself. /// internal interface IOptional { /// /// Gets a whether it has a value. /// bool HasValue { get; } /// /// Gets the raw value. /// /// /// Must NOT throw InvalidOperationException. /// object RawValue { get; } } /// /// Represents a wrapper which may or may not have a value. /// /// Type of the value. [JsonConverter(typeof(OptionalJsonConverter))] public readonly struct Optional : IEquatable>, IEquatable, IOptional { /// /// Static empty . /// public static readonly Optional None = default; /// /// Gets whether this has a value. /// public bool HasValue { get; } /// /// Gets the value of this . /// - /// If this has no value. + /// If this has no value. public T Value => this.HasValue ? this._val : throw new InvalidOperationException("Value is not set."); /// /// Gets the raw value. /// object IOptional.RawValue => this._val; private readonly T _val; /// /// Creates a new with specified value. /// /// Value of this option. [Obsolete("Use Optional.Some")] public Optional(T value) { this._val = value; this.HasValue = true; } [Obsolete("Renamed to Map")] public Optional IfPresent(Func mapper) => this.Map(mapper); /// /// Performs a mapping operation on the current , turning it into an Optional holding a /// instance if the source optional contains a value; otherwise, returns an /// of that same type with no value. /// /// The mapping function to apply on the current value if it exists /// The type of the target value returned by /// /// An containing a value denoted by calling if the current /// contains a value; otherwise, an empty of the target /// type. /// public Optional Map(Func mapper) => this.HasValue ? mapper(this._val) : Optional.None; /// /// Maps to for , to default for null and to the mapped value otherwise./> /// /// The type to map to. /// The function that does the mapping of the non-null . /// The mapped value. public Optional MapOrNull(Func mapper) => this.HasValue ? this._val == null ? default : mapper(this._val) : Optional.None; /// /// Gets the value of the or a specified value, if the has no value. /// /// The value to return if this has no value. /// Either the value of the if present or the provided value. public T ValueOr(T other) => this.HasValue ? this._val : other; /// /// Gets the value of the or the default value for , if the /// has no value. /// /// Either the value of the if present or the type's default value. public T ValueOrDefault() => this.ValueOr(default); /// /// Gets the 's value, or throws the provided exception if it's empty. /// /// The exception to throw if the optional is empty. /// The value of the , if present. public T Expect(Exception err) => !this.HasValue ? throw err : this._val; /// /// Gets the 's value, or throws a standard exception with the provided string if it's /// empty. /// /// The string provided to the exception. /// The value of the , if present. public T Expect(string str) => this.Expect(new InvalidOperationException(str)); /// /// Checks if this has a value and tests the predicate if it does. /// /// The predicate to test if this has a value. /// True if this has a value and the predicate is fulfilled, false otherwise. public bool HasValueAnd(Predicate predicate) => this.HasValue && predicate(this._val); /// /// Returns a string representation of this optional value. /// /// String representation of this optional value. public override string ToString() => $"Optional<{typeof(T)}> ({this.Map(x => x.ToString()).ValueOr("")})"; /// /// Checks whether this (or its value) are equal to another object. /// /// Object to compare to. /// Whether the object is equal to this or its value. public override bool Equals(object obj) => obj switch { T t => this.Equals(t), Optional opt => this.Equals(opt), _ => false, }; /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(Optional e) => (!this.HasValue && !e.HasValue) || (this.HasValue == e.HasValue && this.Value.Equals(e.Value)); /// /// Checks whether the value of this is equal to specified object. /// /// Object to compare to. /// Whether the object is equal to the value of this . public bool Equals(T e) => this.HasValue && ReferenceEquals(this.Value, e); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Map(x => x.GetHashCode()).ValueOrDefault(); public static implicit operator Optional(T val) #pragma warning disable 0618 => new(val); #pragma warning restore 0618 public static explicit operator T(Optional opt) => opt.Value; /// /// Creates an empty optional. /// public static implicit operator Optional(None _) => default; public static bool operator ==(Optional opt1, Optional opt2) => opt1.Equals(opt2); public static bool operator !=(Optional opt1, Optional opt2) => !opt1.Equals(opt2); public static bool operator ==(Optional opt, T t) => opt.Equals(t); public static bool operator !=(Optional opt, T t) => !opt.Equals(t); } /// /// Represents an optional json contract resolver. /// /// internal sealed class OptionalJsonContractResolver : DefaultContractResolver { /// /// Creates the property. /// /// The member. /// The member serialization. protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); var type = property.PropertyType; if (!type.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IOptional))) return property; // we cache the PropertyInfo object here (it's captured in closure). we don't have direct // access to the property value so we have to reflect into it from the parent instance // we use UnderlyingName instead of PropertyName in case the C# name is different from the Json name. var declaringMember = property.DeclaringType.GetTypeInfo().DeclaredMembers .FirstOrDefault(e => e.Name == property.UnderlyingName); switch (declaringMember) { case PropertyInfo declaringProp: property.ShouldSerialize = instance => // instance here is the declaring (parent) type { var optionalValue = declaringProp.GetValue(instance); return (optionalValue as IOptional).HasValue; }; return property; case FieldInfo declaringField: property.ShouldSerialize = instance => // instance here is the declaring (parent) type { var optionalValue = declaringField.GetValue(instance); return (optionalValue as IOptional).HasValue; }; return property; default: throw new InvalidOperationException( "Can only serialize Optional members that are fields or properties"); } } } /// /// Represents an optional json converter. /// internal sealed class OptionalJsonConverter : JsonConverter { /// /// Writes the json. /// /// The writer. /// The value. /// The serializer. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { // we don't check for HasValue here since it's checked in OptionalJsonContractResolver var val = (value as IOptional).RawValue; // JToken.FromObject will throw if `null` so we manually write a null value. if (val == null) { // you can read serializer.NullValueHandling here, but unfortunately you can **not** skip serialization // here, or else you will get a nasty JsonWriterException, so we just ignore its value and manually // write the null. writer.WriteToken(JsonToken.Null); } else { // convert the value to a JSON object and write it to the property value. JToken.FromObject(val).WriteTo(writer); } } /// /// Reads the json. /// /// The reader. /// The object type. /// The existing value. /// The serializer. public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var genericType = objectType.GenericTypeArguments[0]; var constructor = objectType.GetTypeInfo().DeclaredConstructors .FirstOrDefault(e => e.GetParameters()[0].ParameterType == genericType); return constructor.Invoke(new[] { serializer.Deserialize(reader, genericType) }); } /// /// Whether it can convert. /// /// The object type. public override bool CanConvert(Type objectType) => objectType.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IOptional)); } diff --git a/DisCatSharp/Entities/Sticker/DiscordSticker.cs b/DisCatSharp/Entities/Sticker/DiscordSticker.cs index a01fae220..6fe2dab97 100644 --- a/DisCatSharp/Entities/Sticker/DiscordSticker.cs +++ b/DisCatSharp/Entities/Sticker/DiscordSticker.cs @@ -1,233 +1,233 @@ // 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.Enums; using DisCatSharp.Net; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord Sticker. /// public class DiscordSticker : SnowflakeObject, IEquatable { /// /// Gets the Pack ID of this sticker. /// [JsonProperty("pack_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? PackId { get; internal set; } /// /// Gets the Name of the sticker. /// [JsonProperty("name")] public string Name { get; internal set; } /// /// Gets the Description of the sticker. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; internal set; } /// /// Gets the type of sticker. /// [JsonProperty("type")] public StickerType Type { get; internal set; } /// /// For guild stickers, gets the user that made the sticker. /// [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] public DiscordUser User { get; internal set; } /// /// Gets the guild associated with this sticker, if any. /// public DiscordGuild Guild => (this.Discord as DiscordClient).InternalGetCachedGuild(this.GuildId); /// /// Gets the guild id the sticker belongs too. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? GuildId { get; internal set; } /// /// Gets whether this sticker is available. Only applicable to guild stickers. /// [JsonProperty("available", NullValueHandling = NullValueHandling.Ignore)] public bool Available { get; internal set; } /// /// Gets the sticker's sort order, if it's in a pack. /// [JsonProperty("sort_value", NullValueHandling = NullValueHandling.Ignore)] public int? SortValue { get; internal set; } /// /// Gets the list of tags for the sticker. /// [JsonIgnore] public IEnumerable Tags => this.InternalTags != null ? this.InternalTags.Split(',') : Array.Empty(); /// /// Gets the asset hash of the sticker. /// [JsonProperty("asset", NullValueHandling = NullValueHandling.Ignore)] public string Asset { get; internal set; } /// /// Gets the preview asset hash of the sticker. /// [JsonProperty("preview_asset", NullValueHandling = NullValueHandling.Ignore)] public string PreviewAsset { get; internal set; } /// /// Gets the Format type of the sticker. /// [JsonProperty("format_type")] public StickerFormat FormatType { get; internal set; } /// /// Gets the tags of the sticker. /// [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "")] internal string InternalTags { get; set; } /// /// Gets the url of the sticker. /// [JsonIgnore] public string Url => $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.STICKERS}/{this.Id}.{(this.FormatType == StickerFormat.Lottie ? "json" : "png")}"; /// /// Initializes a new instance of the class. /// internal DiscordSticker() { } /// /// Gets the hash code of the current sticker. /// /// The hash code. public override int GetHashCode() => this.PackId != null ? HashCode.Combine(this.Id.GetHashCode(), this.PackId.GetHashCode()) : HashCode.Combine(this.Id.GetHashCode(), this.GuildId.GetHashCode()); /// /// 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 ForumPostTag); /// /// Whether to stickers are equal. /// /// DiscordSticker public bool Equals(DiscordSticker other) => this.Id == other.Id; /// /// Gets the sticker in readable format. /// public override string ToString() => $"Sticker {this.Id}; {this.Name}; {this.FormatType}"; /// /// Modifies the sticker /// /// The name of the sticker /// The description of the sticker /// The name of a unicode emoji representing the sticker's expression /// Audit log reason /// A sticker object - /// Thrown when the sticker could not be found. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Sticker does not belong to a guild. + /// Thrown when the sticker could not be found. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Sticker does not belong to a guild. public Task ModifyAsync(Optional name, Optional description, Optional tags, string reason = null) => !this.GuildId.HasValue ? throw new ArgumentException("This sticker does not belong to a guild.") : name.HasValue && (name.Value.Length < 2 || name.Value.Length > 30) ? throw new ArgumentException("Sticker name needs to be between 2 and 30 characters long.") : description.HasValue && (description.Value.Length < 1 || description.Value.Length > 100) ? throw new ArgumentException("Sticker description needs to be between 1 and 100 characters long.") : tags.HasValue && !DiscordEmoji.TryFromUnicode(this.Discord, tags.Value, out var emoji) ? throw new ArgumentException("Sticker tags needs to be a unicode emoji.") : this.Discord.ApiClient.ModifyGuildStickerAsync(this.GuildId.Value, this.Id, name, description, tags, reason); /// /// Deletes the sticker /// /// Audit log reason - /// Thrown when the sticker could not be found. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Sticker does not belong to a guild. + /// Thrown when the sticker could not be found. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Sticker does not belong to a guild. public Task DeleteAsync(string reason = null) => this.GuildId.HasValue ? this.Discord.ApiClient.DeleteGuildStickerAsync(this.GuildId.Value, this.Id, reason) : throw new ArgumentException("The requested sticker is no guild sticker."); } /// /// The sticker type /// public enum StickerType : long { /// /// Standard nitro sticker /// Standard = 1, /// /// Custom guild sticker /// Guild = 2 } /// /// The sticker type /// public enum StickerFormat : long { /// /// Sticker is a png /// Png = 1, /// /// Sticker is a animated png /// Apng = 2, /// /// Sticker is lottie /// Lottie = 3 } diff --git a/DisCatSharp/Entities/ThreadAndForum/DiscordThreadChannel.cs b/DisCatSharp/Entities/ThreadAndForum/DiscordThreadChannel.cs index df09831fb..04267d221 100644 --- a/DisCatSharp/Entities/ThreadAndForum/DiscordThreadChannel.cs +++ b/DisCatSharp/Entities/ThreadAndForum/DiscordThreadChannel.cs @@ -1,342 +1,342 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net.Models; using DisCatSharp.Net.Serialization; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a discord thread channel. /// public class DiscordThreadChannel : DiscordChannel { /// /// Gets ID of the owner that started this thread. /// [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] public ulong OwnerId { get; internal set; } [JsonProperty("total_message_sent", DefaultValueHandling = DefaultValueHandling.Ignore)] public int TotalMessagesSent { get; internal set; } /// /// Gets an approximate count of messages in a thread, stops counting at 50. /// [JsonProperty("message_count", NullValueHandling = NullValueHandling.Ignore)] public int? MessageCount { get; internal set; } /// /// Gets an approximate count of users in a thread, stops counting at 50. /// [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] public int? MemberCount { get; internal set; } /// /// Represents the current member for this thread. This will have a value if the user has joined the thread. /// [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] public DiscordThreadChannelMember CurrentMember { get; internal set; } /// /// Gets the threads metadata. /// [JsonProperty("thread_metadata", NullValueHandling = NullValueHandling.Ignore)] public DiscordThreadChannelMetadata ThreadMetadata { get; internal set; } /// /// Gets the thread members object. /// [JsonIgnore] public IReadOnlyDictionary ThreadMembers => new ReadOnlyConcurrentDictionary(this.ThreadMembersInternal); [JsonProperty("thread_member", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ThreadMembersInternal; /// /// List of applied tag ids. /// [JsonIgnore] internal IReadOnlyList AppliedTagIds => this.AppliedTagIdsInternal; /// /// List of applied tag ids. /// [JsonProperty("applied_tags", NullValueHandling = NullValueHandling.Ignore)] internal List AppliedTagIdsInternal; /// /// Gets the list of applied tags. /// Only applicable for forum channel posts. /// [JsonIgnore] public IEnumerable AppliedTags => this.AppliedTagIds?.Select(id => this.Parent.GetForumPostTag(id)).Where(x => x != null); /// /// Initializes a new instance of the class. /// internal DiscordThreadChannel() { } #region Methods /// /// Modifies the current thread. /// /// Action to perform on this thread - /// Thrown when the client does not have the permission. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - /// Thrown when the cannot be modified. This happens, when the guild hasn't reached a certain boost . + /// Thrown when the client does not have the permission. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// Thrown when the cannot be modified. This happens, when the guild hasn't reached a certain boost . public Task ModifyAsync(Action action) { var mdl = new ThreadEditModel(); action(mdl); var canContinue = !mdl.AutoArchiveDuration.HasValue || !mdl.AutoArchiveDuration.Value.HasValue || Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.AutoArchiveDuration.Value.Value); if (mdl.Invitable.HasValue) { canContinue = this.Guild.Features.CanCreatePrivateThreads; } return canContinue ? this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, mdl.Name, mdl.Locked, mdl.Archived, mdl.PerUserRateLimit, mdl.AutoArchiveDuration, mdl.Invitable, mdl.AppliedTags, mdl.AuditLogReason) : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(mdl.AutoArchiveDuration.Value.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); } /// /// Add a tag to the current thread. /// /// The tag to add. /// The reason for the audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task AddTagAsync(ForumPostTag tag, string reason = null) => await this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, null, null, null, null, null, new List(this.AppliedTags) { tag }, reason: reason); /// /// Remove a tag from the current thread. /// /// The tag to remove. /// The reason for the audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task RemoveTagAsync(ForumPostTag tag, string reason = null) => await this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, null, null, null, null, null, new List(this.AppliedTags).Where(x => x != tag).ToList(), reason: reason); /// /// Archives a thread. /// /// Whether the thread should be locked. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ArchiveAsync(bool locked = true, string reason = null) => this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, locked, true, null, null, null, null, reason: reason); /// /// Unarchives a thread. /// /// Reason for audit logs. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task UnarchiveAsync(string reason = null) => this.Discord.ApiClient.ModifyThreadAsync(this.Id, this.Parent.Type, null, null, false, null, null, null, null, reason: reason); /// /// Gets the members of a thread. Needs the intent. /// - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task> GetMembersAsync() => await this.Discord.ApiClient.GetThreadMembersAsync(this.Id); /// /// Adds a member to this thread. /// /// The member id to be added. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(ulong memberId) => this.Discord.ApiClient.AddThreadMemberAsync(this.Id, memberId); /// /// Adds a member to this thread. /// /// The member to be added. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(DiscordMember member) => this.AddMemberAsync(member.Id); /// /// Gets a member in this thread. /// /// The member to be added. - /// Thrown when the member is not part of the thread. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the member is not part of the thread. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetMemberAsync(ulong memberId) => this.Discord.ApiClient.GetThreadMemberAsync(this.Id, memberId); /// /// Gets a member in this thread. /// /// The member to be added. - /// Thrown when the member is not part of the thread. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the member is not part of the thread. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task GetMemberAsync(DiscordMember member) => this.Discord.ApiClient.GetThreadMemberAsync(this.Id, member.Id); /// /// Removes a member from this thread. /// /// The member id to be removed. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RemoveMemberAsync(ulong memberId) => this.Discord.ApiClient.RemoveThreadMemberAsync(this.Id, memberId); /// /// Removes a member from this thread. Only applicable to private threads. /// /// The member to be removed. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RemoveMemberAsync(DiscordMember member) => this.RemoveMemberAsync(member.Id); /// /// Adds a role to this thread. Only applicable to private threads. /// /// The role id to be added. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task AddRoleAsync(ulong roleId) { var role = this.Guild.GetRole(roleId); var members = await this.Guild.GetAllMembersAsync(); var roleMembers = members.Where(m => m.Roles.Contains(role)); foreach (var member in roleMembers) { await this.Discord.ApiClient.AddThreadMemberAsync(this.Id, member.Id); } } /// /// Adds a role to this thread. Only applicable to private threads. /// /// The role to be added. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task AddRoleAsync(DiscordRole role) => this.AddRoleAsync(role.Id); /// /// Removes a role from this thread. Only applicable to private threads. /// /// The role id to be removed. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task RemoveRoleAsync(ulong roleId) { var role = this.Guild.GetRole(roleId); var members = await this.Guild.GetAllMembersAsync(); var roleMembers = members.Where(m => m.Roles.Contains(role)); foreach (var member in roleMembers) { await this.Discord.ApiClient.RemoveThreadMemberAsync(this.Id, member.Id); } } /// /// Removes a role from this thread. Only applicable to private threads. /// /// The role to be removed. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task RemoveRoleAsync(DiscordRole role) => this.RemoveRoleAsync(role.Id); /// /// Joins a thread. /// - /// Thrown when the client has no access to this thread. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client has no access to this thread. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task JoinAsync() => this.Discord.ApiClient.JoinThreadAsync(this.Id); /// /// Leaves a thread. /// - /// Thrown when the client has no access to this thread. - /// Thrown when the thread does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client has no access to this thread. + /// Thrown when the thread does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task LeaveAsync() => this.Discord.ApiClient.LeaveThreadAsync(this.Id); /// /// Returns a string representation of this thread. /// /// String representation of this thread. public override string ToString() => this.Type switch { ChannelType.NewsThread => $"News thread {this.Name} ({this.Id})", ChannelType.PublicThread => $"Thread {this.Name} ({this.Id})", ChannelType.PrivateThread => $"Private thread {this.Name} ({this.Id})", _ => $"Thread {this.Name} ({this.Id})", }; #endregion } diff --git a/DisCatSharp/Entities/User/DiscordUser.cs b/DisCatSharp/Entities/User/DiscordUser.cs index 3fe53f558..016ce164e 100644 --- a/DisCatSharp/Entities/User/DiscordUser.cs +++ b/DisCatSharp/Entities/User/DiscordUser.cs @@ -1,493 +1,493 @@ // 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.Globalization; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord user. /// public class DiscordUser : SnowflakeObject, IEquatable { /// /// Initializes a new instance of the class. /// internal DiscordUser() { } /// /// Initializes a new instance of the class. /// /// The transport user. internal DiscordUser(TransportUser transport) { this.Id = transport.Id; this.Username = transport.Username; this.Discriminator = transport.Discriminator; this.AvatarHash = transport.AvatarHash; this.AvatarDecorationHash = transport.AvatarDecorationHash; this.BannerHash = transport.BannerHash; this.BannerColorInternal = transport.BannerColor; this.ThemeColorsInternal = (transport.ThemeColors ?? Array.Empty()).ToList(); this.IsBot = transport.IsBot; this.MfaEnabled = transport.MfaEnabled; this.Verified = transport.Verified; this.Email = transport.Email; this.PremiumType = transport.PremiumType; this.Locale = transport.Locale; this.Flags = transport.Flags; this.OAuthFlags = transport.OAuthFlags; this.Bio = transport.Bio; this.Pronouns = transport.Pronouns; } /// /// Gets this user's username. /// [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] public virtual string Username { get; internal set; } /// /// Gets this user's username with the discriminator. /// Example: Discord#0000 /// [JsonIgnore] public virtual string UsernameWithDiscriminator => $"{this.Username}#{this.Discriminator}"; /// /// Gets the user's 4-digit discriminator. /// [JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)] public virtual string Discriminator { get; internal set; } /// /// Gets the discriminator integer. /// [JsonIgnore] internal int DiscriminatorInt => int.Parse(this.Discriminator, NumberStyles.Integer, CultureInfo.InvariantCulture); /// /// Gets the user's banner color, if set. Mutually exclusive with . /// public virtual DiscordColor? BannerColor => !this.BannerColorInternal.HasValue ? null : new DiscordColor(this.BannerColorInternal.Value); /// /// Gets the user's theme colors, if set. /// public virtual IReadOnlyList? ThemeColors => !(this.ThemeColorsInternal is not null && this.ThemeColorsInternal.Count != 0) ? null : this.ThemeColorsInternal.Select(x => new DiscordColor(x)).ToList(); /// /// Gets the user's banner color integer. /// [JsonProperty("accent_color")] internal int? BannerColorInternal; /// /// Gets the user's theme color integers. /// [JsonProperty("theme_colors", NullValueHandling = NullValueHandling.Ignore)] internal List? ThemeColorsInternal; /// /// Gets the user's banner url /// [JsonIgnore] public string BannerUrl => string.IsNullOrWhiteSpace(this.BannerHash) ? null : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}?size=4096"; /// /// Gets the user's profile banner hash. Mutually exclusive with . /// [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] public virtual string BannerHash { get; internal set; } /// /// Gets the users bio. /// This is not available to bots tho. /// [JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)] public virtual string Bio { get; internal set; } /// /// Gets the user's avatar hash. /// [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] public virtual string AvatarHash { get; internal set; } /// /// Gets the user's avatar decoration hash. /// [JsonProperty("avatar_decoration", NullValueHandling = NullValueHandling.Ignore)] public virtual string AvatarDecorationHash { get; internal set; } /// /// Returns a uri to this users profile. /// [JsonIgnore] public Uri ProfileUri => new($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.USERS}/{this.Id}"); /// /// Returns a string representing the direct URL to this users profile. /// /// The URL of this users profile. [JsonIgnore] public string ProfileUrl => this.ProfileUri.AbsoluteUri; /// /// Gets the user's avatar url. /// [JsonIgnore] public string AvatarUrl => string.IsNullOrWhiteSpace(this.AvatarHash) ? this.DefaultAvatarUrl : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.AvatarHash}.{(this.AvatarHash.StartsWith("a_") ? "gif" : "png")}?size=1024"; /// /// Gets the user's avatar decoration url. /// [JsonIgnore] public string? AvatarDecorationUrl => string.IsNullOrWhiteSpace(this.AvatarDecorationHash) ? null : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS_DECORATIONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.AvatarDecorationHash}.{(this.AvatarDecorationHash.StartsWith("a_") ? "gif" : "png")}?size=1024"; /// /// Gets the URL of default avatar for this user. /// [JsonIgnore] public string DefaultAvatarUrl => $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{(this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture)}.png?size=1024"; /// /// Gets whether the user is a bot. /// [JsonProperty("bot", NullValueHandling = NullValueHandling.Ignore)] public virtual bool IsBot { get; internal set; } /// /// Gets whether the user has multi-factor authentication enabled. /// [JsonProperty("mfa_enabled", NullValueHandling = NullValueHandling.Ignore)] public virtual bool? MfaEnabled { get; internal set; } /// /// Gets whether the user is an official Discord system user. /// [JsonProperty("system", NullValueHandling = NullValueHandling.Ignore)] public bool? IsSystem { get; internal set; } /// /// Gets whether the user is verified. /// This is only present in OAuth. /// [JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)] public virtual bool? Verified { get; internal set; } /// /// Gets the user's email address. /// This is only present in OAuth. /// [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] public virtual string Email { get; internal set; } /// /// Gets the user's premium type. /// [JsonProperty("premium_type", NullValueHandling = NullValueHandling.Ignore)] public virtual PremiumType? PremiumType { get; internal set; } /// /// Gets the user's chosen language /// [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] public virtual string Locale { get; internal set; } /// /// Gets the user's flags for OAuth. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public virtual UserFlags? OAuthFlags { get; internal set; } /// /// Gets the user's flags. /// [JsonProperty("public_flags", NullValueHandling = NullValueHandling.Ignore)] public virtual UserFlags? Flags { get; internal set; } /// /// Gets the user's pronouns. /// [JsonProperty("pronouns", NullValueHandling = NullValueHandling.Ignore)] public virtual string Pronouns { get; internal set; } /// /// Gets the user's mention string. /// [JsonIgnore] public string Mention => Formatter.Mention(this, this is DiscordMember); /// /// Gets whether this user is the Client which created this object. /// [JsonIgnore] public bool IsCurrent => this.Id == this.Discord.CurrentUser.Id; #region Extension of DiscordUser /// /// Whether this member is a /// /// [JsonIgnore] public bool IsMod => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.CertifiedModerator); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsPartner => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.Partner); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsVerifiedBot => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedBot); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsBotDev => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedDeveloper); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsStaff => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.Staff); #endregion /// /// Fetches the user from the API. /// /// The user with fresh data from the API. public async Task GetFromApiAsync() => await this.Discord.ApiClient.GetUserAsync(this.Id); /// /// Whether this user is in a /// /// /// /// DiscordGuild guild = await Client.GetGuildAsync(806675511555915806); /// DiscordUser user = await Client.GetUserAsync(469957180968271873); /// Console.WriteLine($"{user.Username} {(user.IsInGuild(guild) ? "is a" : "is not a")} member of {guild.Name}"); /// /// results to J_M_Lutra is a member of Project Nyaw~. /// /// /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "")] public async Task IsInGuild(DiscordGuild guild) { try { var member = await guild.GetMemberAsync(this.Id); return member is not null; } catch (NotFoundException) { return false; } } /// /// Whether this user is not in a /// /// /// public async Task IsNotInGuild(DiscordGuild guild) => !await this.IsInGuild(guild); /// /// Returns the DiscordMember in the specified /// /// The to get this user on. /// The . - /// Thrown when the user is not part of the guild. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the user is not part of the guild. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task ConvertToMember(DiscordGuild guild) => await guild.GetMemberAsync(this.Id); /// /// Unbans this user from a guild. /// /// Guild to unban this user from. /// Reason for audit logs. - /// Thrown when the client does not have the permission. - /// Thrown when the user does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the user does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task UnbanAsync(DiscordGuild guild, string reason = null) => guild.UnbanMemberAsync(this, reason); /// /// Gets this user's presence. /// [JsonIgnore] public DiscordPresence Presence => this.Discord is DiscordClient dc && dc.Presences.TryGetValue(this.Id, out var presence) ? presence : null; /// /// Gets the user's avatar URL, in requested format and size. /// /// Format of the avatar to get. /// Maximum size of the avatar. Must be a power of two, minimum 16, maximum 2048. /// URL of the user's avatar. public string GetAvatarUrl(ImageFormat fmt, ushort size = 1024) { if (fmt == ImageFormat.Unknown) throw new ArgumentException("You must specify valid image format.", nameof(fmt)); if (size < 16 || size > 2048) throw new ArgumentOutOfRangeException(nameof(size)); var log = Math.Log(size, 2); if (log < 4 || log > 11 || log % 1 != 0) throw new ArgumentOutOfRangeException(nameof(size)); var sfmt = ""; sfmt = fmt switch { ImageFormat.Gif => "gif", ImageFormat.Jpeg => "jpg", ImageFormat.Png => "png", ImageFormat.WebP => "webp", ImageFormat.Auto => !string.IsNullOrWhiteSpace(this.AvatarHash) ? this.AvatarHash.StartsWith("a_") ? "gif" : "png" : "png", _ => throw new ArgumentOutOfRangeException(nameof(fmt)), }; var ssize = size.ToString(CultureInfo.InvariantCulture); if (!string.IsNullOrWhiteSpace(this.AvatarHash)) { var id = this.Id.ToString(CultureInfo.InvariantCulture); return $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{id}/{this.AvatarHash}.{sfmt}?size={ssize}"; } else { var type = (this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture); return $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{type}.{sfmt}?size={ssize}"; } } /// /// Returns a string representation of this user. /// /// String representation of this user. public override string ToString() => $"User {this.Id}; {this.Username}#{this.Discriminator}"; /// /// 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 DiscordUser); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordUser 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 user to compare. /// Second user to compare. /// Whether the two users are equal. public static bool operator ==(DiscordUser e1, DiscordUser 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 user to compare. /// Second user to compare. /// Whether the two users are not equal. public static bool operator !=(DiscordUser e1, DiscordUser e2) => !(e1 == e2); } /// /// Represents a user comparer. /// internal class DiscordUserComparer : IEqualityComparer { /// /// Whether the users are equal. /// /// The first user /// The second user. public bool Equals(DiscordUser x, DiscordUser y) => x.Equals(y); /// /// Gets the hash code. /// /// The user. public int GetHashCode(DiscordUser obj) => obj.Id.GetHashCode(); } diff --git a/DisCatSharp/Entities/Webhook/DiscordWebhook.cs b/DisCatSharp/Entities/Webhook/DiscordWebhook.cs index 90094ebbb..0852aade4 100644 --- a/DisCatSharp/Entities/Webhook/DiscordWebhook.cs +++ b/DisCatSharp/Entities/Webhook/DiscordWebhook.cs @@ -1,279 +1,279 @@ // 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.IO; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents information about a Discord webhook. /// public class DiscordWebhook : SnowflakeObject, IEquatable { /// /// Gets the api client. /// internal DiscordApiClient ApiClient { get; set; } /// /// Gets the id of the guild this webhook belongs to. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] public ulong GuildId { get; internal set; } /// /// Gets the ID of the channel this webhook belongs to. /// [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] public ulong ChannelId { get; internal set; } /// /// Gets the user this webhook was created by. /// [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] public DiscordUser User { get; internal set; } /// /// Gets the default name of this webhook. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets hash of the default avatar for this webhook. /// [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] internal string AvatarHash { get; set; } /// /// Gets the partial source guild for this webhook (For Channel Follower Webhooks). /// [JsonProperty("source_guild", NullValueHandling = NullValueHandling.Ignore)] public DiscordGuild SourceGuild { get; set; } /// /// Gets the partial source channel for this webhook (For Channel Follower Webhooks). /// [JsonProperty("source_channel", NullValueHandling = NullValueHandling.Ignore)] public DiscordChannel SourceChannel { get; set; } /// /// Gets the url used for executing the webhook. /// [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] public string Url { get; set; } /// /// Gets the default avatar url for this webhook. /// public string AvatarUrl => !string.IsNullOrWhiteSpace(this.AvatarHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{this.Id}/{this.AvatarHash}.png?size=1024" : null; /// /// Gets the secure token of this webhook. /// [JsonProperty("token", NullValueHandling = NullValueHandling.Ignore)] public string Token { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordWebhook() { } /// /// Modifies this webhook. /// /// New default name for this webhook. /// New avatar for this webhook. /// The new channel id to move the webhook to. /// Reason for audit logs. /// The modified webhook. - /// Thrown when the client does not have the permission. - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ModifyAsync(string name = null, Optional avatar = default, ulong? channelId = null, string reason = null) { var avatarb64 = ImageTool.Base64FromStream(avatar); var newChannelId = channelId ?? this.ChannelId; return this.Discord.ApiClient.ModifyWebhookAsync(this.Id, newChannelId, name, avatarb64, reason); } /// /// Gets a previously-sent webhook message. /// - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetMessageAsync(ulong messageId) => await (this.Discord?.ApiClient ?? this.ApiClient).GetWebhookMessageAsync(this.Id, this.Token, messageId).ConfigureAwait(false); /// /// Gets a previously-sent webhook message. /// - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task GetMessageAsync(ulong messageId, ulong threadId) => await (this.Discord?.ApiClient ?? this.ApiClient).GetWebhookMessageAsync(this.Id, this.Token, messageId, threadId).ConfigureAwait(false); /// /// Permanently deletes this webhook. /// - /// Thrown when the client does not have the permission. - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the client does not have the permission. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task DeleteAsync() => this.Discord.ApiClient.DeleteWebhookAsync(this.Id, this.Token); /// /// Executes this webhook with the given . /// /// Webhook builder filled with data to send. /// Target thread id (Optional). Defaults to null. - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ExecuteAsync(DiscordWebhookBuilder builder, string threadId = null) => (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookAsync(this.Id, this.Token, builder, threadId); /// /// Executes this webhook in Slack compatibility mode. /// /// JSON containing Slack-compatible payload for this webhook. /// Target thread id (Optional). Defaults to null. - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ExecuteSlackAsync(string json, string threadId = null) => (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookSlackAsync(this.Id, this.Token, json, threadId); /// /// Executes this webhook in GitHub compatibility mode. /// /// JSON containing GitHub-compatible payload for this webhook. /// Target thread id (Optional). Defaults to null. - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task ExecuteGithubAsync(string json, string threadId = null) => (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookGithubAsync(this.Id, this.Token, json, threadId); /// /// Edits a previously-sent webhook message. /// /// The id of the message to edit. /// The builder of the message to edit. /// Target thread id (Optional). Defaults to null. /// The modified - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public async Task EditMessageAsync(ulong messageId, DiscordWebhookBuilder builder, string threadId = null) { builder.Validate(true); if (builder.KeepAttachmentsInternal.HasValue && builder.KeepAttachmentsInternal.Value) { builder.AttachmentsInternal.AddRange(this.ApiClient.GetWebhookMessageAsync(this.Id, this.Token, messageId.ToString(), threadId).Result.Attachments); } else if (builder.KeepAttachmentsInternal.HasValue) { builder.AttachmentsInternal.Clear(); } return await (this.Discord?.ApiClient ?? this.ApiClient).EditWebhookMessageAsync(this.Id, this.Token, messageId.ToString(), builder, threadId).ConfigureAwait(false); } /// /// Deletes a message that was created by the webhook. /// /// The id of the message to delete - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task DeleteMessageAsync(ulong messageId) => (this.Discord?.ApiClient ?? this.ApiClient).DeleteWebhookMessageAsync(this.Id, this.Token, messageId); /// /// Deletes a message that was created by the webhook. /// /// The id of the message to delete /// Target thread id (Optional). Defaults to null. - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. public Task DeleteMessageAsync(ulong messageId, ulong threadId) => (this.Discord?.ApiClient ?? this.ApiClient).DeleteWebhookMessageAsync(this.Id, this.Token, messageId, threadId); /// /// 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 DiscordWebhook); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordWebhook 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 webhook to compare. /// Second webhook to compare. /// Whether the two webhooks are equal. public static bool operator ==(DiscordWebhook e1, DiscordWebhook 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 webhook to compare. /// Second webhook to compare. /// Whether the two webhooks are not equal. public static bool operator !=(DiscordWebhook e1, DiscordWebhook e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs b/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs index c1d110702..fe6d548ff 100644 --- a/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs +++ b/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs @@ -1,462 +1,462 @@ // 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.IO; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; namespace DisCatSharp.Entities; /// /// Constructs ready-to-send webhook requests. /// public sealed class DiscordWebhookBuilder { /// /// Username to use for this webhook request. /// public Optional Username { get; set; } /// /// Avatar url to use for this webhook request. /// public Optional AvatarUrl { get; set; } /// /// Whether this webhook request is text-to-speech. /// public bool IsTts { get; set; } /// /// Message to send on this webhook request. /// public string Content { get => this._content; set { if (value != null && value.Length > 2000) throw new ArgumentException("Content length cannot exceed 2000 characters.", nameof(value)); this._content = value; } } private string _content; /// /// Name of the new thread. /// Only works if the webhook is send in a . /// public string ThreadName { get; set; } /// /// Whether to keep previous attachments. /// internal bool? KeepAttachmentsInternal; /// /// Embeds to send on this webhook request. /// public IReadOnlyList Embeds => this._embeds; private readonly List _embeds = new(); /// /// Files to send on this webhook request. /// public IReadOnlyList Files => this._files; private readonly List _files = new(); /// /// Mentions to send on this webhook request. /// public IReadOnlyList Mentions => this._mentions; private readonly List _mentions = new(); /// /// Gets the components. /// public IReadOnlyList Components => this._components; private readonly List _components = new(); /// /// Attachments to keep on this webhook request. /// public IEnumerable Attachments => this.AttachmentsInternal; internal readonly List AttachmentsInternal = new(); /// /// Constructs a new empty webhook request builder. /// public DiscordWebhookBuilder() { } // I still see no point in initializing collections with empty collections. // /// /// Adds a row of components to the builder, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the builder. /// The current builder to be chained. - /// No components were passed. + /// No components were passed. public DiscordWebhookBuilder AddComponents(params DiscordComponent[] components) => this.AddComponents((IEnumerable)components); /// /// Appends several rows of components to the builder /// /// The rows of components to add, holding up to five each. /// public DiscordWebhookBuilder AddComponents(IEnumerable components) { var ara = components.ToArray(); if (ara.Length + this._components.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) this._components.Add(ar); return this; } /// /// Adds a row of components to the builder, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the builder. /// The current builder to be chained. - /// No components were passed. + /// No components were passed. public DiscordWebhookBuilder AddComponents(IEnumerable components) { var cmpArr = components.ToArray(); var count = cmpArr.Length; if (!cmpArr.Any()) throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); if (count > 5) throw new ArgumentException("Cannot add more than 5 components per action row!"); var comp = new DiscordActionRowComponent(cmpArr); this._components.Add(comp); return this; } /// /// Sets the username for this webhook builder. /// /// Username of the webhook public DiscordWebhookBuilder WithUsername(string username) { this.Username = username; return this; } /// /// Sets the avatar of this webhook builder from its url. /// /// Avatar url of the webhook public DiscordWebhookBuilder WithAvatarUrl(string avatarUrl) { this.AvatarUrl = avatarUrl; return this; } /// /// Indicates if the webhook must use text-to-speech. /// /// Text-to-speech public DiscordWebhookBuilder WithTts(bool tts) { this.IsTts = tts; return this; } /// /// Sets the message to send at the execution of the webhook. /// /// Message to send. public DiscordWebhookBuilder WithContent(string content) { this.Content = content; return this; } /// /// Sets the thread name to create at the execution of the webhook. /// Only works for . /// /// The thread name. public DiscordWebhookBuilder WithThreadName(string name) { this.ThreadName = name; return this; } /// /// Adds an embed to send at the execution of the webhook. /// /// Embed to add. public DiscordWebhookBuilder AddEmbed(DiscordEmbed embed) { if (embed != null) this._embeds.Add(embed); return this; } /// /// Adds the given embeds to send at the execution of the webhook. /// /// Embeds to add. public DiscordWebhookBuilder AddEmbeds(IEnumerable embeds) { this._embeds.AddRange(embeds); return this; } /// /// Adds a file to send at the execution of the webhook. /// /// Name of the file. /// File data. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. public DiscordWebhookBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false, string description = null) { if (this.Files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == filename)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(filename, data, data.Position, description: description)); else this._files.Add(new DiscordMessageFile(filename, data, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. /// public DiscordWebhookBuilder AddFile(FileStream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description)); else this._files.Add(new DiscordMessageFile(stream.Name, stream, null, description: description)); return this; } /// /// Adds the given files to send at the execution of the webhook. /// /// Dictionary of file name and file data. /// Tells the API Client to reset the stream position to what it was after the file is sent. public DiscordWebhookBuilder AddFiles(Dictionary files, bool resetStreamPosition = false) { if (this.Files.Count + files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { if (this._files.Any(x => x.FileName == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(file.Key, file.Value, file.Value.Position)); else this._files.Add(new DiscordMessageFile(file.Key, file.Value, null)); } return this; } /// /// Modifies the given attachments on edit. /// /// Attachments to edit. /// public DiscordWebhookBuilder ModifyAttachments(IEnumerable attachments) { this.AttachmentsInternal.AddRange(attachments); return this; } /// /// Whether to keep the message attachments, if new ones are added. /// /// public DiscordWebhookBuilder KeepAttachments(bool keep) { this.KeepAttachmentsInternal = keep; return this; } /// /// Adds the mention to the mentions to parse, etc. at the execution of the webhook. /// /// Mention to add. public DiscordWebhookBuilder AddMention(IMention mention) { this._mentions.Add(mention); return this; } /// /// Adds the mentions to the mentions to parse, etc. at the execution of the webhook. /// /// Mentions to add. public DiscordWebhookBuilder AddMentions(IEnumerable mentions) { this._mentions.AddRange(mentions); return this; } /// /// Executes a webhook. /// /// The webhook that should be executed. /// The message sent public async Task SendAsync(DiscordWebhook webhook) => await webhook.ExecuteAsync(this).ConfigureAwait(false); /// /// Executes a webhook. /// /// The webhook that should be executed. /// Target thread id. /// The message sent public async Task SendAsync(DiscordWebhook webhook, ulong threadId) => await webhook.ExecuteAsync(this, threadId.ToString()).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The message to modify. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, DiscordMessage message) => await this.ModifyAsync(webhook, message.Id).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The id of the message to modify. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, ulong messageId) => await webhook.EditMessageAsync(messageId, this).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The message to modify. /// Target thread. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, DiscordMessage message, DiscordThreadChannel thread) => await this.ModifyAsync(webhook, message.Id, thread.Id).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The id of the message to modify. /// Target thread id. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, ulong messageId, ulong threadId) => await webhook.EditMessageAsync(messageId, this, threadId.ToString()).ConfigureAwait(false); /// /// Clears all message components on this builder. /// public void ClearComponents() => this._components.Clear(); /// /// Allows for clearing the Webhook Builder so that it can be used again to send a new message. /// public void Clear() { this.Content = ""; this._embeds.Clear(); this.IsTts = false; this._mentions.Clear(); this._files.Clear(); this.AttachmentsInternal.Clear(); this._components.Clear(); this.KeepAttachmentsInternal = false; this.ThreadName = null; } /// /// Does the validation before we send a the Create/Modify request. /// /// Tells the method to perform the Modify Validation or Create Validation. /// Tells the method to perform the follow up message validation. /// Tells the method to perform the interaction response validation. internal void Validate(bool isModify = false, bool isFollowup = false, bool isInteractionResponse = false) { if (isModify) { if (this.Username.HasValue) throw new ArgumentException("You cannot change the username of a message."); if (this.AvatarUrl.HasValue) throw new ArgumentException("You cannot change the avatar of a message."); } else if (isFollowup) { if (this.Username.HasValue) throw new ArgumentException("You cannot change the username of a follow up message."); if (this.AvatarUrl.HasValue) throw new ArgumentException("You cannot change the avatar of a follow up message."); } else if (isInteractionResponse) { if (this.Username.HasValue) throw new ArgumentException("You cannot change the username of an interaction response."); if (this.AvatarUrl.HasValue) throw new ArgumentException("You cannot change the avatar of an interaction response."); } else { if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && !this.Embeds.Any() && !this.Components.Any()) throw new ArgumentException("You must specify content, an embed, a component, or at least one file."); } } } diff --git a/DisCatSharp/Exceptions/BadRequestException.cs b/DisCatSharp/Exceptions/BadRequestException.cs index 490842894..b2420d5de 100644 --- a/DisCatSharp/Exceptions/BadRequestException.cs +++ b/DisCatSharp/Exceptions/BadRequestException.cs @@ -1,86 +1,86 @@ // 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 DisCatSharp.Net; using Newtonsoft.Json.Linq; namespace DisCatSharp.Exceptions; /// /// Represents an exception thrown when a malformed request is sent. /// public class BadRequestException : Exception { /// /// Gets the request that caused the exception. /// public BaseRestRequest WebRequest { get; internal set; } /// /// Gets the response to the request. /// public RestResponse WebResponse { get; internal set; } /// /// Gets the error code for this exception. /// public int Code { get; internal set; } /// /// Gets the JSON message received. /// public string JsonMessage { get; internal set; } /// /// Gets the form error responses in JSON format. /// public string Errors { get; internal set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The request. /// The response. internal BadRequestException(BaseRestRequest request, RestResponse response) : base("Bad request: " + response.ResponseCode) { this.WebRequest = request; this.WebResponse = response; try { var j = JObject.Parse(response.Response); if (j["code"] != null) this.Code = (int)j["code"]; if (j["message"] != null) this.JsonMessage = j["message"].ToString(); if (j["errors"] != null) this.Errors = j["errors"].ToString(); } catch { } } } diff --git a/DisCatSharp/Exceptions/NotFoundException.cs b/DisCatSharp/Exceptions/NotFoundException.cs index 76bb50d24..55232657b 100644 --- a/DisCatSharp/Exceptions/NotFoundException.cs +++ b/DisCatSharp/Exceptions/NotFoundException.cs @@ -1,70 +1,70 @@ // 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 DisCatSharp.Net; using Newtonsoft.Json.Linq; namespace DisCatSharp.Exceptions; /// /// Represents an exception thrown when a requested resource is not found. /// public class NotFoundException : Exception { /// /// Gets the request that caused the exception. /// public BaseRestRequest WebRequest { get; internal set; } /// /// Gets the response to the request. /// public RestResponse WebResponse { get; internal set; } /// /// Gets the JSON received. /// public string JsonMessage { get; internal set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The request. /// The response. internal NotFoundException(BaseRestRequest request, RestResponse response) : base("Not found: " + response.ResponseCode) { this.WebRequest = request; this.WebResponse = response; try { var j = JObject.Parse(response.Response); if (j["message"] != null) this.JsonMessage = j["message"].ToString(); } catch (Exception) { } } } diff --git a/DisCatSharp/Exceptions/ServerErrorException.cs b/DisCatSharp/Exceptions/ServerErrorException.cs index 837c1c1e5..f9cf0be55 100644 --- a/DisCatSharp/Exceptions/ServerErrorException.cs +++ b/DisCatSharp/Exceptions/ServerErrorException.cs @@ -1,70 +1,70 @@ // 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 DisCatSharp.Net; using Newtonsoft.Json.Linq; namespace DisCatSharp.Exceptions; /// /// Represents an exception thrown when Discord returns an Internal Server Error. /// public class ServerErrorException : Exception { /// /// Gets the request that caused the exception. /// public BaseRestRequest WebRequest { get; internal set; } /// /// Gets the response to the request. /// public RestResponse WebResponse { get; internal set; } /// /// Gets the JSON received. /// public string JsonMessage { get; internal set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The request. /// The response. internal ServerErrorException(BaseRestRequest request, RestResponse response) : base("Internal Server Error: " + response.ResponseCode) { this.WebRequest = request; this.WebResponse = response; try { var j = JObject.Parse(response.Response); if (j["message"] != null) this.JsonMessage = j["message"].ToString(); } catch (Exception) { } } } diff --git a/DisCatSharp/Exceptions/UnauthorizedException.cs b/DisCatSharp/Exceptions/UnauthorizedException.cs index aaad0898c..95bc91608 100644 --- a/DisCatSharp/Exceptions/UnauthorizedException.cs +++ b/DisCatSharp/Exceptions/UnauthorizedException.cs @@ -1,70 +1,70 @@ // 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 DisCatSharp.Net; using Newtonsoft.Json.Linq; namespace DisCatSharp.Exceptions; /// /// Represents an exception thrown when requester doesn't have necessary permissions to complete the request. /// public class UnauthorizedException : Exception { /// /// Gets the request that caused the exception. /// public BaseRestRequest WebRequest { get; internal set; } /// /// Gets the response to the request. /// public RestResponse WebResponse { get; internal set; } /// /// Gets the JSON received. /// public string JsonMessage { get; internal set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The request. /// The response. internal UnauthorizedException(BaseRestRequest request, RestResponse response) : base("Unauthorized: " + response.ResponseCode) { this.WebRequest = request; this.WebResponse = response; try { var j = JObject.Parse(response.Response); if (j["message"] != null) this.JsonMessage = j["message"].ToString(); } catch (Exception) { } } } diff --git a/DisCatSharp/RingBuffer.cs b/DisCatSharp/RingBuffer.cs index 0e7d74b98..fd0f7addf 100644 --- a/DisCatSharp/RingBuffer.cs +++ b/DisCatSharp/RingBuffer.cs @@ -1,238 +1,238 @@ // 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; using System.Collections.Generic; using System.Linq; using DisCatSharp.Common; namespace DisCatSharp; /// /// A circular buffer collection. /// /// Type of elements within this ring buffer. public class RingBuffer : ICollection { /// /// Gets the current index of the buffer items. /// public int CurrentIndex { get; protected set; } /// /// Gets the capacity of this ring buffer. /// public int Capacity { get; protected set; } /// /// Gets the number of items in this ring buffer. /// public int Count => this._reachedEnd ? this.Capacity : this.CurrentIndex; /// /// Gets whether this ring buffer is read-only. /// public bool IsReadOnly => false; /// /// Gets or sets the internal collection of items. /// protected T[] InternalBuffer { get; set; } private bool _reachedEnd; /// /// Creates a new ring buffer with specified size. /// /// Size of the buffer to create. - /// + /// public RingBuffer(int size) { if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size), "Size must be positive."); this.CurrentIndex = 0; this.Capacity = size; this.InternalBuffer = new T[this.Capacity]; } /// /// Creates a new ring buffer, filled with specified elements. /// /// Elements to fill the buffer with. - /// - /// + /// + /// public RingBuffer(IEnumerable elements) : this(elements, 0) { } /// /// Creates a new ring buffer, filled with specified elements, and starting at specified index. /// /// Elements to fill the buffer with. /// Starting element index. - /// - /// + /// + /// public RingBuffer(IEnumerable elements, int index) { if (elements == null || !elements.Any()) throw new ArgumentException("The collection cannot be null or empty.", nameof(elements)); this.CurrentIndex = index; this.InternalBuffer = elements.ToArray(); this.Capacity = this.InternalBuffer.Length; if (this.CurrentIndex >= this.InternalBuffer.Length || this.CurrentIndex < 0) throw new ArgumentOutOfRangeException(nameof(index), "Index must be less than buffer capacity, and greater than zero."); } /// /// Inserts an item into this ring buffer. /// /// Item to insert. public void Add(T item) { this.InternalBuffer[this.CurrentIndex++] = item; if (this.CurrentIndex == this.Capacity) { this.CurrentIndex = 0; this._reachedEnd = true; } } /// /// Gets first item from the buffer that matches the predicate. /// /// Predicate used to find the item. /// Item that matches the predicate, or default value for the type of the items in this ring buffer, if one is not found. /// Whether an item that matches the predicate was found or not. public bool TryGet(Func predicate, out T item) { for (var i = this.CurrentIndex; i < this.InternalBuffer.Length; i++) { if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) { item = this.InternalBuffer[i]; return true; } } for (var i = 0; i < this.CurrentIndex; i++) { if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) { item = this.InternalBuffer[i]; return true; } } item = default; return false; } /// /// Clears this ring buffer and resets the current item index. /// public void Clear() { this.InternalBuffer.Populate(default); this.CurrentIndex = 0; } /// /// Checks whether given item is present in the buffer. This method is not implemented. Use instead. /// /// Item to check for. /// Whether the buffer contains the item. - /// + /// public bool Contains(T item) => throw new NotImplementedException("This method is not implemented. Use .Contains(predicate) instead."); /// /// Checks whether given item is present in the buffer using given predicate to find it. /// /// Predicate used to check for the item. /// Whether the buffer contains the item. public bool Contains(Func predicate) => this.InternalBuffer.Any(predicate); /// /// Copies this ring buffer to target array, attempting to maintain the order of items within. /// /// Target array. /// Index starting at which to copy the items to. public void CopyTo(T[] array, int index) { if (array.Length - index < 1) throw new ArgumentException("Target array is too small to contain the elements from this buffer.", nameof(array)); var ci = 0; for (var i = this.CurrentIndex; i < this.InternalBuffer.Length; i++) array[ci++] = this.InternalBuffer[i]; for (var i = 0; i < this.CurrentIndex; i++) array[ci++] = this.InternalBuffer[i]; } /// /// Removes an item from the buffer. This method is not implemented. Use instead. /// /// Item to remove. /// Whether an item was removed or not. public bool Remove(T item) => throw new NotImplementedException("This method is not implemented. Use .Remove(predicate) instead."); /// /// Removes an item from the buffer using given predicate to find it. /// /// Predicate used to find the item. /// Whether an item was removed or not. public bool Remove(Func predicate) { for (var i = 0; i < this.InternalBuffer.Length; i++) { if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) { this.InternalBuffer[i] = default; return true; } } return false; } /// /// Returns an enumerator for this ring buffer. /// /// Enumerator for this ring buffer. public IEnumerator GetEnumerator() => !this._reachedEnd ? this.InternalBuffer.AsEnumerable().GetEnumerator() : this.InternalBuffer.Skip(this.CurrentIndex) .Concat(this.InternalBuffer.Take(this.CurrentIndex)) .GetEnumerator(); /// /// Returns an enumerator for this ring buffer. /// /// Enumerator for this ring buffer. IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); } diff --git a/DisCatSharp/Utilities.cs b/DisCatSharp/Utilities.cs index 566a79c00..c6324ec16 100644 --- a/DisCatSharp/Utilities.cs +++ b/DisCatSharp/Utilities.cs @@ -1,470 +1,470 @@ // 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.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.Net; using Microsoft.Extensions.Logging; namespace DisCatSharp; /// /// Various Discord-related utilities. /// public static class Utilities { /// /// Gets the version of the library /// internal static string VersionHeader { get; set; } /// /// Gets or sets the permission strings. /// internal static Dictionary PermissionStrings { get; set; } /// /// Gets the utf8 encoding /// // ReSharper disable once InconsistentNaming internal static UTF8Encoding UTF8 { get; } = new(false); /// /// Initializes a new instance of the class. /// static Utilities() { PermissionStrings = new Dictionary(); var t = typeof(Permissions); var ti = t.GetTypeInfo(); var vals = Enum.GetValues(t).Cast(); foreach (var xv in vals) { var xsv = xv.ToString(); var xmv = ti.DeclaredMembers.FirstOrDefault(xm => xm.Name == xsv); var xav = xmv.GetCustomAttribute(); PermissionStrings[xv] = xav.String; } var a = typeof(DiscordClient).GetTypeInfo().Assembly; var vs = ""; var iv = a.GetCustomAttribute(); if (iv != null) vs = iv.InformationalVersion; else { var v = a.GetName().Version; vs = v.ToString(3); } VersionHeader = $"DiscordBot (https://github.com/Aiko-IT-Systems/DisCatSharp, v{vs})"; } /// /// Gets the api base uri. /// /// The config /// A string. internal static string GetApiBaseUri(DiscordConfiguration config = null) => config == null ? Endpoints.BASE_URI + "9" : config.UseCanary ? Endpoints.CANARY_URI + config.ApiVersion : Endpoints.BASE_URI + config.ApiVersion; /// /// Gets the api uri for. /// /// The path. /// The config /// An Uri. internal static Uri GetApiUriFor(string path, DiscordConfiguration config) => new($"{GetApiBaseUri(config)}{path}"); /// /// Gets the api uri for. /// /// The path. /// The query string. /// The config /// An Uri. internal static Uri GetApiUriFor(string path, string queryString, DiscordConfiguration config) => new($"{GetApiBaseUri(config)}{path}{queryString}"); /// /// Gets the api uri builder for. /// /// The path. /// The config /// A QueryUriBuilder. internal static QueryUriBuilder GetApiUriBuilderFor(string path, DiscordConfiguration config) => new($"{GetApiBaseUri(config)}{path}"); /// /// Gets the formatted token. /// /// The client. /// A string. internal static string GetFormattedToken(BaseDiscordClient client) => GetFormattedToken(client.Configuration); /// /// Gets the formatted token. /// /// The config. /// A string. internal static string GetFormattedToken(DiscordConfiguration config) => config.TokenType switch { TokenType.Bearer => $"Bearer {config.Token}", TokenType.Bot => $"Bot {config.Token}", _ => throw new ArgumentException("Invalid token type specified.", nameof(config)), }; /// /// Gets the base headers. /// /// A Dictionary. internal static Dictionary GetBaseHeaders() => new(); /// /// Gets the user agent. /// /// A string. internal static string GetUserAgent() => VersionHeader; /// /// Contains the user mentions. /// /// The message. /// A bool. internal static bool ContainsUserMentions(string message) { var pattern = @"<@(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the nickname mentions. /// /// The message. /// A bool. internal static bool ContainsNicknameMentions(string message) { var pattern = @"<@!(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the channel mentions. /// /// The message. /// A bool. internal static bool ContainsChannelMentions(string message) { var pattern = @"<#(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the role mentions. /// /// The message. /// A bool. internal static bool ContainsRoleMentions(string message) { var pattern = @"<@&(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the emojis. /// /// The message. /// A bool. internal static bool ContainsEmojis(string message) { var pattern = @""; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Gets the user mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetUserMentions(DiscordMessage message) { var regex = new Regex(@"<@!?(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); return from Match match in matches select ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the role mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetRoleMentions(DiscordMessage message) { var regex = new Regex(@"<@&(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); return from Match match in matches select ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the channel mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetChannelMentions(DiscordMessage message) { var regex = new Regex(@"<#(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); return from Match match in matches select ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the emojis. /// /// The message. /// A list of ulong. internal static IEnumerable GetEmojis(DiscordMessage message) { var regex = new Regex(@"", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); return from Match match in matches select ulong.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); } /// /// Are the valid slash command name. /// /// The name. /// A bool. internal static bool IsValidSlashCommandName(string name) { var regex = new Regex(@"^[\w-]{1,32}$"); return regex.IsMatch(name); } /// /// Checks the thread auto archive duration feature. /// /// The guild. /// The taad. /// A bool. internal static bool CheckThreadAutoArchiveDurationFeature(DiscordGuild guild, ThreadAutoArchiveDuration taad) => true; /// /// Checks the thread private feature. /// /// The guild. /// A bool. internal static bool CheckThreadPrivateFeature(DiscordGuild guild) => guild.PremiumTier.HasFlag(PremiumTier.TierTwo) || guild.Features.CanCreatePrivateThreads; /// /// Have the message intents. /// /// The intents. /// A bool. internal static bool HasMessageIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages); /// /// Have the message intents. /// /// The intents. /// A bool. internal static bool HasMessageContentIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.MessageContent); /// /// Have the reaction intents. /// /// The intents. /// A bool. internal static bool HasReactionIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessageReactions) || intents.HasIntent(DiscordIntents.DirectMessageReactions); /// /// Have the typing intents. /// /// The intents. /// A bool. internal static bool HasTypingIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessageTyping) || intents.HasIntent(DiscordIntents.DirectMessageTyping); // https://discord.com/developers/docs/topics/gateway#sharding-sharding-formula /// /// Gets a shard id from a guild id and total shard count. /// /// The guild id the shard is on. /// The total amount of shards. /// The shard id. public static int GetShardId(ulong guildId, int shardCount) => (int)(guildId >> 22) % shardCount; /// - /// Helper method to create a from Unix time seconds for targets that do not support this natively. + /// Helper method to create a from Unix time seconds for targets that do not support this natively. /// /// Unix time seconds to convert. /// Whether the method should throw on failure. Defaults to true. - /// Calculated . + /// Calculated . public static DateTimeOffset GetDateTimeOffset(long unixTime, bool shouldThrow = true) { try { return DateTimeOffset.FromUnixTimeSeconds(unixTime); } catch (Exception) { if (shouldThrow) throw; return DateTimeOffset.MinValue; } } /// - /// Helper method to create a from Unix time milliseconds for targets that do not support this natively. + /// Helper method to create a from Unix time milliseconds for targets that do not support this natively. /// /// Unix time milliseconds to convert. /// Whether the method should throw on failure. Defaults to true. - /// Calculated . + /// Calculated . public static DateTimeOffset GetDateTimeOffsetFromMilliseconds(long unixTime, bool shouldThrow = true) { try { return DateTimeOffset.FromUnixTimeMilliseconds(unixTime); } catch (Exception) { if (shouldThrow) throw; return DateTimeOffset.MinValue; } } /// - /// Helper method to calculate Unix time seconds from a for targets that do not support this natively. + /// Helper method to calculate Unix time seconds from a for targets that do not support this natively. /// - /// to calculate Unix time for. + /// to calculate Unix time for. /// Calculated Unix time. public static long GetUnixTime(DateTimeOffset dto) => dto.ToUnixTimeMilliseconds(); /// /// Computes a timestamp from a given snowflake. /// /// Snowflake to compute a timestamp from. /// Computed timestamp. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DateTimeOffset GetSnowflakeTime(this ulong snowflake) => DiscordClient.DiscordEpoch.AddMilliseconds(snowflake >> 22); /// /// Converts this into human-readable format. /// /// Permissions enumeration to convert. /// Human-readable permissions. public static string ToPermissionString(this Permissions perm) { if (perm == Permissions.None) return PermissionStrings[perm]; perm &= PermissionMethods.FullPerms; var strs = PermissionStrings .Where(xkvp => xkvp.Key != Permissions.None && (perm & xkvp.Key) == xkvp.Key) .Select(xkvp => xkvp.Value); return string.Join(", ", strs.OrderBy(xs => xs)); } /// /// Checks whether this string contains given characters. /// /// String to check. /// Characters to check for. /// Whether the string contained these characters. public static bool Contains(this string str, params char[] characters) { foreach (var xc in str) if (characters.Contains(xc)) return true; return false; } /// /// Logs the task fault. /// /// The task. /// The logger. /// The level. /// The event id. /// The message. internal static void LogTaskFault(this Task task, ILogger logger, LogLevel level, EventId eventId, string message) { if (task == null) throw new ArgumentNullException(nameof(task)); if (logger == null) return; task.ContinueWith(t => logger.Log(level, eventId, t.Exception, message), TaskContinuationOptions.OnlyOnFaulted); } /// /// Deconstructs the. /// /// The kvp. /// The key. /// The value. internal static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) { key = kvp.Key; value = kvp.Value; } }