UNPKG

actions-on-google

Version:
623 lines (593 loc) 18 kB
/** * Copyright 2018 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. */ import * as Api from './api/v2'; import * as ActionsApi from '../actionssdk/api/v2'; import {AppHandler, attach} from '../../assistant'; import { ExceptionHandler, Traversed, Argument, ConversationApp, ConversationAppOptions, UnauthorizedError, } from '../actionssdk'; import * as common from '../../common'; import {Contexts, Parameters} from './context'; import {DialogflowConversation} from './conv'; import {OAuth2Client} from 'google-auth-library'; import {BuiltinFrameworkMetadata} from '../../framework'; /** @public */ export interface DialogflowIntentHandler< TConvData, TUserStorage, TContexts extends Contexts, TConversation extends DialogflowConversation< TConvData, TUserStorage, TContexts >, TParameters extends Parameters, TArgument extends Argument > { /** @public */ ( conv: TConversation, params: TParameters, /** * The first argument value from the current intent. * See {@link Arguments#get|Arguments.get} * Same as `conv.arguments.parsed.list[0]` */ argument: TArgument, /** * The first argument status from the current intent. * See {@link Arguments#status|Arguments.status} * Same as `conv.arguments.status.list[0]` */ status: ActionsApi.GoogleRpcStatus | undefined // tslint:disable-next-line:no-any allow developer to return any just detect if is promise ): Promise<any> | any; } /** @hidden */ export interface DialogflowIntentHandlers { [event: string]: Function | string | undefined; } /** @hidden */ export interface DialogflowHandlers< TConvData, TUserStorage, TContexts extends Contexts, TConversation extends DialogflowConversation< TConvData, TUserStorage, TContexts > > { intents: DialogflowIntentHandlers; catcher: ExceptionHandler<TUserStorage, TConversation>; fallback?: Function | string; } /** @public */ export interface DialogflowMiddleware< TConversationPlugin extends DialogflowConversation > { ( /** @public */ conv: DialogflowConversation, /** @public */ framework: BuiltinFrameworkMetadata ): | (DialogflowConversation & TConversationPlugin) | void | Promise<DialogflowConversation & TConversationPlugin> | Promise<void>; } /** @public */ export type DefaultDialogflowIntent = | 'Default Welcome Intent' | 'Default Fallback Intent'; /** @public */ export interface DialogflowApp< TConvData, TUserStorage, TContexts extends Contexts, TConversation extends DialogflowConversation< TConvData, TUserStorage, TContexts > > extends ConversationApp<TConvData, TUserStorage> { /** @hidden */ _handlers: DialogflowHandlers< TConvData, TUserStorage, TContexts, TConversation >; /** * Sets the IntentHandler to be execute when the fulfillment is called * with a given Dialogflow intent name. * * @param intent The Dialogflow intent name to match. * When given an array, sets the IntentHandler for any intent name in the array. * @param handler The IntentHandler to be executed when the intent name is matched. * When given a string instead of a function, the intent fulfillment will be redirected * to the IntentHandler of the redirected intent name. * @public */ intent<TParameters extends Parameters>( intent: DefaultDialogflowIntent | DefaultDialogflowIntent[], handler: | DialogflowIntentHandler< TConvData, TUserStorage, TContexts, TConversation, TParameters, Argument > | string ): this; /** * Sets the IntentHandler to be execute when the fulfillment is called * with a given Dialogflow intent name. * * @param intent The Dialogflow intent name to match. * When given an array, sets the IntentHandler for any intent name in the array. * @param handler The IntentHandler to be executed when the intent name is matched. * When given a string instead of a function, the intent fulfillment will be redirected * to the IntentHandler of the redirected intent name. * @public */ intent<TArgument extends Argument>( intent: DefaultDialogflowIntent | DefaultDialogflowIntent[], handler: | DialogflowIntentHandler< TConvData, TUserStorage, TContexts, TConversation, Parameters, TArgument > | string ): this; /** * Sets the IntentHandler to be execute when the fulfillment is called * with a given Dialogflow intent name. * * @param intent The Dialogflow intent name to match. * When given an array, sets the IntentHandler for any intent name in the array. * @param handler The IntentHandler to be executed when the intent name is matched. * When given a string instead of a function, the intent fulfillment will be redirected * to the IntentHandler of the redirected intent name. * @public */ intent<TParameters extends Parameters, TArgument extends Argument>( intent: DefaultDialogflowIntent | DefaultDialogflowIntent[], handler: | DialogflowIntentHandler< TConvData, TUserStorage, TContexts, TConversation, TParameters, TArgument > | string ): this; /** * Sets the IntentHandler to be execute when the fulfillment is called * with a given Dialogflow intent name. * * @param intent The Dialogflow intent name to match. * When given an array, sets the IntentHandler for any intent name in the array. * @param handler The IntentHandler to be executed when the intent name is matched. * When given a string instead of a function, the intent fulfillment will be redirected * to the IntentHandler of the redirected intent name. * @public */ intent<TParameters extends Parameters>( intent: string | string[], handler: | DialogflowIntentHandler< TConvData, TUserStorage, TContexts, TConversation, TParameters, Argument > | string ): this; /** * Sets the IntentHandler to be execute when the fulfillment is called * with a given Dialogflow intent name. * * @param intent The Dialogflow intent name to match. * When given an array, sets the IntentHandler for any intent name in the array. * @param handler The IntentHandler to be executed when the intent name is matched. * When given a string instead of a function, the intent fulfillment will be redirected * to the IntentHandler of the redirected intent name. * @public */ intent<TArgument extends Argument>( intent: string | string[], handler: | DialogflowIntentHandler< TConvData, TUserStorage, TContexts, TConversation, Parameters, TArgument > | string ): this; /** * Sets the IntentHandler to be execute when the fulfillment is called * with a given Dialogflow intent name. * * @param intent The Dialogflow intent name to match. * When given an array, sets the IntentHandler for any intent name in the array. * @param handler The IntentHandler to be executed when the intent name is matched. * When given a string instead of a function, the intent fulfillment will be redirected * to the IntentHandler of the redirected intent name. * @public */ intent<TParameters extends Parameters, TArgument extends Argument>( intent: string | string[], handler: | DialogflowIntentHandler< TConvData, TUserStorage, TContexts, TConversation, TParameters, TArgument > | string ): this; /** @public */ catch(catcher: ExceptionHandler<TUserStorage, TConversation>): this; /** @public */ fallback( handler: | DialogflowIntentHandler< TConvData, TUserStorage, TContexts, TConversation, Parameters, Argument > | string ): this; /** @hidden */ _middlewares: DialogflowMiddleware< DialogflowConversation<{}, {}, Contexts> >[]; /** @public */ middleware< TConversationPlugin extends DialogflowConversation<{}, {}, Contexts> >( middleware: DialogflowMiddleware<TConversationPlugin> ): this; /** @public */ verification?: DialogflowVerification | DialogflowVerificationHeaders; } /** @public */ export interface DialogflowVerificationHeaders { /** * A header key value pair to check against. * @public */ [key: string]: string; } /** @public */ export interface DialogflowVerification { /** * An object representing the header key to value map to check against, * @public */ headers: DialogflowVerificationHeaders; /** * Custom status code to return on verification error. * @public */ status?: number; /** * Custom error message as a string or a function that returns a string * given the original error message set by the library. * * The message will get sent back in the JSON top level `error` property. * @public */ error?: string | ((error: string) => string); } /** @public */ export interface DialogflowOptions<TConvData, TUserStorage> extends ConversationAppOptions<TConvData, TUserStorage> { /** * Verifies whether the request comes from Dialogflow. * Uses header keys and values to check against ones specified by the developer * in the Dialogflow Fulfillment settings of the app. * * HTTP Code 403 will be thrown by default on verification error. * * @public */ verification?: DialogflowVerification | DialogflowVerificationHeaders; } /** @public */ export interface Dialogflow { /** @public */ < TConvData, TUserStorage, TContexts extends Contexts = Contexts, Conversation extends DialogflowConversation< TConvData, TUserStorage, TContexts > = DialogflowConversation<TConvData, TUserStorage, TContexts> >( options?: DialogflowOptions<TConvData, TUserStorage> ): AppHandler & DialogflowApp<TConvData, TUserStorage, TContexts, Conversation>; /** @public */ < TContexts extends Contexts, Conversation extends DialogflowConversation< {}, {}, TContexts > = DialogflowConversation<{}, {}, TContexts> >( options?: DialogflowOptions<{}, {}> ): AppHandler & DialogflowApp<{}, {}, TContexts, Conversation>; /** @public */ < TConversation extends DialogflowConversation< {}, {} > = DialogflowConversation<{}, {}> >( options?: DialogflowOptions<{}, {}> ): AppHandler & DialogflowApp<{}, {}, Contexts, TConversation>; } const isVerification = ( verification: DialogflowVerification | DialogflowVerificationHeaders ): verification is DialogflowVerification => typeof (verification as DialogflowVerification).headers === 'object'; /** * This is the function that creates the app instance which on new requests, * creates a way to handle the communication with Dialogflow's fulfillment API. * * Supports Dialogflow v1 and v2. * * @example * ```javascript * * const app = dialogflow() * * app.intent('Default Welcome Intent', conv => { * conv.ask('How are you?') * }) * ``` * * @public */ export const dialogflow: Dialogflow = < TConvData, TUserStorage, TContexts extends Contexts, TConversation extends DialogflowConversation< TConvData, TUserStorage, TContexts > >( options: DialogflowOptions<TConvData, TUserStorage> = {} ) => attach<DialogflowApp<TConvData, TUserStorage, TContexts, TConversation>>( { _handlers: { intents: {}, catcher: (conv, e) => { throw e; }, }, _middlewares: [], intent<TParameters extends Parameters, TArgument extends Argument>( this: DialogflowApp<TConvData, TUserStorage, TContexts, TConversation>, intents: string | string[], handler: DialogflowIntentHandler< TConvData, TUserStorage, TContexts, TConversation, TParameters, TArgument > ) { for (const intent of common.toArray(intents)) { this._handlers.intents[intent] = handler; } return this; }, catch( this: DialogflowApp<TConvData, TUserStorage, TContexts, TConversation>, catcher ) { this._handlers.catcher = catcher; return this; }, fallback( this: DialogflowApp<TConvData, TUserStorage, TContexts, TConversation>, handler ) { this._handlers.fallback = handler; return this; }, middleware( this: DialogflowApp<TConvData, TUserStorage, TContexts, TConversation>, middleware ) { this._middlewares.push(middleware); return this; }, init: options.init, verification: options.verification, _client: options.clientId ? new OAuth2Client(options.clientId) : undefined, auth: options.clientId ? { client: { id: options.clientId, }, } : undefined, ordersv3: options.ordersv3 || false, async handler( this: AppHandler & DialogflowApp<TConvData, TUserStorage, TContexts, TConversation>, body: Api.GoogleCloudDialogflowV2WebhookRequest, headers, metadata = {} ) { const {debug, init, verification, ordersv3} = this; if (verification) { const { headers: verificationHeaders, status = 403, error = (e: string) => e, } = isVerification(verification) ? verification : ({headers: verification} as DialogflowVerification); for (const key in verification) { const check = headers[key.toLowerCase()]; if (!check) { return { status, body: { error: typeof error === 'string' ? error : error('A verification header key was not found'), }, }; } const value = verificationHeaders[key]; const checking = common.toArray(check); if (checking.indexOf(value) < 0) { return { status, body: { error: typeof error === 'string' ? error : error('A verification header value was invalid'), }, }; } } } let conv = new DialogflowConversation< TConvData, TUserStorage, TContexts >({ body, headers, init: init && init(), debug, ordersv3, }); if (conv.user.profile.token) { await conv.user._verifyProfile(this._client!, this.auth!.client.id); } for (const middleware of this._middlewares) { // tslint:disable-next-line:no-any genericize Conversation type const result = middleware(conv as any, metadata); conv = ( result instanceof DialogflowConversation ? result : (await result) || conv ) as DialogflowConversation<TConvData, TUserStorage, TContexts>; } const log = debug ? common.info : common.debug; log( 'Conversation', common.stringify(conv, 'request', 'headers', 'body') ); const {intent} = conv; const traversed: Traversed = {}; let handler: typeof this._handlers.intents[string] = intent; while (typeof handler !== 'function') { if (typeof handler === 'undefined') { if (!this._handlers.fallback) { if (!intent) { throw new Error( 'No intent was provided and fallback handler is not defined.' ); } throw new Error( `Dialogflow IntentHandler not found for intent: ${intent}` ); } handler = this._handlers.fallback; continue; } if (traversed[handler]) { throw new Error( `Circular intent map detected: "${handler}" traversed twice` ); } traversed[handler] = true; handler = this._handlers.intents[handler]; } try { try { await handler( conv, conv.parameters, conv.arguments.parsed.list[0], conv.arguments.status.list[0] ); } catch (e) { await this._handlers.catcher(conv as TConversation, e); } } catch (e) { if (e instanceof UnauthorizedError) { return { status: 401, headers: {}, body: {}, }; } throw e; } return { status: 200, headers: {}, body: conv.serialize(), }; }, }, options );