using IronPython.Compiler; using IronPython.Hosting; using IronPython.Runtime; using Jint; using RuriLib.Models; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Windows.Media; namespace RuriLib.LS { /// /// The supported scripting languages. /// public enum ScriptingLanguage { /// The JavaScript language. JavaScript, /// The Python language that interacts with the .NET framework. IronPython } /// /// Represents a LoliScript script that can be run line by line. /// public class LoliScript { /// The actual script as a string containing linebreaks. public string Script { get; set; } /// The list of all lines of the script, where expanded blocks have been compressed into one-liners. private string[] CompressedLines { get { int i = 0; bool isScript = false; var compressed = Script.Split(new string[] { Environment.NewLine }, StringSplitOptions.None).ToList(); while (i < compressed.Count - 1) { if (!isScript && BlockParser.IsBlock(compressed[i]) && (compressed[i + 1].StartsWith(" ") || compressed[i + 1].StartsWith("\t"))) { compressed[i] += $" {compressed[i + 1].Trim()}"; compressed.RemoveAt(i + 1); } else if (!isScript && BlockParser.IsBlock(compressed[i]) && (compressed[i + 1].StartsWith("! ") || compressed[i + 1].StartsWith("!\t"))) { compressed[i] += $" {compressed[i + 1].Substring(1).Trim()}"; compressed.RemoveAt(i + 1); } else { if (compressed[i].StartsWith("BEGIN SCRIPT")) isScript = true; else if (compressed[i].StartsWith("END SCRIPT")) isScript = false; i++; } } return compressed.ToArray(); } } // Runtime counter private int i = 0; private string[] lines = new string[] { }; // Needed for BEGIN SCRIPT directives private string otherScript = ""; private ScriptingLanguage language = ScriptingLanguage.JavaScript; /// The current line being processed. public string CurrentLine { get; set; } = ""; /// The next block to be processed. Empty if the script has no more blocks to execute. public string NextBlock { get { for (int j = i; j < lines.Count(); j++) { var line = lines[j]; if (IsEmptyOrCommentOrDisabled(line) || !BlockParser.IsBlock(line)) continue; var label = ""; if (lines[j].StartsWith("#")) label = LineParser.ParseLabel(ref line); var blockName = LineParser.ParseToken(ref line, TokenType.Parameter, false, false); if (label != "") return $"{blockName} ({label})"; else return blockName; } return ""; } } /// The current block being processed. public string CurrentBlock { get; set; } = ""; /// Whether the script can proceed the execution or not. public bool CanProceed { get { return i < lines.Count() && lines.Skip(i).Any(l => !IsEmptyOrCommentOrDisabled(l)); } } /// /// Constructs a LoliScript object with an empty script. /// public LoliScript() { Script = ""; } /// /// Constructs a LoliScript object with a given script. /// /// The LoliScript script public LoliScript(string script) { Script = script; } #region Conversion /// /// Transforms the script into a list of blocks. The blocks that cannot be converted will be created as BlockLSCode blocks. /// /// public List ToBlocks() { List list = new List(); var compressed = CompressedLines; List buffer = new List(); var isScript = false; foreach (var c in compressed.Where(c => !string.IsNullOrEmpty(c.Trim()))) { if (!isScript && BlockParser.IsBlock(c)) { if (buffer.Count > 0) { var b = new BlockLSCode(); list.Add(b.FromLS(buffer)); buffer.Clear(); } try { list.Add(BlockParser.Parse(c)); } catch (Exception ex) { var line = c; line = BlockBase.TruncatePretty(line, 50); throw new Exception($"Exception while parsing block {line}\nReason: {ex.Message}"); } } else { buffer.Add(c); if (c.StartsWith("BEGIN SCRIPT")) isScript = true; else if (c.StartsWith("END SCRIPT")) isScript = false; } } if (buffer.Count > 0) { var b = new BlockLSCode(); list.Add(b.FromLS(buffer)); buffer.Clear(); } return list; } /// /// Sets the script from a list of blocks. /// /// The list of blocks which inherit from the BlockBase type public void FromBlocks(List blocks) { Script = ""; foreach (var block in blocks) { Script += block.ToLS() + Environment.NewLine + Environment.NewLine; } } #endregion /// /// Resets the line counter so the script can be run again. /// public void Reset() { i = 0; otherScript = ""; language = ScriptingLanguage.JavaScript; // Separate the script into lines lines = Regex.Split(Script, "\r\n|\r|\n"); } /// /// Executes a line of the script. /// /// The BotData needed for variable replacement public void TakeStep(BotData data) { // Clean the inner Log data.LogBuffer.Clear(); if (data.Status != BotStatus.NONE && data.Status != BotStatus.SUCCESS) { i = lines.Count(); // Go to the end return; } TAKELINE: CurrentLine = lines[i]; // Skip comments and blank lines if (IsEmptyOrCommentOrDisabled(CurrentLine)) { i++; // Go to the next goto TAKELINE; } // Lookahead to compact lines. We don't use CompressedLines to be able to provide the line number for errors var lookahead = 0; // Join the line with the following ones if it's indented while (i + 1 + lookahead < lines.Count()) { var nextLine = lines[i + 1 + lookahead]; if (nextLine.StartsWith(" ") || nextLine.StartsWith("\t")) CurrentLine += $" {nextLine.Trim()}"; else break; lookahead++; } try { // If Block -> Process Block if (BlockParser.IsBlock(CurrentLine)) { BlockBase block = null; try { block = BlockParser.Parse(CurrentLine); CurrentBlock = block.Label; if (!block.Disabled) block.Process(data); } catch (Exception ex) { // We log the error message data.LogBuffer.Add(new LogEntry("ERROR: " + ex.Message, Colors.Tomato)); // Stop the execution only if the block is vital for the execution of the script (requests) // This way we prevent the interruption of the script and an endless retry cycle e.g. if we fail to parse a response given a specific input if (block != null && ( block.GetType() == typeof(BlockRequest) || block.GetType() == typeof(BlockBypassCF) || block.GetType() == typeof(BlockImageCaptcha) || block.GetType() == typeof(BlockRecaptcha))) { data.Status = BotStatus.ERROR; throw new BlockProcessingException(ex.Message); } } } // If Command -> Process Command else if (CommandParser.IsCommand(CurrentLine)) { try { var action = CommandParser.Parse(CurrentLine, data); action?.Invoke(); } catch (Exception ex) { data.LogBuffer.Add(new LogEntry("ERROR: " + ex.Message, Colors.Tomato)); data.Status = BotStatus.ERROR; } } // Try to Process Flow Control else { var cfLine = CurrentLine; var token = LineParser.ParseToken(ref cfLine, TokenType.Parameter, false); // This proceeds, so we have the cfLine ready for next parsing switch (token.ToUpper()) { case "IF": // Check condition, if not true jump to line after first ELSE or ENDIF (check both at the same time on lines, not separately) if (!ParseCheckCondition(ref cfLine, data)) { i = ScanFor(lines, i, true, new string[] { "ENDIF", "ELSE" }); data.LogBuffer.Add(new LogEntry($"Jumping to line {i + 1}", Colors.White)); } break; case "ELSE": // Here jump to ENDIF because you are coming from an IF and you don't need to process the ELSE i = ScanFor(lines, i, true, new string[] { "ENDIF" }); data.LogBuffer.Add(new LogEntry($"Jumping to line {i + 1}", Colors.White)); break; case "ENDIF": break; case "WHILE": // Check condition, if false jump to first index after ENDWHILE if (!ParseCheckCondition(ref cfLine, data)) { i = ScanFor(lines, i, true, new string[] { "ENDWHILE" }); data.LogBuffer.Add(new LogEntry($"Jumping to line {i + 1}", Colors.White)); } break; case "ENDWHILE": // Jump back to the previous WHILE index i = ScanFor(lines, i, false, new string[] { "WHILE" }) - 1; data.LogBuffer.Add(new LogEntry($"Jumping to line {i + 1}", Colors.White)); break; case "JUMP": var label = ""; try { label = LineParser.ParseToken(ref cfLine, TokenType.Label, true); i = ScanFor(lines, -1, true, new string[] { $"{label}" }) - 1; data.LogBuffer.Add(new LogEntry($"Jumping to line {i + 2}", Colors.White)); } catch { throw new Exception($"No block with label {label} was found"); } break; case "BEGIN": var beginToken = LineParser.ParseToken(ref cfLine, TokenType.Parameter, true); switch (beginToken.ToUpper()) { case "SCRIPT": language = (ScriptingLanguage)LineParser.ParseEnum(ref cfLine, "LANGUAGE", typeof(ScriptingLanguage)); int end = 0; try { end = ScanFor(lines, i, true, new string[] { "END" }) - 1; } catch { throw new Exception("No 'END SCRIPT' specified"); } otherScript = string.Join(Environment.NewLine, lines.Skip(i + 1).Take(end - i)); i = end; data.LogBuffer.Add(new LogEntry($"Jumping to line {i + 2}", Colors.White)); break; } break; case "END": var endToken = LineParser.ParseToken(ref cfLine, TokenType.Parameter, true); switch (endToken.ToUpper()) { case "SCRIPT": LineParser.EnsureIdentifier(ref cfLine, "->"); LineParser.EnsureIdentifier(ref cfLine, "VARS"); var outputs = LineParser.ParseLiteral(ref cfLine, "OUTPUTS"); try { if (otherScript != "") RunScript(otherScript, language, outputs, data); } catch (Exception ex) { data.LogBuffer.Add(new LogEntry($"The script failed to be executed: {ex.Message}", Colors.Tomato)); } break; } break; default: break; } } } catch (BlockProcessingException) { throw; } // Rethrow the Block Processing Exception so the error can be displayed in the view above catch (Exception e) { throw new Exception($"Parsing Exception on line {i + 1}: {e.Message}"); } // Catch inner and throw line exception i += 1 + lookahead; } /// /// Tests if a line is empty, a comment (starting with ##) or if it's disabled (starting with !). /// /// The line to test. /// Whether the line needs to be skipped private static bool IsEmptyOrCommentOrDisabled(string line) { try { return line.Trim() == "" || line.StartsWith("##") || line.StartsWith("!"); } catch { return true; } } /// /// Scans for a given set of identifiers in the script and returns the line index value of the first found. /// /// The lines of the script /// The index of the current line /// Whether to scan downwards or upwards /// The target identifiers /// public static int ScanFor(string[] lines, int current, bool downwards, string[] options) { var i = downwards ? current + 1 : current - 1; var found = false; while (i >= 0 && i < lines.Count()) { try { var token = LineParser.ParseToken(ref lines[i], TokenType.Parameter, false, false); if (options.Any(o => token.ToUpper() == o.ToUpper())) { found = true; break; } } catch { } if (downwards) i++; else i--; } if (found) return i; else throw new Exception("Not found"); } /// /// Parses a condition made of left-hand term, condition type and right-hand term and verifies if it's true. /// /// The reference to the line to parse /// The BotData needed for variable replacement /// public static bool ParseCheckCondition(ref string cfLine, BotData data) { var first = LineParser.ParseLiteral(ref cfLine, "STRING"); var condition = (Condition)LineParser.ParseEnum(ref cfLine, "CONDITION", typeof(Condition)); var second = ""; if (condition != Condition.Exists && condition != Condition.DoesNotExist) second = LineParser.ParseLiteral(ref cfLine, "STRING"); return (ConditionChecker.Verify(first, condition, second, data)); } /// /// Runs a script with a different language inside the LoliScript. /// /// The script as a string with linebreaks /// The language of the script /// The variables that should be extracted from the script's scope and set into the BotData local variables /// The BotData needed for variable replacement private void RunScript(string script, ScriptingLanguage language, string outputs, BotData data) { // Set the console output to stringwriter var sw = new StringWriter(); Console.SetOut(sw); Console.SetError(sw); // Parse variables to get out List outVarList = new List(); if (outputs != "") { try { outVarList = outputs.Split(',').Select(x => x.Trim()).ToList(); } catch { } } var start = DateTime.Now; try { switch (language) { case ScriptingLanguage.JavaScript: // Redefine log() function var jsengine = new Engine().SetValue("log", new Action(Console.WriteLine)); // Add in all the variables foreach (var variable in data.Variables.All) { try { jsengine.SetValue(variable.Name, variable.Value); } catch { } } // Execute JS jsengine.Execute(script); // Print results to log data.Log(new LogEntry($"DEBUG LOG: {sw.ToString()}", Colors.White)); // Get variables out data.Log(new LogEntry($"Parsing {outVarList.Count} variables", Colors.White)); foreach (var name in outVarList) { try { // Add it to the variables and print info var value = jsengine.Global.GetProperty(name).Value; var isArray = value.IsArray(); if (isArray) data.Variables.Set(new CVar(name, CVar.VarType.List, value.TryCast>())); else data.Variables.Set(new CVar(name, CVar.VarType.Single, value.ToString())); data.Log(new LogEntry($"SET VARIABLE {name} WITH VALUE {value.ToString()}", Colors.Yellow)); } catch { data.Log(new LogEntry($"COULD NOT FIND VARIABLE {name}", Colors.Tomato)); } } // Print other info if (jsengine.GetCompletionValue() != null) data.Log(new LogEntry($"Completion value: {jsengine.GetCompletionValue()}", Colors.White)); break; case ScriptingLanguage.IronPython: // Initialize the engine var runtime = Python.CreateRuntime(); var pyengine = runtime.GetEngine("py"); PythonCompilerOptions pco = (PythonCompilerOptions)pyengine.GetCompilerOptions(); pco.Module &= ~ModuleOptions.Optimized; //var pyengine = Python.CreateEngine(); var scope = pyengine.CreateScope(); var code = pyengine.CreateScriptSourceFromString(script); // Add in all the variables foreach (var variable in data.Variables.All) try { scope.SetVariable(variable.Name, variable.Value); } catch { } // Execute it var result = code.Execute(scope); //var result = pyengine.Execute(script, scope); // Print the logs data.Log(new LogEntry($"DEBUG LOG: {sw.ToString()}", Colors.White)); // Get variables out data.Log(new LogEntry($"Parsing {outVarList.Count} variables", Colors.White)); foreach (var name in outVarList) { try { // Add it to the variables and print info var value = scope.GetVariable(name); data.Variables.Set(new CVar(name, (value.GetType() == typeof(string[])) ? CVar.VarType.List : CVar.VarType.Single, value.ToString())); data.Log(new LogEntry($"SET VARIABLE {name} WITH VALUE {value}", Colors.Yellow)); } catch { data.Log(new LogEntry($"COULD NOT FIND VARIABLE {name}", Colors.Tomato)); } } // Print other info if (result != null) data.Log(new LogEntry($"Completion value: {result}", Colors.White)); break; default: break; } data.Log(new LogEntry($"Execution completed in {(DateTime.Now - start).TotalSeconds} seconds", Colors.GreenYellow)); } catch (Exception e) { data.Log(new LogEntry($"[ERROR] INFO: {e.Message}", Colors.White)); } finally { var standardOutput = new StreamWriter(Console.OpenStandardOutput()); var standardError = new StreamWriter(Console.OpenStandardError()); standardOutput.AutoFlush = true; standardError.AutoFlush = true; Console.SetOut(standardOutput); Console.SetError(standardError); } } } }