/// <summary> /// Using the given subcommand, retrieve the parsed subcommand information (such as arguments. NOT the values, just what the argument 'is') /// </summary> /// <param name="subcommand"></param> /// <returns></returns> public ModuleSubcommandInfo?ParseSubcommandInfo(Table subcommand) { if (subcommand == null) { return(null); } var result = new ModuleSubcommandInfo() { Arguments = new List <ModuleArgumentInfo?>(), Description = subcommand.Get(config.DescriptionKey)?.String, FunctionName = subcommand.Get(config.SubcommandFunctionKey)?.String }; //Find the args var subcmdargs = subcommand.Get(config.ArgumentsKey).Table; //Now we can REALLY parse the args! if (subcmdargs != null) { foreach (var arg in subcmdargs.Values.Select(x => x.String)) { if (string.IsNullOrWhiteSpace(arg)) { throw new InvalidOperationException("Argument specifier was the wrong type! It needs to be a string!"); } var argparts = arg.Split("_".ToCharArray(), StringSplitOptions.RemoveEmptyEntries); if (argparts.Length != 2) { throw new InvalidOperationException("Argument specifier not in the right format! name_type"); } //Remember: all we're doing is figuring out the information available from the argument list result.Arguments.Add(new ModuleArgumentInfo() { name = argparts[0], type = argparts[1] }); } } return(result); }
/// <summary> /// Using the given argument information, fill (as in actually mutate) the given list of argument values /// </summary> /// <param name="argumentInfos"></param> /// <param name="arglist"></param> /// <param name="existingArgs"></param> public void ParseArgs(ModuleSubcommandInfo subcommandInfo, string arglist, List <object> existingArgs) { using var search = dbFactory.CreateSearch(); var forcedFinal = false; foreach (var argInfo in subcommandInfo.Arguments) { //forcedFinal is specifically for "freeform" arguments, which MUST come at the end! It just //eats up the rest of the input if (forcedFinal) { throw new InvalidOperationException("No argument can come after a 'freeform' argument!"); } //Kind of wasteful, but just easier to always start with a clean slate after ripping args out arglist = arglist.Trim(); //Matcher function for generic regex matches. Useful for words, users, etc. Action <string, Action <Match> > genericMatch = (r, a) => { var regex = new Regex(r, RegexOptions.Compiled | RegexOptions.IgnoreCase); var match = regex.Match(arglist); //An argument of a given type must ALWAYS be a pure match. if (!match.Success) { throw new RequestException($"Parse error in argument '{argInfo?.name}', not of type '{argInfo?.type}'"); } //Get rid of the argument from the remaining arglist arglist = regex.Replace(arglist, ""); //Try to do whatever the user wanted (probably adding to existingArgs) try { a(match); } catch (Exception ex) { throw new RequestException($"{ex.Message} (Parse error in argument '{argInfo?.name}' of type '{argInfo?.type}')", ex); } }; //Parse arguments differently based on type switch (argInfo?.type) { case "user": genericMatch(@"^([0-9]+)(\([^\s]+\))?", m => { //Check if user exists! var uid = long.Parse(m.Groups[1].Value); //Yes this is BLOCKING, this entire module system is blocking because lua/etc //var users = userSource.SimpleSearchAsync(new UserSearch() { Ids = new List<long>{uid}}).Result; try { var user = search.GetById <UserView>(RequestType.user, uid); } catch (NotFoundException ex) { throw new RequestException($"User not found: {uid}", ex); } existingArgs.Add(uid); }); break; case "int": // This will throw an exception on error, not telling the user much genericMatch(@"^([0-9]+)(\([^\s]+\))?", m => { existingArgs.Add(long.Parse(m.Groups[1].Value)); }); break; case "word": genericMatch(@"^([^\s]+)", m => { existingArgs.Add(m.Groups[1].Value); }); break; case "freeform": existingArgs.Add(arglist); //Just append whatever is left forcedFinal = true; break; default: throw new InvalidOperationException($"Unknown argument type: {argInfo?.type} ({argInfo?.name})"); } } }
/// <summary> /// Run the given argument list (as taken directly from a request) with the given module for the given requester. Parses the arglist, /// finds the module, runs the command and returns the output. Things like sending messages to other users is also performed, but /// against the database and in the background /// </summary> /// <param name="module"></param> /// <param name="arglist"></param> /// <param name="requester"></param> /// <returns></returns> public string RunCommand(string module, string arglist, Requester requester, long parentId = 0) { LoadedModule mod = GetModule(module); if (mod == null) { throw new BadRequestException($"No module with name {module}"); } arglist = arglist?.Trim(); //By DEFAULT, we call the default function with whatever is leftover in the arglist var cmdfuncname = config.DefaultFunction; List <object> scriptArgs = new List <object> { requester.userId }; //Args always includes the calling user first //Can only check for subcommands if there's an argument list! if (arglist != null) { var match = Regex.Match(arglist, @"^\s*(\w+)\s*(.*)$"); //NOTE: Subcommand currently case sensitive! if (match.Success) { var newArglist = match.Groups[2].Value.Trim(); ModuleSubcommandInfo subcommandInfo = null; mod.subcommands.TryGetValue(match.Groups[1].Value, out subcommandInfo); //ParseSubcommandInfo(mod, match.Groups[1].Value); //Special re-check: sometimes, we can have commands that have NO subcommand name, or the "blank" subcommand. Try that one. if (subcommandInfo == null) //subcommandExists == true) { newArglist = arglist; //undo the parsing mod.subcommands.TryGetValue("", out subcommandInfo); //subcommandInfo = ParseSubcommandInfo(mod, ""); } //There is a defined subcommand, which means we may need to parse the input and call //a different function than the default! if (subcommandInfo != null) //subcommandExists == true) //subcommandInfo != null) { arglist = newArglist; cmdfuncname = subcommandInfo.FunctionName; //Arguments were defined! From this point on, we're being VERY strict with parsing! This could throw exceptions! if (subcommandInfo.Arguments != null) { ParseArgs(subcommandInfo, arglist, scriptArgs); } } } } if (!mod.script.Globals.Keys.Any(x => x.String == cmdfuncname)) { throw new BadRequestException($"Command function '{cmdfuncname}' not found in module {module}"); } //Oops, didn't fill up the arglist with anything! Careful, this is dangerous! if (scriptArgs.Count == 1) { scriptArgs.Add(arglist); } //We lock so nobody else can run commands while we're running them. This guarantees thread safety //within the modules so they don't have to worry about it. lock (moduleLocks.GetOrAdd(module, s => new object())) { using (mod.dataConnection = new SqliteConnection(config.ModuleDataConnectionString)) { mod.dataConnection.Open(); mod.currentRequester = requester; mod.currentFunction = cmdfuncname; mod.currentParentId = parentId; mod.currentArgs = arglist; DynValue res = mod.script.Call(mod.script.Globals[cmdfuncname], scriptArgs.ToArray()); return(res.String); } } }