diff --git a/DisCatSharp.CommandsNext/CommandsNextUtilities.cs b/DisCatSharp.CommandsNext/CommandsNextUtilities.cs index 7873c9b92..4d04b9874 100644 --- a/DisCatSharp.CommandsNext/CommandsNextUtilities.cs +++ b/DisCatSharp.CommandsNext/CommandsNextUtilities.cs @@ -1,431 +1,432 @@ // 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.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using System.Threading.Tasks; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Converters; using DisCatSharp.Entities; +using DisCatSharp.Common.RegularExpressions; using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.CommandsNext { /// /// Various CommandsNext-related utilities. /// public static class CommandsNextUtilities { /// /// Gets the user regex. /// - private static Regex UserRegex { get; } = new Regex(@"<@\!?(\d+?)> ", RegexOptions.ECMAScript); + private static Regex UserRegex { get; } = DiscordRegEx.User; /// /// Checks whether the message has a specified string prefix. /// /// Message to check. /// String to check for. /// Method of string comparison for the purposes of finding prefixes. /// Positive number if the prefix is present, -1 otherwise. public static int GetStringPrefixLength(this DiscordMessage msg, string str, StringComparison comparisonType = StringComparison.Ordinal) { var content = msg.Content; return str.Length >= content.Length ? -1 : !content.StartsWith(str, comparisonType) ? -1 : str.Length; } /// /// Checks whether the message contains a specified mention prefix. /// /// Message to check. /// User to check for. /// Positive number if the prefix is present, -1 otherwise. public static int GetMentionPrefixLength(this DiscordMessage msg, DiscordUser user) { var content = msg.Content; if (!content.StartsWith("<@")) return -1; var cni = content.IndexOf('>'); if (cni == -1 || content.Length <= cni + 2) return -1; var cnp = content.Substring(0, cni + 2); var m = UserRegex.Match(cnp); if (!m.Success) return -1; var userId = ulong.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture); return user.Id != userId ? -1 : m.Value.Length; } //internal static string ExtractNextArgument(string str, out string remainder) /// /// Extracts the next argument. /// /// The string. /// The start position. internal static string ExtractNextArgument(this string str, ref int startPos) { if (string.IsNullOrWhiteSpace(str)) return null; var inBacktick = false; var inTripleBacktick = false; var inQuote = false; var inEscape = false; var removeIndices = new List(str.Length - startPos); var i = startPos; for (; i < str.Length; i++) if (!char.IsWhiteSpace(str[i])) break; startPos = i; var endPosition = -1; var startPosition = startPos; for (i = startPosition; i < str.Length; i++) { if (char.IsWhiteSpace(str[i]) && !inQuote && !inTripleBacktick && !inBacktick && !inEscape) endPosition = i; if (str[i] == '\\' && str.Length > i + 1) { if (!inEscape && !inBacktick && !inTripleBacktick) { inEscape = true; if (str.IndexOf("\\`", i) == i || str.IndexOf("\\\"", i) == i || str.IndexOf("\\\\", i) == i || (str.Length >= i && char.IsWhiteSpace(str[i + 1]))) removeIndices.Add(i - startPosition); i++; } else if ((inBacktick || inTripleBacktick) && str.IndexOf("\\`", i) == i) { inEscape = true; removeIndices.Add(i - startPosition); i++; } } if (str[i] == '`' && !inEscape) { var tripleBacktick = str.IndexOf("```", i) == i; if (inTripleBacktick && tripleBacktick) { inTripleBacktick = false; i += 2; } else if (!inBacktick && tripleBacktick) { inTripleBacktick = true; i += 2; } if (inBacktick && !tripleBacktick) inBacktick = false; else if (!inTripleBacktick && tripleBacktick) inBacktick = true; } if (str[i] == '"' && !inEscape && !inBacktick && !inTripleBacktick) { removeIndices.Add(i - startPosition); inQuote = !inQuote; } if (inEscape) inEscape = false; if (endPosition != -1) { startPos = endPosition; return startPosition != endPosition ? str.Substring(startPosition, endPosition - startPosition).CleanupString(removeIndices) : null; } } startPos = str.Length; return startPos != startPosition ? str.Substring(startPosition).CleanupString(removeIndices) : null; } /// /// Cleanups the string. /// /// The string. /// The indices. internal static string CleanupString(this string s, IList indices) { if (!indices.Any()) return s; var li = indices.Last(); var ll = 1; for (var x = indices.Count - 2; x >= 0; x--) { if (li - indices[x] == ll) { ll++; continue; } s = s.Remove(li - ll + 1, ll); li = indices[x]; ll = 1; } return s.Remove(li - ll + 1, ll); } #pragma warning disable IDE1006 // Naming Styles /// /// Binds the arguments. /// /// The command context. /// If true, ignore further text in string. internal static async Task BindArguments(CommandContext ctx, bool ignoreSurplus) #pragma warning restore IDE1006 // Naming Styles { var command = ctx.Command; var overload = ctx.Overload; var args = new object[overload.Arguments.Count + 2]; args[1] = ctx; var rawArgumentList = new List(overload.Arguments.Count); var argString = ctx.RawArgumentString; var foundAt = 0; var argValue = ""; for (var i = 0; i < overload.Arguments.Count; i++) { var arg = overload.Arguments[i]; if (arg.IsCatchAll) { if (arg.IsArray) { while (true) { argValue = ExtractNextArgument(argString, ref foundAt); if (argValue == null) break; rawArgumentList.Add(argValue); } break; } else { if (argString == null) break; argValue = argString.Substring(foundAt).Trim(); argValue = argValue == "" ? null : argValue; foundAt = argString.Length; rawArgumentList.Add(argValue); break; } } else { argValue = ExtractNextArgument(argString, ref foundAt); rawArgumentList.Add(argValue); } if (argValue == null && !arg.IsOptional && !arg.IsCatchAll) return new ArgumentBindingResult(new ArgumentException("Not enough arguments supplied to the command.")); else if (argValue == null) rawArgumentList.Add(null); } if (!ignoreSurplus && foundAt < argString.Length) return new ArgumentBindingResult(new ArgumentException("Too many arguments were supplied to this command.")); for (var i = 0; i < overload.Arguments.Count; i++) { var arg = overload.Arguments[i]; if (arg.IsCatchAll && arg.IsArray) { var array = Array.CreateInstance(arg.Type, rawArgumentList.Count - i); var start = i; while (i < rawArgumentList.Count) { try { array.SetValue(await ctx.CommandsNext.ConvertArgument(rawArgumentList[i], ctx, arg.Type).ConfigureAwait(false), i - start); } catch (Exception ex) { return new ArgumentBindingResult(ex); } i++; } args[start + 2] = array; break; } else { try { args[i + 2] = rawArgumentList[i] != null ? await ctx.CommandsNext.ConvertArgument(rawArgumentList[i], ctx, arg.Type).ConfigureAwait(false) : arg.DefaultValue; } catch (Exception ex) { return new ArgumentBindingResult(ex); } } } return new ArgumentBindingResult(args, rawArgumentList); } /// /// Whether this module is a candidate type. /// /// The type. internal static bool IsModuleCandidateType(this Type type) => type.GetTypeInfo().IsModuleCandidateType(); /// /// Whether this module is a candidate type. /// /// The type info. internal static bool IsModuleCandidateType(this TypeInfo ti) { // check if compiler-generated if (ti.GetCustomAttribute(false) != null) return false; // check if derives from the required base class var tmodule = typeof(BaseCommandModule); var timodule = tmodule.GetTypeInfo(); if (!timodule.IsAssignableFrom(ti)) return false; // check if anonymous if (ti.IsGenericType && ti.Name.Contains("AnonymousType") && (ti.Name.StartsWith("<>") || ti.Name.StartsWith("VB$")) && (ti.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic) return false; // check if abstract, static, or not a class if (!ti.IsClass || ti.IsAbstract) return false; // check if delegate type var tdelegate = typeof(Delegate).GetTypeInfo(); if (tdelegate.IsAssignableFrom(ti)) return false; // qualifies if any method or type qualifies return ti.DeclaredMethods.Any(xmi => xmi.IsCommandCandidate(out _)) || ti.DeclaredNestedTypes.Any(xti => xti.IsModuleCandidateType()); } /// /// Whether this is a command candidate. /// /// The method. /// The parameters. internal static bool IsCommandCandidate(this MethodInfo method, out ParameterInfo[] parameters) { parameters = null; // check if exists if (method == null) return false; // check if static, non-public, abstract, a constructor, or a special name if (method.IsStatic || method.IsAbstract || method.IsConstructor || method.IsSpecialName) return false; // check if appropriate return and arguments parameters = method.GetParameters(); if (!parameters.Any() || parameters.First().ParameterType != typeof(CommandContext) || method.ReturnType != typeof(Task)) return false; // qualifies return true; } /// /// Creates the instance. /// /// The type. /// The services provider. internal static object CreateInstance(this Type t, IServiceProvider services) { var ti = t.GetTypeInfo(); var constructors = ti.DeclaredConstructors .Where(xci => xci.IsPublic) .ToArray(); if (constructors.Length != 1) throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor."); var constructor = constructors[0]; var constructorArgs = constructor.GetParameters(); var args = new object[constructorArgs.Length]; if (constructorArgs.Length != 0 && services == null) throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors."); // inject via constructor if (constructorArgs.Length != 0) for (var i = 0; i < args.Length; i++) args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); var moduleInstance = Activator.CreateInstance(t, args); // inject into properties var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic); foreach (var prop in props) { if (prop.GetCustomAttribute() != null) continue; var service = services.GetService(prop.PropertyType); if (service == null) continue; prop.SetValue(moduleInstance, service); } // inject into fields var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic); foreach (var field in fields) { if (field.GetCustomAttribute() != null) continue; var service = services.GetService(field.FieldType); if (service == null) continue; field.SetValue(moduleInstance, service); } return moduleInstance; } } } diff --git a/DisCatSharp.CommandsNext/Converters/EntityConverters.cs b/DisCatSharp.CommandsNext/Converters/EntityConverters.cs index 9590879ea..7f65ec12c 100644 --- a/DisCatSharp.CommandsNext/Converters/EntityConverters.cs +++ b/DisCatSharp.CommandsNext/Converters/EntityConverters.cs @@ -1,641 +1,600 @@ // 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.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using DisCatSharp.Common.RegularExpressions; using DisCatSharp.Entities; namespace DisCatSharp.CommandsNext.Converters { /// /// Represents a discord user converter. /// public class DiscordUserConverter : IArgumentConverter { /// /// Gets the user regex. /// private static Regex UserRegex { get; } /// /// Initializes a new instance of the class. /// static DiscordUserConverter() { -#if NETSTANDARD1_3 - UserRegex = new Regex(@"^<@\!?(\d+?)>$", RegexOptions.ECMAScript); -#else UserRegex = DiscordRegEx.User; -#endif } /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var uid)) { var result = await ctx.Client.GetUserAsync(uid).ConfigureAwait(false); var ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); return ret; } var m = UserRegex.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out uid)) { var result = await ctx.Client.GetUserAsync(uid).ConfigureAwait(false); var ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); return ret; } var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var di = value.IndexOf('#'); var un = di != -1 ? value.Substring(0, di) : value; var dv = di != -1 ? value.Substring(di + 1) : null; var us = ctx.Client.Guilds.Values .SelectMany(xkvp => xkvp.Members.Values) .Where(xm => (cs ? xm.Username : xm.Username.ToLowerInvariant()) == un && ((dv != null && xm.Discriminator == dv) || dv == null)); var usr = us.FirstOrDefault(); return usr != null ? Optional.FromValue(usr) : Optional.FromNoValue(); } } /// /// Represents a discord member converter. /// public class DiscordMemberConverter : IArgumentConverter { /// /// Gets the user regex. /// private static Regex UserRegex { get; } /// /// Initializes a new instance of the class. /// static DiscordMemberConverter() { -#if NETSTANDARD1_3 - UserRegex = new Regex(@"^<@\!?(\d+?)>$", RegexOptions.ECMAScript); -#else UserRegex = DiscordRegEx.User; -#endif } /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ctx.Guild == null) return Optional.FromNoValue(); if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var uid)) { var result = await ctx.Guild.GetMemberAsync(uid).ConfigureAwait(false); var ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); return ret; } var m = UserRegex.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out uid)) { var result = await ctx.Guild.GetMemberAsync(uid).ConfigureAwait(false); var ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); return ret; } var searchResult = await ctx.Guild.SearchMembersAsync(value).ConfigureAwait(false); if (searchResult.Any()) return Optional.FromValue(searchResult.First()); var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var di = value.IndexOf('#'); var un = di != -1 ? value.Substring(0, di) : value; var dv = di != -1 ? value.Substring(di + 1) : null; var us = ctx.Guild.Members.Values .Where(xm => ((cs ? xm.Username : xm.Username.ToLowerInvariant()) == un && ((dv != null && xm.Discriminator == dv) || dv == null)) || (cs ? xm.Nickname : xm.Nickname?.ToLowerInvariant()) == value); var mbr = us.FirstOrDefault(); return mbr != null ? Optional.FromValue(mbr) : Optional.FromNoValue(); } } /// /// Represents a discord channel converter. /// public class DiscordChannelConverter : IArgumentConverter { /// /// Gets the channel regex. /// private static Regex ChannelRegex { get; } /// /// Initializes a new instance of the class. /// static DiscordChannelConverter() { -#if NETSTANDARD1_3 - ChannelRegex = new Regex(@"^<#(\d+)>$", RegexOptions.ECMAScript); -#else ChannelRegex = DiscordRegEx.Channel; -#endif } /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var cid)) { var result = await ctx.Client.GetChannelAsync(cid).ConfigureAwait(false); var ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); return ret; } var m = ChannelRegex.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out cid)) { var result = await ctx.Client.GetChannelAsync(cid).ConfigureAwait(false); var ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); return ret; } var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var chn = ctx.Guild?.Channels.Values.FirstOrDefault(xc => (cs ? xc.Name : xc.Name.ToLowerInvariant()) == value); return chn != null ? Optional.FromValue(chn) : Optional.FromNoValue(); } } /// /// Represents a discord thread channel converter. /// public class DiscordThreadChannelConverter : IArgumentConverter { /// /// Gets the channel regex. /// private static Regex ChannelRegex { get; } /// /// Initializes a new instance of the class. /// static DiscordThreadChannelConverter() { -#if NETSTANDARD1_3 - ChannelRegex = new Regex(@"^<#(\d+)>$", RegexOptions.ECMAScript); -#else ChannelRegex = DiscordRegEx.Channel; -#endif } /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var tid)) { var result = await ctx.Client.GetThreadAsync(tid).ConfigureAwait(false); var ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); return ret; } var m = ChannelRegex.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out tid)) { var result = await ctx.Client.GetThreadAsync(tid).ConfigureAwait(false); var ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); return ret; } var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var tchn = ctx.Guild?.Threads.Values.FirstOrDefault(xc => (cs ? xc.Name : xc.Name.ToLowerInvariant()) == value); return tchn != null ? Optional.FromValue(tchn) : Optional.FromNoValue(); } } /// /// Represents a discord role converter. /// public class DiscordRoleConverter : IArgumentConverter { /// /// Gets the role regex. /// private static Regex RoleRegex { get; } /// /// Initializes a new instance of the class. /// static DiscordRoleConverter() { -#if NETSTANDARD1_3 - RoleRegex = new Regex(@"^<@&(\d+?)>$", RegexOptions.ECMAScript); -#else RoleRegex = DiscordRegEx.Role; -#endif } /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ctx.Guild == null) return Task.FromResult(Optional.FromNoValue()); if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rid)) { var result = ctx.Guild.GetRole(rid); var ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); return Task.FromResult(ret); } var m = RoleRegex.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out rid)) { var result = ctx.Guild.GetRole(rid); var ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); return Task.FromResult(ret); } var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var rol = ctx.Guild.Roles.Values.FirstOrDefault(xr => (cs ? xr.Name : xr.Name.ToLowerInvariant()) == value); return Task.FromResult(rol != null ? Optional.FromValue(rol) : Optional.FromNoValue()); } } /// /// Represents a discord guild converter. /// public class DiscordGuildConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var gid)) { return ctx.Client.Guilds.TryGetValue(gid, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); } var cs = ctx.Config.CaseSensitive; if (!cs) value = value?.ToLowerInvariant(); var gld = ctx.Client.Guilds.Values.FirstOrDefault(xg => (cs ? xg.Name : xg.Name.ToLowerInvariant()) == value); return Task.FromResult(gld != null ? Optional.FromValue(gld) : Optional.FromNoValue()); } } /// /// Represents a discord invite converter. /// public class DiscordInviteConverter : IArgumentConverter { /// /// Gets the invite regex. /// private static Regex InviteRegex { get; } /// /// Initializes a new instance of the class. /// static DiscordInviteConverter() { -#if NETSTANDARD1_3 - InviteRegex = new Regex(@"^(https?:\/\/)?(www\.)?(discord\.(gg|io|me|li)|discordapp\.com\/invite)\/(.+[a-z])$", RegexOptions.ECMAScript); -#else InviteRegex = DiscordRegEx.Invite; -#endif } /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { var m = InviteRegex.Match(value); if (m.Success) { var result = await ctx.Client.GetInviteByCodeAsync(m.Groups[5].Value).ConfigureAwait(false); var ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); return ret; } var cs = ctx.Config.CaseSensitive; if (!cs) value = value?.ToLowerInvariant(); var inv = await ctx.Client.GetInviteByCodeAsync(value); return inv != null ? Optional.FromValue(inv) : Optional.FromNoValue(); } } /// /// Represents a discord message converter. /// public class DiscordMessageConverter : IArgumentConverter { /// /// Gets the message path regex. /// private static Regex MessagePathRegex { get; } /// /// Initializes a new instance of the class. /// static DiscordMessageConverter() { -#if NETSTANDARD1_3 - MessagePathRegex = new Regex(@"^\/channels\/(?(?:\d+|@me))\/(?\d+)\/(?\d+)\/?$", RegexOptions.ECMAScript); -#else MessagePathRegex = DiscordRegEx.MessageLink; -#endif } /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (string.IsNullOrWhiteSpace(value)) return Optional.FromNoValue(); var msguri = value.StartsWith("<") && value.EndsWith(">") ? value.Substring(1, value.Length - 2) : value; ulong mid; if (Uri.TryCreate(msguri, UriKind.Absolute, out var uri)) { if (uri.Host != "discordapp.com" && uri.Host != "discord.com" && !uri.Host.EndsWith(".discordapp.com") && !uri.Host.EndsWith(".discord.com")) return Optional.FromNoValue(); var uripath = MessagePathRegex.Match(uri.AbsolutePath); if (!uripath.Success || !ulong.TryParse(uripath.Groups["channel"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var cid) || !ulong.TryParse(uripath.Groups["message"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out mid)) return Optional.FromNoValue(); var chn = await ctx.Client.GetChannelAsync(cid).ConfigureAwait(false); if (chn == null) return Optional.FromNoValue(); var msg = await chn.GetMessageAsync(mid).ConfigureAwait(false); return msg != null ? Optional.FromValue(msg) : Optional.FromNoValue(); } if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out mid)) { var result = await ctx.Channel.GetMessageAsync(mid).ConfigureAwait(false); return result != null ? Optional.FromValue(result) : Optional.FromNoValue(); } return Optional.FromNoValue(); } } /// /// Represents a discord scheduled event converter. /// public class DiscordScheduledEventConverter : IArgumentConverter { /// /// Gets the event regex. /// private static Regex EventRegex { get; } /// /// Initializes a new instance of the class. /// static DiscordScheduledEventConverter() { -#if NETSTANDARD1_3 - EventRegex = new Regex(@"^\/events\/(?\d+)\/(?\d+)$", RegexOptions.ECMAScript); -#else EventRegex = DiscordRegEx.Event; -#endif } /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (string.IsNullOrWhiteSpace(value)) return Optional.FromNoValue(); var msguri = value.StartsWith("<") && value.EndsWith(">") ? value.Substring(1, value.Length - 2) : value; ulong seid; if (Uri.TryCreate(msguri, UriKind.Absolute, out var uri)) { if (uri.Host != "discordapp.com" && uri.Host != "discord.com" && !uri.Host.EndsWith(".discordapp.com") && !uri.Host.EndsWith(".discord.com")) return Optional.FromNoValue(); var uripath = EventRegex.Match(uri.AbsolutePath); if (!uripath.Success || !ulong.TryParse(uripath.Groups["guild"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var gid) || !ulong.TryParse(uripath.Groups["event"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out seid)) return Optional.FromNoValue(); var guild = await ctx.Client.GetGuildAsync(gid).ConfigureAwait(false); if (guild == null) return Optional.FromNoValue(); var ev = await guild.GetScheduledEventAsync(seid).ConfigureAwait(false); return ev != null ? Optional.FromValue(ev) : Optional.FromNoValue(); } if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out seid)) { var result = await ctx.Guild.GetScheduledEventAsync(seid).ConfigureAwait(false); return result != null ? Optional.FromValue(result) : Optional.FromNoValue(); } return Optional.FromNoValue(); } } /// /// Represents a discord emoji converter. /// public class DiscordEmojiConverter : IArgumentConverter { /// /// Gets the emote regex. /// private static Regex EmoteRegex { get; } /// /// Initializes a new instance of the class. /// static DiscordEmojiConverter() { -#if NETSTANDARD1_3 - EmoteRegex = new Regex(@"^$", RegexOptions.ECMAScript); -#else EmoteRegex = DiscordRegEx.Emoji; -#endif } /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (DiscordEmoji.TryFromUnicode(ctx.Client, value, out var emoji)) { var result = emoji; var ret = Optional.FromValue(result); return Task.FromResult(ret); } var m = EmoteRegex.Match(value); if (m.Success) { var sid = m.Groups["id"].Value; var name = m.Groups["name"].Value; var anim = m.Groups["animated"].Success; return !ulong.TryParse(sid, NumberStyles.Integer, CultureInfo.InvariantCulture, out var id) ? Task.FromResult(Optional.FromNoValue()) : DiscordEmoji.TryFromGuildEmote(ctx.Client, id, out emoji) ? Task.FromResult(Optional.FromValue(emoji)) : Task.FromResult(Optional.FromValue(new DiscordEmoji { Discord = ctx.Client, Id = id, Name = name, IsAnimated = anim, RequiresColons = true, IsManaged = false })); } return Task.FromResult(Optional.FromNoValue()); } } /// /// Represents a discord color converter. /// public class DiscordColorConverter : IArgumentConverter { /// /// Gets the color regex hex. /// private static Regex ColorRegexHex { get; } /// /// Gets the color regex rgb. /// private static Regex ColorRegexRgb { get; } /// /// Initializes a new instance of the class. /// static DiscordColorConverter() { -#if NETSTANDARD1_3 - ColorRegexHex = new Regex(@"^#?([a-fA-F0-9]{6})$", RegexOptions.ECMAScript); - ColorRegexRgb = new Regex(@"^(\d{1,3})\s*?,\s*?(\d{1,3}),\s*?(\d{1,3})$", RegexOptions.ECMAScript); -#else - ColorRegexHex = DiscordRegEx.HexColorString; - ColorRegexRgb = DiscordRegEx.RgbColorString; -#endif + ColorRegexHex = CommonRegEx.HexColorString; + ColorRegexRgb = CommonRegEx.RgbColorString; } /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { var m = ColorRegexHex.Match(value); if (m.Success && int.TryParse(m.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var clr)) return Task.FromResult(Optional.FromValue(clr)); m = ColorRegexRgb.Match(value); if (m.Success) { var p1 = byte.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var r); var p2 = byte.TryParse(m.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var g); var p3 = byte.TryParse(m.Groups[3].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var b); return !(p1 && p2 && p3) ? Task.FromResult(Optional.FromNoValue()) : Task.FromResult(Optional.FromValue(new DiscordColor(r, g, b))); } return Task.FromResult(Optional.FromNoValue()); } } } diff --git a/DisCatSharp.CommandsNext/Converters/TimeConverters.cs b/DisCatSharp.CommandsNext/Converters/TimeConverters.cs index c4675b2c1..f9832adee 100644 --- a/DisCatSharp.CommandsNext/Converters/TimeConverters.cs +++ b/DisCatSharp.CommandsNext/Converters/TimeConverters.cs @@ -1,148 +1,145 @@ // 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; +using DisCatSharp.Common.RegularExpressions; 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. /// 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 + TimeSpanRegex = CommonRegEx.TimeSpan; } /// /// 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/RegularExpressions/CommonRegEx.cs b/DisCatSharp.Common/RegularExpressions/CommonRegEx.cs new file mode 100644 index 000000000..95744fcaf --- /dev/null +++ b/DisCatSharp.Common/RegularExpressions/CommonRegEx.cs @@ -0,0 +1,53 @@ +// 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 System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace DisCatSharp.Common.RegularExpressions +{ + /// + /// Provides common regex. + /// + public static class CommonRegEx + { + /// + /// Represents a hex color string. + /// + public static Regex HexColorString + => new(@"^#?([a-fA-F0-9]{6})$", RegexOptions.ECMAScript | RegexOptions.Compiled); + + /// + /// Represents a rgp color string. + /// + public static Regex RgbColorString + => new(@"^(\d{1,3})\s*?,\s*?(\d{1,3}),\s*?(\d{1,3})$", RegexOptions.ECMAScript | RegexOptions.Compiled); + + /// + /// Represents a timespan. + /// + public static Regex TimeSpan + => new(@"^(?\d+d\s*)?(?\d{1,2}h\s*)?(?\d{1,2}m\s*)?(?\d{1,2}s\s*)?$", RegexOptions.ECMAScript | RegexOptions.Compiled); + } +} diff --git a/DisCatSharp.Common/RegularExpressions/DiscordRegEx.cs b/DisCatSharp.Common/RegularExpressions/DiscordRegEx.cs index 85755045b..8350a3964 100644 --- a/DisCatSharp.Common/RegularExpressions/DiscordRegEx.cs +++ b/DisCatSharp.Common/RegularExpressions/DiscordRegEx.cs @@ -1,137 +1,125 @@ // 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 System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; namespace DisCatSharp.Common.RegularExpressions { /// /// Provides common regex for discord related things. /// public static class DiscordRegEx { /// /// Represents a invite. /// public static Regex Invite => new(@"^(https?:\/\/)?(www\.)?(discord\.(gg|io|me|li)|discordapp\.(com|net)\/invite)\/(.+[a-z])$", RegexOptions.ECMAScript | RegexOptions.Compiled); /// /// Represents a message link. /// public static Regex MessageLink => new(@"^\/channels\/(?(?:\d+|@me))\/(?\d+)\/(?\d+)\/?$", RegexOptions.ECMAScript | RegexOptions.Compiled); /// /// Represents a emoji. /// public static Regex Emoji => new(@"^<(?a)?:(?[a-zA-Z0-9_]+?):(?\d+?)>$", RegexOptions.ECMAScript | RegexOptions.Compiled); /// /// Represents a animated emoji. /// public static Regex AnimatedEmoji => new(@"^<(?a):(?\w{2,32}):(?\d{17,20})>$", RegexOptions.ECMAScript | RegexOptions.Compiled); /// /// Represents a non-animated emoji. /// public static Regex StaticEmoji => new(@"^<:(?\w{2,32}):(?\d{17,20})>$", RegexOptions.ECMAScript | RegexOptions.Compiled); /// /// Represents a timestamp. /// public static Regex Timestamp => new(@"^-?\d{1,13})(:(?