/* eslint-disable no-prototype-builtins */
'use strict';
const { CommonProvider } = require("../../common/provider");
const { CommonValidator } = require("../../common/validator");
const { ERROR } = require("../../common/error");
const { MessageModel } = require("../message/messageModel");
const { MessageFactory, RawMessage, NonRawMessage } = require("../../typings/lib2");
const VARIABLE = {
type: "string",
entity: false
};
const CONST = {
NLPRESULT_TYPE: 'nlpresult',
SYSTEM_INVALID_USER_INPUT: 'system.invalidUserInput'
};
/**
* Wrapper object for accessing nlpresult
* @alias NLPResult
*/
const NLPResult = class {
constructor(nlpresult) {
this._nlpresult = nlpresult;
}
/**
* Returns matches for the specified entity; may be an empty collection.
* If no entity is specified, returns the map of all entities.
* @param {string} [entity] - name of the entity
* @return {object} The entity match result.
*/
entityMatches(entity) {
if (!this._nlpresult) {
return entity === undefined ? {} : [];
}
if (entity === undefined) {
// Retrieving entityMatches collection, or an empty collection if none
return this._nlpresult.entityMatches ? this._nlpresult.entityMatches : {};
} else {
if (this._nlpresult.entityMatches) {
return this._nlpresult.entityMatches[entity] ? this._nlpresult.entityMatches[entity] : [];
} else {
return [];
}
}
}
/**
* Returns full payload matches for the specified entity; may be an empty collection.
* If no entity is specified, returns the full payload map of all entities.
* @param {string} [entity] - name of the entity
* @return {object} The full entity match result.
*/
fullEntityMatches(entity) {
if (!this._nlpresult) {
return entity === undefined ? {} : [];
}
if (entity === undefined) {
// Retrieving fullEntityMatches collection, or an empty collection if none
return this._nlpresult.fullEntityMatches ? this._nlpresult.fullEntityMatches : {};
} else {
if (this._nlpresult.fullEntityMatches) {
return this._nlpresult.fullEntityMatches[entity] ? this._nlpresult.fullEntityMatches[entity] : [];
} else {
return [];
}
}
}
/**
* Returns intent matches if any.
* Intent matches are returned in descending order of score,
* @return {object[]} The intent match results, each match with properties score and intent.
*/
intentMatches() {
if (!this._nlpresult) {
return [];
}
if (this._nlpresult.intentMatches && Array.isArray(this._nlpresult.intentMatches.summary) && this._nlpresult.intentMatches.summary.length > 0) {
return this._nlpresult.intentMatches.summary;
} else {
return null;
}
}
/**
* Returns top intent match (with highest score), if any.
* @return {object} The top intent match (with properties score and intent)
*/
topIntentMatch() {
var intentMatches = this.intentMatches();
return (intentMatches && intentMatches.length > 0 ? intentMatches[0] : {});
}
query() {
return (this._nlpresult ? this._nlpresult.query : "");
}
}
/**
* Super class for concrete context classes used to invoke custom components
* and event handlers
* @memberof module:Lib
* @alias BaseContext
*/
class BaseContext {
/**
* @param {object} request - invocation request payload
* @param {object} response - initial response payload
* @param {Function} [validationSchema] - callback returns a schema for request body validation
*/
constructor(request, response, validationSchema) {
const validation = validationSchema ? validateRequestSchema(request, validationSchema) : null;
if (validation && validation.error) {
const err = new Error('Request body malformed');
err.name = 'badRequest';
err.details = createErrorDetails('Request body malformed',
JSON.stringify(validation.error),
'BOTS-1000', {
requestBody: request,
});
throw err;
}
// filling in response context vars fromn request
response.context = Object.assign({variables: {}}, request.context);
this._request = request;
this._response = response;
this._logger = CommonProvider.getLogger();
}
/**
* Retrieves the request object.
* @return {object} The request object.
*/
getRequest() {
return this._request;
}
/**
* Retrieves the response object.
* @return {object} The response object.
*/
getResponse() {
return this._response;
}
/**
* Retrieves the logger object.
* @return {object} The logger object.
*/
logger() {
// this function is replaced with mixin logger when deployed to embedded functions to enable viewing logs in bots UI
return this._logger;
}
/**
* Retrieves the logger object.
* @return {object} The logger object.
* @deprecated use logger() function instead
*/
getLogger() {
this.logger().warn('The getLogger() method is deprecated, and will be removed in a future release. Use logger() method instead.');
return this.logger();
}
/**
* Returns an NLPResult helper object for working with nlpresult variables.
* See the NLPResult documentation for more information.
* <p>
* If your skill uses visual flows, you don't need to specify a variable name.
* If your skill uses a YAML flow, you may specify a particular nlpresult by name (if you have multiple
* nlpresult variables defined in the flow), or omit the name if you only have 1 nlpresult.
* </p>
*
* @param {string} [nlpVariableName] - variable that holds the nlpResult
* @return {NLPResult} The nlp resolution result.
*/
nlpResult(nlpVariableName) {
let isVisualDialog = this.getRequest().taskFlow;
if (isVisualDialog) {
nlpVariableName = 'skill.system.nlpresult';
} else if (nlpVariableName === undefined) {
for (let name in this.getResponse().context.variables) {
if (this.getResponse().context.variables[name].type === CONST.NLPRESULT_TYPE) {
this.logger().debug('SDK: using implicitly found nlpresult=' + name);
nlpVariableName = name;
break;
}
}
if (nlpVariableName === undefined) {
throw new Error('SDK: no nlpresult variable present');
}
}
const nlpVariable = this.variable(nlpVariableName);
if (nlpVariable === undefined) {
throw new Error('SDK: undefined var=' + nlpVariableName);
}
if (!isVisualDialog && this.getResponse().context.variables[nlpVariableName].type !== CONST.NLPRESULT_TYPE) {
throw new Error('SDK: var=' + nlpVariableName + ' not of type nlpresult');
}
return new NLPResult(nlpVariable);
}
/**
* Read or write variables defined in the current flow.
* It is not possible to change the type of an existing variable through
* this method. It is the caller's responsibility to ensure that the
* value being set on a variable is of the correct type. (e.g. entity,
* string or other primitive, etc).
* <p>
* A new variable can be created. However, since the variable is not
* defined in the flow, using it in the flow subsequently may be flagged
* for validation warnings.
* </p>
* This function takes a variable number of arguments.
* <p>
* The first form:
* variable(name);
* reads the variable called "name", returning its value.
* The name could be in the form of <scope>.<variableName>. For example, a variable firstName in the
* profile scope needs to be retrieved as variable("profile.firstName").
* </p>
* The second form:
* variable(name, value);
* writes the value "value" to the variable called "name".
*
* @param {string} name - The name of variable to be set or read
* @param {string} [value] - value to be set for variable
* @example
* let firstName = conversation.variable("profile.firstName");
* let lastName = conversation.variable("profile.lastName");
* conversation.variable("fullName", firstName + ' ' + lastName);
*/
variable(name, value) {
var context = this.getResponse().context;
var scopeName = null;
var nameToUse = name;
var index = name.indexOf(".");
if (index > -1) {
scopeName = name.substring(0, index);
var possibleScope = context;
while (possibleScope) {
if (possibleScope.scope === scopeName) {
context = possibleScope;
nameToUse = name.substring(index + 1, name.length);
break;
} else {
//this is to handle the case when the variable name is system.XXX but system is not a scope
if (possibleScope.variables && possibleScope.variables.hasOwnProperty(nameToUse)) {
context = possibleScope;
break;
}
possibleScope = possibleScope.parent;
}
}
}
if (value === undefined) {
if (!context.variables || !context.variables.hasOwnProperty(nameToUse)) {
return undefined;
}
return context.variables[nameToUse].value;
} else {
if (!context.variables) {
context.variables = {};
}
if (!context.variables[nameToUse]) {
context.variables[nameToUse] = Object.assign({}, VARIABLE);
}
context.variables[nameToUse].value = value;
this.getResponse().modifyContext = true;
return this;
}
}
/**
* Get the definition of a variable
*
* @param {string} name - The name of the variable
*/
getVariableDefinition(name) {
let context = this.getResponse().context;
let scopeName = null;
let nameToUse = name;
let index = name.indexOf('.');
if (index > -1) {
scopeName = name.substring(0, index);
let possibleScope = context;
while (possibleScope) {
if (possibleScope.scope === scopeName) {
context = possibleScope;
nameToUse = name.substring(index + 1, name.length);
break;
} else {
// this is to handle the case when the variable name is system.XXX but system is not a scope
if (possibleScope.variables && possibleScope.variables.hasOwnProperty(nameToUse)) {
context = possibleScope;
break;
}
possibleScope = possibleScope.parent;
}
}
}
if (!context.variables || !context.variables.hasOwnProperty(nameToUse)) {
return undefined;
} else {
return context.variables[nameToUse];
}
}
/**
* Returns the value of a context or user variable
* @return {object} variable value
* @param {string} name - name of the variable
*/
getVariable(name) {
return this.variable(name);
}
/**
* Sets the value of a context or user variable
* @param {string} name - name of the variable
* @param {object} value - value of the variable
*/
setVariable(name,value) {
return this.variable(name,value);
}
/**
* Get translated string using a resource bundle key defined in the skill.
* @return {string} resource bundle freemarker expression that will be resolved when event handler or custom component response is
* received by dialog engine
* @param {string} rbKey - key of the resource bundle entry defined with the skill that should be used to translate
* @param {string} rbArgs - substitution variables
*/
translate(rbKey, ...rbArgs) {
// create freemarker expression that will be resolved in runtime after event handler or custom component response is received
let exp = "${rb('"+rbKey+"'";
for (let arg of rbArgs) {
// MIECS-38051: only string args should be enclosed in quotes
typeof arg === 'string' ? exp += ",'" + arg + "'" : exp += "," + arg;
}
exp += ")}";
return exp;
}
/**
* Return the channel conversation type
* @return {string} the channel type
*/
getChannelType() {
return this.getRequest().message.channelConversation.type;
}
/**
* Returns the last user message.
* @return {NonRawMessage} the last user message. You can cast this message to the appropriate message type.
*/
getUserMessage() {
return this.getMessageFactory().messageFromJson(this.getRequest().message.messagePayload);
}
/**
* Returns the MessageModel class for creating or validating messages to or from bots.
* @see MessageModel.js
* @return {MessageModel} The MessageModel class
* @deprecated Use getMessageFactory() instead
*/
getMessageModel() {
return MessageModel;
}
/**
* Returns the MessageFactory class for creating bots messages
* @return {MessageFactory} The MessageFactory class
*/
getMessageFactory() {
return MessageFactory;
}
/**
* Creates a message payload object
* @param {object} payload - can take a string message, a message created by the MessageFactory, or a message created by the
* deprecated MessageModel.
* @returns {object} message payload in JSON format
*/
constructMessagePayload(payload) {
var messagePayload;
var messageModel;
if (payload instanceof RawMessage || payload instanceof NonRawMessage) {
// message created with new MessageFactory
return payload.toJson();
}
// Message created with deprecated MessageModel, keep for backwards compatibility
if (payload instanceof MessageModel) {
this.logger().debug('messageModel payload provided');
messageModel = payload;
} else {
this.logger().debug('creating messageModel with payload');
messageModel = new MessageModel(payload);
}
if (messageModel.isValid()) {
this.logger().debug('valid messageModel');
messagePayload = messageModel.messagePayload();
} else {
this.logger().debug('message model validation error:', messageModel.validationError());
this.logger().debug('using rawPayload');
var rawMessagePayload = MessageModel.rawConversationMessage(payload);
messageModel = new MessageModel(rawMessagePayload);
if (messageModel.isValid()) {
this.logger().debug('valid messageModel for rawMessagePayload');
messagePayload = messageModel.messagePayload();
} else {
this.logger().debug('message model raw message validation error:', messageModel.validationError());
}
}
return messagePayload;
}
}
function createErrorDetails(title, detail, errorCode, errorDetails) {
const details = Object.assign({}, ERROR);
details.title = title;
details.detail = detail;
details['o:errorCode'] = errorCode;
details['o:errorDetails'] = errorDetails;
return details;
}
function validateRequestSchema(reqBody, validationSchema) {
return CommonValidator.validate(validationSchema, reqBody, { allowUnknown: true });
}
module.exports = {
BaseContext,
};