mirror of
https://github.com/gosticks/DefinitelyTyped.git
synced 2026-07-01 07:40:10 +00:00
Temporarily stage the 1.5.1 .js files
This commit is contained in:
38
types/actions-on-google/staging/actions-on-google.js
Normal file
38
types/actions-on-google/staging/actions-on-google.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright 2016 Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Actions on Google client library.
|
||||
* https://developers.google.com/actions/
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const AssistantApp = require('./assistant-app');
|
||||
|
||||
module.exports = {
|
||||
AssistantApp: AssistantApp.AssistantApp,
|
||||
State: AssistantApp.State,
|
||||
ActionsSdkApp: require('./actions-sdk-app'),
|
||||
DialogflowApp: require('./dialogflow-app'),
|
||||
Transactions: require('./transactions'),
|
||||
Responses: require('./response-builder'),
|
||||
// Backwards compatibility
|
||||
Assistant: AssistantApp.AssistantApp,
|
||||
ActionsSdkAssistant: require('./actions-sdk-app'),
|
||||
ApiAiAssistant: require('./dialogflow-app'),
|
||||
ApiAiApp: require('./dialogflow-app')
|
||||
};
|
||||
877
types/actions-on-google/staging/actions-sdk-app.js
Normal file
877
types/actions-on-google/staging/actions-sdk-app.js
Normal file
@@ -0,0 +1,877 @@
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Debug = require('debug');
|
||||
const debug = Debug('actions-on-google:debug');
|
||||
const error = Debug('actions-on-google:error');
|
||||
const app = require('./assistant-app');
|
||||
const AssistantApp = app.AssistantApp;
|
||||
const State = app.State;
|
||||
const transformToCamelCase = require('./utils/transform').transformToCamelCase;
|
||||
|
||||
// Constants
|
||||
const CONVERSATION_API_AGENT_VERSION_HEADER = 'Agent-Version-Label';
|
||||
const RESPONSE_CODE_OK = 200;
|
||||
const INPUTS_MAX = 3;
|
||||
const CONVERSATION_API_SIGNATURE_HEADER = 'authorization';
|
||||
|
||||
// Configure logging for hosting platforms that only support console.log and console.error
|
||||
debug.log = console.log.bind(console);
|
||||
error.log = console.error.bind(console);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Actions SDK support
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This is the class that handles the conversation API directly from Assistant,
|
||||
* providing implementation for all the methods available in the API.
|
||||
*/
|
||||
class ActionsSdkApp extends AssistantApp {
|
||||
/**
|
||||
* Constructor for ActionsSdkApp object.
|
||||
* To be used in the Actions SDK HTTP endpoint logic.
|
||||
*
|
||||
* @example
|
||||
* const ActionsSdkApp = require('actions-on-google').ActionsSdkApp;
|
||||
* const app = new ActionsSdkApp({request: request, response: response,
|
||||
* sessionStarted:sessionStarted});
|
||||
*
|
||||
* @param {Object} options JSON configuration.
|
||||
* @param {Object} options.request Express HTTP request object.
|
||||
* @param {Object} options.response Express HTTP response object.
|
||||
* @param {Function=} options.sessionStarted Function callback when session starts.
|
||||
* @actionssdk
|
||||
*/
|
||||
constructor (options) {
|
||||
debug('ActionsSdkApp constructor');
|
||||
super(options, () => this.body_);
|
||||
|
||||
// If request is from AoG and in Proto2 format, convert to Proto3.
|
||||
if (this.body_ && !this.isNotApiVersionOne_()) {
|
||||
this.body_ = transformToCamelCase(this.body_);
|
||||
}
|
||||
|
||||
if (this.body_ &&
|
||||
this.body_.conversation &&
|
||||
this.body_.conversation.type &&
|
||||
this.body_.conversation.type === this.ConversationStages.NEW &&
|
||||
this.sessionStarted_ && typeof this.sessionStarted_ === 'function') {
|
||||
this.sessionStarted_();
|
||||
} else if (this.sessionStarted_ && typeof this.sessionStarted_ !== 'function') {
|
||||
this.handleError_('options.sessionStarted must be a Function');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates whether request is from Assistant through signature verification.
|
||||
* Uses Google-Auth-Library to verify authorization token against given
|
||||
* Google Cloud Project ID. Auth token is given in request header with key,
|
||||
* "Authorization".
|
||||
*
|
||||
* @example
|
||||
* const app = new ActionsSdkApp({request, response});
|
||||
* app.isRequestFromAssistant('nodejs-cloud-test-project-1234')
|
||||
* .then(() => {
|
||||
* app.ask('Hey there, thanks for stopping by!');
|
||||
* })
|
||||
* .catch(err => {
|
||||
* response.status(400).send();
|
||||
* });
|
||||
*
|
||||
* @param {string} projectId Google Cloud Project ID for the Assistant app.
|
||||
* @return {Promise<LoginTicket>} Promise resolving with google-auth-library LoginTicket
|
||||
* if request is from a valid source, otherwise rejects with the error reason
|
||||
* for an invalid token.
|
||||
* @actionssdk
|
||||
*/
|
||||
isRequestFromAssistant (projectId) {
|
||||
debug('isRequestFromAssistant: projectId=%s', projectId);
|
||||
const googleAuthClient = require('./utils/auth').googleAuthClient;
|
||||
const jwtToken = this.request_.get(CONVERSATION_API_SIGNATURE_HEADER);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!jwtToken) {
|
||||
const errorMsg = 'No incoming API Signature JWT token';
|
||||
error(errorMsg);
|
||||
reject(errorMsg);
|
||||
}
|
||||
googleAuthClient.verifyIdToken(jwtToken, projectId, (err, login) => {
|
||||
if (err) {
|
||||
error('ID token verification Failed: ' + err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(login);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the request Conversation API version.
|
||||
*
|
||||
* @example
|
||||
* const app = new ActionsSdkApp({request: request, response: response});
|
||||
* const apiVersion = app.getApiVersion();
|
||||
*
|
||||
* @return {string} Version value or null if no value.
|
||||
* @actionssdk
|
||||
*/
|
||||
getApiVersion () {
|
||||
debug('getApiVersion');
|
||||
return this.apiVersion_ || this.actionsApiVersion_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user's raw input query.
|
||||
*
|
||||
* @example
|
||||
* const app = new ActionsSdkApp({request: request, response: response});
|
||||
* app.tell('You said ' + app.getRawInput());
|
||||
*
|
||||
* @return {string} User's raw query or null if no value.
|
||||
* @actionssdk
|
||||
*/
|
||||
getRawInput () {
|
||||
debug('getRawInput');
|
||||
const input = this.getTopInput_();
|
||||
if (!input) {
|
||||
error('Failed to get top Input.');
|
||||
return null;
|
||||
}
|
||||
if (!input.rawInputs || input.rawInputs.length === 0) {
|
||||
error('Missing user raw input');
|
||||
return null;
|
||||
}
|
||||
const rawInput = input.rawInputs[0];
|
||||
if (!rawInput.query) {
|
||||
error('Missing query for user raw input');
|
||||
return null;
|
||||
}
|
||||
return rawInput.query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets previous JSON dialog state that the app sent to Assistant.
|
||||
* Alternatively, use the app.data field to store JSON values between requests.
|
||||
*
|
||||
* @example
|
||||
* const app = new ActionsSdkApp({request: request, response: response});
|
||||
* const dialogState = app.getDialogState();
|
||||
*
|
||||
* @return {Object} JSON object provided to the Assistant in the previous
|
||||
* user turn or {} if no value.
|
||||
* @actionssdk
|
||||
*/
|
||||
getDialogState () {
|
||||
debug('getDialogState');
|
||||
if (this.body_.conversation && this.body_.conversation.conversationToken) {
|
||||
return JSON.parse(this.body_.conversation.conversationToken);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the "versionLabel" specified inside the Action Package.
|
||||
* Used by app to do version control.
|
||||
*
|
||||
* @example
|
||||
* const app = new ActionsSdkApp({request: request, response: response});
|
||||
* const actionVersionLabel = app.getActionVersionLabel();
|
||||
*
|
||||
* @return {string} The specified version label or null if unspecified.
|
||||
* @actionssdk
|
||||
*/
|
||||
getActionVersionLabel () {
|
||||
debug('getActionVersionLabel');
|
||||
const versionLabel = this.request_.get(CONVERSATION_API_AGENT_VERSION_HEADER);
|
||||
if (versionLabel) {
|
||||
return versionLabel;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the unique conversation ID. It's a new ID for the initial query,
|
||||
* and stays the same until the end of the conversation.
|
||||
*
|
||||
* @example
|
||||
* const app = new ActionsSdkApp({request: request, response: response});
|
||||
* const conversationId = app.getConversationId();
|
||||
*
|
||||
* @return {string} Conversation ID or null if no value.
|
||||
* @actionssdk
|
||||
*/
|
||||
getConversationId () {
|
||||
debug('getConversationId');
|
||||
if (!this.body_.conversation || !this.body_.conversation.conversationId) {
|
||||
error('No conversation ID');
|
||||
return null;
|
||||
}
|
||||
return this.body_.conversation.conversationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current intent. Alternatively, using a handler Map with
|
||||
* {@link AssistantApp#handleRequest|handleRequest}, the client library will
|
||||
* automatically handle the incoming intents.
|
||||
*
|
||||
* @example
|
||||
* const app = new ActionsSdkApp({request: request, response: response});
|
||||
*
|
||||
* function responseHandler (app) {
|
||||
* const intent = app.getIntent();
|
||||
* switch (intent) {
|
||||
* case app.StandardIntents.MAIN:
|
||||
* const inputPrompt = app.buildInputPrompt(false, 'Welcome to action snippets! Say anything.');
|
||||
* app.ask(inputPrompt);
|
||||
* break;
|
||||
*
|
||||
* case app.StandardIntents.TEXT:
|
||||
* app.tell('You said ' + app.getRawInput());
|
||||
* break;
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* app.handleRequest(responseHandler);
|
||||
*
|
||||
* @return {string} Intent id or null if no value.
|
||||
* @actionssdk
|
||||
*/
|
||||
getIntent () {
|
||||
debug('getIntent');
|
||||
const input = this.getTopInput_();
|
||||
if (!input) {
|
||||
error('Missing intent from request body');
|
||||
return null;
|
||||
}
|
||||
return input.intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the argument value by name from the current intent. If the argument
|
||||
* is not a text argument, the entire argument object is returned.
|
||||
*
|
||||
* Note: If incoming request is using an API version under 2 (e.g. 'v1'),
|
||||
* the argument object will be in Proto2 format (snake_case, etc).
|
||||
*
|
||||
* @param {string} argName Name of the argument.
|
||||
* @return {string} Argument value matching argName
|
||||
* or null if no matching argument.
|
||||
* @actionssdk
|
||||
*/
|
||||
getArgument (argName) {
|
||||
return this.getArgumentCommon(argName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the option key user chose from options response.
|
||||
*
|
||||
* @example
|
||||
* const app = new App({request: req, response: res});
|
||||
*
|
||||
* function pickOption (app) {
|
||||
* if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) {
|
||||
* app.askWithCarousel('Which of these looks good?',
|
||||
* app.buildCarousel().addItems(
|
||||
* app.buildOptionItem('another_choice', ['Another choice']).
|
||||
* setTitle('Another choice').setDescription('Choose me!')));
|
||||
* } else {
|
||||
* app.ask('What would you like?');
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* function optionPicked (app) {
|
||||
* app.ask('You picked ' + app.getSelectedOption());
|
||||
* }
|
||||
*
|
||||
* const actionMap = new Map();
|
||||
* actionMap.set(app.StandardIntents.TEXT, pickOption);
|
||||
* actionMap.set(app.StandardIntents.OPTION, optionPicked);
|
||||
*
|
||||
* app.handleRequest(actionMap);
|
||||
*
|
||||
* @return {string} Option key of selected item. Null if no option selected or
|
||||
* if current intent is not OPTION intent.
|
||||
* @actionssdk
|
||||
*/
|
||||
getSelectedOption () {
|
||||
debug('getSelectedOption');
|
||||
if (this.getArgument(this.BuiltInArgNames.OPTION)) {
|
||||
return this.getArgument(this.BuiltInArgNames.OPTION);
|
||||
}
|
||||
debug('Failed to get selected option');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks to collect user's input; all user's queries need to be sent to
|
||||
* the app.
|
||||
* {@link https://developers.google.com/actions/policies/general-policies#user_experience|The guidelines when prompting the user for a response must be followed at all times}.
|
||||
*
|
||||
* @example
|
||||
* const app = new ActionsSdkApp({request: request, response: response});
|
||||
*
|
||||
* function mainIntent (app) {
|
||||
* const inputPrompt = app.buildInputPrompt(true, '<speak>Hi! <break time="1"/> ' +
|
||||
* 'I can read out an ordinal like ' +
|
||||
* '<say-as interpret-as="ordinal">123</say-as>. Say a number.</speak>',
|
||||
* ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']);
|
||||
* app.ask(inputPrompt);
|
||||
* }
|
||||
*
|
||||
* function rawInput (app) {
|
||||
* if (app.getRawInput() === 'bye') {
|
||||
* app.tell('Goodbye!');
|
||||
* } else {
|
||||
* const inputPrompt = app.buildInputPrompt(true, '<speak>You said, <say-as interpret-as="ordinal">' +
|
||||
* app.getRawInput() + '</say-as></speak>',
|
||||
* ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']);
|
||||
* app.ask(inputPrompt);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* const actionMap = new Map();
|
||||
* actionMap.set(app.StandardIntents.MAIN, mainIntent);
|
||||
* actionMap.set(app.StandardIntents.TEXT, rawInput);
|
||||
*
|
||||
* app.handleRequest(actionMap);
|
||||
*
|
||||
* @param {Object|SimpleResponse|RichResponse} inputPrompt Holding initial and
|
||||
* no-input prompts.
|
||||
* @param {Object=} dialogState JSON object the app uses to hold dialog state that
|
||||
* will be circulated back by App.
|
||||
* @return {(Object|null)} The response that is sent to Assistant to ask user to provide input.
|
||||
* @actionssdk
|
||||
*/
|
||||
ask (inputPrompt, dialogState) {
|
||||
debug('ask: inputPrompt=%s, dialogState=%s',
|
||||
JSON.stringify(inputPrompt), JSON.stringify(dialogState));
|
||||
const expectedIntent = this.buildExpectedIntent_(this.StandardIntents.TEXT, []);
|
||||
if (!expectedIntent) {
|
||||
error('Error in building expected intent');
|
||||
return null;
|
||||
}
|
||||
return this.buildAskHelper_(inputPrompt, [expectedIntent], dialogState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks to collect user's input with a list.
|
||||
*
|
||||
* @example
|
||||
* const app = new ActionsSdkApp({request, response});
|
||||
*
|
||||
* function welcomeIntent (app) {
|
||||
* app.askWithlist('Which of these looks good?',
|
||||
* app.buildList('List title')
|
||||
* .addItems([
|
||||
* app.buildOptionItem(SELECTION_KEY_ONE,
|
||||
* ['synonym of KEY_ONE 1', 'synonym of KEY_ONE 2'])
|
||||
* .setTitle('Number one'),
|
||||
* app.buildOptionItem(SELECTION_KEY_TWO,
|
||||
* ['synonym of KEY_TWO 1', 'synonym of KEY_TWO 2'])
|
||||
* .setTitle('Number two'),
|
||||
* ]));
|
||||
* }
|
||||
*
|
||||
* function optionIntent (app) {
|
||||
* if (app.getSelectedOption() === SELECTION_KEY_ONE) {
|
||||
* app.tell('Number one is a great choice!');
|
||||
* } else {
|
||||
* app.tell('Number two is a great choice!');
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* const actionMap = new Map();
|
||||
* actionMap.set(app.StandardIntents.TEXT, welcomeIntent);
|
||||
* actionMap.set(app.StandardIntents.OPTION, optionIntent);
|
||||
* app.handleRequest(actionMap);
|
||||
*
|
||||
* @param {Object|SimpleResponse|RichResponse} inputPrompt Holding initial and
|
||||
* no-input prompts. Cannot contain basic card.
|
||||
* @param {List} list List built with {@link AssistantApp#buildList|buildList}.
|
||||
* @param {Object=} dialogState JSON object the app uses to hold dialog state that
|
||||
* will be circulated back by Assistant.
|
||||
* @return {(Object|null)} The response that is sent to Assistant to ask user to provide input.
|
||||
* @actionssdk
|
||||
*/
|
||||
askWithList (inputPrompt, list, dialogState) {
|
||||
debug('askWithList: inputPrompt=%s, list=%s, dialogState=%s',
|
||||
JSON.stringify(inputPrompt), JSON.stringify(list), JSON.stringify(dialogState));
|
||||
if (!list || typeof list !== 'object') {
|
||||
this.handleError_('Invalid list');
|
||||
return null;
|
||||
}
|
||||
if (list.items.length < 2) {
|
||||
this.handleError_('List requires at least 2 items');
|
||||
return null;
|
||||
}
|
||||
const expectedIntent = this.buildExpectedIntent_(this.StandardIntents.OPTION, []);
|
||||
if (!expectedIntent) {
|
||||
error('Error in building expected intent');
|
||||
return null;
|
||||
}
|
||||
if (this.isNotApiVersionOne_()) {
|
||||
expectedIntent.inputValueData = Object.assign({
|
||||
[this.ANY_TYPE_PROPERTY_]: this.InputValueDataTypes_.OPTION
|
||||
}, {
|
||||
listSelect: list
|
||||
});
|
||||
} else {
|
||||
expectedIntent.inputValueSpec = {
|
||||
optionValueSpec: {
|
||||
listSelect: list
|
||||
}
|
||||
};
|
||||
}
|
||||
return this.buildAskHelper_(inputPrompt, [expectedIntent], dialogState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks to collect user's input with a carousel.
|
||||
*
|
||||
* @example
|
||||
* const app = new ActionsSdkApp({request, response});
|
||||
*
|
||||
* function welcomeIntent (app) {
|
||||
* app.askWithCarousel('Which of these looks good?',
|
||||
* app.buildCarousel()
|
||||
* .addItems([
|
||||
* app.buildOptionItem(SELECTION_KEY_ONE,
|
||||
* ['synonym of KEY_ONE 1', 'synonym of KEY_ONE 2'])
|
||||
* .setTitle('Number one'),
|
||||
* app.buildOptionItem(SELECTION_KEY_TWO,
|
||||
* ['synonym of KEY_TWO 1', 'synonym of KEY_TWO 2'])
|
||||
* .setTitle('Number two'),
|
||||
* ]));
|
||||
* }
|
||||
*
|
||||
* function optionIntent (app) {
|
||||
* if (app.getSelectedOption() === SELECTION_KEY_ONE) {
|
||||
* app.tell('Number one is a great choice!');
|
||||
* } else {
|
||||
* app.tell('Number two is a great choice!');
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* const actionMap = new Map();
|
||||
* actionMap.set(app.StandardIntents.TEXT, welcomeIntent);
|
||||
* actionMap.set(app.StandardIntents.OPTION, optionIntent);
|
||||
* app.handleRequest(actionMap);
|
||||
*
|
||||
* @param {Object|SimpleResponse|RichResponse} inputPrompt Holding initial and
|
||||
* no-input prompts. Cannot contain basic card.
|
||||
* @param {Carousel} carousel Carousel built with
|
||||
* {@link AssistantApp#buildCarousel|buildCarousel}.
|
||||
* @param {Object=} dialogState JSON object the app uses to hold dialog state that
|
||||
* will be circulated back by Assistant.
|
||||
* @return {(Object|null)} The response that is sent to Assistant to ask user to provide input.
|
||||
* @actionssdk
|
||||
*/
|
||||
askWithCarousel (inputPrompt, carousel, dialogState) {
|
||||
debug('askWithCarousel: inputPrompt=%s, carousel=%s, dialogState=%s',
|
||||
JSON.stringify(inputPrompt), JSON.stringify(carousel), JSON.stringify(dialogState));
|
||||
if (!carousel || typeof carousel !== 'object') {
|
||||
this.handleError_('Invalid carousel');
|
||||
return null;
|
||||
}
|
||||
if (carousel.items.length < 2) {
|
||||
this.handleError_('Carousel requires at least 2 items');
|
||||
return null;
|
||||
}
|
||||
const expectedIntent = this.buildExpectedIntent_(this.StandardIntents.OPTION, []);
|
||||
if (!expectedIntent) {
|
||||
error('Error in building expected intent');
|
||||
return null;
|
||||
}
|
||||
if (this.isNotApiVersionOne_()) {
|
||||
expectedIntent.inputValueData = Object.assign({
|
||||
[this.ANY_TYPE_PROPERTY_]: this.InputValueDataTypes_.OPTION
|
||||
}, {
|
||||
carouselSelect: carousel
|
||||
});
|
||||
} else {
|
||||
expectedIntent.inputValueSpec = {
|
||||
optionValueSpec: {
|
||||
carouselSelect: carousel
|
||||
}
|
||||
};
|
||||
}
|
||||
return this.buildAskHelper_(inputPrompt, [expectedIntent], dialogState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells Assistant to render the speech response and close the mic.
|
||||
*
|
||||
* @example
|
||||
* const app = new ActionsSdkApp({request: request, response: response});
|
||||
*
|
||||
* function mainIntent (app) {
|
||||
* const inputPrompt = app.buildInputPrompt(true, '<speak>Hi! <break time="1"/> ' +
|
||||
* 'I can read out an ordinal like ' +
|
||||
* '<say-as interpret-as="ordinal">123</say-as>. Say a number.</speak>',
|
||||
* ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']);
|
||||
* app.ask(inputPrompt);
|
||||
* }
|
||||
*
|
||||
* function rawInput (app) {
|
||||
* if (app.getRawInput() === 'bye') {
|
||||
* app.tell('Goodbye!');
|
||||
* } else {
|
||||
* const inputPrompt = app.buildInputPrompt(true, '<speak>You said, <say-as interpret-as="ordinal">' +
|
||||
* app.getRawInput() + '</say-as></speak>',
|
||||
* ['I didn\'t hear a number', 'If you\'re still there, what\'s the number?', 'What is the number?']);
|
||||
* app.ask(inputPrompt);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* const actionMap = new Map();
|
||||
* actionMap.set(app.StandardIntents.MAIN, mainIntent);
|
||||
* actionMap.set(app.StandardIntents.TEXT, rawInput);
|
||||
*
|
||||
* app.handleRequest(actionMap);
|
||||
*
|
||||
* @param {string|SimpleResponse|RichResponse} textToSpeech Final response.
|
||||
* Spoken response can be SSML.
|
||||
* @return {(Object|null)} The HTTP response that is sent back to Assistant.
|
||||
* @actionssdk
|
||||
*/
|
||||
tell (textToSpeech) {
|
||||
debug('tell: textToSpeech=%s', textToSpeech);
|
||||
if (!textToSpeech) {
|
||||
this.handleError_('Invalid speech response');
|
||||
return null;
|
||||
}
|
||||
const finalResponse = {};
|
||||
if (typeof textToSpeech === 'string') {
|
||||
if (this.isSsml_(textToSpeech)) {
|
||||
finalResponse.speechResponse = {
|
||||
ssml: textToSpeech
|
||||
};
|
||||
} else {
|
||||
finalResponse.speechResponse = {
|
||||
textToSpeech: textToSpeech
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (textToSpeech.items) {
|
||||
finalResponse.richResponse = textToSpeech;
|
||||
} else if (textToSpeech.speech) {
|
||||
finalResponse.richResponse = this.buildRichResponse()
|
||||
.addSimpleResponse(textToSpeech);
|
||||
} else {
|
||||
this.handleError_('Invalid speech response. Must be string, ' +
|
||||
'RichResponse or SimpleResponse.');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const response = this.buildResponseHelper_(null, false, null, finalResponse);
|
||||
return this.doResponse_(response, RESPONSE_CODE_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the {@link https://developers.google.com/actions/reference/conversation#InputPrompt|InputPrompt object}
|
||||
* from initial prompt and no-input prompts.
|
||||
*
|
||||
* The App needs one initial prompt to start the conversation. If there is no user response,
|
||||
* the App re-opens the mic and renders the no-input prompts three times
|
||||
* (one for each no-input prompt that was configured) to help the user
|
||||
* provide the right response.
|
||||
*
|
||||
* Note: we highly recommend app to provide all the prompts required here in order to ensure a
|
||||
* good user experience.
|
||||
*
|
||||
* @example
|
||||
* const inputPrompt = app.buildInputPrompt(false, 'Welcome to action snippets! Say a number.',
|
||||
* ['Say any number', 'Pick a number', 'What is the number?']);
|
||||
* app.ask(inputPrompt);
|
||||
*
|
||||
* @param {boolean} isSsml Indicates whether the text to speech is SSML or not.
|
||||
* @param {string} initialPrompt The initial prompt the App asks the user.
|
||||
* @param {Array<string>=} noInputs Array of re-prompts when the user does not respond (max 3).
|
||||
* @return {Object} An {@link https://developers.google.com/actions/reference/conversation#InputPrompt|InputPrompt object}.
|
||||
* @actionssdk
|
||||
*/
|
||||
buildInputPrompt (isSsml, initialPrompt, noInputs) {
|
||||
debug('buildInputPrompt: isSsml=%s, initialPrompt=%s, noInputs=%s',
|
||||
isSsml, initialPrompt, noInputs);
|
||||
const initials = [];
|
||||
|
||||
if (noInputs) {
|
||||
if (noInputs.length > INPUTS_MAX) {
|
||||
error('Invalid number of no inputs');
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
noInputs = [];
|
||||
}
|
||||
|
||||
this.maybeAddItemToArray_(initialPrompt, initials);
|
||||
if (isSsml) {
|
||||
return {
|
||||
initialPrompts: this.buildPromptsFromSsmlHelper_(initials),
|
||||
noInputPrompts: this.buildPromptsFromSsmlHelper_(noInputs)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
initialPrompts: this.buildPromptsFromPlainTextHelper_(initials),
|
||||
noInputPrompts: this.buildPromptsFromPlainTextHelper_(noInputs)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get the top most Input object.
|
||||
*
|
||||
* @return {Object} Input object.
|
||||
* @private
|
||||
* @actionssdk
|
||||
*/
|
||||
getTopInput_ () {
|
||||
debug('getTopInput_');
|
||||
if (!this.body_.inputs || this.body_.inputs.length === 0) {
|
||||
error('Missing inputs from request body');
|
||||
return null;
|
||||
}
|
||||
return this.body_.inputs[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the response to send back to Assistant.
|
||||
*
|
||||
* @param {string} conversationToken The dialog state.
|
||||
* @param {boolean} expectUserResponse The expected user response.
|
||||
* @param {Object} expectedInput The expected response.
|
||||
* @param {boolean} finalResponse The final response.
|
||||
* @return {Object} Final response returned to server.
|
||||
* @private
|
||||
* @actionssdk
|
||||
*/
|
||||
buildResponseHelper_ (conversationToken, expectUserResponse, expectedInput, finalResponse) {
|
||||
debug('buildResponseHelper_: conversationToken=%s, expectUserResponse=%s, ' +
|
||||
'expectedInput=%s, finalResponse=%s',
|
||||
conversationToken, expectUserResponse, JSON.stringify(expectedInput),
|
||||
JSON.stringify(finalResponse));
|
||||
const response = {};
|
||||
if (conversationToken) {
|
||||
response.conversationToken = conversationToken;
|
||||
}
|
||||
response.expectUserResponse = expectUserResponse;
|
||||
if (expectedInput) {
|
||||
response.expectedInputs = expectedInput;
|
||||
}
|
||||
if (!expectUserResponse && finalResponse) {
|
||||
response.finalResponse = finalResponse;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to add item to an array.
|
||||
*
|
||||
* @param {*} item Item to add to the array.
|
||||
* @param {Array} array Target array.
|
||||
* @return {undefined}
|
||||
* @private
|
||||
* @actionssdk
|
||||
*/
|
||||
maybeAddItemToArray_ (item, array) {
|
||||
debug('maybeAddItemToArray_: item=%s, array=%s', item, array);
|
||||
if (!array) {
|
||||
error('Invalid array');
|
||||
return;
|
||||
}
|
||||
if (!item) {
|
||||
// ignore add
|
||||
return;
|
||||
}
|
||||
array.push(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract session data from the incoming JSON request.
|
||||
*
|
||||
* @return {undefined}
|
||||
* @private
|
||||
* @actionssdk
|
||||
*/
|
||||
extractData_ () {
|
||||
debug('extractData_');
|
||||
if (this.body_.conversation &&
|
||||
this.body_.conversation.conversationToken) {
|
||||
const json = JSON.parse(this.body_.conversation.conversationToken);
|
||||
this.data = json.data;
|
||||
this.state = json.state;
|
||||
} else {
|
||||
this.data = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a PermissionsValueSpec object to construct and send a
|
||||
* permissions request to user.
|
||||
*
|
||||
* @param {Object} permissionsSpec PermissionsValueSpec object containing
|
||||
* the permissions prefix and the permissions requested.
|
||||
* @param {Object} dialogState JSON object the app uses to hold dialog state that
|
||||
* will be circulated back by Assistant.
|
||||
* @return {Object} HTTP response object.
|
||||
* @private
|
||||
* @actionssdk
|
||||
*/
|
||||
fulfillPermissionsRequest_ (permissionsSpec, dialogState) {
|
||||
debug('fulfillPermissionsRequest_: permissionsSpec=%s, dialogState=%s',
|
||||
JSON.stringify(permissionsSpec), JSON.stringify(dialogState));
|
||||
if (this.isNotApiVersionOne_()) {
|
||||
return this.fulfillSystemIntent_(this.StandardIntents.PERMISSION,
|
||||
this.InputValueDataTypes_.PERMISSION, permissionsSpec,
|
||||
'PLACEHOLDER_FOR_PERMISSION', dialogState);
|
||||
} else {
|
||||
// Build an Expected Intent object.
|
||||
const expectedIntent = {
|
||||
intent: this.StandardIntents.PERMISSION
|
||||
};
|
||||
expectedIntent.inputValueSpec = {
|
||||
permissionValueSpec: permissionsSpec
|
||||
};
|
||||
const inputPrompt = this.buildInputPrompt(false,
|
||||
'PLACEHOLDER_FOR_PERMISSION');
|
||||
return this.buildAskHelper_(inputPrompt, [expectedIntent], dialogState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a given intent spec to construct and send a non-TEXT intent response
|
||||
* to Google.
|
||||
*
|
||||
* @param {string} intent Name of the intent to fulfill. One of
|
||||
* {@link AssistantApp#StandardIntents|StandardIntents}.
|
||||
* @param {string} specType Type of the related intent spec. One of
|
||||
* {@link AssistantApp#InputValueDataTypes_|InputValueDataTypes_}.
|
||||
* @param {Object} intentSpec Intent Spec object. Pass null to leave empty.
|
||||
* @param {string=} promptPlaceholder Some placeholder text for the response
|
||||
* prompt.
|
||||
* @param {Object=} dialogState JSON object the app uses to hold dialog state that
|
||||
* will be circulated back by Assistant.
|
||||
* @return {Object} HTTP response.
|
||||
* @private
|
||||
* @actionssdk
|
||||
*/
|
||||
fulfillSystemIntent_ (intent, specType, intentSpec, promptPlaceholder,
|
||||
dialogState) {
|
||||
debug('fulfillSystemIntent_: intent=%s, specType=%s, intentSpec=%s, ' +
|
||||
'promptPlaceholder=%s dialogState=%s', intent, specType,
|
||||
JSON.stringify(intentSpec), promptPlaceholder, JSON.stringify(dialogState));
|
||||
// Build an Expected Intent object.
|
||||
const expectedIntent = this.buildExpectedIntent_(intent);
|
||||
if (!expectedIntent) {
|
||||
error('Error in building expected intent');
|
||||
return null;
|
||||
}
|
||||
expectedIntent.inputValueData = {};
|
||||
if (intentSpec) {
|
||||
expectedIntent.inputValueData = Object.assign({
|
||||
[this.ANY_TYPE_PROPERTY_]: specType
|
||||
}, intentSpec);
|
||||
}
|
||||
// Send an Ask request to Assistant.
|
||||
const inputPrompt = this.buildInputPrompt(false, promptPlaceholder ||
|
||||
'PLACEHOLDER_FOR_INTENT');
|
||||
return this.buildAskHelper_(inputPrompt, [expectedIntent], dialogState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the ask response to send back to Assistant.
|
||||
*
|
||||
* @param {Object} inputPrompt Holding initial and no-input prompts.
|
||||
* @param {Array} possibleIntents Array of ExpectedIntents.
|
||||
* @param {Object} dialogState JSON object the app uses to hold dialog state that
|
||||
* will be circulated back by Assistant.
|
||||
* @return {(Object|null)} The response that is sent to Assistant to ask user to provide input.
|
||||
* @private
|
||||
* @actionssdk
|
||||
*/
|
||||
buildAskHelper_ (inputPrompt, possibleIntents, dialogState) {
|
||||
debug('buildAskHelper_: inputPrompt=%s, possibleIntents=%s, dialogState=%s',
|
||||
inputPrompt, possibleIntents, JSON.stringify(dialogState));
|
||||
if (!inputPrompt) {
|
||||
error('Invalid input prompt');
|
||||
return null;
|
||||
}
|
||||
if (typeof inputPrompt === 'string') {
|
||||
inputPrompt = this.buildInputPrompt(this.isSsml_(inputPrompt), inputPrompt);
|
||||
} else {
|
||||
if (inputPrompt.speech) {
|
||||
inputPrompt = { richInitialPrompt: this.buildRichResponse()
|
||||
.addSimpleResponse(inputPrompt) };
|
||||
} else if (inputPrompt.items) {
|
||||
inputPrompt = { richInitialPrompt: inputPrompt };
|
||||
}
|
||||
}
|
||||
if (!dialogState) {
|
||||
dialogState = {
|
||||
'state': (this.state instanceof State ? this.state.getName() : this.state),
|
||||
'data': this.data
|
||||
};
|
||||
} else if (Array.isArray(dialogState)) {
|
||||
error('Invalid dialog state');
|
||||
return null;
|
||||
}
|
||||
const expectedInputs = [{
|
||||
inputPrompt: inputPrompt,
|
||||
possibleIntents: possibleIntents
|
||||
}];
|
||||
const response = this.buildResponseHelper_(
|
||||
JSON.stringify(dialogState),
|
||||
true, // expectedUserResponse
|
||||
expectedInputs,
|
||||
null // finalResponse is null b/c dialog is active
|
||||
);
|
||||
return this.doResponse_(response, RESPONSE_CODE_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an ExpectedIntent object. Refer to {@link ActionsSdkApp#newRuntimeEntity} to create the list
|
||||
* of runtime entities required by this method. Runtime entities need to be defined in
|
||||
* the Action Package.
|
||||
*
|
||||
* @param {string} intent Developer specified in-dialog intent inside the Action
|
||||
* Package or an App built-in intent like
|
||||
* 'assistant.intent.action.TEXT'.
|
||||
* @return {Object} An {@link https://developers.google.com/actions/reference/conversation#ExpectedIntent|ExpectedIntent object}
|
||||
encapsulating the intent and the runtime entities.
|
||||
* @private
|
||||
* @actionssdk
|
||||
*/
|
||||
buildExpectedIntent_ (intent) {
|
||||
debug('buildExpectedIntent_: intent=%s', intent);
|
||||
if (!intent || intent === '') {
|
||||
error('Invalid intent');
|
||||
return null;
|
||||
}
|
||||
const expectedIntent = { intent };
|
||||
return expectedIntent;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ActionsSdkApp;
|
||||
2262
types/actions-on-google/staging/assistant-app.js
Normal file
2262
types/actions-on-google/staging/assistant-app.js
Normal file
File diff suppressed because it is too large
Load Diff
1105
types/actions-on-google/staging/dialogflow-app.js
Normal file
1105
types/actions-on-google/staging/dialogflow-app.js
Normal file
File diff suppressed because it is too large
Load Diff
800
types/actions-on-google/staging/response-builder.js
Normal file
800
types/actions-on-google/staging/response-builder.js
Normal file
@@ -0,0 +1,800 @@
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A collection of response builders.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Debug = require('debug');
|
||||
const debug = Debug('actions-on-google:debug');
|
||||
const warn = Debug('actions-on-google:warn');
|
||||
const error = Debug('actions-on-google:error');
|
||||
|
||||
const LIST_ITEM_LIMIT = 30;
|
||||
const CAROUSEL_ITEM_LIMIT = 10;
|
||||
|
||||
/**
|
||||
* Simple Response type.
|
||||
* @typedef {Object} SimpleResponse
|
||||
* @property {string} speech - Speech to be spoken to user. SSML allowed.
|
||||
* @property {string} displayText - Optional text to be shown to user
|
||||
*/
|
||||
|
||||
/**
|
||||
* Suggestions to show with response.
|
||||
* @typedef {Object} Suggestion
|
||||
* @property {string} title - Text of the suggestion.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Link Out Suggestion. Used in rich response as a suggestion chip which, when
|
||||
* selected, links out to external URL.
|
||||
* @typedef {Object} LinkOutSuggestion
|
||||
* @property {string} title - Text shown on the suggestion chip.
|
||||
* @property {string} url - String URL to open.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Image type shown on visual elements.
|
||||
* @typedef {Object} Image
|
||||
* @property {string} url - Image source URL.
|
||||
* @property {string} accessibilityText - Text to replace for image for
|
||||
* accessibility.
|
||||
* @property {number} width - Width of the image.
|
||||
* @property {number} height - Height of the image.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Basic Card Button. Shown below basic cards. Open a URL when selected.
|
||||
* @typedef {Object} Button
|
||||
* @property {string} title - Text shown on the button.
|
||||
* @property {Object} openUrlAction - Action to take when selected.
|
||||
* @property {string} openUrlAction.url - String URL to open.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Option item. Used in actions.intent.OPTION intent.
|
||||
* @typedef {Object} OptionItem
|
||||
* @property {OptionInfo} optionInfo - Option item identifier information.
|
||||
* @property {string} title - Name of the item.
|
||||
* @property {string} description - Optional text describing the item.
|
||||
* @property {Image} image - Square image to show for this item.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Option info. Provides unique identifier for a given OptionItem.
|
||||
* @typedef {Object} OptionInfo
|
||||
* @property {string} key - Unique string ID for this option.
|
||||
* @property {Array<string>} synonyms - Synonyms that can be used by the user
|
||||
* to indicate this option if they do not use the key.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class for initializing and constructing Rich Responses with chainable interface.
|
||||
*/
|
||||
const RichResponse = class {
|
||||
/**
|
||||
* Constructor for RichResponse. Accepts optional RichResponse to clone.
|
||||
*
|
||||
* @param {RichResponse=} richResponse Optional RichResponse to clone.
|
||||
*/
|
||||
constructor (richResponse) {
|
||||
/**
|
||||
* Ordered list of either SimpleResponse objects or BasicCard objects.
|
||||
* First item must be SimpleResponse. There can be at most one card.
|
||||
* @type {Array<SimpleResponse|BasicCard>}
|
||||
*/
|
||||
this.items = [];
|
||||
|
||||
/**
|
||||
* Ordered list of text suggestions to display. Optional.
|
||||
* @type {Array<Suggestion>}
|
||||
*/
|
||||
this.suggestions = [];
|
||||
|
||||
/**
|
||||
* Link Out Suggestion chip for this rich response. Optional.
|
||||
* @type {LinkOutSuggestion}
|
||||
*/
|
||||
this.linkOutSuggestion = undefined;
|
||||
|
||||
if (richResponse) {
|
||||
if (richResponse.items) {
|
||||
this.items = richResponse.items;
|
||||
for (let item of this.items) {
|
||||
if (item.basicCard) {
|
||||
item.basicCard = new BasicCard(item.basicCard);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (richResponse.suggestions) {
|
||||
this.suggestions = richResponse.suggestions;
|
||||
}
|
||||
if (richResponse.linkOutSuggestion) {
|
||||
this.linkOutSuggestion = richResponse.linkOutSuggestion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a SimpleResponse to list of items.
|
||||
*
|
||||
* @param {string|SimpleResponse} simpleResponse Simple response to present to
|
||||
* user. If just a string, display text will not be set.
|
||||
* @return {RichResponse} Returns current constructed RichResponse.
|
||||
*/
|
||||
addSimpleResponse (simpleResponse) {
|
||||
if (!simpleResponse) {
|
||||
error('Invalid simpleResponse');
|
||||
return this;
|
||||
}
|
||||
// Validate if RichResponse already contains two SimpleResponse objects
|
||||
let simpleResponseCount = 0;
|
||||
for (let item of this.items) {
|
||||
if (item.simpleResponse) {
|
||||
simpleResponseCount++;
|
||||
}
|
||||
if (simpleResponseCount >= 2) {
|
||||
error('Cannot include >2 SimpleResponses in RichResponse');
|
||||
return this;
|
||||
}
|
||||
}
|
||||
const simpleResponseObj = {
|
||||
simpleResponse: this.buildSimpleResponseHelper_(simpleResponse)
|
||||
};
|
||||
// Check first if needs to replace BasicCard at beginning of items list
|
||||
if (this.items.length > 0 && (this.items[0].basicCard ||
|
||||
this.items[0].structuredResponse)) {
|
||||
this.items.unshift(simpleResponseObj);
|
||||
} else {
|
||||
this.items.push(simpleResponseObj);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a BasicCard to list of items.
|
||||
*
|
||||
* @param {BasicCard} basicCard Basic card to include in response.
|
||||
* @return {RichResponse} Returns current constructed RichResponse.
|
||||
*/
|
||||
addBasicCard (basicCard) {
|
||||
if (!basicCard) {
|
||||
error('Invalid basicCard');
|
||||
return this;
|
||||
}
|
||||
// Validate if basic card is already present
|
||||
for (let item of this.items) {
|
||||
if (item.basicCard) {
|
||||
error('Cannot include >1 BasicCard in RichResponse');
|
||||
return this;
|
||||
}
|
||||
}
|
||||
this.items.push({
|
||||
basicCard: basicCard
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single suggestion or list of suggestions to list of items.
|
||||
*
|
||||
* @param {string|Array<string>} suggestions Either a single string suggestion
|
||||
* or list of suggestions to add.
|
||||
* @return {RichResponse} Returns current constructed RichResponse.
|
||||
*/
|
||||
addSuggestions (suggestions) {
|
||||
if (!suggestions) {
|
||||
error('Invalid suggestions');
|
||||
return this;
|
||||
}
|
||||
if (Array.isArray(suggestions)) {
|
||||
for (let suggestion of suggestions) {
|
||||
if (this.isValidSuggestionText(suggestion)) {
|
||||
this.suggestions.push({title: suggestion});
|
||||
} else {
|
||||
warn('Suggestion text can\'t be longer than 25 characters: ' + suggestion +
|
||||
'. This suggestion won\'t be added to the list.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.isValidSuggestionText(suggestions)) {
|
||||
this.suggestions.push({title: suggestions});
|
||||
} else {
|
||||
warn('Suggestion text can\'t be longer than 25 characters: ' + suggestions +
|
||||
'. This suggestion won\'t be added to the list.');
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given suggestion text is valid to be added to the suggestion list. A valid
|
||||
* text string is not longer than 25 characters.
|
||||
*
|
||||
* @param {string} suggestionText Text to validate as suggestion.
|
||||
* @return {boolean} True if the text is valid, false otherwise.s
|
||||
*/
|
||||
isValidSuggestionText (suggestionText) {
|
||||
return suggestionText && suggestionText.length && suggestionText.length <= 25;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the suggestion link for this rich response.
|
||||
*
|
||||
* @param {string} destinationName Name of the link out destination.
|
||||
* @param {string} suggestionUrl - String URL to open when suggestion is used.
|
||||
* @return {RichResponse} Returns current constructed RichResponse.
|
||||
*/
|
||||
addSuggestionLink (destinationName, suggestionUrl) {
|
||||
if (!destinationName) {
|
||||
error('destinationName cannot be empty');
|
||||
return this;
|
||||
}
|
||||
if (!suggestionUrl) {
|
||||
error('suggestionUrl cannot be empty');
|
||||
return this;
|
||||
}
|
||||
this.linkOutSuggestion = {
|
||||
destinationName: destinationName,
|
||||
url: suggestionUrl
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an order update to this response. Use after a successful transaction
|
||||
* decision to confirm the order.
|
||||
*
|
||||
* @param {OrderUpdate} orderUpdate OrderUpdate object to add.
|
||||
* @return {RichResponse} Returns current constructed RichResponse.
|
||||
*/
|
||||
addOrderUpdate (orderUpdate) {
|
||||
if (!orderUpdate) {
|
||||
error('Invalid orderUpdate');
|
||||
return this;
|
||||
}
|
||||
// Validate if RichResponse already contains StructuredResponse object
|
||||
for (let item of this.items) {
|
||||
if (item.structuredResponse) {
|
||||
debug('Cannot include >1 StructuredResponses in RichResponse');
|
||||
return this;
|
||||
}
|
||||
}
|
||||
this.items.push({
|
||||
structuredResponse: {
|
||||
orderUpdate: orderUpdate
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to build SimpleResponse from speech and display text.
|
||||
*
|
||||
* @param {string|SimpleResponse} response String to speak, or SimpleResponse.
|
||||
* SSML allowed.
|
||||
* @param {string} response.speech If using SimpleResponse, speech to be spoken
|
||||
* to user.
|
||||
* @param {string=} response.displayText If using SimpleResponse, text to be shown
|
||||
* to user.
|
||||
* @return {Object} Appropriate SimpleResponse object.
|
||||
* @private
|
||||
*/
|
||||
buildSimpleResponseHelper_ (response) {
|
||||
if (!response) {
|
||||
error('Invalid response');
|
||||
return null;
|
||||
}
|
||||
debug('buildSimpleResponseHelper_: response=%s', JSON.stringify(response));
|
||||
let simpleResponseObj = {};
|
||||
if (typeof response === 'string') {
|
||||
simpleResponseObj = isSsml(response)
|
||||
? { ssml: response } : { textToSpeech: response };
|
||||
} else if (response.speech) {
|
||||
simpleResponseObj = isSsml(response.speech)
|
||||
? { ssml: response.speech } : { textToSpeech: response.speech };
|
||||
simpleResponseObj.displayText = response.displayText;
|
||||
} else {
|
||||
error('SimpleResponse requires a speech parameter.');
|
||||
return null;
|
||||
}
|
||||
return simpleResponseObj;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Class for initializing and constructing Basic Cards with chainable interface.
|
||||
*/
|
||||
const BasicCard = class {
|
||||
/**
|
||||
* Constructor for BasicCard. Accepts optional BasicCard to clone.
|
||||
*
|
||||
* @param {BasicCard=} basicCard Optional BasicCard to clone.
|
||||
*/
|
||||
constructor (basicCard) {
|
||||
/**
|
||||
* Title of the card. Optional.
|
||||
* @type {string}
|
||||
*/
|
||||
this.title = undefined;
|
||||
|
||||
/**
|
||||
* Body text to show on the card. Required, unless image is present.
|
||||
* @type {string}
|
||||
*/
|
||||
this.formattedText = '';
|
||||
|
||||
/**
|
||||
* Subtitle of the card. Optional.
|
||||
* @type {string}
|
||||
*/
|
||||
this.subtitle = undefined;
|
||||
|
||||
/**
|
||||
* Image to show on the card. Optional.
|
||||
* @type {Image}
|
||||
*/
|
||||
this.image = undefined;
|
||||
|
||||
/**
|
||||
* Ordered list of buttons to show below card. Optional.
|
||||
* @type {Array<Button>}
|
||||
*/
|
||||
this.buttons = [];
|
||||
|
||||
if (basicCard) {
|
||||
if (basicCard.formattedText) {
|
||||
this.formattedText = basicCard.formattedText;
|
||||
}
|
||||
if (basicCard.buttons) {
|
||||
this.buttons = basicCard.buttons;
|
||||
}
|
||||
if (basicCard.title) {
|
||||
this.title = basicCard.title;
|
||||
}
|
||||
if (basicCard.subtitle) {
|
||||
this.subtitle = basicCard.subtitle;
|
||||
}
|
||||
if (basicCard.image) {
|
||||
this.image = basicCard.image;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title for this Basic Card.
|
||||
*
|
||||
* @param {string} title Title to show on card.
|
||||
* @return {BasicCard} Returns current constructed BasicCard.
|
||||
*/
|
||||
setTitle (title) {
|
||||
if (!title) {
|
||||
error('title cannot be empty');
|
||||
return this;
|
||||
}
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the subtitle for this Basic Card.
|
||||
*
|
||||
* @param {string} subtitle Subtitle to show on card.
|
||||
* @return {BasicCard} Returns current constructed BasicCard.
|
||||
*/
|
||||
setSubtitle (subtitle) {
|
||||
if (!subtitle) {
|
||||
error('subtitle cannot be empty');
|
||||
return this;
|
||||
}
|
||||
this.subtitle = subtitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the body text for this Basic Card.
|
||||
*
|
||||
* @param {string} bodyText Body text to show on card.
|
||||
* @return {BasicCard} Returns current constructed BasicCard.
|
||||
*/
|
||||
setBodyText (bodyText) {
|
||||
if (!bodyText) {
|
||||
error('bodyText cannot be empty');
|
||||
return this;
|
||||
}
|
||||
this.formattedText = bodyText;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the image for this Basic Card.
|
||||
*
|
||||
* @param {string} url Image source URL.
|
||||
* @param {string} accessibilityText Text to replace for image for
|
||||
* accessibility.
|
||||
* @param {number=} width Width of the image.
|
||||
* @param {number=} height Height of the image.
|
||||
* @return {BasicCard} Returns current constructed BasicCard.
|
||||
*/
|
||||
setImage (url, accessibilityText, width, height) {
|
||||
if (!url) {
|
||||
error('url cannot be empty');
|
||||
return this;
|
||||
}
|
||||
if (!accessibilityText) {
|
||||
error('accessibilityText cannot be empty');
|
||||
return this;
|
||||
}
|
||||
this.image = { url: url, accessibilityText: accessibilityText };
|
||||
if (width) {
|
||||
this.image.width = width;
|
||||
}
|
||||
if (height) {
|
||||
this.image.height = height;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a button below card.
|
||||
*
|
||||
* @param {string} text Text to show on button.
|
||||
* @param {string} url URL to open when button is selected.
|
||||
* @return {BasicCard} Returns current constructed BasicCard.
|
||||
*/
|
||||
addButton (text, url) {
|
||||
if (!text) {
|
||||
error('text cannot be empty');
|
||||
return this;
|
||||
}
|
||||
if (!url) {
|
||||
error('url cannot be empty');
|
||||
return this;
|
||||
}
|
||||
this.buttons.push({
|
||||
title: text,
|
||||
openUrlAction: {
|
||||
url: url
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Class for initializing and constructing Lists with chainable interface.
|
||||
*/
|
||||
const List = class {
|
||||
/**
|
||||
* Constructor for List. Accepts optional List to clone, string title, or
|
||||
* list of items to copy.
|
||||
*
|
||||
* @param {(List|string|Array<OptionItem>)=} list Either a list to clone, a title
|
||||
* to set for a new List, or an array of OptionItem to initialize a new
|
||||
* list.
|
||||
*/
|
||||
constructor (list) {
|
||||
/**
|
||||
* Title of the list. Optional.
|
||||
* @type {string}
|
||||
*/
|
||||
this.title = undefined;
|
||||
|
||||
/**
|
||||
* List of 2-20 items to show in this list. Required.
|
||||
* @type {Array<OptionItems>}
|
||||
*/
|
||||
this.items = [];
|
||||
|
||||
if (list) {
|
||||
if (typeof list === 'string') {
|
||||
this.title = list;
|
||||
} else if (Array.isArray(list)) {
|
||||
for (let item of list) {
|
||||
this.items.push(new OptionItem(item));
|
||||
}
|
||||
} else if (typeof list === 'object') {
|
||||
if (list.title) {
|
||||
this.title = list.title;
|
||||
}
|
||||
if (list.items) {
|
||||
for (let item of list.items) {
|
||||
this.items.push(new OptionItem(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title for this List.
|
||||
*
|
||||
* @param {string} title Title to show on list.
|
||||
* @return {List} Returns current constructed List.
|
||||
*/
|
||||
setTitle (title) {
|
||||
if (!title) {
|
||||
error('title cannot be empty');
|
||||
return this;
|
||||
}
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single item or list of items to the list.
|
||||
*
|
||||
* @param {OptionItem|Array<OptionItem>} optionItems OptionItems to add.
|
||||
* @return {List} Returns current constructed List.
|
||||
*/
|
||||
addItems (optionItems) {
|
||||
if (!optionItems) {
|
||||
error('optionItems cannot be null');
|
||||
return this;
|
||||
}
|
||||
if (Array.isArray(optionItems)) {
|
||||
for (let item of optionItems) {
|
||||
this.items.push(item);
|
||||
}
|
||||
} else {
|
||||
this.items.push(optionItems);
|
||||
}
|
||||
if (this.items.length > LIST_ITEM_LIMIT) {
|
||||
this.items = this.items.slice(0, LIST_ITEM_LIMIT);
|
||||
error('List can have no more than ' + LIST_ITEM_LIMIT +
|
||||
' items');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Class for initializing and constructing Carousel with chainable interface.
|
||||
*/
|
||||
const Carousel = class {
|
||||
/**
|
||||
* Constructor for Carousel. Accepts optional Carousel to clone or list of
|
||||
* items to copy.
|
||||
*
|
||||
* @param {(Carousel|Array<OptionItem>)=} carousel Either a carousel to clone
|
||||
* or an array of OptionItem to initialize a new carousel
|
||||
*/
|
||||
constructor (carousel) {
|
||||
/**
|
||||
* List of 2-20 items to show in this carousel. Required.
|
||||
* @type {Array<OptionItems>}
|
||||
*/
|
||||
this.items = [];
|
||||
|
||||
if (carousel) {
|
||||
if (Array.isArray(carousel)) {
|
||||
for (let item of carousel) {
|
||||
this.items.push(new OptionItem(item));
|
||||
}
|
||||
} else if (typeof carousel === 'object') {
|
||||
if (carousel.items) {
|
||||
for (let item of carousel.items) {
|
||||
this.items.push(new OptionItem(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single item or list of items to the carousel.
|
||||
*
|
||||
* @param {OptionItem|Array<OptionItem>} optionItems OptionItems to add.
|
||||
* @return {Carousel} Returns current constructed Carousel.
|
||||
*/
|
||||
addItems (optionItems) {
|
||||
if (!optionItems) {
|
||||
error('optionItems cannot be null');
|
||||
return this;
|
||||
}
|
||||
if (Array.isArray(optionItems)) {
|
||||
for (let item of optionItems) {
|
||||
this.items.push(item);
|
||||
}
|
||||
} else {
|
||||
this.items.push(optionItems);
|
||||
}
|
||||
if (this.items.length > CAROUSEL_ITEM_LIMIT) {
|
||||
this.items = this.items.slice(0, CAROUSEL_ITEM_LIMIT);
|
||||
error('Carousel can have no more than ' + CAROUSEL_ITEM_LIMIT +
|
||||
' items');
|
||||
}
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Class for initializing and constructing Option Items with chainable interface.
|
||||
*/
|
||||
const OptionItem = class {
|
||||
/**
|
||||
* Constructor for OptionItem. Accepts optional OptionItem to clone.
|
||||
*
|
||||
* @param {OptionItem=} optionItem Optional OptionItem to clone.
|
||||
*/
|
||||
constructor (optionItem) {
|
||||
/**
|
||||
* Option info of the option item. Required.
|
||||
* @type {OptionInfo}
|
||||
*/
|
||||
this.optionInfo = {
|
||||
key: '',
|
||||
synonyms: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Title of the option item. Required.
|
||||
* @type {string}
|
||||
*/
|
||||
this.title = '';
|
||||
|
||||
/**
|
||||
* Description text of the item. Optional.
|
||||
* @type {string}
|
||||
*/
|
||||
this.description = undefined;
|
||||
|
||||
/**
|
||||
* Image to show on item. Optional.
|
||||
* @type {Image}
|
||||
*/
|
||||
this.image = undefined;
|
||||
|
||||
if (optionItem) {
|
||||
if (optionItem.optionInfo) {
|
||||
if (optionItem.optionInfo.key) {
|
||||
this.optionInfo.key = optionItem.optionInfo.key;
|
||||
}
|
||||
if (optionItem.optionInfo.synonyms) {
|
||||
this.optionInfo.synonyms = optionItem.optionInfo.synonyms;
|
||||
}
|
||||
}
|
||||
if (optionItem.title) {
|
||||
this.title = optionItem.title;
|
||||
}
|
||||
if (optionItem.description) {
|
||||
this.description = optionItem.description;
|
||||
}
|
||||
if (optionItem.image) {
|
||||
this.image = optionItem.image;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the title for this Option Item.
|
||||
*
|
||||
* @param {string} title Title to show on item.
|
||||
* @return {OptionItem} Returns current constructed OptionItem.
|
||||
*/
|
||||
setTitle (title) {
|
||||
if (!title) {
|
||||
error('title cannot be empty');
|
||||
return this;
|
||||
}
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description for this Option Item.
|
||||
*
|
||||
* @param {string} description Description to show on item.
|
||||
* @return {OptionItem} Returns current constructed OptionItem.
|
||||
*/
|
||||
setDescription (description) {
|
||||
if (!description) {
|
||||
error('descriptions cannot be empty');
|
||||
return this;
|
||||
}
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the image for this Option Item.
|
||||
*
|
||||
* @param {string} url Image source URL.
|
||||
* @param {string} accessibilityText Text to replace for image for
|
||||
* accessibility.
|
||||
* @param {number=} width Width of the image.
|
||||
* @param {number=} height Height of the image.
|
||||
* @return {OptionItem} Returns current constructed OptionItem.
|
||||
*/
|
||||
setImage (url, accessibilityText, width, height) {
|
||||
if (!url) {
|
||||
error('url cannot be empty');
|
||||
return this;
|
||||
}
|
||||
if (!accessibilityText) {
|
||||
error('accessibilityText cannot be empty');
|
||||
return this;
|
||||
}
|
||||
this.image = { url: url, accessibilityText: accessibilityText };
|
||||
if (width) {
|
||||
this.image.width = width;
|
||||
}
|
||||
if (height) {
|
||||
this.image.height = height;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the key for the OptionInfo of this Option Item. This will be returned
|
||||
* as an argument in the resulting actions.intent.OPTION intent.
|
||||
*
|
||||
* @param {string} key Key to uniquely identify this item.
|
||||
* @return {OptionItem} Returns current constructed OptionItem.
|
||||
*/
|
||||
setKey (key) {
|
||||
if (!key) {
|
||||
error('key cannot be empty');
|
||||
return this;
|
||||
}
|
||||
this.optionInfo.key = key;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single synonym or list of synonyms to item.
|
||||
*
|
||||
* @param {string|Array<string>} synonyms Either a single string synonyms
|
||||
* or list of synonyms to add.
|
||||
* @return {OptionItem} Returns current constructed OptionItem.
|
||||
*/
|
||||
addSynonyms (synonyms) {
|
||||
if (!synonyms) {
|
||||
error('Invalid synonyms');
|
||||
return this;
|
||||
}
|
||||
if (Array.isArray(synonyms)) {
|
||||
for (let synonym of synonyms) {
|
||||
this.optionInfo.synonyms.push(synonym);
|
||||
}
|
||||
} else {
|
||||
this.optionInfo.synonyms.push(synonyms);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if given text contains SSML.
|
||||
*
|
||||
* @param {string} text Text to check.
|
||||
* @return {boolean} True if text contains SSML, false otherwise.
|
||||
*/
|
||||
function isSsml (text) {
|
||||
return /^<speak\b[^>]*>([^]*?)<\/speak>$/gi.test(text);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RichResponse,
|
||||
BasicCard,
|
||||
List,
|
||||
Carousel,
|
||||
OptionItem,
|
||||
isSsml
|
||||
};
|
||||
1439
types/actions-on-google/staging/transactions.js
Normal file
1439
types/actions-on-google/staging/transactions.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user