/** * 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, 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 } /** @hidden */ export interface DialogflowIntentHandlers { [event: string]: Function | string | undefined } /** @hidden */ export interface DialogflowHandlers< TConvData, TUserStorage, TContexts extends Contexts, TConversation extends DialogflowConversation, > { intents: DialogflowIntentHandlers catcher: ExceptionHandler fallback?: Function | string } /** @public */ export interface DialogflowMiddleware< TConversationPlugin extends DialogflowConversation > { ( /** @public */ conv: DialogflowConversation, /** @public */ framework: BuiltinFrameworkMetadata, ): (DialogflowConversation & TConversationPlugin) | void | Promise | Promise } /** @public */ export type DefaultDialogflowIntent = 'Default Welcome Intent' | 'Default Fallback Intent' /** @public */ export interface DialogflowApp< TConvData, TUserStorage, TContexts extends Contexts, TConversation extends DialogflowConversation, > extends ConversationApp { /** @hidden */ _handlers: DialogflowHandlers /** * 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( 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( 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( 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( 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( 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( intent: string | string[], handler: DialogflowIntentHandler< TConvData, TUserStorage, TContexts, TConversation, TParameters, TArgument > | string, ): this /** @public */ catch(catcher: ExceptionHandler): this /** @public */ fallback( handler: DialogflowIntentHandler< TConvData, TUserStorage, TContexts, TConversation, Parameters, Argument > | string, ): this /** @hidden */ _middlewares: DialogflowMiddleware>[] /** @public */ middleware>( middleware: DialogflowMiddleware, ): 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 { /** * 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 = DialogflowConversation, >( options?: DialogflowOptions, ): AppHandler & DialogflowApp< TConvData, TUserStorage, TContexts, Conversation > /** @public */ < TContexts extends Contexts, Conversation extends DialogflowConversation<{}, {}, TContexts> = DialogflowConversation<{}, {}, TContexts>, >( options?: DialogflowOptions<{}, {}>, ): AppHandler & DialogflowApp< {}, {}, TContexts, Conversation > /** @public */ = 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 >( options: DialogflowOptions = {}, ) => attach>({ _handlers: { intents: {}, catcher: (conv, e) => { throw e }, }, _middlewares: [], intent( this: DialogflowApp, 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, catcher) { this._handlers.catcher = catcher return this }, fallback(this: DialogflowApp, handler) { this._handlers.fallback = handler return this }, middleware( this: DialogflowApp, 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, 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({ 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 ) } 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)