diff --git a/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs b/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs index 09916871f..623c545c2 100644 --- a/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs +++ b/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs @@ -1,160 +1,160 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Converters; using DisCatSharp.Entities; using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.CommandsNext { /// /// Represents a delegate for a function that takes a message, and returns the position of the start of command invocation in the message. It has to return -1 if prefix is not present. /// /// It is recommended that helper methods and /// be used internally for checking. Their output can be passed through. /// /// /// Message to check for prefix. /// Position of the command invocation or -1 if not present. public delegate Task PrefixResolverDelegate(DiscordMessage msg); /// /// Represents a configuration for . /// public sealed class CommandsNextConfiguration { /// /// Sets the string prefixes used for commands. /// Defaults to no value (disabled). /// public IEnumerable StringPrefixes { internal get; set; } /// /// Sets the custom prefix resolver used for commands. /// Defaults to none (disabled). /// public PrefixResolverDelegate PrefixResolver { internal get; set; } = null; /// /// Sets whether to allow mentioning the bot to be used as command prefix. /// Defaults to true. /// public bool EnableMentionPrefix { internal get; set; } = true; /// /// Sets whether strings should be matched in a case-sensitive manner. /// This switch affects the behaviour of default prefix resolver, command searching, and argument conversion. /// Defaults to false. /// public bool CaseSensitive { internal get; set; } = false; /// /// Sets whether to enable default help command. /// Disabling this will allow you to make your own help command. /// - /// Modifying default help can be achieved via custom help formatters (see and for more details). + /// Modifying default help can be achieved via custom help formatters (see and for more details). /// It is recommended to use help formatter instead of disabling help. /// /// Defaults to true. /// public bool EnableDefaultHelp { internal get; set; } = true; /// /// Controls whether the default help will be sent via DMs or not. /// Enabling this will make the bot respond with help via direct messages. /// Defaults to false. /// public bool DmHelp { internal get; set; } = false; /// /// Sets the default pre-execution checks for the built-in help command. /// Only applicable if default help is enabled. /// Defaults to null. /// public IEnumerable DefaultHelpChecks { internal get; set; } = null; /// /// Sets whether commands sent via direct messages should be processed. /// Defaults to true. /// public bool EnableDms { internal get; set; } = true; /// /// Sets the service provider for this CommandsNext instance. /// Objects in this provider are used when instantiating command modules. This allows passing data around without resorting to static members. /// Defaults to null. /// public IServiceProvider ServiceProvider { internal get; set; } = new ServiceCollection().BuildServiceProvider(true); /// /// Gets whether any extra arguments passed to commands should be ignored or not. If this is set to false, extra arguments will throw, otherwise they will be ignored. /// Defaults to false. /// public bool IgnoreExtraArguments { internal get; set; } = false; /// /// Gets or sets whether to automatically enable handling commands. /// If this is set to false, you will need to manually handle each incoming message and pass it to CommandsNext. /// Defaults to true. /// public bool UseDefaultCommandHandler { internal get; set; } = true; /// /// Creates a new instance of . /// public CommandsNextConfiguration() { } /// /// Initializes a new instance of the class. /// /// The service provider. [ActivatorUtilitiesConstructor] public CommandsNextConfiguration(IServiceProvider provider) { this.ServiceProvider = provider; } /// /// Creates a new instance of , copying the properties of another configuration. /// /// Configuration the properties of which are to be copied. public CommandsNextConfiguration(CommandsNextConfiguration other) { this.CaseSensitive = other.CaseSensitive; this.PrefixResolver = other.PrefixResolver; this.DefaultHelpChecks = other.DefaultHelpChecks; this.EnableDefaultHelp = other.EnableDefaultHelp; this.EnableDms = other.EnableDms; this.EnableMentionPrefix = other.EnableMentionPrefix; this.IgnoreExtraArguments = other.IgnoreExtraArguments; this.UseDefaultCommandHandler = other.UseDefaultCommandHandler; this.ServiceProvider = other.ServiceProvider; this.StringPrefixes = other.StringPrefixes?.ToArray(); this.DmHelp = other.DmHelp; } } } diff --git a/DisCatSharp.CommandsNext/Converters/TimeConverters.cs b/DisCatSharp.CommandsNext/Converters/TimeConverters.cs index c4675b2c1..7e7c77dfb 100644 --- a/DisCatSharp.CommandsNext/Converters/TimeConverters.cs +++ b/DisCatSharp.CommandsNext/Converters/TimeConverters.cs @@ -1,148 +1,148 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Globalization; using System.Text.RegularExpressions; using System.Threading.Tasks; using DisCatSharp.Entities; namespace DisCatSharp.CommandsNext.Converters { /// /// Represents a date time converter. /// public class DateTimeConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { return DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result) ? Task.FromResult(new Optional(result)) : Task.FromResult(Optional.FromNoValue()); } } /// /// Represents a date time offset converter. /// public class DateTimeOffsetConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { return DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); } } /// /// Represents a time span converter. /// public class TimeSpanConverter : IArgumentConverter { /// /// Gets or sets the time span regex. /// private static Regex TimeSpanRegex { get; set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// static TimeSpanConverter() { #if NETSTANDARD1_3 TimeSpanRegex = new Regex(@"^(?\d+d\s*)?(?\d{1,2}h\s*)?(?\d{1,2}m\s*)?(?\d{1,2}s\s*)?$", RegexOptions.ECMAScript); #else TimeSpanRegex = new Regex(@"^(?\d+d\s*)?(?\d{1,2}h\s*)?(?\d{1,2}m\s*)?(?\d{1,2}s\s*)?$", RegexOptions.ECMAScript | RegexOptions.Compiled); #endif } /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (value == "0") return Task.FromResult(Optional.FromValue(TimeSpan.Zero)); if (int.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out _)) return Task.FromResult(Optional.FromNoValue()); if (!ctx.Config.CaseSensitive) value = value.ToLowerInvariant(); if (TimeSpan.TryParse(value, CultureInfo.InvariantCulture, out var result)) return Task.FromResult(Optional.FromValue(result)); var gps = new string[] { "days", "hours", "minutes", "seconds" }; var mtc = TimeSpanRegex.Match(value); if (!mtc.Success) return Task.FromResult(Optional.FromNoValue()); var d = 0; var h = 0; var m = 0; var s = 0; foreach (var gp in gps) { var gpc = mtc.Groups[gp].Value; if (string.IsNullOrWhiteSpace(gpc)) continue; var gpt = gpc[gpc.Length - 1]; int.TryParse(gpc.Substring(0, gpc.Length - 1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val); switch (gpt) { case 'd': d = val; break; case 'h': h = val; break; case 'm': m = val; break; case 's': s = val; break; } } result = new TimeSpan(d, h, m, s); return Task.FromResult(Optional.FromValue(result)); } } } diff --git a/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs b/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs index 55fcd8beb..b3f04c0c4 100644 --- a/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs +++ b/DisCatSharp.Common/Attributes/DateTimeFormatAttribute.cs @@ -1,133 +1,133 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Globalization; namespace DisCatSharp.Common.Serialization { /// - /// Defines the format for string-serialized and objects. + /// Defines the format for string-serialized and objects. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] public sealed class DateTimeFormatAttribute : SerializationAttribute { /// /// Gets the ISO 8601 format string of "yyyy-MM-ddTHH:mm:ss.fffzzz". /// public const string FormatISO8601 = "yyyy-MM-ddTHH:mm:ss.fffzzz"; /// /// Gets the RFC 1123 format string of "R". /// public const string FormatRFC1123 = "R"; /// /// Gets the general long format. /// public const string FormatLong = "G"; /// /// Gets the general short format. /// public const string FormatShort = "g"; /// /// Gets the custom datetime format string to use. /// public string Format { get; } /// /// Gets the predefined datetime format kind. /// public DateTimeFormatKind Kind { get; } /// /// Specifies a predefined format to use. /// /// Predefined format kind to use. public DateTimeFormatAttribute(DateTimeFormatKind kind) { if (kind < 0 || kind > DateTimeFormatKind.InvariantLocaleShort) throw new ArgumentOutOfRangeException(nameof(kind), "Specified format kind is not legal or supported."); this.Kind = kind; this.Format = null; } /// /// Specifies a custom format to use. /// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings for more details. /// /// Custom format string to use. public DateTimeFormatAttribute(string format) { if (string.IsNullOrWhiteSpace(format)) throw new ArgumentNullException(nameof(format), "Specified format cannot be null or empty."); this.Kind = DateTimeFormatKind.Custom; this.Format = format; } } /// - /// Defines which built-in format to use for for and serialization. + /// Defines which built-in format to use for for and serialization. /// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings and https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings for more details. /// public enum DateTimeFormatKind : int { /// /// Specifies ISO 8601 format, which is equivalent to .NET format string of "yyyy-MM-ddTHH:mm:ss.fffzzz". /// ISO8601 = 0, /// /// Specifies RFC 1123 format, which is equivalent to .NET format string of "R". /// RFC1123 = 1, /// - /// Specifies a format defined by , with a format string of "G". This format is not recommended for portability reasons. + /// 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. + /// 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". + /// Specifies a format defined by , with a format string of "G". /// InvariantLocaleLong = 4, /// - /// Specifies a format defined by , with a format string of "g". + /// 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 cacfe8d67..c97653667 100644 --- a/DisCatSharp.Common/Attributes/TimeSpanAttributes.cs +++ b/DisCatSharp.Common/Attributes/TimeSpanAttributes.cs @@ -1,42 +1,42 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; namespace DisCatSharp.Common.Serialization { /// - /// Specifies that this 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, AllowMultiple = false, Inherited = true)] 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, AllowMultiple = false, Inherited = true)] public sealed class TimeSpanMillisecondsAttribute : SerializationAttribute { } } diff --git a/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs b/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs index e0df68803..78eeb6fc5 100644 --- a/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs +++ b/DisCatSharp.Common/Attributes/TimeSpanFormatAttribute.cs @@ -1,133 +1,133 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Globalization; namespace DisCatSharp.Common.Serialization { /// - /// Defines the format for string-serialized objects. + /// Defines the format for string-serialized objects. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] public sealed class TimeSpanFormatAttribute : SerializationAttribute { /// /// Gets the ISO 8601 format string of @"ddThh\:mm\:ss\.fff". /// public const string FormatISO8601 = @"ddThh\:mm\:ss\.fff"; /// /// Gets the constant format. /// public const string FormatConstant = "c"; /// /// Gets the general long format. /// public const string FormatLong = "G"; /// /// Gets the general short format. /// public const string FormatShort = "g"; /// /// Gets the custom datetime format string to use. /// public string Format { get; } /// /// Gets the predefined datetime format kind. /// public 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". + /// 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. + /// 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. + /// 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. + /// 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. + /// 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 1579de6d5..aadb6bff6 100644 --- a/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs +++ b/DisCatSharp.Common/Attributes/UnixTimestampAttributes.cs @@ -1,42 +1,42 @@ -// This file is part of the DisCatSharp project. +// This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; namespace DisCatSharp.Common.Serialization { /// - /// Specifies that this or will be serialized as Unix timestamp seconds. + /// Specifies that this or will be serialized as Unix timestamp seconds. /// This value will always be serialized as a number. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] public sealed class UnixSecondsAttribute : SerializationAttribute { } /// - /// Specifies that this or will be serialized as Unix timestamp milliseconds. + /// Specifies that this or will be serialized as Unix timestamp milliseconds. /// This value will always be serialized as a number. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] public sealed class UnixMillisecondsAttribute : SerializationAttribute { } } diff --git a/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs b/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs index 06c75336e..40cc56b0a 100644 --- a/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs +++ b/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs @@ -1,830 +1,830 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections; 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; } = 0; /// /// 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 object(); /// /// 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; } #if NETCOREAPP set => this.TryInsertInternal(new string(key), value, true); #else set { unsafe { fixed (char* chars = &key.GetPinnableReference()) this.TryInsertInternal(new string(chars, 0, key.Length), value, true); } } #endif } 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 Dictionary InternalBuckets { get; } /// /// 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) #if NETCOREAPP { if (!this.TryInsertInternal(new string(key), value, false)) throw new ArgumentException("Given key is already present in the dictionary.", nameof(key)); } #else { 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)); } } #endif /// /// 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) #if NETCOREAPP => this.TryInsertInternal(new string(key), value, false); #else { unsafe { fixed (char* chars = &key.GetPinnableReference()) return this.TryInsertInternal(new string(chars, 0, key.Length), value, false); } } #endif /// /// 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 DictionaryEntry(this.Current.Key, this.Current.Value); /// /// Gets the internal dictionary. /// private CharSpanLookupDictionary InternalDictionary { get; } /// /// Gets the internal enumerator. /// private IEnumerator> InternalEnumerator { get; } /// /// Gets or sets the current value. /// private KeyedValue CurrentValue { get; set; } = null; /// /// 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 775141380..8f955eb5d 100644 --- a/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs +++ b/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs @@ -1,417 +1,417 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections; 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 IReadOnlyDictionary InternalBuckets { get; } /// /// 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 CharSpanLookupReadOnlyDictionary InternalDictionary { get; } /// /// Gets the internal enumerator. /// private IEnumerator> InternalEnumerator { get; } /// /// Gets or sets the current value. /// private KeyedValue CurrentValue { get; set; } = null; /// /// 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 e1736cd76..53a9cde41 100644 --- a/DisCatSharp.Common/Types/ContinuousMemoryBuffer.cs +++ b/DisCatSharp.Common/Types/ContinuousMemoryBuffer.cs @@ -1,274 +1,274 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.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.Slice(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); #if HAS_SPAN_STREAM_OVERLOADS stream.Read(this._buff.Slice(this._pos).Span); #else var memo = ArrayPool.Shared.Rent((int)stream.Length); try { var br = stream.Read(memo, 0, memo.Length); memo.AsSpan(0, br).CopyTo(this._buff.Slice(this._pos).Span); } finally { ArrayPool.Shared.Return(memo); } #endif this._pos += (int)stream.Length; } /// /// Writes the stream unseekable. /// /// The stream. private void WriteStreamUnseekable(Stream stream) { #if HAS_SPAN_STREAM_OVERLOADS var br = 0; do { this.EnsureSize(this._pos + 4096); br = stream.Read(this._buff.Slice(this._pos).Span); this._pos += br; } while (br != 0); #else 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.Slice(this._pos).Span); this._pos += br; } } finally { ArrayPool.Shared.Return(memo); } #endif } /// 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.Slice(start, this._pos - start).Span; var dbuff = MemoryMarshal.AsBytes(destination); if (sbuff.Length > dbuff.Length) sbuff = sbuff.Slice(0, 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.Slice(0, this._pos).Span).ToArray(); } /// public void CopyTo(Stream destination) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); #if HAS_SPAN_STREAM_OVERLOADS destination.Write(this._buff.Slice(0, this._pos).Span); #else var buff = this._buff.Slice(0, this._pos).ToArray(); destination.Write(buff, 0, buff.Length); #endif } /// 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 935f7eefa..696c86215 100644 --- a/DisCatSharp.Common/Types/MemoryBuffer.cs +++ b/DisCatSharp.Common/Types/MemoryBuffer.cs @@ -1,356 +1,356 @@ -// This file is part of the DisCatSharp project. +// This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.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 = new List>(initialSegmentCount + 1); for (var i = 0; i < initialSegmentCount; i++) this._segments.Add(this._pool.Rent(this._segmentSize)); 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.Slice(this._lastSegmentLength); src.Slice(0, avs).CopyTo(dmem.Span); src = src.Slice(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); #if !HAS_SPAN_STREAM_OVERLOADS var buff = new byte[this._segmentSize]; #endif 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.Slice(this._lastSegmentLength); #if HAS_SPAN_STREAM_OVERLOADS stream.Read(dmem.Span); #else var lsl = this._lastSegmentLength; var slen = dmem.Span.Length - lsl; stream.Read(buff, 0, slen); buff.AsSpan(0, slen).CopyTo(dmem.Span); #endif 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; #if HAS_SPAN_STREAM_OVERLOADS Span buffs = stackalloc byte[this._segmentSize]; while ((read = stream.Read(buffs)) != 0) #else var buff = new byte[this._segmentSize]; var buffs = buff.AsSpan(); while ((read = stream.Read(buff, 0, buff.Length - this._lastSegmentLength)) != 0) #endif this.Write(MemoryMarshal.Cast(buffs.Slice(0, 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.Slice(sri); sri = 0; } if (itemsWritten + src.Length > dl) src = src.Slice(0, dl - itemsWritten); if (src.Length > dst.Length) src = src.Slice(0, dst.Length); src.CopyTo(dst); dst = dst.Slice(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."); #if HAS_SPAN_STREAM_OVERLOADS foreach (var seg in this._segments) destination.Write(seg.Memory.Span); #else 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); } #endif } /// 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/Utilities/AsyncEvent/AsyncEvent.cs b/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs index 94bf639fb..755d9fa04 100644 --- a/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs +++ b/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs @@ -1,204 +1,204 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.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 alloted 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 object(); 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; for (var i = 0; i < handlers.Length; i++) { var handler = handlers[i]; 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 665c2c837..3ea58eb38 100644 --- a/DisCatSharp.Common/Utilities/AsyncManualResetEvent.cs +++ b/DisCatSharp.Common/Utilities/AsyncManualResetEvent.cs @@ -1,81 +1,81 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.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 13b5a06f9..31b54a8fb 100644 --- a/DisCatSharp.Common/Utilities/Extensions.cs +++ b/DisCatSharp.Common/Utilities/Extensions.cs @@ -1,490 +1,490 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.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 nuembr 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 nuembr 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 nuembr 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 nuembr length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this ulong num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(num)) + 1; /// /// Tests wheter 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 wheter 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 wheter 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 wheter 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 wheter 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 wheter 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 wheter 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 wheter 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 wheter 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 wheter 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[s.Length - 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)); /// /// Firsts the two or default. /// /// The enumerable. /// A (T first, T second) . internal static (T first, T second) FirstTwoOrDefault(this IEnumerable enumerable) { using var enumerator = enumerable.GetEnumerator(); if (!enumerator.MoveNext()) return (default, default); var first = enumerator.Current; if (!enumerator.MoveNext()) return (first, default); return (first, enumerator.Current); } /// /// Knuths the. /// /// The chars. /// An ulong. private static ulong Knuth(ReadOnlySpan chars) { var hash = 3074457345618258791ul; for (var i = 0; i < chars.Length; i++) hash = (hash + chars[i]) * 3074457345618258799ul; return hash; } } } diff --git a/DisCatSharp.Docs/filter_config.yml b/DisCatSharp.Docs/filter_config.yml index 0dee89b81..2dc66c164 100644 --- a/DisCatSharp.Docs/filter_config.yml +++ b/DisCatSharp.Docs/filter_config.yml @@ -1,83 +1,92 @@ apiRules: - exclude: uidRegex: ^System\.Collections\.Immutable$ - exclude: uidRegex: ^System\.Runtime\.CompilerServices\.Unsafe$ - exclude: uidRegex: ^System\.Runtime\.CompilerServices$ - exclude: uidRegex: ^System\..*$ - exclude: uidRegex: ^DisCatSharp\.Hosting\.Tests$ - exclude: uidRegex: ^DisCatSharp\.Configuration\.Tests$ attributeRules: - exclude: uidRegex: ^System\.Collections\.Immutable$ type: Namespace +- exclude: + uidRegex: ^Microsoft\.Extensions\.Logging$ + type: Namespace +- exclude: + uidRegex: ^Microsoft\.Extensions\.Hosting\.BackgroundService$ + type: Namespace +- exclude: + uidRegex: ^Newtonsoft\.Json$ + type: Namespace - exclude: uidRegex: ^System\.Runtime\.CompilerServices\.Unsafe$ type: Namespace - exclude: uidRegex: ^System\.Runtime\.CompilerServices$ type: Namespace - exclude: uidRegex: ^System\.ComponentModel\.Design$ type: Namespace - exclude: uidRegex: ^System\.ComponentModel\.Design\.Serialization$ type: Namespace - exclude: uidRegex: ^System\.Xml\.Serialization$ type: Namespace - exclude: uidRegex: ^System\.Web\.Compilation$ type: Namespace - exclude: uidRegex: ^System\.Runtime\.Versioning$ type: Namespace - exclude: uidRegex: ^System\.Runtime\.ConstrainedExecution$ type: Namespace - exclude: uidRegex: ^System\.EnterpriseServices$ type: Namespace - exclude: uidRegex: ^System\.Diagnostics\.CodeAnalysis$ type: Namespace - include: uidRegex: ^System\.Diagnostics\.(ConditionalAttribute|EventLogPermissionAttribute|PerformanceCounterPermissionAttribute)$ type: Type - exclude: uidRegex: '^System\.Diagnostics\.[^.]+$' type: Type - include: uidRegex: ^System\.ComponentModel\.(BindableAttribute|BrowsableAttribute|ComplexBindingPropertiesAttribute|DataObjectAttribute|DefaultBindingPropertyAttribute|ListBindableAttribute|LookupBindingPropertiesAttribute|SettingsBindableAttribute|TypeConverterAttribute)$ type: Type - exclude: uidRegex: '^System\.ComponentModel\.[^.]+$' type: Type - exclude: uidRegex: ^System\.Reflection\.DefaultMemberAttribute$ type: Type - exclude: uidRegex: ^System\.CodeDom\.Compiler\.GeneratedCodeAttribute$ type: Type - exclude: uidRegex: '^System\.Runtime\.CompilerServices\.[^.]+$' type: Type - exclude: uidRegex: '^System\.Runtime\.InteropServices\.[^.]+$' type: Type - include: uidRegex: ^System\.Security\.(SecurityCriticalAttribute|SecurityTreatAsSafeAttribute|AllowPartiallyTrustedCallersAttribute)$ type: Type - exclude: uidRegex: '^System\.Security\.[^.]+$' type: Type - exclude: uidRegex: '^System\.Web\.UI\.[^.]+$' type: Type - exclude: uidRegex: '^System\.Windows\.Markup\.[^.]+$' type: Type diff --git a/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs b/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs index 2c1f9397f..e66096ef9 100644 --- a/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs +++ b/DisCatSharp.Interactivity/Extensions/MessageExtensions.cs @@ -1,243 +1,243 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using DisCatSharp.Interactivity.EventHandling; namespace DisCatSharp.Interactivity.Extensions { /// /// Interactivity extension methods for . /// public static class MessageExtensions { /// /// Waits for the next message that has the same author and channel as this message. /// /// Original message. /// Overrides the timeout set in public static Task> GetNextMessageAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null) => message.Channel.GetNextMessageAsync(message.Author, timeoutOverride); /// /// Waits for the next message with the same author and channel as this message, which also satisfies a predicate. /// /// Original message. /// A predicate that should return if a message matches. /// Overrides the timeout set in public static Task> GetNextMessageAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => message.Channel.GetNextMessageAsync(msg => msg.Author.Id == message.Author.Id && message.ChannelId == msg.ChannelId && predicate(msg), timeoutOverride); /// /// Waits for any button to be pressed on the specified message. /// /// The message to wait on. public static Task> WaitForButtonAsync(this DiscordMessage message) => GetInteractivity(message).WaitForButtonAsync(message); /// /// Waits for any button to be pressed on the specified message. /// /// The message to wait on. /// Overrides the timeout set in public static Task> WaitForButtonAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForButtonAsync(message, timeoutOverride); /// /// Waits for any button to be pressed on the specified message. /// /// The message to wait on. /// A custom cancellation token that can be cancelled at any point. public static Task> WaitForButtonAsync(this DiscordMessage message, CancellationToken token) => GetInteractivity(message).WaitForButtonAsync(message, token); /// /// Waits for a button with the specified Id to be pressed on the specified message. /// /// The message to wait on. /// The Id of the button to wait for. /// Overrides the timeout set in public static Task> WaitForButtonAsync(this DiscordMessage message, string id, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForButtonAsync(message, id, timeoutOverride); /// /// Waits for a button with the specified Id to be pressed on the specified message. /// /// The message to wait on. /// The Id of the button to wait for. /// A custom cancellation token that can be cancelled at any point. public static Task> WaitForButtonAsync(this DiscordMessage message, string id, CancellationToken token) => GetInteractivity(message).WaitForButtonAsync(message, id, token); /// /// Waits for any button to be pressed on the specified message by the specified user. /// /// The message to wait on. /// The user to wait for button input from. /// Overrides the timeout set in public static Task> WaitForButtonAsync(this DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForButtonAsync(message, user, timeoutOverride); /// /// Waits for any button to be pressed on the specified message by the specified user. /// /// The message to wait on. /// The user to wait for button input from. /// A custom cancellation token that can be cancelled at any point. public static Task> WaitForButtonAsync(this DiscordMessage message, DiscordUser user, CancellationToken token) => GetInteractivity(message).WaitForButtonAsync(message, user, token); /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. /// Override the timeout specified in public static Task> WaitForButtonAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForButtonAsync(message, predicate, timeoutOverride); /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. - /// A token to cancel interactivity with at any time. Pass to wait indefinitely. + /// A token to cancel interactivity with at any time. Pass to wait indefinitely. public static Task> WaitForButtonAsync(this DiscordMessage message, Func predicate, CancellationToken token) => GetInteractivity(message).WaitForButtonAsync(message, predicate, token); /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. /// Override the timeout period specified in . /// Thrown when the message doesn't contain any dropdowns public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => GetInteractivity(message).WaitForSelectAsync(message, predicate, timeoutOverride); /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. - /// A token that can be used to cancel interactivity. Pass to wait indefinitely. + /// 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. 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. 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. 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. 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 460a3836e..fc93fcfaf 100644 --- a/DisCatSharp.Interactivity/InteractivityExtension.cs +++ b/DisCatSharp.Interactivity/InteractivityExtension.cs @@ -1,930 +1,930 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using DisCatSharp.Interactivity.EventHandling; using DisCatSharp.Common.Utilities; using DisCatSharp.Enums; using System.Threading; using System.Globalization; namespace DisCatSharp.Interactivity { /// /// Extension class for DisCatSharp.Interactivity /// public class InteractivityExtension : BaseExtension { #pragma warning disable IDE1006 // Naming Styles /// /// Gets the config. /// internal InteractivityConfiguration Config { get; } private EventWaiter MessageCreatedWaiter; private EventWaiter MessageReactionAddWaiter; private EventWaiter TypingStartWaiter; private EventWaiter ComponentInteractionWaiter; private ComponentEventWaiter ComponentEventWaiter; private ReactionCollector ReactionCollector; private Poller Poller; private Paginator Paginator; private ComponentPaginator _compPaginator; #pragma warning restore IDE1006 // Naming Styles /// /// Initializes a new instance of the class. /// /// The configuration. internal InteractivityExtension(InteractivityConfiguration cfg) { this.Config = new InteractivityConfiguration(cfg); } /// /// Setups the Interactivity Extension. /// /// Discord client. protected internal override void Setup(DiscordClient client) { this.Client = client; this.MessageCreatedWaiter = new EventWaiter(this.Client); this.MessageReactionAddWaiter = new EventWaiter(this.Client); this.ComponentInteractionWaiter = new EventWaiter(this.Client); this.TypingStartWaiter = new EventWaiter(this.Client); this.Poller = new Poller(this.Client); this.ReactionCollector = new ReactionCollector(this.Client); this.Paginator = new Paginator(this.Client); this._compPaginator = new(this.Client, this.Config); this.ComponentEventWaiter = new(this.Client, this.Config); } /// /// Makes a poll and returns poll results. /// /// Message to create poll on. /// Emojis to use for this poll. /// What to do when the poll ends. /// override timeout period. /// public async Task> DoPollAsync(DiscordMessage m, IEnumerable emojis, PollBehaviour? behaviour = default, TimeSpan? timeout = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); if (!emojis.Any()) throw new ArgumentException("You need to provide at least one emoji for a poll!"); foreach (var em in emojis) await m.CreateReactionAsync(em).ConfigureAwait(false); var res = await this.Poller.DoPollAsync(new PollRequest(m, timeout ?? this.Config.Timeout, emojis)).ConfigureAwait(false); var pollbehaviour = behaviour ?? this.Config.PollBehaviour; var thismember = await m.Channel.Guild.GetMemberAsync(this.Client.CurrentUser.Id).ConfigureAwait(false); if (pollbehaviour == PollBehaviour.DeleteEmojis && m.Channel.PermissionsFor(thismember).HasPermission(Permissions.ManageMessages)) await m.DeleteAllReactionsAsync().ConfigureAwait(false); return new ReadOnlyCollection(res.ToList()); } /// /// Waits for any button in the specified collection to be pressed. /// /// The message to wait on. /// A collection of buttons to listen for. /// Override the timeout period in . /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when 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. public async Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!buttons.Any()) throw new ArgumentException("You must specify at least one button to listen for."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Provided Message does not contain any button components."); var res = await this.ComponentEventWaiter .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && buttons.Any(b => b.CustomId == c.Id), token)).ConfigureAwait(false); return new(res is null, res); } /// /// Waits for any button on the specified message to be pressed. /// /// The message to wait for the button on. /// Override the timeout period specified in . /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when 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. public async Task> WaitForButtonAsync(DiscordMessage message, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var ids = message.Components.SelectMany(m => m.Components).Select(c => c.CustomId); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && ids.Contains(c.Id), token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for any button on the specified message to be pressed by the specified user. /// /// The message to wait for the button on. /// The user to wait for the button press from. /// Override the timeout period specified in . /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when 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. public async Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.User == user, token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for a button with the specified Id to be pressed. /// /// The message to wait for the button on. /// The Id of the button to wait for. /// Override the timeout period specified in . /// A with the result of the operation. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when 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. public async Task> WaitForButtonAsync(DiscordMessage message, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); if (!message.Components.SelectMany(c => c.Components).OfType().Any(c => c.CustomId == id)) throw new ArgumentException($"Message does not contain button with Id of '{id}'."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.Id == id, token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. /// Override the timeout specified in public Task> WaitForButtonAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, predicate, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. - /// A token to cancel interactivity with at any time. Pass to wait indefinitely. + /// A token to cancel interactivity with at any time. Pass to wait indefinitely. public async Task> WaitForButtonAsync(DiscordMessage message, Func predicate, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType is ComponentType.Button && predicate(c), token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. /// Override the timeout period specified in . /// Thrown when the Provided message does not contain any dropdowns public Task> WaitForSelectAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, predicate, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. - /// A token that can be used to cancel interactivity. Pass to wait indefinitely. + /// A token that can be used to cancel interactivity. Pass to wait indefinitely. /// Thrown when the Provided message does not contain any dropdowns public async Task> WaitForSelectAsync(DiscordMessage message, Func predicate, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType is ComponentType.Select && predicate(c), token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for a dropdown to be interacted with. /// /// This is here for backwards-compatibility and will internally create a cancellation token. /// The message to wait on. /// The Id of the dropdown to wait on. /// Override the timeout period specified in . /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public Task> WaitForSelectAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, id, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a dropdown to be interacted with. /// /// The message to wait on. /// The Id of the dropdown to wait on. /// A custom cancellation token that can be cancelled at any point. /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public async Task> WaitForSelectAsync(DiscordMessage message, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id)) throw new ArgumentException($"Message does not contain select component with Id of '{id}'."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Select && c.Id == id, token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for a dropdown to be interacted with by a specific user. /// /// The message to wait on. /// The user to wait on. /// The Id of the dropdown to wait on. /// Override the timeout period specified in . /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, user, id, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a dropdown to be interacted with by a specific user. /// /// The message to wait on. /// The user to wait on. /// The Id of the dropdown to wait on. /// A custom cancellation token that can be cancelled at any point. /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public async Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id)) throw new ArgumentException($"Message does not contain select with Id of '{id}'."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, (c) => c.Id == id && c.User == user, token)).ConfigureAwait(false); return new(result is null, result); } /// /// Waits for a specific message. /// /// Predicate to match. /// override timeout period. public async Task> WaitForMessageAsync(Func predicate, TimeSpan? timeoutoverride = null) { if (!Utilities.HasMessageIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No message intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.MessageCreatedWaiter.WaitForMatchAsync(new MatchRequest(x => predicate(x.Message), timeout)).ConfigureAwait(false); return new InteractivityResult(returns == null, returns?.Message); } /// /// Wait for a specific reaction. /// /// Predicate to match. /// override timeout period. public async Task> WaitForReactionAsync(Func predicate, TimeSpan? timeoutoverride = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.MessageReactionAddWaiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Wait for a specific reaction. /// For this Event you need the intent specified in /// /// Message reaction was added to. /// User that made the reaction. /// override timeout period. public async Task> WaitForReactionAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutoverride = null) => await this.WaitForReactionAsync(x => x.User.Id == user.Id && x.Message.Id == message.Id, timeoutoverride).ConfigureAwait(false); /// /// Waits for a specific reaction. /// For this Event you need the intent specified in /// /// Predicate to match. /// Message reaction was added to. /// User that made the reaction. /// override timeout period. public async Task> WaitForReactionAsync(Func predicate, DiscordMessage message, DiscordUser user, TimeSpan? timeoutoverride = null) => await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id && x.Message.Id == message.Id, timeoutoverride).ConfigureAwait(false); /// /// Waits for a specific reaction. /// For this Event you need the intent specified in /// /// predicate to match. /// User that made the reaction. /// Override timeout period. public async Task> WaitForReactionAsync(Func predicate, DiscordUser user, TimeSpan? timeoutoverride = null) => await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id, timeoutoverride).ConfigureAwait(false); /// /// Waits for a user to start typing. /// /// User that starts typing. /// Channel the user is typing in. /// Override timeout period. public async Task> WaitForUserTypingAsync(DiscordUser user, DiscordChannel channel, TimeSpan? timeoutoverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.TypingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.User.Id == user.Id && x.Channel.Id == channel.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Waits for a user to start typing. /// /// User that starts typing. /// Override timeout period. public async Task> WaitForUserTypingAsync(DiscordUser user, TimeSpan? timeoutoverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.TypingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.User.Id == user.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Waits for any user to start typing. /// /// Channel to type in. /// Override timeout period. public async Task> WaitForTypingAsync(DiscordChannel channel, TimeSpan? timeoutoverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.TypingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.Channel.Id == channel.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Collects reactions on a specific message. /// /// Message to collect reactions on. /// Override timeout period. public async Task> CollectReactionsAsync(DiscordMessage m, TimeSpan? timeoutoverride = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var collection = await this.ReactionCollector.CollectAsync(new ReactionCollectRequest(m, timeout)).ConfigureAwait(false); return collection; } /// /// Waits for specific event args to be received. Make sure the appropriate are registered, if needed. /// /// /// The predicate. /// Override timeout period. public async Task> WaitForEventArgsAsync(Func predicate, TimeSpan? timeoutoverride = null) where T : AsyncEventArgs { var timeout = timeoutoverride ?? this.Config.Timeout; using var waiter = new EventWaiter(this.Client); var res = await waiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false); return new InteractivityResult(res == null, res); } /// /// Collects the event arguments. /// /// The predicate. /// Override timeout period. public async Task> CollectEventArgsAsync(Func predicate, TimeSpan? timeoutoverride = null) where T : AsyncEventArgs { var timeout = timeoutoverride ?? this.Config.Timeout; using var waiter = new EventWaiter(this.Client); var res = await waiter.CollectMatchesAsync(new CollectRequest(predicate, timeout)).ConfigureAwait(false); return res; } /// /// Sends a paginated message with buttons. /// /// The channel to send it on. /// User to give control. /// The pages. /// Pagination buttons (pass null to use buttons defined in ). /// Pagination behaviour. /// Deletion behaviour /// A custom cancellation token that can be cancelled at any point. public async Task SendPaginatedMessageAsync( DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) { var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.ButtonBehavior; var bts = buttons ?? this.Config.PaginationButtons; bts = new(bts); if (bhv is PaginationBehaviour.Ignore) { bts.SkipLeft.Disable(); bts.Left.Disable(); } var builder = new DiscordMessageBuilder() .WithContent(pages.First().Content) .WithEmbed(pages.First().Embed) .AddComponents(bts.ButtonArray); var message = await builder.SendAsync(channel).ConfigureAwait(false); var req = new ButtonPaginationRequest(message, user, bhv, del, bts, pages.ToArray(), token == default ? this.GetCancellationToken() : token); await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false); } /// /// Sends a paginated message with buttons. /// /// The channel to send it on. /// User to give control. /// The pages. /// Pagination buttons (pass null to use buttons defined in ). /// Pagination behaviour. /// Deletion behaviour /// Override timeout period. public Task SendPaginatedMessageAsync( DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) => this.SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, this.GetCancellationToken(timeoutoverride)); /// /// Sends the paginated message. /// /// The channel. /// The user. /// The pages. /// The behaviour. /// The deletion. /// The token. /// A Task. public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) => this.SendPaginatedMessageAsync(channel, user, pages, default, behaviour, deletion, token); /// /// Sends the paginated message. /// /// The channel. /// The user. /// The pages. /// The timeoutoverride. /// The behaviour. /// The deletion. /// A Task. public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) => this.SendPaginatedMessageAsync(channel, user, pages, timeoutoverride, behaviour, deletion); /// /// Sends a paginated message. /// For this Event you need the intent specified in /// /// Channel to send paginated message in. /// User to give control. /// Pages. /// Pagination emojis. /// Pagination behaviour (when hitting max and min indices). /// Deletion behaviour. /// Override timeout period. public async Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis, PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutoverride = null) { var builder = new DiscordMessageBuilder() .WithContent(pages.First().Content) .WithEmbed(pages.First().Embed); var m = await builder.SendAsync(channel).ConfigureAwait(false); var timeout = timeoutoverride ?? this.Config.Timeout; var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.PaginationDeletion; var ems = emojis ?? this.Config.PaginationEmojis; var prequest = new PaginationRequest(m, user, bhv, del, ems, timeout, pages.ToArray()); await this.Paginator.DoPaginationAsync(prequest).ConfigureAwait(false); } /// /// Sends a paginated message in response to an interaction. /// /// Pass the interaction directly. Interactivity will ACK it. /// /// /// The interaction to create a response to. /// Whether the response should be ephemeral. /// The user to listen for button presses from. /// The pages to paginate. /// Optional: custom buttons /// Pagination behaviour. /// Deletion behaviour /// A custom cancellation token that can be cancelled at any point. public async Task SendPaginatedResponseAsync(DiscordInteraction interaction, bool ephemeral, DiscordUser user, IEnumerable pages, PaginationButtons buttons = null, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) { var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.ButtonBehavior; var bts = buttons ?? this.Config.PaginationButtons; bts = new(bts); if (bhv is PaginationBehaviour.Ignore) { bts.SkipLeft.Disable(); bts.Left.Disable(); } var builder = new DiscordInteractionResponseBuilder() .WithContent(pages.First().Content) .AddEmbed(pages.First().Embed) .AsEphemeral(ephemeral) .AddComponents(bts.ButtonArray); await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder).ConfigureAwait(false); var message = await interaction.GetOriginalResponseAsync().ConfigureAwait(false); var req = new InteractionPaginationRequest(interaction, message, user, bhv, del, bts, pages, token); await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false); } /// /// Waits for a custom pagination request to finish. /// This does NOT handle removing emojis after finishing for you. /// /// /// public async Task WaitForCustomPaginationAsync(IPaginationRequest request) => await this.Paginator.DoPaginationAsync(request).ConfigureAwait(false); /// /// Waits for custom button-based pagination request to finish. ///
/// This does not invoke . ///
/// The request to wait for. public async Task WaitForCustomComponentPaginationAsync(IPaginationRequest request) => await this._compPaginator.DoPaginationAsync(request).ConfigureAwait(false); /// /// Generates pages from a string, and puts them in message content. /// /// Input string. /// How to split input string. /// public IEnumerable GeneratePagesInContent(string input, SplitType splittype = SplitType.Character) { if (string.IsNullOrEmpty(input)) throw new ArgumentException("You must provide a string that is not null or empty!"); var result = new List(); List split; switch (splittype) { default: case SplitType.Character: split = this.SplitString(input, 500).ToList(); break; case SplitType.Line: var subsplit = input.Split('\n'); split = new List(); var s = ""; for (var i = 0; i < subsplit.Length; i++) { s += subsplit[i]; if (i >= 15 && i % 15 == 0) { split.Add(s); s = ""; } } if (split.All(x => x != s)) split.Add(s); break; } var page = 1; foreach (var s in split) { result.Add(new Page($"Page {page}:\n{s}")); page++; } return result; } /// /// Generates pages from a string, and puts them in message embeds. /// /// Input string. /// How to split input string. /// Base embed for output embeds. /// public IEnumerable GeneratePagesInEmbed(string input, SplitType splittype = SplitType.Character, DiscordEmbedBuilder embedbase = null) { if (string.IsNullOrEmpty(input)) throw new ArgumentException("You must provide a string that is not null or empty!"); var embed = embedbase ?? new DiscordEmbedBuilder(); var result = new List(); List split; switch (splittype) { default: case SplitType.Character: split = this.SplitString(input, 500).ToList(); break; case SplitType.Line: var subsplit = input.Split('\n'); split = new List(); var s = ""; for (var i = 0; i < subsplit.Length; i++) { s += $"{subsplit[i]}\n"; if (i % 15 == 0 && i != 0) { split.Add(s); s = ""; } } if (!split.Any(x => x == s)) split.Add(s); break; } var page = 1; foreach (var s in split) { result.Add(new Page("", new DiscordEmbedBuilder(embed).WithDescription(s).WithFooter($"Page {page}/{split.Count}"))); page++; } return result; } /// /// Splits the string. /// /// The string. /// The chunk size. private List SplitString(string str, int chunkSize) { var res = new List(); var len = str.Length; var i = 0; while (i < len) { var size = Math.Min(len - i, chunkSize); res.Add(str.Substring(i, size)); i += size; } return res; } /// /// Gets the cancellation token. /// /// The timeout. private CancellationToken GetCancellationToken(TimeSpan? timeout = null) => new CancellationTokenSource(timeout ?? this.Config.Timeout).Token; /// /// Handles an invalid interaction. /// /// The interaction. private async Task HandleInvalidInteraction(DiscordInteraction interaction) { var at = this.Config.ResponseBehavior switch { InteractionResponseBehavior.Ack => interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate), InteractionResponseBehavior.Respond => interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new() { Content = this.Config.ResponseMessage, IsEphemeral = true}), InteractionResponseBehavior.Ignore => Task.CompletedTask, _ => throw new ArgumentException("Unknown enum value.") }; await at; } } } diff --git a/DisCatSharp/DiscordConfiguration.cs b/DisCatSharp/DiscordConfiguration.cs index a7d6f85b5..0c3a70b96 100644 --- a/DisCatSharp/DiscordConfiguration.cs +++ b/DisCatSharp/DiscordConfiguration.cs @@ -1,275 +1,275 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Net; 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 9. /// public string ApiVersion { internal get; set; } = "9"; /// /// 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; } = 0; /// /// 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; } = null; /// /// 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; } = false; /// /// 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; } = null; /// /// Sets if the bot's status should show the mobile icon. /// Defaults to false. /// public bool MobileStatus { internal get; set; } = false; /// /// Use canary. /// Defaults to false. /// public bool UseCanary { internal get; set; } = false; /// /// Refresh full guild channel cache. /// Defaults to false. /// public bool AutoRefreshChannelCache { internal get; set; } = false; /// /// Do not use, this is meant for DisCatSharp Devs. /// Defaults to null. /// public string Override { internal get; set; } = null; /// /// Sets the service provider. /// This allows passing data around without resorting to static members. /// Defaults to null. /// 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.AutoRefreshChannelCache = other.AutoRefreshChannelCache; this.ApiVersion = other.ApiVersion; this.ServiceProvider = other.ServiceProvider; this.Override = other.Override; } } } diff --git a/DisCatSharp/EventArgs/Interaction/ContextMenuInteractionCreateEventArgs.cs b/DisCatSharp/EventArgs/Interaction/ContextMenuInteractionCreateEventArgs.cs index c50242a4b..d3b68258b 100644 --- a/DisCatSharp/EventArgs/Interaction/ContextMenuInteractionCreateEventArgs.cs +++ b/DisCatSharp/EventArgs/Interaction/ContextMenuInteractionCreateEventArgs.cs @@ -1,61 +1,61 @@ // This file is part of the DisCatSharp project, a fork of DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using DisCatSharp.Entities; using DisCatSharp.Enums; namespace DisCatSharp.EventArgs { /// /// The context menu interaction create event args. /// public sealed class ContextMenuInteractionCreateEventArgs : InteractionCreateEventArgs { /// - /// The type of context menu that was used. This is never . + /// The type of context menu that was used. This is never . /// public ApplicationCommandType Type { get; internal set; } /// /// The user that invoked this interaction. Can be casted to a member if this was on a guild. /// public DiscordUser User => this.Interaction.User; /// /// The user this interaction targets, if applicable. /// public DiscordUser TargetUser { get; internal set; } /// /// The message this interaction targets, if applicable. /// public DiscordMessage TargetMessage { get; internal set; } /// /// Initializes a new instance of the class. /// /// The provider. public ContextMenuInteractionCreateEventArgs(IServiceProvider provider) : base(provider) { } } }