bot-script
Version:
Scripting tool to Write Bot's Workflow
742 lines (701 loc) • 21.6 kB
JavaScript
/**
* 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;