UNPKG

bot-script

Version:

Scripting tool to Write Bot's Workflow

742 lines (701 loc) 21.6 kB
/** * Created by agnibha on 12/1/17. */ var path = require("path"); var fs = require("fs"); // var newLineChar = require('os').EOL; var newLineChar = "\n"; var USER = "user"; var BOT = "bot"; var COMMON = "common"; var PERSISTENT_MENU = "persistentMenu"; var COMMON_LIKE_SECTIONS = [ COMMON, PERSISTENT_MENU ]; var CALL = "call"; var GOTO = "goto"; var RETURN = "return"; var DONE = "done"; var CONTINUE = "continue"; var DEFEX = "defex"; var DELAY = "delay"; var ON_EXCEPTION = "onException"; var SMALLTALK_OFF = "smallTalkOff"; var action_regex = /(:(((goto\s+|call\s+\w+\.)\w+)|continue|return|defex|onException|smallTalkOff)\s*$)/; var label_regex = /^[a-zA-Z]+\w+:/; var indentation_regex = /^\s*/; var component_regex = /^\s*(\[)(\w+)(])\s*$/; var placeholder_regex = /\{\{([\w]*)(\:)?(.*?)\}\}/; var comment_regex = /^#(.)*/; var event_regex = /^<(.*?)>/; var delay_regex = /^\(\(delay\s+(\d+)\)\)$/; var tabs_regex = /\t/g; var goto_regex = /goto\s+(\w+)/; var qr_md_regex = /(.*?)\[\[(.+?)]]/; var call_regex = /call\s+(\w+\.\w+)/; var scr_extension_regex = /\.scr$/; var js_extension_regex = /\.js$/; var FILE = "file"; var EXECUTOR = "executor"; var default_tabs_to_space = 4; var cachedScript = undefined; var cachedExecutors = undefined; var qr_template = { type : "quick_reply", content: {type: "text", text: ""}, msgid : "", options: [] }; var cached_refmsgid_map = {}; function parseLine(index, line) { var line_desc = {}; // This is a bug fix to show \n as intended not \n the character line = line.split("\\n").join("\n"); if ( line.match(comment_regex) != null ) { line_desc.type = "COMMENT"; line_desc.message = line; return line_desc; } if ( line.trim().length == 0 ) { line_desc.type = "BLANK"; return line_desc; } line_desc.type = "LINE"; if ( line.match(component_regex) != null && line.match(component_regex).length > 0 ) { line_desc.isComponent = true; line_desc.component = line.match(component_regex)[ 2 ]; } else { line_desc.isComponent = false; var indentation = line.match(indentation_regex)[ 0 ]; line_desc.indentation = indentation.replace( tabs_regex, Array(default_tabs_to_space + 1).join(" ") ).length; line = line.replace(indentation_regex, ""); if ( line.match(action_regex) != null && line.match(action_regex).length > 0 ) { //has an action in the line. var action = line.match(action_regex)[ 2 ].trim(); if ( action.match(goto_regex) != null ) { line_desc.action = new Action(GOTO, line.match(goto_regex)[ 1 ], null); } else if ( action.match(call_regex) != null ) { line_desc.action = new Action(CALL, null, line.match(call_regex)[ 1 ]); } else { if ( action.match(CONTINUE) ) { line_desc.action = new Action(CONTINUE, null, null); } else if ( action.match(RETURN) ) { line_desc.action = new Action(RETURN, null, null); } else if ( action.match(DEFEX) ) { line_desc.action = new Action(DEFEX, null, null); } else if ( action.match(ON_EXCEPTION) ) { line_desc.action = new Action(ON_EXCEPTION, null, null); } else if ( action.match(SMALLTALK_OFF) ) { line_desc.action = new Action(SMALLTALK_OFF, null, null); } } line = line.replace(action_regex, ""); } else { line_desc.action = new Action(DONE, null, null); } if ( line.match(label_regex) != null && line.match(label_regex).length == 1 ) { line_desc.label = line.match(label_regex)[ 0 ].replace(":", ""); line = line.replace(label_regex, ""); line_desc.default_label = false; } else { line_desc.label = "default_".concat(index); // Adding Default Label line_desc.default_label = true; } line_desc.hasPlaceHolder = line.match(placeholder_regex) != null; //removing excess whitespace chars because of nlp check. line = line.trim(); if ( line.length > 0 ) { if ( !line.match(delay_regex) ) { if ( line.match(event_regex) ) { line_desc.eventType = line.match(event_regex)[ 1 ]; line = line.replace(event_regex, ""); } if ( line.match(qr_md_regex) !== null ) { var new_qr = Object.assign({}, qr_template); new_qr.content.text = line.match(qr_md_regex)[ 1 ]; new_qr.options = line.match(qr_md_regex)[ 2 ].split(","); line_desc.message = JSON.stringify(new_qr); line_desc.message_type = "JSON"; } else { line_desc.message = line; line_desc.message_type = tryParseJSON(line) ? "JSON" : "TEXT"; } } else { var delayTime = line.match(delay_regex)[ 1 ]; line_desc.message = ""; line_desc.message_type = "TEXT"; line_desc.action = new Action(DELAY, null, null, delayTime); } } else { line_desc.message_type = "TEXT"; line_desc.message = line; } } return line_desc; } function ScriptLoader(options, current_dir) { if ( !cachedScript && !cachedExecutors ) { if ( !current_dir ) { current_dir = __dirname; } var files = []; var executors = {}; loadEntities(current_dir, files, null, FILE); loadEntities(current_dir, files, executors, EXECUTOR); options.executors = executors; parse(files, options); } else { console.log("Serving from the cache"); options.success(cachedScript, cachedExecutors, cached_refmsgid_map); } } function loadEntities(dirName, scriptFiles, executors, type) { var files = fs.readdirSync(dirName); for ( var index = 0; index < files.length; index++ ) { var data = files[ index ]; if ( fs.statSync(dirName.concat("/", data)).isDirectory() ) { loadEntities(dirName.concat("/", data), scriptFiles, executors, type); } else { switch ( type ) { case FILE: if ( data.match(scr_extension_regex) != null ) { scriptFiles.push({ filename: data, location: dirName.replace(__dirname, "") }); } break; case EXECUTOR: if ( data.match(js_extension_regex) != null ) { for ( var property in scriptFiles ) { if ( scriptFiles.hasOwnProperty(property) ) { if ( scriptFiles[ property ].filename === data.replace(js_extension_regex, "").concat(".scr") ) { var file_name = data.replace(js_extension_regex, ""); executors[ file_name ] = { filename: file_name, location: dirName.replace(__dirname, ".") }; } } } } break; } } } } function parse(files, options) { var file_meta = files.pop(); if ( !options.script ) { options.script = {}; } fs.readFile( path.join(file_meta.location, file_meta.filename), "utf8", function (err, data) { try { if ( err ) { if ( options.error ) { options.error(err); } else { console.error(err); } } parseScript( file_meta.filename.replace(scr_extension_regex, ""), data.split(newLineChar), options.script ); if ( files.length == 0 ) { colorStates(options.script); var scriptValidator = new ScriptValidator(); if ( options.DEBUG && options.DEBUG === 1 ) { console.log("Script => " + JSON.stringify(options.script)); } if ( scriptValidator.validate(options.script, options) ) { cachedScript = options.script; cachedExecutors = options.executors; options.success( options.script, options.executors, cached_refmsgid_map ); } } else { parse(files, options); } } catch ( e ) { if ( options.error ) { options.error(e); } else { console.error(e); } } } ); } function parseScript(file_name, lines, script) { var parents = []; var prev_indentation; var previous_state; var current_component; var current_parent; var isCommon = false; var isPersistentMenu = false; for ( var index = 0; index < lines.length; index++ ) { current_parent = null; if ( lines[ index ].length > 0 ) { var lineDesc = parseLine(index, lines[ index ]); if ( !lineDesc ) { continue; } if ( lineDesc.type !== "LINE" ) { if ( current_component ) { script[ file_name + "." + current_component ][ "default_" + index ] = new State(lineDesc); } continue; } lineDesc.index = index; if ( lineDesc.isComponent ) { isCommon = lineDesc.component === COMMON; isPersistentMenu = lineDesc.component === PERSISTENT_MENU; current_component = lineDesc.component; script[ file_name + "." + current_component ] = {}; parents = []; prev_indentation = -1; previous_state = null; } if ( !current_component ) { throw new Error( "Component not found. You Should Start With a componenet. Please look into file => " + file_name.concat(".scr") ); } else if ( !lineDesc.isComponent ) { var state; if ( prev_indentation == -1 ) { prev_indentation = lineDesc.indentation; state = new State(lineDesc); state.isCommon = isCommon; state.isPersistentMenu = isPersistentMenu; state.isStartState = true; } else { var parent; do { parent = parents.pop(); } while ( parent && parent.indentation >= lineDesc.indentation ); if ( parent ) { script[ file_name + "." + current_component ][ parent.label ].nextstates.push(lineDesc.label); parents.push(parent); lineDesc.parent_label = parent.label; } prev_indentation = lineDesc.indentation; state = new State(lineDesc); state.isCommon = isCommon; state.isStartState = false; state.isPersistentMenu = isPersistentMenu; state.parent_label = lineDesc.parent_label; } if ( lineDesc.message_type === "JSON" ) { var structuredMessage = JSON.parse(lineDesc.message); if ( structuredMessage.msgid ) { var msgid = structuredMessage.msgid; if ( !cached_refmsgid_map.hasOwnProperty(msgid) ) { cached_refmsgid_map[ msgid ] = { state : state, section: file_name + "." + current_component }; } else { var cachedState = cached_refmsgid_map[ msgid ].state; console.error( "Same msgid found at " + (state.default_label ? "line " + (state.index + 1) : "state " + state.label) + " & at " + (cachedState.default_label ? "line " + (cachedState.index + 1) : "state " + cachedState.label) ); console.info("Ignoring State"); } } } script[ file_name + "." + current_component ][ lineDesc.label ] = state; parents.push(new Parent(lineDesc.label, lineDesc.indentation)); } } } setReturn(script); return script; } function parseScriptFileFromSource(fileName, lines, onSuccess) { cached_refmsgid_map = {}; let script = parseScript(fileName, lines, {}); colorStates(script); let parsedObject = { scriptJSON: script, cached_refmsgid_map }; return parsedObject; } function Action(action, label, section, delay) { this.action = action; this.label = label; this.section = section; this.delay = delay; } function State(lineDesc) { this.nextstates = []; this.action = lineDesc.action; this.type = ""; this.hasPlaceHolder = lineDesc.hasPlaceHolder; this.message_type = lineDesc.message_type; this.indentation = lineDesc.indentation; this.default_label = lineDesc.default_label; this.isCommon = false; this.isPersistentMenu = false; this.message = lineDesc.message; this.label = lineDesc.label; this.index = lineDesc.index; this.line_type = lineDesc.type; this.eventType = lineDesc.eventType; } function setReturn(script) { for ( var section in script ) { if ( script.hasOwnProperty(section) ) { for ( var label in script[ section ] ) { if ( script[ section ].hasOwnProperty(label) && COMMON_LIKE_SECTIONS.indexOf(section.split(".")[1]) === -1 ) { script[ section ][ label ].return = script[ section ][ label ].nextstates.length === 0; } } } } } function ScriptValidator() { this.validate = function (script, options) { try { for ( var section in script ) { if ( script.hasOwnProperty(section) ) { validateStartState(script[ section ], section); for ( var label in script[ section ] ) { if ( script[ section ].hasOwnProperty(label) ) { var state_data = script[ section ][ label ]; if ( state_data.line_type === "LINE" ) { validateAction(state_data, script, section, label); if ( state_data.type === USER ) { validateChilds(script[ section ], state_data, section, label); } } } } } } return true; } catch ( e ) { options.error(e); return false; } }; } function validateAction(state_data, script, section, label) { switch ( state_data.action.action ) { case GOTO: if ( !(Object.keys(script[ section ]).indexOf(state_data.action.label) > -1) ) { if ( !state_data.default_label ) { throw new Error( "For section ".concat( section, " and label ", label, " the GOTO location is ", state_data.action.label, " but that is not in the section. Please verify the GOTO statement" ) ); } else { throw new Error( "For section ".concat( section, " and at line ", String(label.split("_")[ 1 ] - 1 + 2), " the GOTO location is ", state_data.action.label, " but that is not in the section. Please verify the GOTO statement" ) ); } } else if ( state_data.nextstates && state_data.nextstates.length > 0 ) { if ( !state_data.default_label ) { console.log( "[SCRIPT_WARN] In section " + section + " state " + label + " there are next states though having a goto. These next states don't have a value and can be removed. Please remove unnecessary steps." ); } else { console.log( "[SCRIPT_WARN] In section " + section + " at line " + String(label.split("_")[ 1 ] - 1 + 2) + " there are next states though having a goto. These next states don't have a value and can be removed. Please remove unnecessary steps." ); } } break; case CALL: if ( !(Object.keys(script).indexOf(state_data.action.section) > -1) ) { if ( !state_data.default_label ) { throw new Error( "For section ".concat( section, " and label ", label, " the CALL statement leads to section", state_data.action.section, " that cannot be found. Please review the same." ) ); } else { throw new Error( "For section ".concat( section, " and at line ", String(label.split("_")[ 1 ] - 1 + 2), " the CALL statement leads to section", state_data.action.section, " that cannot be found. Please review the same." ) ); } } else if ( state_data.nextstates && state_data.nextstates.length > 0 ) { if ( !state_data.default_label ) { console.log( "[SCRIPT_WARN] In section " + section + " state " + label + " there are next states though having a goto. These next states don't have a value and can be removed. Please remove unnecessary steps." ); } else { console.log( "[SCRIPT_WARN] In section " + section + " at line " + String(label.split("_")[ 1 ] - 1 + 2) + " there are next states though having a goto. These next states don't have a value and can be removed. Please remove unnecessary steps." ); } } break; case CONTINUE: if ( state_data.type === USER ) { if ( !state_data.default_label ) { throw new Error( "For section ".concat( section, " and label ", label, " the CONTINUE statement is Associated with an USER State. That is not allowed. Please verify the same." ) ); } else { throw new Error( "For section ".concat( section, " and at line ", String(label.split("_")[ 1 ] - 1 + 2), " the CONTINUE is associated with an USER state. That is not allowed. Please verify the same." ) ); } } break; } } function validateChilds(states, state_data, section, label) { for ( var array_index in state_data.nextstates ) { var next_state = state_data.nextstates[ array_index ]; if ( states[ next_state ].type === USER ) { if ( !state_data.default_label ) { throw new Error( "For section".concat( section, " Label ", label, "the type of state is USER and it has next State", next_state, " which is of type USER, that is not possible. Please review the same." ) ); } else { throw new Error( "For section".concat( section, " data ", state_data.input ? state_data.input : state_data.output, "the type of state is USER and it has next State", next_state, " which is of type USER, that is not possible. Please review the same." ) ); } } } } function validateStartState(section_data, section) { if ( COMMON_LIKE_SECTIONS.indexOf(section.split(".")[1]) === -1 && !(section_data[ Object.keys(section_data)[ 0 ] ].type === BOT) ) { throw new Error( "Start State of section ".concat( section, " is not a bot state. Please review the same." ) ); } } function Parent(label, indentation) { this.label = label; this.indentation = indentation; } function colorStates(script) { for ( var script_section in script ) { if ( COMMON_LIKE_SECTIONS.indexOf(script_section.split(".")[ 1 ]) === -1 ) { var startState = getStartState(script, script_section); if ( startState ) { colorState(script, script_section, startState, true); } else { throw new Error("Error while getting the first state "); } } else { var common_states = Object.keys(script[ script_section ]); for ( var index in common_states ) { if ( script[ script_section ][ common_states[ index ] ].indentation === 0 ) { colorState(script, script_section, common_states[ index ], false); } } } } } function colorState(script, script_section, currentState, isBot) { if ( script.hasOwnProperty(script_section) && script[ script_section ].hasOwnProperty(currentState) ) { var currentState = script[ script_section ][ currentState ]; currentState.type = isBot ? BOT : USER; if ( isBot ) { currentState.output = currentState.message; currentState.parser = currentState.label; } else { currentState.input = currentState.message; currentState.handler = currentState.label; } if ( currentState.nextstates.length === 0 ) { return; } for ( var array_index in currentState.nextstates ) { if ( currentState.action.action === CONTINUE ) { colorState( script, script_section, currentState.nextstates[ array_index ], isBot ); } else if ( currentState.action.action === CALL || currentState.action.action === DELAY ) { colorState( script, script_section, currentState.nextstates[ array_index ], true ); } else { colorState( script, script_section, currentState.nextstates[ array_index ], !isBot ); } } } else { if ( !script.hasOwnProperty(script_section) ) { throw new Error("Script does not have " + script_section + " within."); } if ( !script[ script_section ].hasOwnProperty(currentState) ) { throw new Error( "Script section" + script_section + " does not have the state " + currentState + " within." ); } } } function getStartState(script, section) { if ( script[ section ] ) { for ( var state in script[ section ] ) { if ( script[ section ].hasOwnProperty(state) ) { if ( script[ section ][ state ].isStartState ) { return state; } } } } throw new Error( section + " not found within the script. Please review it once." ); } function tryParseJSON(jsonStr) { try { var parsedValue = JSON.parse(jsonStr); if ( parsedValue && typeof parsedValue === "object" ) { return parsedValue; } } catch ( e ) { } return false; } module.exports.BOT = BOT; module.exports.USER = USER; module.exports.COMMON = COMMON; module.exports.PERSISTENT_MENU = PERSISTENT_MENU; module.exports.GOTO = GOTO; module.exports.CALL = CALL; module.exports.RETURN = RETURN; module.exports.DONE = DONE; module.exports.CONTINUE = CONTINUE; module.exports.DEFEX = DEFEX; module.exports.DELAY = DELAY; module.exports.ON_EXCEPTION = ON_EXCEPTION; module.exports.SMALLTALK_OFF = SMALLTALK_OFF; module.exports.parse = ScriptLoader; module.exports.parseScript = parseScript; module.exports.parseScriptFileFromSource = parseScriptFileFromSource;