/// <summary> /// Executes a line of the script. /// </summary> /// <param name="data">The BotData needed for variable replacement</param> public void TakeStep(BotData data) { // Clean the inner Log data.LogBuffer.Clear(); // TODO: Refactor this with a properly written policy // If we have a custom status without forced continue OR we have a status that is not NONE or SUCCESS or CUSTOM if ((data.Status == BotStatus.CUSTOM && !data.ConfigSettings.ContinueOnCustom) || (data.Status != BotStatus.NONE && data.Status != BotStatus.SUCCESS && data.Status != BotStatus.CUSTOM)) { i = lines.Count(); // Go to the end return; } TAKELINE: CurrentLine = lines[i]; Line = 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()) { //SelectLine?.Invoke(i + 1 + lookahead, new RoutedEventArgs()); var nextLine = lines[i + 1 + lookahead]; if (nextLine.StartsWith(" ") || nextLine.StartsWith("\t")) { CurrentLine += $" {nextLine.Trim()}"; } else { break; } lookahead++; } bool LSBlock = false; 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) { try { if (File.Exists("Log Exception.txt")) { File.WriteAllText("Log Exception.txt", ex.ToString().ToBase64() + "/"); } } catch { } // 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; LSBlock = true; 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)); try { jsFilePath = LineParser.ParseLiteral(ref cfLine, "PATH"); } catch { jsFilePath = string.Empty; } 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 != string.Empty || jsFilePath != string.Empty) { RunScript(otherScript, language, outputs, data, jsFilePath); } } 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 {(LSBlock ? i++ : i + 1)}: {e.Message}"); } // Catch inner and throw line exception i += 1 + lookahead; }