UNPKG

multivocal

Version:

A node.js library to assist with building best practice, configuration driven, Actions for the Google Assistant.

1,574 lines (1,309 loc) 47.8 kB
const Template = require('./template'); const Response = require('./response'); const Util = require('./util'); const Auth = require('./auth'); const Log = require('./log'); /**===================================================================*/ exports.Config = { Simple: require('./config-simple'), KeyValue: require('./config-key-value'), Merge: require('./config-merge'), Firebase: require('./config-firebase'), Firestore: require('./config-firestore') }; console.log('Creating Config'); var Config = new exports.Config.Merge( [], {addToMultivocal:false} ); var setConfig = function( conf ){ Config = conf; }; exports.setConfig = setConfig; var addConfig = function( conf ){ Config.add( conf ); }; exports.addConfig = addConfig; var getConfig = function(){ return Config.get(); }; exports.getConfig = getConfig; new exports.Config.KeyValue( process.env, {root:'Process/Env'} ); const DefCon = new exports.Config.Simple( require( '../config/defcon.js' ), {addToMultivocal:false} ); /**===================================================================*/ var timing = function( env, tag, label ){ Util.setObjPath( env, `Timing/Tag/${tag}/${label}`, Date.now() ); return Promise.resolve( env ); }; var timingBegin = function( env, tag ){ Util.incObjPath( env, 'Timing/CurrentLevel' ); Util.incObjPath( env, 'Timing/CurrentOrder' ); Util.setObjPathFrom( env, `Timing/Tag/${tag}/Level`, 'Timing/CurrentLevel' ); Util.setObjPathFrom( env, `Timing/Tag/${tag}/Order`, 'Timing/CurrentOrder' ); return timing( env, tag, 'Begin' ); }; var timingStart = timingBegin; exports.timingBegin = timingBegin; exports.timingStart = timingBegin; var timingEnd = function( env, tag ){ Util.incObjPath( env, 'Timing/CurrentLevel', -1 ); return timing( env, tag, 'End' ); }; var timingStop = timingEnd; exports.timingEnd = timingEnd; exports.timingStop = timingEnd; var timingBlocks = function( env ){ let timing = Util.objPathsDefault( env, 'Timing/Tag', [] ); let keys = Object.keys( timing ); let blocks = keys.map( key => { let val = timing[key]; let diff = (val.End ? val.End : Date.now()) - val.Begin; return Object.assign({ Label: key, Diff: diff }, val); }); Util.setObjPath( env, 'Timing/Blocks', blocks ); return Promise.resolve( env ); }; /**===================================================================*/ var preprocessors = []; var addPreprocessor = function( func ){ preprocessors.push( function( env ){ if( env.Preprocess.Fail ) return Promise.resolve( env ); return func( env ); }) }; exports.addPreprocessor = addPreprocessor; var preprocessDialogflowPing = function( env ){ var isPing = false; var inputs = Util.pathSetting( env, 'Precondition/DialogflowPing'); var argumentName = Util.setting( env, 'Precondition/DialogflowPing/ArgumentName' ); for( var co=0; inputs && co<inputs.length && !isPing; co++ ){ var input = inputs[co]; var arguments = input.arguments || []; for( var c1=0; c1<arguments.length && !isPing; c1++ ){ var argument = arguments[c1]; var name = argument.name; var boolValue = argument.boolValue; if( name === argumentName ){ isPing = boolValue; } } } if( isPing ){ env.Preprocess = { Fail: true, Msg: { Text: "pong" } }; } return Promise.resolve( env ); }; addPreprocessor( preprocessDialogflowPing ); var preprocessGooglePing = function( env ){ var isPing = false; var value = Util.pathSetting( env, 'Precondition/GooglePing' ); var expectedValue = Util.setting( env, 'Precondition/GooglePing/ExceptedValue' ); isPing = (value && expectedValue && expectedValue === value); if( isPing ){ env.Preprocess = { Fail: true, Msg: { Text: "Pong" } }; } return Promise.resolve( env ); } addPreprocessor( preprocessGooglePing ); var preprocessVerifyRequestJWT = function( ruleName, env ){ return timingBegin( env, `Verify ${ruleName}` ) .then( env => { var ruleBase = `Precondition/Verify/Rules/${ruleName}`; var token = Util.pathSetting( env, ruleBase ); var sources = Util.setting( env, `${ruleBase}/Auth` ); Auth.addSources( env, sources ); return Auth.verify( token, env ) // If we get something back, then it resolved ok .then( () => Promise.resolve( true ) ) // If it didn't resolve, suppress the error, but return false .catch( err => { console.error('Unable to verify request JWT', token, err); return Promise.resolve( false ); }) }) .then( ret => { return timingEnd( env, `Verify ${ruleName}` ) .then( env => ret ); }) } var preprocessVerifyRequestRule = function( ruleName, env ){ var ruleBase = `Precondition/Verify/Rules/${ruleName}`; var criteria = Util.setting( env, `${ruleBase}/Criteria` ); var shouldEvaluate = Template.evalBoolean( criteria, env ); if( shouldEvaluate ){ var processor = Util.setting( env, `${ruleBase}/Processor` ); switch( processor ){ case 'SimpleProcessor': return Promise.resolve( true ); case 'JWTProcessor': return preprocessVerifyRequestJWT( ruleName, env ); } } return Promise.resolve( false ); } var preprocessVerifyRequest = function( env ){ return timingBegin( env, "Verify" ) .then( env => { var rules = Util.setting( env, 'Precondition/Verify/Rules' ) || {}; var ruleNames = Object.keys( rules ); var verifierPromises = ruleNames.map( ruleName => preprocessVerifyRequestRule( ruleName, env ) ); return Promise.all( verifierPromises ); }) .then( results => { var isValid = results.reduce( (ret, result) => ret || result ); if( !isValid ){ env.Preprocess = { Fail: true, Msg: { Text: "Failed validation." } } } return timingEnd( env, "Verify" ); }); } addPreprocessor( preprocessVerifyRequest ); var preprocessFormat = function( env ){ if( !env.Preprocess.Fail ){ return Promise.resolve( env ); } env.Msg = env.Preprocess.Msg || {Text:"preprocess fail"}; return formatMessage( env ) .then( env => formatJson( env ) ); }; var preprocess = function( env ){ env.Preprocess = { }; return timingBegin( env, 'Multivocal' ) .then( env => timingBegin( env, 'Preprocess' ) ) .then( env => loadConfig( env ) ) .then( env => buildEnvPlatform( env ) ) .then( env => envFunctionsRecursive( env, preprocessors ) ) .then( env => preprocessFormat( env ) ) .then( env => timingEnd( env, 'Preprocess' ) ); }; exports.preprocess = preprocess; /**===================================================================*/ var builders = []; exports.builders = builders; var addBuilder = function( func ){ builders.push( func ); }; exports.addBuilder = addBuilder; var buildEnvPlatform = function( env ){ return timingBegin( env, 'Platform' ) .then( env => { var rules = Util.setting( env, 'Platform/RuleCriteria' ); Object.keys( rules ).forEach( ruleName => { //timingBegin( env, `Platform ${ruleName}` ); var rule = rules[ruleName]; var val; if( ruleName.startsWith('is') || ruleName.startsWith('Is') ){ val = Template.evalBoolean( rule, env ); } else { val = Template.eval( rule, env ); } Util.setObjPath( env, `Platform/${ruleName}`, val ); //timingEnd( env, `Platform ${ruleName}` ); }); console.log( 'Multivocal buildEnvPlatform', env.Platform ); return Promise.resolve( env ); }) .then( env => timingEnd( env, 'Platform' ) ); }; var buildEnvHostname = function( env ){ env.Hostname = Util.setting( env, 'Hostname/Template', Template.Methods.Str ); return Promise.resolve( env ); } var buildEnvLocale = function( env ){ var locale = Util.pathSetting( env, 'Locale' ); var dashIndex = locale.indexOf('-'); var lang = 'und'; if( dashIndex > 0 ){ lang = locale.substring( 0, dashIndex ); var country = locale.substring( dashIndex+1 ); locale = lang+'-'+country.toUpperCase(); } env.Locale = locale; env.Lang = lang; return Promise.resolve( env ); }; var buildEnvParameters = function( env ){ env.ParameterInfo = Util.pathSetting( env, 'Parameters/All' ); env.Parameter = {}; Object.keys( env.ParameterInfo ).forEach( param => { var val = env.ParameterInfo[param]; if( typeof val === 'object' ){ var valuePaths = Util.setting( env, 'Parameters/Value/PathList' ); // We need the list, not the value val = Util.objPathsDefault( val, valuePaths ); } if( val ){ env.Parameter[param] = val; } }); return Promise.resolve( env ); }; var buildEnvContexts = function( env ){ env.Context = {}; // Load any contexts into a more useful environment attribute var contexts = Util.pathSetting( env, 'Contexts' ); if( Array.isArray(contexts) ){ // Dialogflow format for( var co=0; co<contexts.length; co++ ){ var context = contexts[co]; var contextName = context.name; var lastSlash = contextName.lastIndexOf( '/' ); contextName = contextName.substr( lastSlash+1 ); env.Context[contextName] = context; } } else if( typeof contexts === 'object' ) { // AoG 3 / AB format var keys = Object.keys( contexts ); keys.forEach( key => { var context = { name: key, parameters: contexts[key] }; env.Context[key] = context; }); } return Promise.resolve( env ); }; var buildEnvStatePath = function( env, path ){ var val = Util.pathSetting( env, path ); if( typeof val === 'string' ){ val = JSON.parse( val ); } Util.setObjPath( env, path, val ); }; var buildEnvState = function( env ){ // FIXME - this should be a PathList setting var settingsPathList = [ 'User/State', 'Session/State', 'Session/Counter', 'Session/Consecutive', 'Session/Stack' ]; settingsPathList.map( path => buildEnvStatePath( env, path ) ); // Get the session start time, or set it to now var sessionStart = Util.pathSetting( env, 'Session/StartTime' ); if( !sessionStart ){ sessionStart = Date.now(); } Util.setObjPath( env, 'Session/StartTime', sessionStart ); return Promise.resolve( env ); }; var buildEnvOption = function( env ){ var optionValue = Util.pathSetting( env, 'Option' ); var optionPrefix = Util.setting( env, 'Option/Prefix' ); if( optionValue && optionValue.startsWith( optionPrefix ) ){ optionValue = optionValue.substring( optionPrefix.length ); optionValue = parseInt( optionValue, 10 ); } Util.setObjPath( env, "Option", optionValue ); return Promise.resolve( env ); }; var buildEnvPermissions = function( env ){ var permissionList = Util.setting( env, 'Requirements/Permission/List' ); permissionList.map( entry => { var val = Util.objPath( env, entry.Source ); if( typeof val !== 'undefined' ){ Util.setObjPath( env, entry.Target, val ); } }); return Promise.resolve( env ); }; var buildEnvAuthId = function( env ){ // Set the anonymous user ID var idNativeValue = Util.pathSetting( env, 'User/Id' ); if( !idNativeValue ){ const {v4:uuid} = require('uuid'); idNativeValue = uuid(); } var statePath = Util.setting( env, 'User/Id/State' ); Util.setObjPath( env, statePath, idNativeValue ); var idTemplate = Util.setting( env, 'User/Id/Template' ); var idValue = Template.eval( idTemplate, env ); Util.setObjPath( env, 'User/Id', idValue ); return Promise.resolve( env ); }; var buildEnvAuthAccessToken = function( env ){ var accessToken = Util.pathSetting( env, 'User/AccessToken' ); // No access token. Skip it if( !accessToken ){ return Promise.resolve( env ); } Util.setObjPath( env, 'User/AccessToken', accessToken ); Util.setObjPath( env, 'User/IsAuthenticated', true ); return Promise.resolve( env ); }; var buildEnvAuthProfileIdentity = function( env ){ var profileToken = Util.objPath( env, 'User/IdentityToken' ); // If we have an identity token, validate it var sources = Util.setting( env, 'Requirements/Auth' ); Auth.addSources( env, sources ); return Auth.verify( profileToken ) // If it has validated, then save the profile .then( profile => { //console.log('buildEnvAuthIdentity profile', profile); Util.setObjPath( env, 'User/Profile', profile ); Util.setObjPath( env, 'User/IsAuthenticated', true ); return Promise.resolve( env ); }) // Log the error, but return as normal .catch( err => { console.error('Unable to verify token', profileToken, err); return Promise.resolve( env ); }) }; var buildEnvAuthProfileNormalize = function( env ){ if( Util.objPath( env, 'Platform/DialogflowIntegration' ) === 'hangouts' ){ Util.setObjPath( env, 'User/IsAuthenticated', true ); // TODO - All of this should really be derived from settings or something var sub = Util.objPathsDefault( env, 'User/ProfileOrig/name', '' ); sub = sub.substring( 'users/'.length ); Util.setObjPath( env, 'User/Profile', {sub} ); Util.setObjPathFrom( env, 'User/Profile/name', 'User/ProfileOrig/displayName' ); Util.setObjPathFrom( env, 'User/Profile/email', 'User/ProfileOrig/email' ); Util.setObjPathFrom( env, 'User/Profile/picture', 'User/ProfileOrig/avatarUrl' ); } else { // Can't normalize it Util.setObjPathFrom( env, 'User/Profile', 'User/Profile/Orig' ); } console.log('buildEnvAuthProfileNormalize',env.User.Profile); return Promise.resolve( env ); }; var buildEnvAuthProfile = function( env ){ var profileToken = Util.pathSetting( env, 'User/Profile' ); console.log('buildEnvAuthProfile profileToken',profileToken); // No identity token. Skip it if( !profileToken ){ return Promise.resolve( env ); } else if( typeof profileToken === 'string' ){ Util.setObjPath( env, 'User/IdentityToken', profileToken ); return buildEnvAuthProfileIdentity( env ); } else { Util.setObjPath( env, 'User/ProfileOrig', profileToken ); return buildEnvAuthProfileNormalize( env ); } }; var buildEnvAuth = function( env ){ return buildEnvAuthId( env ) .then( env => buildEnvAuthAccessToken( env ) ) .then( env => buildEnvAuthProfile( env ) ) }; var buildEnvFeatureGroup = function( env, destinationPath, arrayMod ){ var surfaces = Util.pathSetting( env, destinationPath ); if( !Array.isArray(surfaces) ){ surfaces = [surfaces]; } surfaces.map( surface => { var capabilities = surface.capabilities || [surface]; var val = true; // I assume the surface will have a name someday, put it here capabilities.map( capability => { var name = capability.name || capability; var dot = name.lastIndexOf( '.' )+1; name = name.substring( dot ); Util.setObjPath( env, `${destinationPath}/${name}${arrayMod}`, val ); }); }); }; var buildEnvFeatures = function( env ){ buildEnvFeatureGroup( env, 'Session/Feature', '' ); buildEnvFeatureGroup( env, 'User/Feature', '[+]' ); return Promise.resolve( env ); }; var buildEnvMedia = function( env ){ let mediaStatus = Util.setting( env, 'Media/Status' ); if( !mediaStatus ){ // It wasn't at a normal path, try checking arrays where we have to get the name var inputs = Util.pathSetting( env, 'Media/Status/Inputs' ); //console.log('buildEnvMedia inputs', inputs); for( var ico = 0; inputs && ico<inputs.length; ico++ ){ var input = inputs[ico]; var arguments = input.arguments; for( var aco=0; arguments && aco<arguments.length; aco++ ){ var argument = arguments[aco]; if( argument.name === 'MEDIA_STATUS' ){ var status = argument.extension.status; Util.setObjPath( env, 'MediaStatus', status ); } } } } if( mediaStatus ){ Util.setObjPath( env, 'MediaStatus', status ); } const mediaProgress = Util.pathSetting( env, 'Media/Progress' ); Util.setObjPath( env, 'MediaProgress', mediaProgress ); return Promise.resolve( env ); }; var buildEnvIntents = function( env ){ env.IntentName = Util.pathSetting( env, 'Intent' ); env.Intent = Util.setting( env, 'Intent/Template', Template.Methods.Str ); env.NodeName = Util.pathSetting( env, 'Node' ); env.Node = Util.setting( env, 'Node/Template', Template.Methods.Str ); env.ActionName = Util.pathSetting( env, 'Action' ); env.Action = Util.setting( env, 'Action/Template', Template.Methods.Str ); env.Default = Util.setting( env, 'Default/Template', Template.Methods.Str ); return Promise.resolve( env ); }; var envFunctionsRecursive = function( env, functions, index=0 ){ if( index >= functions.length ){ return Promise.resolve( env ); } var func = functions[index]; return func( env ) .then( env => { return envFunctionsRecursive( env, functions, index+1 ); }); }; var buildEnv = function( env ){ return timingBegin( env, 'BuildEnv' ) .then( env => buildEnvHostname( env ) ) .then( env => buildEnvLocale( env ) ) .then( env => buildEnvParameters( env ) ) .then( env => buildEnvContexts( env ) ) .then( env => buildEnvState( env ) ) .then( env => buildEnvOption( env ) ) .then( env => buildEnvPermissions( env ) ) .then( env => buildEnvAuth( env ) ) .then( env => buildEnvFeatures( env ) ) .then( env => buildEnvMedia( env ) ) .then( env => buildEnvIntents( env ) ) .then( env => envFunctionsRecursive( env, builders ) ) .then( env => timingEnd( env, 'BuildEnv' ) ); }; /**===================================================================*/ var loadConfig = function( env ){ return timingBegin( env, 'Config' ) .then( () => Config.get() ) .then( config =>{ env.Config = config; return DefCon.get(); }) .then( defcon => { env.DefCon = defcon; return timingEnd( env, 'Config' ); }); }; /**===================================================================*/ var loadVoice = function( env ){ var sessionData = env.Session.State; // We must load the voices first, since picking one may require this var voices = Util.pathSetting( env, 'Voice/Voices' ); env.Voices = voices; var voiceName = Util.pathSetting( env, 'Voice/Name' ); var voice = voices[voiceName]; console.log('loadVoice', {voices,voiceName,voice}); voice.Name = voiceName; env.Voice = voice; env.VoiceName = voiceName; env.Session.State.Voice = voiceName; return Promise.resolve( env ); }; exports.loadVoice = loadVoice; /**===================================================================*/ var requesters = {}; var setRequirementRequest = function( requirement, requester ){ requesters[requirement] = requester; }; exports.setRequirementRequest = setRequirementRequest; var getReqirementRequest = function( requirement ){ return requesters[requirement]; }; exports.getRequirementRequest = getReqirementRequest; var requestRequirementsContext = function( env, additionalParameters ){ var parameters = { action: env.Action, actionName: env.ActionName, intent: env.Intent, intentName: env.IntentName }; if( typeof additionalParameters === 'object' ){ Object.assign( parameters, additionalParameters ); } var context = { name: 'multivocal_requirements', lifespan: 1, parameters: parameters }; Util.setObjPath( env, 'Requirements/Context', context ); }; /** * * @param env * @param name * @param additionalParameters truthy if the multivocal_requirements context should be set * (if an object, it will be added to the parameters) * @return {Promise<any>} */ var requestDefault = function( env, name, additionalParameters ){ if( name ){ // Save environment information if we expect to use it if( additionalParameters ){ requestRequirementsContext( env, additionalParameters ); } // Change the environment to reflect the request var request = `Request.${name}`; Util.setObjPath( env, 'Requirements/RequestName', name ); Util.setObjPath( env, 'Requirements/Request', request ); Util.setObjPath( env, 'Action', `${request}.${env.Action}`); Util.setObjPath( env, 'Intent', `${request}.${env.Intent}`); Util.setObjPath( env, 'Default', `${request}.${env.Default}`); } return Promise.resolve( env ); }; exports.requestDefault = requestDefault; var requestPermission = function( env ){ var permissionList = Util.setting( env, 'Requirements/Permission/List' ); var requirements = Util.objPath( env, 'Requirements/Requested' ); var permissions; if( requirements ){ var permissionsHash = {}; permissionList.map( entry => { var requirement = entry.Target; if( requirements.includes(requirement) ){ permissionsHash[entry.Permission] = true; } }); permissions = Object.keys(permissionsHash).sort(); } if( permissions ){ var intent = { "intent": "actions.intent.PERMISSION", "data": { "@type": "type.googleapis.com/google.actions.v2.PermissionValueSpec", "permissions": permissions } }; Util.setObjPath( env, 'Requirements/Intent', intent ); } return requestDefault( env, 'Permission', true ); }; DefCon.get().then( defcon => { defcon.Setting.Requirements.Permission.List.map( entry => { var target = entry.Target; if( target ){ requesters[target] = requestPermission; } }); }); var requestSignIn = function( env ){ var status = Util.pathSetting( env, 'Requirements/SignIn/Status' ); var additionalParameters = false; if( status ){ // We aren't authenticated, but we have authentication status info, // so we shouldn't ask for it again, but will set the level with this. Util.setObjPath( env, 'IntentLevel', status ); Util.setObjPath( env, 'ActionLevel', status ); } else { // There is no status set, so this is probably first time requesting, // so we set the Intent we ask for. var intent = Util.setting( env, 'Requirements/SignIn/Intent', Template.evalIdentity ); Util.setObjPath( env, 'Requirements/Intent', intent ); additionalParameters = true; } return requestDefault( env, 'SignIn', additionalParameters ); }; requesters['User/IsAuthenticated'] = requestSignIn; // FIXME var requestRequirements = function( env ){ var requirements = Util.pathSetting( env, 'Requirements' ); if( typeof requirements === 'undefined' ){ requirements = []; } if( !Array.isArray(requirements) ){ requirements = [requirements]; } Util.setObjPath( env, 'Requirements/Requested', requirements ); // Check our requirements to see if we have everything we need var requester; for( var co=0; co<requirements.length && !requester; co++ ){ var requirement = requirements[co]; var val = Util.objPath( env, requirement ); if( typeof val === 'undefined' ){ requester = requesters[requirement]; if( !requester ){ console.error('Multivocal requestRequirements no requester for requirement',requirement); } } } // If we're missing something, request it requester = requester || requestDefault; return requester( env ) .catch( err => { console.error( 'Multivocal requestRequirements err', err ); }); }; /**===================================================================*/ var handlers = {}; exports.handlers = handlers; var addHandler = function( intentActionName, func ){ handlers[intentActionName] = func; }; exports.addHandler = addHandler; var addIntentHandler = function( intentName, func ){ handlers[`Intent.${intentName}`] = func; }; exports.addIntentHandler = addIntentHandler; var addActionHandler = function( actionName, func ){ handlers[`Action.${actionName}`] = func; }; exports.addActionHandler = addActionHandler; var processCounters = function( env ){ // Handle the special case of the total number of visits for this session // by incrementing the value under State and recording that we should // handle the "NumVisits" counter in the usual way. Util.incObjPath( env, 'Session/State/NumVisits' ); Util.setObjPath( env, 'Counter[+]', 'NumVisits' ); // Store the counters for the node, action, intent, and outent // based on what they're set to. Util.setObjPathFrom( env, 'Counter[+]', 'Node' ); Util.setObjPathFrom( env, 'Counter[+]', 'Action' ); Util.setObjPathFrom( env, 'Counter[+]', 'Intent' ); Util.setObjPathFrom( env, 'Counter[+]', 'Outent' ); // Get the counters that have been set and de-dupe them var counterArray = Util.objPathsDefault( env, 'Counter', [] ); counterArray = [... new Set( counterArray )]; // Go through the Consecutive list and remove those not present in the counterArray var consecutive = Util.objPathsDefault( env, 'Session/Consecutive', {} ); var keys = Object.keys( consecutive ); for( var co=0; co<keys.length; co++ ){ var key = keys[co]; if( counterArray.indexOf( key ) == -1 ){ delete( consecutive[key] ); } } Util.setObjPath( env, 'Session/Consecutive', consecutive ); // Go through the counterArray and increment the session counter // and consecutive counter for( co=0; co<counterArray.length; co++ ){ var counter = counterArray[co]; Util.incObjPath( env, `Session/Counter/${counter}` ); Util.incObjPath( env, `Session/Consecutive/${counter}` ); } return Promise.resolve( env ); }; var processLevelString = function( levelDef, env ){ return Template.eval( levelDef, env ); }; var processLevelArray = function( levelDef, env ){ var ret = 0; for( var co=0; co<levelDef.length && !ret; co++ ){ var def = levelDef[co]; var result = Template.evalBoolean( def, env ); console.log('level', def, result); if( result ){ ret = co+1; } } return ret; }; var processLevelDef = function( levelDef, env ){ if( typeof levelDef === 'string' ){ return processLevelString( levelDef, env ); } else if( Array.isArray( levelDef ) ){ return processLevelArray( levelDef, env ) || ''; } else { return ''; } }; var processLevel = function( env ){ if( !env.IntentLevel ){ var intentLevelDef = Util.pathSetting( env, 'IntentLevel' ); env.IntentLevel = processLevelDef( intentLevelDef, env ); } console.log('processLevel IntentLevel', env.IntentLevel); if( !env.ActionLevel ){ var actionLevelDef = Util.pathSetting( env, 'ActionLevel' ); env.ActionLevel = processLevelDef( actionLevelDef, env ); } console.log('processLevel ActionLevel', env.ActionLevel); return Promise.resolve( env ); }; var processStackNamed = function( env, stackName, maxSize ){ // Get the current stack, if it exists, and what the latest value might be var path = `Session/Stack/${stackName}`; var stack = Util.objPathsDefault( env, path, [] ); var latest = stack[0]; // Get the value we want to save (from the environment with this stack name) var val = Util.objPath( env, stackName ); // If there is a value, and if its different than the latest value in the stack // then we should save it. if( typeof val !== 'undefined' && latest !== val ){ // Put it at the front var size = stack.unshift( val ); // If it is oversize, remove values from the back // A size <=0 means "unlimited" while( maxSize && maxSize > 0 && size > maxSize ){ size = stack.pop(); } Util.setObjPath( env, path, stack ); } } var processStack = function( env ){ var stackSize = Util.setting( env, 'Session/Stack/Size' ); var stacks = Object.keys( stackSize ); stacks.forEach( stack => processStackNamed( env, stack, stackSize[stacks] ) ); return Promise.resolve( env ); } var processFlexResponseIteration = function( env, targets ){ var target = targets.shift(); env['_Target'] = target; return Response.getFromSettings( env, target ) .then( env => targets.length ? processFlexResponseIteration( env, targets ) : env ); } var processFlexResponse = function( env ){ var targets = Util.setting( env, 'FlexResponse/Targets' ).slice(); return processFlexResponseIteration( env, targets ); } var processReloadVoice = function( env ){ var shouldReload = Util.setting( env, 'Voice/ShouldReload/Criteria', Template.Methods.Bool ); console.log('processReloadVoice',shouldReload); if( shouldReload ){ return loadVoice( env ); } return Promise.resolve( env ); } var handleDefault = function( env ){ return processCounters( env ) .then( env => processLevel( env ) ) .then( env => processStack( env ) ) .then( env => processFlexResponse( env ) ) .then( env => processReloadVoice( env ) ); }; handlers['Default'] = handleDefault; exports.handleDefault = handleDefault; var handle = function( env ){ if( env.Sent ){ return Promise.resolve( env ); } return timingBegin( env, 'Handle' ) .then( env => { // Get the possible handler names we might use // and locate a handler that has been registered for that name. var handler; var handlerName; var handlerNames = Util.setting( env, 'Handler/Names', Template.Methods.Array ); for( var co=0; co<handlerNames.length && !handler; co++ ){ handlerName = handlerNames[co]; handler = handlers[handlerName]; } // Store the handler name and determine what counter to use to record this Util.setObjPath( env, 'HandlerName', handlerName ); var handlerCounter = Util.setting( env, 'Handler/Counter', Template.Methods.Str ); Util.setObjPath( env, 'Counter[+]', handlerCounter ); // Call the handler return timingBegin( env, `Handle ${handlerName}` ) .then( env => handler( env ) ) .then( env => timingEnd( env, `Handle ${handlerName}`) ); }) .then( env => timingEnd( env, 'Handle' ) ) .catch( err => { console.error( 'Multivocal Problem with handler', err ); return Promise.reject( err ); }); }; /**===================================================================*/ var addSuffix = function( env ){ if( env.Sent ){ return Promise.resolve( env ); } var noSuffixNeeded = Util.setting( env, 'NoSuffixNeeded/Criteria', Template.Methods.Bool ); Util.setObjPath( env, 'NoSuffixNeeded', noSuffixNeeded ); if( noSuffixNeeded ){ return Promise.resolve( env ); } return Response.getFromSettings( env, 'Suffix' ); }; /**===================================================================*/ var formatMessageTemplate = function( env, source, target ){ var template = Util.objPath( source, 'Template' ); if( typeof template !== 'undefined' ){ var val = Template.eval( template, env, Template.Methods.Str ); console.log('formatMessageTemplate',source,val); if( typeof val !== 'undefined' ){ Util.setObjPath( env, target, val ); } } return Promise.resolve( env ); }; var formatMessageCopyFirst = function( env, source, target ){ var copyFirst = source.CopyFirst; if( !Array.isArray(copyFirst) ){ copyFirst = [copyFirst]; } var sourcePaths = copyFirst.map( path => `${path}/${source.Target}` ); var val = Util.objPathsDefault( env, sourcePaths ); if( typeof val !== 'undefined' ){ Util.setObjPath( env, target, val ); } return Promise.resolve( env ); }; var formatMessageSource = function( env, source ){ var target = source.Target; if( !target ){ console.log('formatMessageSource no target', source); } // If a value has already been set, skip it. var sendPath = `Send/${target}`; if( Util.objPath( env, sendPath ) ){ return Promise.resolve( env ); } if( source.Template ){ return formatMessageTemplate( env, source, sendPath ); } else if( source.CopyFirst ){ return formatMessageCopyFirst( env, source, sendPath ); } else { console.log('formatMessageSource unable to handle', source); } }; var formatMessageSources = function( env ){ var sources = Util.setting( env, 'Send' ); var sourcePromises = sources.map( source => formatMessageSource(env, source) ); return Promise.all( sourcePromises ) .then( () => Promise.resolve( env ) ); }; var formatMessage = function( env ){ return formatMessageSources( env ) .then( env => { var shouldRepeat = Util.objPathsDefault( env, 'Response/ShouldRepeat', false ); Util.setObjPath( env, 'Send/Remember/name', 'multivocal_repeat' ); Util.setObjPath( env, 'Send/Remember/lifespan', 1 ); if( shouldRepeat ){ // If we're repeating, copy the last repeat context Util.setObjPathFrom( env, 'Send/Remember/parameters', 'Context/multivocal_repeat/parameters' ); } else { // This is a normal message, so save the info in case we repeat next time Util.setObjPathFrom( env, 'Send/Remember/parameters/Ssml', 'Send/Ssml' ); Util.setObjPathFrom( env, 'Send/Remember/parameters/Text', 'Send/Text' ); } return Promise.resolve( env ); }); }; /**===================================================================*/ var formatSessionValue = function( env, paths, stringify ){ var ret = Util.objPathsDefault( env, paths, {} ); if( stringify ){ ret = JSON.stringify( ret ); } return ret; }; var formatSession = function( env ){ // TODO: stringify for Dialogflow 2, but not 3 // TODO: make these more configurable var stringify = Util.objPath( env, 'Platform/IsDialogFlow' ); var state = formatSessionValue( env, 'Session/State', stringify ); var counter = formatSessionValue( env, 'Session/Counter', stringify ); var consecutive = formatSessionValue( env, 'Session/Consecutive', stringify ); var stack = formatSessionValue( env, 'Session/Stack', stringify ); var context = { name: 'multivocal_session', lifespan: 99, parameters: { state: state, counter: counter, consecutive: consecutive, stack: stack, startTime: Util.objPath( env, 'Session/StartTime' ) } }; Util.setObjPath( env, 'Send/Session', context ); return Promise.resolve( env ); }; var formatContext = function( env, context ){ if( typeof context === 'string' ){ context = { name: context, lifespan: 5 } } if( !context.parameters ){ context.parameters = {}; } var pathName = `Send/Context/${context.name}`; Util.setObjPath( env, pathName, context ); return Promise.resolve( context ); }; var formatContextList = function( env, contextPath ){ var contextList = Util.objPath( env, contextPath ); console.log('formatContextList',contextPath,contextList); if( !contextList ){ return Promise.resolve( null ); } // If there is just one Context, not a list, process it if( !Array.isArray( contextList ) ){ return formatContext( env, contextList ); } // We have a list of Contexts, process all of them var promises = contextList.map( context => formatContext( env, context ) ); return Promise.all( promises ) .catch( err => { console.error( 'Multivocal formatContextList err', err ); return Promise.reject( err ); }); }; var formatContexts = function( env ){ // This isn't a Path since we need to get the results from each one var contextPathList = Util.setting( env, 'Context/PathList' ); var promises = contextPathList.map( contextPath => formatContextList( env, contextPath ) ); return Promise.all( promises ) .then( result => { var contexts = Util.objPathsDefault( env, 'Send/Context', {} ); // var contextList = Object.values(contexts); // Requires Node 7+. So use next line instead. var contextList = Object.keys(contexts).map(k=>contexts[k]); Util.setObjPath( env, 'Send/ContextList', contextList ); return Promise.resolve( env ); }) .catch( err => { console.error( 'Multivocal formatContexts err', err ); return Promise.reject( err ); }); }; var formatTable = function( env ){ var data = Util.objPath( env, 'Msg/Table/Data' ); if( data ){ Util.setObjPathFrom( env, 'Send/Table/title', 'Msg/Table/Title' ); Util.setObjPathFrom( env, 'Send/Table/image/url', 'Msg/Table/ImageUrl' ); Util.setObjPathFrom( env, 'Send/Table/image/accessibilityText', 'Msg/Table/ImageText' ); var headers = Util.objPathsDefault( env, 'Msg/Table/Headers', [] ); for( var co=0; co<headers.length; co++ ){ Util.setObjPath( env, 'Send/Table/columnProperties[+]/header', headers[co] ); Util.setObjPath( env, 'Send/Table/columnProperties[=]/horizontalAlignment', 'LEADING' ); } for( var cr=0; cr<data.length; cr++ ){ var row = data[cr]; Util.setObjPath( env, 'Send/Table/rows[+]', {} ); for( var cc=0; cc<row.length; cc++ ){ var cell = row[cc]; Util.setObjPath( env, 'Send/Table/rows[=]/cells[+]/text', cell ); } Util.setObjPath( env, 'Send/Table/rows[=]/dividerAfter', false ); } } return Promise.resolve( env ); }; var formatPage = function( env ){ var pageUrl = Util.setting( env, 'Page/Url', Template.Methods.Str ); var pageCriteria = Util.setting( env, 'Page/Criteria', Template.Methods.Bool ); // Only continue if we have a URL set and the device can support the feature console.log('formatPage', pageCriteria, pageUrl); if( !pageCriteria || !pageUrl ){ return Promise.resolve( env ); } var pageUrlStatePath = Util.setting( env, 'Page/UrlState/Path' ); if( Array.isArray( pageUrlStatePath ) ){ pageUrlStatePath = pageUrlStatePath[0]; } var pageUrlState = Util.objPath( env, pageUrlStatePath ); console.log('formatPage', pageUrl, pageUrlState, pageUrlStatePath ); if( pageUrl ){ if( pageUrl !== pageUrlState ){ Util.setObjPath( env, 'Send/Page/Url', pageUrl ); Util.setObjPath( env, pageUrlStatePath, pageUrl ); } // Supplement Data with some standard state var pageData = Util.pathSetting( env, 'Page/Data' ); if( !pageData ){ pageData = {}; } var addStateSource = Util.setting( env, 'Page/IncludeEnvironment' ); addStateSource.map( source => { var val = Util.objPath( env, source ); Util.setObjPath( pageData, source, val ); }); var suppressMic = Util.pathSetting( env, 'Page/SuppressMic' ); Util.setObjPath( env, 'Send/Page/SuppressMic', suppressMic ); Util.setObjPath( env, 'Send/Page/Data', pageData ); } return Promise.resolve( env ); }; /** * Needs to be done before we format session variables, * since we might change the NodeName stack * @param env * @return {Promise<any>} */ var formatNextNode = function( env ){ var nextNode = Util.pathSetting( env, 'NextNode' ); console.log('formatNextNode',{nextNode}); if( nextNode && nextNode.match(/^\.+$/) ){ var len = nextNode.length; var stack = Util.objPathsDefault( env, 'Session/Stack/NodeName', [] ); for( var co=0; co<len; co++ ){ nextNode = stack.shift(); } Util.setObjPath( env, 'Session/Stack/NodeName', stack ); } Util.setObjPath( env, 'Send/NextNode', nextNode ); return Promise.resolve( env ); } var formatShouldClose = function( env ){ var shouldClose = Util.pathSetting( env, 'ShouldClose' ); Util.setObjPath( env, 'Send/ShouldClose', shouldClose ); return Promise.resolve( env ); }; var formatRequirementsIntent = function( env ){ var requirementsIntent = Util.objPath( env, 'Requirements/Intent' ); if( requirementsIntent ){ var sendIntent = Template.evalObj( requirementsIntent, env ); if( sendIntent ){ Util.setObjPath( env, 'Send/Intent', sendIntent ); } } return Promise.resolve( env ); }; var formatTypeValues = function( nameKey, name, values ){ if( typeof values === 'string' ){ values = [values]; } if( values.indexOf(name) === -1 ){ values.push( name ); } var entity = { synonyms: values }; entity[nameKey] = name; return entity; }; var formatTypeDialogflow = function( name, values, env ){ var sessionName = Util.pathSetting( env, 'Session/Id' ); var typeName = `${sessionName}/entityTypes/${name}`; var valueNames = Object.keys( values ); var entities = valueNames.map( valueName => formatTypeValues( 'value', valueName, values[valueName] ) ); var type = { name: typeName, entityOverrideMode: 'ENTITY_OVERRIDE_MODE_OVERRIDE', entities: entities }; return type; }; var formatTypeActionsBuilder = function( name, values, env ){ var valueNames = Object.keys( values ); var entities = valueNames.map( valueName => formatTypeValues( 'name', valueName, values[valueName] ) ); var type = { name: name, mode: 'TYPE_REPLACE', synonym: { entries: entities } }; return type; } var formatType = function( name, values, env ){ if( Util.objPath( env, 'Platform/IsActionsOnGoogle') && Util.objPath( env, 'Platform/ActionsSDKVersion') === '3' ){ return formatTypeActionsBuilder( name, values, env ); } else { return formatTypeDialogflow( name, values, env ); } } var formatTypes = function( env ){ var types = Util.objPathsDefault( env, 'Types', {} ); var typeNames = Object.keys( types ); var entityTypes = typeNames.map( typeName => formatType( typeName, types[typeName], env ) ); Util.setObjPath( env, 'Send/Types', entityTypes ); return Promise.resolve( env ); }; var formatDebug = function( env ){ var context = { name: 'multivocal_debug', lifespan: 1, parameters: {} } var addSource = Util.setting( env, "Debug/PathList" ); addSource.forEach( source => { var val = Util.objPath( env, source ); var target = source; target = target.replace( /\/Debug$/i, '' ); if( target === 'Debug' ){ context.parameters = Object.assign( context.parameters, val ); } else { Util.setObjPath( context.parameters, target, val ); } }) Util.setObjPath( env, 'Send/Debug', context ); return Promise.resolve( env ); } var JsonFormatter = require('./formatter'); var formatJson = function( env ){ if( env.Sent ){ return Promise.resolve( env ); } if( !Util.objPath( env, 'Send/Json' ) ){ var json = JsonFormatter.format( env ); Util.setObjPath( env, 'Send/Json', json ); } return Promise.resolve( env ); }; var format = function( env ){ if( env.Sent ){ return Promise.resolve( env ); } env.Send = { ViaApp: false }; console.log('Multivocal format', JSON.stringify(env.Msg,null,1)); return timingBegin( env, 'Format' ) .then( env => formatMessage( env ) ) .then( env => formatPage( env ) ) .then( env => formatNextNode( env ) ) .then( env => formatSession( env ) ) .then( env => formatTable( env ) ) .then( env => formatShouldClose( env ) ) .then( env => formatRequirementsIntent( env ) ) .then( env => formatTypes( env ) ) .then( env => formatDebug( env ) ) .then( env => formatContexts( env ) ) .then( env => formatJson( env ) ) .then( env => timingEnd( env, 'Format' ) ) .catch( err => { console.error( 'Multivocal format err', err ); return Promise.reject( err ); }); }; /**===================================================================*/ require('./standard').init(); var doprocess = function( env ){ if( env.Sent || Util.objPath( env, 'Send/Json' ) ){ return Promise.resolve( env ); } return timingBegin( env, 'Process' ) // Build the initial environment .then( env => buildEnv( env ) ) // Set the "voice" field for the environment .then( env => loadVoice( env ) ) // See if there are any prerequisites/requirements .then( env => requestRequirements( env ) ) // Determine what handler we should call and call it .then( env => handle( env ) ) // If there needs to be anything else on the reply (like asking // a question) figure that out here. .then( env => addSuffix( env ) ) // Prepare a response if one hasn't already been sent .then( env => format( env ) ) .then( env => timingEnd( env, 'Process' ) ) .catch( err => { console.error( 'Problem during processing', err ); return Promise.reject( err ); }); }; exports.process = doprocess; /**===================================================================*/ var postprocessors = []; var addPostprocessor = function( func ){ postprocessors.push( func ); }; exports.addPostprocessor = addPostprocessor; var postprocessTimer = function( env ){ let now = Date.now(); return timingBlocks( env ) .then( env => { // Sort them by end time ascending (later times last) let timing = env.Timing.Blocks.slice(); timing.sort( (a,b) => { let as = a.End ? a.End : now; let bs = b.End ? b.End : now; let ret = as-bs; // If they end at the same time, use how long they've been recording if( !ret ){ as = a.Diff ? a.Diff : 0; bs = b.Diff ? b.Diff : 0; ret = as-bs; } return ret; }); // Display the results let results = 'timing results:\n'; for( let co=0; co<timing.length; co++ ){ var t = timing[co]; results += ` ${'.'.repeat(t.Level-1)}${t.Label} ${t.Diff}ms\n`; } console.log( results ); return Promise.resolve( env ); }); }; addPostprocessor( postprocessTimer ); var postprocess = function( env ){ return timingBegin( env, 'Postprocess' ) .then( env => envFunctionsRecursive( env, postprocessors ) ) .then( env => timingEnd( env, 'Postprocess') ) .catch( err => { console.error( 'Problem during postprocessing', err ); return Promise.reject( err ); }); }; exports.postprocess = postprocess; /**===================================================================*/ var processExpressParameters = function( request, response ){ var env = { Body: request.body, Req: request, Res: response }; return Promise.resolve( env ); }; var processExpressResponse = function( env ){ var json = Util.objPath( env, 'Send/Json' ); if( !env.Sent && json ){ env.Res.send( json ); env.Sent = true; } return Promise.resolve( env ); }; var processExpressWebhook = function( request, response ){ return processExpressParameters( request, response ) .then( env => preprocess( env ) ) .then( env => doprocess( env ) ) .then( env => processExpressResponse( env ) ) .then( env => postprocess( env ) ); }; exports.processExpressWebhook = processExpressWebhook; exports.processGCFWebhook = processExpressWebhook; const FirebaseFunctions = require('firebase-functions'); var processFirebaseWebhook = FirebaseFunctions.https.onRequest( (request, response) => processExpressWebhook( request, response ) ); exports.processFirebaseWebhook = processFirebaseWebhook; var processLambdaParameters = function( event, context, callback ){ var env = { Body: JSON.parse( event.body ), Lambda: { Event: event, Context: context, Callback: callback } }; return Promise.resolve( env ); }; var processLambdaResponse = function( env ){ var json = Util.objPath( env, 'Send/Json' ); if( !env.Sent && json ){ var response = { statusCode: 200, headers: {}, body: JSON.stringify( json ) }; env.Lambda.Callback( null, response ); env.Sent = true; } return Promise.resolve( env ); }; var processLambdaWebhook = function( event, context, callback ){ return processLambdaParameters( event, context, callback ) .then( env => preprocess( env ) ) .then( env => doprocess( env ) ) .then( env => processLambdaResponse( env ) ) .then( env => postprocess( env ) ); }; exports.processLambdaWebhook = processLambdaWebhook;