UNPKG

7.28 kBJavaScriptView Raw
1'use strict';
2const _ = require('lodash');
3const debug = require('debug')('alexia:debug');
4const info = require('debug')('alexia:info');
5const parseError = require('./error-handler').parseError;
6
7/**
8 * Handles request and calls done when finished
9 * @param {Object} app - Application object
10 * @param {Object} data - Request JSON to be handled
11 * @param {Function} handlers - Handlers to be called. Contains onStart, onEnd, actionFail
12 * @param {Function} done - Callback to be called when request is handled. Callback is called with one argument - response JSON
13 */
14module.exports = (app, data, handlers, done) => {
15 const appId = data.session.application.applicationId;
16 const options = app.options;
17
18 // Application ids is specified and does not contain app id in request
19 if (options && options.ids && options.ids.length > 0 && options.ids.indexOf(appId) === -1) {
20 const e = parseError(new Error(`Application id: '${appId}' is not valid`));
21 throw e;
22 }
23
24 if (data.session.new) {
25 data.session.attributes = {
26 previousIntent: '@start'
27 };
28 } else if (!data.session.attributes) {
29 data.session.attributes = {};
30 }
31
32 const requestType = data.request.type;
33
34 info(`Handling request: "${requestType}"`);
35 debug(`Request payload: ${JSON.stringify(data, null, 2)}`);
36 switch (requestType) {
37
38 case 'LaunchRequest':
39 callHandler(handlers.onStart, null, data.session.attributes, app, data, done);
40 break;
41
42 case 'IntentRequest':
43 const intentName = data.request.intent.name;
44 const intent = app.intents[data.request.intent.name];
45
46 info(`Handling intent: "${intentName}"`);
47 if (!intent) {
48 const e = parseError(new Error(`Nonexistent intent: '${intentName}'`));
49 throw e;
50 }
51
52 checkActionsAndHandle(intent, data.request.intent.slots, data.session.attributes, app, handlers, data, done);
53 break;
54
55 case 'SessionEndedRequest':
56 callHandler(handlers.onEnd, null, data.session.attributes, app, data, done);
57 break;
58
59 default:
60 const e = parseError(new Error(`Unsupported request: '${requestType}'`));
61 throw e;
62 }
63
64};
65
66const callHandler = (handler, slots, attrs, app, data, done) => {
67
68 // Transform slots into simple key:value schema
69 slots = _.transform(slots, (result, value) => {
70 result[value.name] = value.value;
71 }, {});
72
73 const optionsReady = (options) => {
74 done(createResponse(options, slots, attrs, app));
75 };
76
77 // Handle intent synchronously if has < 3 arguments. 3rd is `done`
78 if (handler.length < 3) {
79 optionsReady(handler(slots, attrs, data));
80
81 } else {
82 handler(slots, attrs, data, optionsReady);
83 }
84};
85
86/**
87 * Checks for `actions` presence to help us with Alexa coznversation workflow configuration
88 *
89 * 1) no actions: just call the intent.handler method without any checks
90 * 2) with actions: check if action for current intent transition is found
91 * a) action found: call its `if` function and if condition fails run `fail` function
92 * b) no action: call default `fail` function
93 *
94 * @param intent
95 * @param slots
96 * @param attrs
97 * @param app
98 * @param handlers
99 * @param data
100 * @param done
101 */
102const checkActionsAndHandle = (intent, slots, attrs, app, handlers, data, done) => {
103
104 if (app.actions.length === 0) {
105 // There are no actions. Just call handler on this intent
106 attrs.previousIntent = intent.name;
107 callHandler(intent.handler, slots, attrs, app, data, done);
108
109 } else {
110 // If there are some actions, try to validate current transition
111 let action = _.find(app.actions, {from: attrs.previousIntent, to: intent.name});
112
113 // Try to find action with wildcards if no action was found
114 if (!action) {
115 action = _.find(app.actions, {from: attrs.previousIntent, to: '*'});
116 }
117 if (!action) {
118 action = _.find(app.actions, {from: '*', to: intent.name});
119 }
120
121 if (action) {
122
123 // Action was found. Check if this transition is valid
124 if (action.if ? action.if(slots, attrs) : true) {
125
126 // Transition is valid. Remember intentName and handle intent
127 attrs.previousIntent = intent.name;
128 callHandler(intent.handler, slots, attrs, app, data, done);
129
130 } else {
131 // Transition is invalid. Call fail function
132 if (action.fail) {
133 callHandler(action.fail, slots, attrs, app, data, done);
134 } else {
135 callHandler(handlers.defaultActionFail, slots, attrs, app, data, done);
136 }
137 }
138
139 } else {
140 callHandler(handlers.defaultActionFail, slots, attrs, app, data, done);
141 }
142 }
143};
144
145/**
146 * Creates card object with default card type
147 * @param {Object} card - Card object from responseData options
148 * @returns {Object} card - Card object or undefined if card is not specified
149 */
150const createCardObject = (card) => {
151 if (card) {
152 // Set default card type to 'Simple'
153 if (!card.type) {
154 card.type = 'Simple';
155 }
156 return card;
157 }
158};
159
160/**
161 * Reads options.end and returns bool indicating whether to end session
162 * @param {object} [options] Options object
163 * @param {bool} options.end Indicates whether to end session. Defaults to true
164 * @returns bool from options.end or by default true
165 */
166const getShouldEndSession = (options) => {
167 if (!options || options.end === undefined) {
168 return true;
169 }
170 return options.end;
171};
172
173const createResponse = (options, slots, attrs, app) => {
174 // Convert text options to object
175 if (typeof (options) === 'string') {
176 options = {
177 text: options
178 };
179 }
180
181 // Create outputSpeech object for text or ssml
182 const outputSpeech = createOutputSpeechObject(options.text, options.ssml);
183
184 let sessionAttributes;
185 if (options.attrs) {
186 // Use session attributes from responseObject and remember previousIntent
187 sessionAttributes = options.attrs;
188 sessionAttributes.previousIntent = attrs.previousIntent;
189
190 } else {
191 // No session attributes specified in user response
192 sessionAttributes = attrs;
193 }
194
195 let responseObject = {
196 version: app.options ? app.options.version : '0.0.1',
197 sessionAttributes: sessionAttributes,
198 response: {
199 outputSpeech: outputSpeech,
200 shouldEndSession: getShouldEndSession(options)
201 }
202 };
203
204 if (options.reprompt) {
205 responseObject.response.reprompt = {
206 outputSpeech: createOutputSpeechObject(options.reprompt, options.ssml)
207 };
208
209 }
210
211 let card = createCardObject(options.card);
212 if (card) {
213 responseObject.response.card = card;
214 }
215
216 return responseObject;
217};
218
219/**
220 * Creates output speech object used for text response or reprompt
221 * @param {string} text - Text or Speech Synthesis Markup (see sendResponse docs)
222 * @param {bool} ssml - Whether to use ssml
223 * @returns {Object} outputSpeechObject in one of text or ssml formats
224 */
225const createOutputSpeechObject = (text, ssml) => {
226 let outputSpeech = {};
227 if (!ssml) {
228 outputSpeech.type = 'PlainText';
229 outputSpeech.text = text;
230 } else {
231 outputSpeech.type = 'SSML';
232 outputSpeech.ssml = text;
233 }
234 return outputSpeech;
235};