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