diff --git a/DisCatSharp.CommandsNext/CommandsNextExtension.cs b/DisCatSharp.CommandsNext/CommandsNextExtension.cs index dc519b047..ba53c09bc 100644 --- a/DisCatSharp.CommandsNext/CommandsNextExtension.cs +++ b/DisCatSharp.CommandsNext/CommandsNextExtension.cs @@ -1,1083 +1,1085 @@ // 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.Reflection; using System.Threading.Tasks; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Builders; using DisCatSharp.CommandsNext.Converters; using DisCatSharp.CommandsNext.Entities; using DisCatSharp.CommandsNext.Exceptions; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Common.Utilities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace DisCatSharp.CommandsNext { /// /// This is the class which handles command registration, management, and execution. /// public class CommandsNextExtension : BaseExtension { /// /// Gets the config. /// private CommandsNextConfiguration Config { get; } /// /// Gets the help formatter. /// private HelpFormatterFactory HelpFormatter { get; } /// /// Gets the convert generic. /// private MethodInfo ConvertGeneric { get; } /// /// Gets the user friendly type names. /// private Dictionary UserFriendlyTypeNames { get; } /// /// Gets the argument converters. /// internal Dictionary ArgumentConverters { get; } /// /// Gets the service provider this CommandsNext module was configured with. /// public IServiceProvider Services => this.Config.ServiceProvider; /// /// Initializes a new instance of the class. /// /// The cfg. internal CommandsNextExtension(CommandsNextConfiguration cfg) { this.Config = new CommandsNextConfiguration(cfg); this.TopLevelCommands = new Dictionary(); this._registeredCommandsLazy = new Lazy>(() => new ReadOnlyDictionary(this.TopLevelCommands)); this.HelpFormatter = new HelpFormatterFactory(); this.HelpFormatter.SetFormatterType(); this.ArgumentConverters = new Dictionary { [typeof(string)] = new StringConverter(), [typeof(bool)] = new BoolConverter(), [typeof(sbyte)] = new Int8Converter(), [typeof(byte)] = new Uint8Converter(), [typeof(short)] = new Int16Converter(), [typeof(ushort)] = new Uint16Converter(), [typeof(int)] = new Int32Converter(), [typeof(uint)] = new Uint32Converter(), [typeof(long)] = new Int64Converter(), [typeof(ulong)] = new Uint64Converter(), [typeof(float)] = new Float32Converter(), [typeof(double)] = new Float64Converter(), [typeof(decimal)] = new Float128Converter(), [typeof(DateTime)] = new DateTimeConverter(), [typeof(DateTimeOffset)] = new DateTimeOffsetConverter(), [typeof(TimeSpan)] = new TimeSpanConverter(), [typeof(Uri)] = new UriConverter(), [typeof(DiscordUser)] = new DiscordUserConverter(), [typeof(DiscordMember)] = new DiscordMemberConverter(), [typeof(DiscordRole)] = new DiscordRoleConverter(), [typeof(DiscordChannel)] = new DiscordChannelConverter(), [typeof(DiscordGuild)] = new DiscordGuildConverter(), [typeof(DiscordMessage)] = new DiscordMessageConverter(), [typeof(DiscordEmoji)] = new DiscordEmojiConverter(), [typeof(DiscordThreadChannel)] = new DiscordThreadChannelConverter(), [typeof(DiscordInvite)] = new DiscordInviteConverter(), - [typeof(DiscordColor)] = new DiscordColorConverter() + [typeof(DiscordColor)] = new DiscordColorConverter(), + [typeof(DiscordScheduledEvent)] = new DiscordScheduledEventConverter(), }; this.UserFriendlyTypeNames = new Dictionary() { [typeof(string)] = "string", [typeof(bool)] = "boolean", [typeof(sbyte)] = "signed byte", [typeof(byte)] = "byte", [typeof(short)] = "short", [typeof(ushort)] = "unsigned short", [typeof(int)] = "int", [typeof(uint)] = "unsigned int", [typeof(long)] = "long", [typeof(ulong)] = "unsigned long", [typeof(float)] = "float", [typeof(double)] = "double", [typeof(decimal)] = "decimal", [typeof(DateTime)] = "date and time", [typeof(DateTimeOffset)] = "date and time", [typeof(TimeSpan)] = "time span", [typeof(Uri)] = "URL", [typeof(DiscordUser)] = "user", [typeof(DiscordMember)] = "member", [typeof(DiscordRole)] = "role", [typeof(DiscordChannel)] = "channel", [typeof(DiscordGuild)] = "guild", [typeof(DiscordMessage)] = "message", [typeof(DiscordEmoji)] = "emoji", [typeof(DiscordThreadChannel)] = "thread", [typeof(DiscordInvite)] = "invite", - [typeof(DiscordColor)] = "color" + [typeof(DiscordColor)] = "color", + [typeof(DiscordScheduledEvent)] = "event" }; var ncvt = typeof(NullableConverter<>); var nt = typeof(Nullable<>); var cvts = this.ArgumentConverters.Keys.ToArray(); foreach (var xt in cvts) { var xti = xt.GetTypeInfo(); if (!xti.IsValueType) continue; var xcvt = ncvt.MakeGenericType(xt); var xnt = nt.MakeGenericType(xt); if (this.ArgumentConverters.ContainsKey(xcvt)) continue; var xcv = Activator.CreateInstance(xcvt) as IArgumentConverter; this.ArgumentConverters[xnt] = xcv; this.UserFriendlyTypeNames[xnt] = this.UserFriendlyTypeNames[xt]; } var t = typeof(CommandsNextExtension); var ms = t.GetTypeInfo().DeclaredMethods; var m = ms.FirstOrDefault(xm => xm.Name == "ConvertArgument" && xm.ContainsGenericParameters && !xm.IsStatic && xm.IsPublic); this.ConvertGeneric = m; } /// /// Sets the help formatter to use with the default help command. /// /// Type of the formatter to use. public void SetHelpFormatter() where T : BaseHelpFormatter => this.HelpFormatter.SetFormatterType(); #region DiscordClient Registration /// /// DO NOT USE THIS MANUALLY. /// /// DO NOT USE THIS MANUALLY. /// protected internal override void Setup(DiscordClient client) { if (this.Client != null) throw new InvalidOperationException("What did I tell you?"); this.Client = client; this._executed = new AsyncEvent("COMMAND_EXECUTED", TimeSpan.Zero, this.Client.EventErrorHandler); this._error = new AsyncEvent("COMMAND_ERRORED", TimeSpan.Zero, this.Client.EventErrorHandler); if (this.Config.UseDefaultCommandHandler) this.Client.MessageCreated += this.HandleCommandsAsync; else this.Client.Logger.LogWarning(CommandsNextEvents.Misc, "Not attaching default command handler - if this is intentional, you can ignore this message"); if (this.Config.EnableDefaultHelp) { this.RegisterCommands(typeof(DefaultHelpModule), null, null, out var tcmds); if (this.Config.DefaultHelpChecks != null) { var checks = this.Config.DefaultHelpChecks.ToArray(); for (var i = 0; i < tcmds.Count; i++) tcmds[i].WithExecutionChecks(checks); } if (tcmds != null) foreach (var xc in tcmds) this.AddToCommandDictionary(xc.Build(null)); } } #endregion #region Command Handling /// /// Handles the commands async. /// /// The sender. /// The e. /// A Task. private async Task HandleCommandsAsync(DiscordClient sender, MessageCreateEventArgs e) { if (e.Author.IsBot) // bad bot return; if (!this.Config.EnableDms && e.Channel.IsPrivate) return; var mpos = -1; if (this.Config.EnableMentionPrefix) mpos = e.Message.GetMentionPrefixLength(this.Client.CurrentUser); if (this.Config.StringPrefixes?.Any() == true) foreach (var pfix in this.Config.StringPrefixes) if (mpos == -1 && !string.IsNullOrWhiteSpace(pfix)) mpos = e.Message.GetStringPrefixLength(pfix, this.Config.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); if (mpos == -1 && this.Config.PrefixResolver != null) mpos = await this.Config.PrefixResolver(e.Message).ConfigureAwait(false); if (mpos == -1) return; var pfx = e.Message.Content.Substring(0, mpos); var cnt = e.Message.Content.Substring(mpos); var __ = 0; var fname = cnt.ExtractNextArgument(ref __); var cmd = this.FindCommand(cnt, out var args); var ctx = this.CreateContext(e.Message, pfx, cmd, args); if (cmd == null) { await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = new CommandNotFoundException(fname) }).ConfigureAwait(false); return; } _ = Task.Run(async () => await this.ExecuteCommandAsync(ctx).ConfigureAwait(false)); } /// /// Finds a specified command by its qualified name, then separates arguments. /// /// Qualified name of the command, optionally with arguments. /// Separated arguments. /// Found command or null if none was found. public Command FindCommand(string commandString, out string rawArguments) { rawArguments = null; var ignoreCase = !this.Config.CaseSensitive; var pos = 0; var next = commandString.ExtractNextArgument(ref pos); if (next == null) return null; if (!this.RegisteredCommands.TryGetValue(next, out var cmd)) { if (!ignoreCase) return null; next = next.ToLowerInvariant(); var cmdKvp = this.RegisteredCommands.FirstOrDefault(x => x.Key.ToLowerInvariant() == next); if (cmdKvp.Value == null) return null; cmd = cmdKvp.Value; } if (cmd is not CommandGroup) { rawArguments = commandString.Substring(pos).Trim(); return cmd; } while (cmd is CommandGroup) { var cm2 = cmd as CommandGroup; var oldPos = pos; next = commandString.ExtractNextArgument(ref pos); if (next == null) break; if (ignoreCase) { next = next.ToLowerInvariant(); cmd = cm2.Children.FirstOrDefault(x => x.Name.ToLowerInvariant() == next || x.Aliases?.Any(xx => xx.ToLowerInvariant() == next) == true); } else { cmd = cm2.Children.FirstOrDefault(x => x.Name == next || x.Aliases?.Contains(next) == true); } if (cmd == null) { cmd = cm2; pos = oldPos; break; } } rawArguments = commandString.Substring(pos).Trim(); return cmd; } /// /// Creates a command execution context from specified arguments. /// /// Message to use for context. /// Command prefix, used to execute commands. /// Command to execute. /// Raw arguments to pass to command. /// Created command execution context. public CommandContext CreateContext(DiscordMessage msg, string prefix, Command cmd, string rawArguments = null) { var ctx = new CommandContext { Client = this.Client, Command = cmd, Message = msg, Config = this.Config, RawArgumentString = rawArguments ?? "", Prefix = prefix, CommandsNext = this, Services = this.Services }; if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null)) { var scope = ctx.Services.CreateScope(); ctx.ServiceScopeContext = new CommandContext.ServiceContext(ctx.Services, scope); ctx.Services = scope.ServiceProvider; } return ctx; } /// /// Executes specified command from given context. /// /// Context to execute command from. /// public async Task ExecuteCommandAsync(CommandContext ctx) { try { var cmd = ctx.Command; await this.RunAllChecksAsync(cmd, ctx).ConfigureAwait(false); var res = await cmd.ExecuteAsync(ctx).ConfigureAwait(false); if (res.IsSuccessful) await this._executed.InvokeAsync(this, new CommandExecutionEventArgs(this.Client.ServiceProvider) { Context = res.Context }).ConfigureAwait(false); else await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = res.Context, Exception = res.Exception }).ConfigureAwait(false); } catch (Exception ex) { await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = ex }).ConfigureAwait(false); } finally { if (ctx.ServiceScopeContext.IsInitialized) ctx.ServiceScopeContext.Dispose(); } } /// /// Runs the all checks async. /// /// The cmd. /// The ctx. /// A Task. private async Task RunAllChecksAsync(Command cmd, CommandContext ctx) { if (cmd.Parent != null) await this.RunAllChecksAsync(cmd.Parent, ctx).ConfigureAwait(false); var fchecks = await cmd.RunChecksAsync(ctx, false).ConfigureAwait(false); if (fchecks.Any()) throw new ChecksFailedException(cmd, ctx, fchecks); } #endregion #region Command Registration /// /// Gets a dictionary of registered top-level commands. /// public IReadOnlyDictionary RegisteredCommands => this._registeredCommandsLazy.Value; /// /// Gets or sets the top level commands. /// private Dictionary TopLevelCommands { get; set; } private readonly Lazy> _registeredCommandsLazy; /// /// Registers all commands from a given assembly. The command classes need to be public to be considered for registration. /// /// Assembly to register commands from. public void RegisterCommands(Assembly assembly) { var types = assembly.ExportedTypes.Where(xt => { var xti = xt.GetTypeInfo(); return xti.IsModuleCandidateType() && !xti.IsNested; }); foreach (var xt in types) this.RegisterCommands(xt); } /// /// Registers all commands from a given command class. /// /// Class which holds commands to register. public void RegisterCommands() where T : BaseCommandModule { var t = typeof(T); this.RegisterCommands(t); } /// /// Registers all commands from a given command class. /// /// Type of the class which holds commands to register. public void RegisterCommands(Type t) { if (t == null) throw new ArgumentNullException(nameof(t), "Type cannot be null."); if (!t.IsModuleCandidateType()) throw new ArgumentNullException(nameof(t), "Type must be a class, which cannot be abstract or static."); this.RegisterCommands(t, null, null, out var tempCommands); if (tempCommands != null) foreach (var command in tempCommands) this.AddToCommandDictionary(command.Build(null)); } /// /// Registers the commands. /// /// The type. /// The current parent. /// The inherited checks. /// The found commands. private void RegisterCommands(Type t, CommandGroupBuilder currentParent, IEnumerable inheritedChecks, out List foundCommands) { var ti = t.GetTypeInfo(); var lifespan = ti.GetCustomAttribute(); var moduleLifespan = lifespan != null ? lifespan.Lifespan : ModuleLifespan.Singleton; var module = new CommandModuleBuilder() .WithType(t) .WithLifespan(moduleLifespan) .Build(this.Services); // restrict parent lifespan to more or equally restrictive if (currentParent?.Module is TransientCommandModule && moduleLifespan != ModuleLifespan.Transient) throw new InvalidOperationException("In a transient module, child modules can only be transient."); // check if we are anything var groupBuilder = new CommandGroupBuilder(module); var isModule = false; var moduleAttributes = ti.GetCustomAttributes(); var moduleHidden = false; var moduleChecks = new List(); foreach (var xa in moduleAttributes) { switch (xa) { case GroupAttribute g: isModule = true; var moduleName = g.Name; if (moduleName == null) { moduleName = ti.Name; if (moduleName.EndsWith("Group") && moduleName != "Group") moduleName = moduleName.Substring(0, moduleName.Length - 5); else if (moduleName.EndsWith("Module") && moduleName != "Module") moduleName = moduleName.Substring(0, moduleName.Length - 6); else if (moduleName.EndsWith("Commands") && moduleName != "Commands") moduleName = moduleName.Substring(0, moduleName.Length - 8); } if (!this.Config.CaseSensitive) moduleName = moduleName.ToLowerInvariant(); groupBuilder.WithName(moduleName); if (inheritedChecks != null) foreach (var chk in inheritedChecks) groupBuilder.WithExecutionCheck(chk); foreach (var mi in ti.DeclaredMethods.Where(x => x.IsCommandCandidate(out _) && x.GetCustomAttribute() != null)) groupBuilder.WithOverload(new CommandOverloadBuilder(mi)); break; case AliasesAttribute a: foreach (var xalias in a.Aliases) groupBuilder.WithAlias(this.Config.CaseSensitive ? xalias : xalias.ToLowerInvariant()); break; case HiddenAttribute h: groupBuilder.WithHiddenStatus(true); moduleHidden = true; break; case DescriptionAttribute d: groupBuilder.WithDescription(d.Description); break; case CheckBaseAttribute c: moduleChecks.Add(c); groupBuilder.WithExecutionCheck(c); break; default: groupBuilder.WithCustomAttribute(xa); break; } } if (!isModule) { groupBuilder = null; if (inheritedChecks != null) moduleChecks.AddRange(inheritedChecks); } // candidate methods var methods = ti.DeclaredMethods; var commands = new List(); var commandBuilders = new Dictionary(); foreach (var m in methods) { if (!m.IsCommandCandidate(out _)) continue; var attrs = m.GetCustomAttributes(); if (attrs.FirstOrDefault(xa => xa is CommandAttribute) is not CommandAttribute cattr) continue; var commandName = cattr.Name; if (commandName == null) { commandName = m.Name; if (commandName.EndsWith("Async") && commandName != "Async") commandName = commandName.Substring(0, commandName.Length - 5); } if (!this.Config.CaseSensitive) commandName = commandName.ToLowerInvariant(); if (!commandBuilders.TryGetValue(commandName, out var commandBuilder)) { commandBuilders.Add(commandName, commandBuilder = new CommandBuilder(module).WithName(commandName)); if (!isModule) if (currentParent != null) currentParent.WithChild(commandBuilder); else commands.Add(commandBuilder); else groupBuilder.WithChild(commandBuilder); } commandBuilder.WithOverload(new CommandOverloadBuilder(m)); if (!isModule && moduleChecks.Any()) foreach (var chk in moduleChecks) commandBuilder.WithExecutionCheck(chk); foreach (var xa in attrs) { switch (xa) { case AliasesAttribute a: foreach (var xalias in a.Aliases) commandBuilder.WithAlias(this.Config.CaseSensitive ? xalias : xalias.ToLowerInvariant()); break; case CheckBaseAttribute p: commandBuilder.WithExecutionCheck(p); break; case DescriptionAttribute d: commandBuilder.WithDescription(d.Description); break; case HiddenAttribute h: commandBuilder.WithHiddenStatus(true); break; default: commandBuilder.WithCustomAttribute(xa); break; } } if (!isModule && moduleHidden) commandBuilder.WithHiddenStatus(true); } // candidate types var types = ti.DeclaredNestedTypes .Where(xt => xt.IsModuleCandidateType() && xt.DeclaredConstructors.Any(xc => xc.IsPublic)); foreach (var type in types) { this.RegisterCommands(type.AsType(), groupBuilder, !isModule ? moduleChecks : null, out var tempCommands); if (isModule) foreach (var chk in moduleChecks) groupBuilder.WithExecutionCheck(chk); if (isModule && tempCommands != null) foreach (var xtcmd in tempCommands) groupBuilder.WithChild(xtcmd); else if (tempCommands != null) commands.AddRange(tempCommands); } if (isModule && currentParent == null) commands.Add(groupBuilder); else if (isModule) currentParent.WithChild(groupBuilder); foundCommands = commands; } /// /// Builds and registers all supplied commands. /// /// Commands to build and register. public void RegisterCommands(params CommandBuilder[] cmds) { foreach (var cmd in cmds) this.AddToCommandDictionary(cmd.Build(null)); } /// /// Unregisters specified commands from CommandsNext. /// /// Commands to unregister. public void UnregisterCommands(params Command[] cmds) { if (cmds.Any(x => x.Parent != null)) throw new InvalidOperationException("Cannot unregister nested commands."); var keys = this.RegisteredCommands.Where(x => cmds.Contains(x.Value)).Select(x => x.Key).ToList(); foreach (var key in keys) this.TopLevelCommands.Remove(key); } /// /// Adds the to command dictionary. /// /// The cmd. private void AddToCommandDictionary(Command cmd) { if (cmd.Parent != null) return; if (this.TopLevelCommands.ContainsKey(cmd.Name) || (cmd.Aliases != null && cmd.Aliases.Any(xs => this.TopLevelCommands.ContainsKey(xs)))) throw new DuplicateCommandException(cmd.QualifiedName); this.TopLevelCommands[cmd.Name] = cmd; if (cmd.Aliases != null) foreach (var xs in cmd.Aliases) this.TopLevelCommands[xs] = cmd; } #endregion #region Default Help /// /// Represents the default help module. /// [ModuleLifespan(ModuleLifespan.Transient)] public class DefaultHelpModule : BaseCommandModule { /// /// Defaults the help async. /// /// The ctx. /// The command. /// A Task. [Command("help"), Description("Displays command help.")] public async Task DefaultHelpAsync(CommandContext ctx, [Description("Command to provide help for.")] params string[] command) { var topLevel = ctx.CommandsNext.TopLevelCommands.Values.Distinct(); var helpBuilder = ctx.CommandsNext.HelpFormatter.Create(ctx); if (command != null && command.Any()) { Command cmd = null; var searchIn = topLevel; foreach (var c in command) { if (searchIn == null) { cmd = null; break; } cmd = ctx.Config.CaseSensitive ? searchIn.FirstOrDefault(xc => xc.Name == c || (xc.Aliases != null && xc.Aliases.Contains(c))) : searchIn.FirstOrDefault(xc => xc.Name.ToLowerInvariant() == c.ToLowerInvariant() || (xc.Aliases != null && xc.Aliases.Select(xs => xs.ToLowerInvariant()).Contains(c.ToLowerInvariant()))); if (cmd == null) break; var failedChecks = await cmd.RunChecksAsync(ctx, true).ConfigureAwait(false); if (failedChecks.Any()) throw new ChecksFailedException(cmd, ctx, failedChecks); searchIn = cmd is CommandGroup ? (cmd as CommandGroup).Children : null; } if (cmd == null) throw new CommandNotFoundException(string.Join(" ", command)); helpBuilder.WithCommand(cmd); if (cmd is CommandGroup group) { var commandsToSearch = group.Children.Where(xc => !xc.IsHidden); var eligibleCommands = new List(); foreach (var candidateCommand in commandsToSearch) { if (candidateCommand.ExecutionChecks == null || !candidateCommand.ExecutionChecks.Any()) { eligibleCommands.Add(candidateCommand); continue; } var candidateFailedChecks = await candidateCommand.RunChecksAsync(ctx, true).ConfigureAwait(false); if (!candidateFailedChecks.Any()) eligibleCommands.Add(candidateCommand); } if (eligibleCommands.Any()) helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name)); } } else { var commandsToSearch = topLevel.Where(xc => !xc.IsHidden); var eligibleCommands = new List(); foreach (var sc in commandsToSearch) { if (sc.ExecutionChecks == null || !sc.ExecutionChecks.Any()) { eligibleCommands.Add(sc); continue; } var candidateFailedChecks = await sc.RunChecksAsync(ctx, true).ConfigureAwait(false); if (!candidateFailedChecks.Any()) eligibleCommands.Add(sc); } if (eligibleCommands.Any()) helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name)); } var helpMessage = helpBuilder.Build(); var builder = new DiscordMessageBuilder().WithContent(helpMessage.Content).WithEmbed(helpMessage.Embed); if (!ctx.Config.DmHelp || ctx.Channel is DiscordDmChannel || ctx.Guild == null) await ctx.RespondAsync(builder).ConfigureAwait(false); else await ctx.Member.SendMessageAsync(builder).ConfigureAwait(false); } } #endregion #region Sudo /// /// Creates a fake command context to execute commands with. /// /// The user or member to use as message author. /// The channel the message is supposed to appear from. /// Contents of the message. /// Command prefix, used to execute commands. /// Command to execute. /// Raw arguments to pass to command. /// Created fake context. public CommandContext CreateFakeContext(DiscordUser actor, DiscordChannel channel, string messageContents, string prefix, Command cmd, string rawArguments = null) { var epoch = new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero); var now = DateTimeOffset.UtcNow; var timeSpan = (ulong)(now - epoch).TotalMilliseconds; // create fake message var msg = new DiscordMessage { Discord = this.Client, Author = actor, ChannelId = channel.Id, Content = messageContents, Id = timeSpan << 22, Pinned = false, MentionEveryone = messageContents.Contains("@everyone"), IsTTS = false, _attachments = new List(), _embeds = new List(), TimestampRaw = now.ToString("yyyy-MM-ddTHH:mm:sszzz"), _reactions = new List() }; var mentionedUsers = new List(); var mentionedRoles = msg.Channel.Guild != null ? new List() : null; var mentionedChannels = msg.Channel.Guild != null ? new List() : null; if (!string.IsNullOrWhiteSpace(msg.Content)) { if (msg.Channel.Guild != null) { mentionedUsers = Utilities.GetUserMentions(msg).Select(xid => msg.Channel.Guild._members.TryGetValue(xid, out var member) ? member : null).Cast().ToList(); mentionedRoles = Utilities.GetRoleMentions(msg).Select(xid => msg.Channel.Guild.GetRole(xid)).ToList(); mentionedChannels = Utilities.GetChannelMentions(msg).Select(xid => msg.Channel.Guild.GetChannel(xid)).ToList(); } else { mentionedUsers = Utilities.GetUserMentions(msg).Select(this.Client.GetCachedOrEmptyUserInternal).ToList(); } } msg._mentionedUsers = mentionedUsers; msg._mentionedRoles = mentionedRoles; msg._mentionedChannels = mentionedChannels; var ctx = new CommandContext { Client = this.Client, Command = cmd, Message = msg, Config = this.Config, RawArgumentString = rawArguments ?? "", Prefix = prefix, CommandsNext = this, Services = this.Services }; if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null)) { var scope = ctx.Services.CreateScope(); ctx.ServiceScopeContext = new CommandContext.ServiceContext(ctx.Services, scope); ctx.Services = scope.ServiceProvider; } return ctx; } #endregion #region Type Conversion /// /// Converts a string to specified type. /// /// Type to convert to. /// Value to convert. /// Context in which to convert to. /// Converted object. #pragma warning disable IDE1006 // Naming Styles public async Task ConvertArgument(string value, CommandContext ctx) #pragma warning restore IDE1006 // Naming Styles { var t = typeof(T); if (!this.ArgumentConverters.ContainsKey(t)) throw new ArgumentException("There is no converter specified for given type.", nameof(T)); if (this.ArgumentConverters[t] is not IArgumentConverter cv) throw new ArgumentException("Invalid converter registered for this type.", nameof(T)); var cvr = await cv.ConvertAsync(value, ctx).ConfigureAwait(false); return !cvr.HasValue ? throw new ArgumentException("Could not convert specified value to given type.", nameof(value)) : cvr.Value; } /// /// Converts a string to specified type. /// /// Value to convert. /// Context in which to convert to. /// Type to convert to. /// Converted object. #pragma warning disable IDE1006 // Naming Styles public async Task ConvertArgument(string value, CommandContext ctx, Type type) #pragma warning restore IDE1006 // Naming Styles { var m = this.ConvertGeneric.MakeGenericMethod(type); try { return await (m.Invoke(this, new object[] { value, ctx }) as Task).ConfigureAwait(false); } catch (TargetInvocationException ex) { throw ex.InnerException; } } /// /// Registers an argument converter for specified type. /// /// Type for which to register the converter. /// Converter to register. public void RegisterConverter(IArgumentConverter converter) { if (converter == null) throw new ArgumentNullException(nameof(converter), "Converter cannot be null."); var t = typeof(T); var ti = t.GetTypeInfo(); this.ArgumentConverters[t] = converter; if (!ti.IsValueType) return; var nullableConverterType = typeof(NullableConverter<>).MakeGenericType(t); var nullableType = typeof(Nullable<>).MakeGenericType(t); if (this.ArgumentConverters.ContainsKey(nullableType)) return; var nullableConverter = Activator.CreateInstance(nullableConverterType) as IArgumentConverter; this.ArgumentConverters[nullableType] = nullableConverter; } /// /// Unregisters an argument converter for specified type. /// /// Type for which to unregister the converter. public void UnregisterConverter() { var t = typeof(T); var ti = t.GetTypeInfo(); if (this.ArgumentConverters.ContainsKey(t)) this.ArgumentConverters.Remove(t); if (this.UserFriendlyTypeNames.ContainsKey(t)) this.UserFriendlyTypeNames.Remove(t); if (!ti.IsValueType) return; var nullableType = typeof(Nullable<>).MakeGenericType(t); if (!this.ArgumentConverters.ContainsKey(nullableType)) return; this.ArgumentConverters.Remove(nullableType); this.UserFriendlyTypeNames.Remove(nullableType); } /// /// Registers a user-friendly type name. /// /// Type to register the name for. /// Name to register. public void RegisterUserFriendlyTypeName(string value) { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value), "Name cannot be null or empty."); var t = typeof(T); var ti = t.GetTypeInfo(); if (!this.ArgumentConverters.ContainsKey(t)) throw new InvalidOperationException("Cannot register a friendly name for a type which has no associated converter."); this.UserFriendlyTypeNames[t] = value; if (!ti.IsValueType) return; var nullableType = typeof(Nullable<>).MakeGenericType(t); this.UserFriendlyTypeNames[nullableType] = value; } /// /// Converts a type into user-friendly type name. /// /// Type to convert. /// User-friendly type name. public string GetUserFriendlyTypeName(Type t) { if (this.UserFriendlyTypeNames.ContainsKey(t)) return this.UserFriendlyTypeNames[t]; var ti = t.GetTypeInfo(); if (ti.IsGenericTypeDefinition && t.GetGenericTypeDefinition() == typeof(Nullable<>)) { var tn = ti.GenericTypeArguments[0]; return this.UserFriendlyTypeNames.ContainsKey(tn) ? this.UserFriendlyTypeNames[tn] : tn.Name; } return t.Name; } #endregion #region Helpers /// /// Gets the configuration-specific string comparer. This returns or , /// depending on whether is set to or . /// /// A string comparer. internal IEqualityComparer GetStringComparer() => this.Config.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; #endregion #region Events /// /// Triggered whenever a command executes successfully. /// public event AsyncEventHandler CommandExecuted { add { this._executed.Register(value); } remove { this._executed.Unregister(value); } } private AsyncEvent _executed; /// /// Triggered whenever a command throws an exception during execution. /// public event AsyncEventHandler CommandErrored { add { this._error.Register(value); } remove { this._error.Unregister(value); } } private AsyncEvent _error; /// /// Ons the command executed. /// /// The e. /// A Task. private Task OnCommandExecuted(CommandExecutionEventArgs e) => this._executed.InvokeAsync(this, e); /// /// Ons the command errored. /// /// The e. /// A Task. private Task OnCommandErrored(CommandErrorEventArgs e) => this._error.InvokeAsync(this, e); #endregion } } 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 669686b2a..7f65ec12c 100644 --- a/DisCatSharp.CommandsNext/Converters/EntityConverters.cs +++ b/DisCatSharp.CommandsNext/Converters/EntityConverters.cs @@ -1,578 +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.Collections.Generic; 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 = new Regex(@"^<@\!?(\d+?)>$", RegexOptions.ECMAScript | RegexOptions.Compiled); -#endif + UserRegex = DiscordRegEx.User; } /// /// 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 = new Regex(@"^<@\!?(\d+?)>$", RegexOptions.ECMAScript | RegexOptions.Compiled); -#endif + UserRegex = DiscordRegEx.User; } /// /// 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 = new Regex(@"^<#(\d+)>$", RegexOptions.ECMAScript | RegexOptions.Compiled); -#endif + ChannelRegex = DiscordRegEx.Channel; } /// /// 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 = new Regex(@"^<#(\d+)>$", RegexOptions.ECMAScript | RegexOptions.Compiled); -#endif + ChannelRegex = DiscordRegEx.Channel; } /// /// 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 = new Regex(@"^<@&(\d+?)>$", RegexOptions.ECMAScript | RegexOptions.Compiled); -#endif + RoleRegex = DiscordRegEx.Role; } /// /// 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 = new Regex(@"^(https?:\/\/)?(www\.)?(discord\.(gg|io|me|li)|discordapp\.com\/invite)\/(.+[a-z])$", RegexOptions.ECMAScript | RegexOptions.Compiled); -#endif + InviteRegex = DiscordRegEx.Invite; } /// /// 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 = new Regex(@"^\/channels\/(?(?:\d+|@me))\/(?\d+)\/(?\d+)\/?$", RegexOptions.ECMAScript | RegexOptions.Compiled); -#endif + MessagePathRegex = DiscordRegEx.MessageLink; } /// /// 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() + { + EventRegex = DiscordRegEx.Event; + } + + /// + /// 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 = new Regex(@"^<(?a)?:(?[a-zA-Z0-9_]+?):(?\d+?)>$", RegexOptions.ECMAScript | RegexOptions.Compiled); -#endif + EmoteRegex = DiscordRegEx.Emoji; } /// /// 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 = new Regex(@"^#?([a-fA-F0-9]{6})$", RegexOptions.ECMAScript | RegexOptions.Compiled); - ColorRegexRgb = new Regex(@"^(\d{1,3})\s*?,\s*?(\d{1,3}),\s*?(\d{1,3})$", RegexOptions.ECMAScript | RegexOptions.Compiled); -#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.CommandsNext/DisCatSharp.CommandsNext.csproj b/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj index cc520bb74..3fd81da16 100644 --- a/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj +++ b/DisCatSharp.CommandsNext/DisCatSharp.CommandsNext.csproj @@ -1,43 +1,44 @@ DisCatSharp.CommandsNext DisCatSharp.CommandsNext Library netstandard2.0 DisCatSharp.CommandsNext CommandNext extension for DisCatSharp. discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, commands, commandsnext LICENSE.md + True 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 new file mode 100644 index 000000000..8350a3964 --- /dev/null +++ b/DisCatSharp.Common/RegularExpressions/DiscordRegEx.cs @@ -0,0 +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})(:(?