diff --git a/hello-world/hello-world.js b/hello-world/hello-world.js index 3cd8c808..fbef7427 100644 --- a/hello-world/hello-world.js +++ b/hello-world/hello-world.js @@ -52,10 +52,6 @@ app.onBeforeReplySent(request => { }); app.onState("likesVoxa?", request => { - if (!request.intent) { - throw new Error("Not an intent request"); - } - if (request.intent.name === "YesIntent") { return { tell: "doesLikeVoxa" }; } diff --git a/src/StateMachine/StateMachine.ts b/src/StateMachine/StateMachine.ts index 7a1ae0da..39b7ef6b 100644 --- a/src/StateMachine/StateMachine.ts +++ b/src/StateMachine/StateMachine.ts @@ -24,7 +24,7 @@ import * as bluebird from "bluebird"; import * as _ from "lodash"; import { UnhandledState, UnknownState } from "../errors"; -import { IVoxaEvent, IVoxaIntent } from "../VoxaEvent"; +import { IVoxaIntent, IVoxaIntentEvent } from "../VoxaEvent"; import { IVoxaReply } from "../VoxaReply"; import { isState, @@ -35,18 +35,18 @@ import { } from "./transitions"; export type IStateMachineCb = ( - event: IVoxaEvent, + event: IVoxaIntentEvent, reply: IVoxaReply, transition: ITransition, ) => Promise; export type IUnhandledStateCb = ( - event: IVoxaEvent, + event: IVoxaIntentEvent, stateName: string, ) => Promise; export type IOnBeforeStateChangedCB = ( - event: IVoxaEvent, + event: IVoxaIntentEvent, reply: IVoxaReply, state: IState, ) => Promise; @@ -95,7 +95,7 @@ export class StateMachine { * we use the onUnhandledState handler */ public async checkOnUnhandledState( - voxaEvent: IVoxaEvent, + voxaEvent: IVoxaIntentEvent, voxaReply: IVoxaReply, transition: ITransition, ): Promise { @@ -131,7 +131,7 @@ export class StateMachine { * in the entry controller */ public async checkForEntryFallback( - voxaEvent: IVoxaEvent, + voxaEvent: IVoxaIntentEvent, reply: IVoxaReply, transition: ITransition, ): Promise { @@ -142,10 +142,6 @@ export class StateMachine { if (!transition && this.currentState.name !== "entry") { // If no response try falling back to entry - if (!voxaEvent.intent) { - throw new Error("Running the state machine without an intent"); - } - voxaEvent.log.debug( `No reply for ${voxaEvent.intent.name} in [${ this.currentState.name @@ -159,7 +155,7 @@ export class StateMachine { } public async onAfterStateChanged( - voxaEvent: IVoxaEvent, + voxaEvent: IVoxaIntentEvent, reply: IVoxaReply, transition: ITransition, ): Promise { @@ -185,7 +181,7 @@ export class StateMachine { public async runTransition( currentStateName: string, - voxaEvent: IVoxaEvent, + voxaEvent: IVoxaIntentEvent, reply: IVoxaReply, ): Promise { this.currentState = this.getCurrentState( @@ -231,7 +227,7 @@ export class StateMachine { } public async runCurrentState( - voxaEvent: IVoxaEvent, + voxaEvent: IVoxaIntentEvent, reply: IVoxaReply, ): Promise { if (!voxaEvent.intent) { @@ -307,7 +303,7 @@ export class StateMachine { return { to: dest }; } - protected runCallbacks(fn: IUnhandledStateCb, voxaEvent: IVoxaEvent) { + protected runCallbacks(fn: IUnhandledStateCb, voxaEvent: IVoxaIntentEvent) { if (!isState(this.currentState)) { throw new Error("this.currentState is not a state"); } @@ -328,7 +324,7 @@ export class StateMachine { } protected async runOnBeforeStateChanged( - voxaEvent: IVoxaEvent, + voxaEvent: IVoxaIntentEvent, reply: IVoxaReply, ) { const onBeforeState = this.onBeforeStateChangedCallbacks; @@ -345,7 +341,7 @@ export class StateMachine { protected getFinalTransition( sysTransition: SystemTransition, - voxaEvent: IVoxaEvent, + voxaEvent: IVoxaIntentEvent, reply: IVoxaReply, ) { let to; diff --git a/src/VoxaApp.ts b/src/VoxaApp.ts index 320d4d17..d6082b23 100644 --- a/src/VoxaApp.ts +++ b/src/VoxaApp.ts @@ -51,7 +51,7 @@ import { ITransition, StateMachine, } from "./StateMachine"; -import { IBag, IVoxaEvent } from "./VoxaEvent"; +import { IBag, IVoxaEvent, IVoxaIntentEvent } from "./VoxaEvent"; import { IVoxaReply } from "./VoxaReply"; export interface IVoxaAppConfig extends IRendererConfig { @@ -153,7 +153,7 @@ export class VoxaApp { } public async handleOnSessionEnded( - event: IVoxaEvent, + event: IVoxaIntentEvent, response: IVoxaReply, ): Promise { const sessionEndedHandlers = this.getOnSessionEndedHandlers( @@ -221,11 +221,13 @@ export class VoxaApp { }, ); - // call all onSessionStarted callbacks serially. - await bluebird.mapSeries( - this.getOnSessionStartedHandlers(voxaEvent.platform.name), - (fn: IEventHandler) => fn(voxaEvent, reply), - ); + if (voxaEvent.session.new) { + // call all onSessionStarted callbacks serially. + await bluebird.mapSeries( + this.getOnSessionStartedHandlers(voxaEvent.platform.name), + (fn: IEventHandler) => fn(voxaEvent, reply), + ); + } // Route the request to the proper handler which may have been overriden. return await requestHandler(voxaEvent, reply); } @@ -428,7 +430,7 @@ export class VoxaApp { } public async runStateMachine( - voxaEvent: IVoxaEvent, + voxaEvent: IVoxaIntentEvent, response: IVoxaReply, ): Promise { let fromState = voxaEvent.session.new @@ -487,9 +489,10 @@ export class VoxaApp { const directivesKeyOrder = _.map(directiveClasses, "key"); if (transition.reply) { // special handling for `transition.reply` - const reply = await voxaEvent.t(transition.reply, { - returnObjects: true, - }); + const reply = await voxaEvent.renderer.renderPath( + transition.reply, + voxaEvent, + ); const replyKeys = _.keys(reply); const replyTransition = _(replyKeys) .map((key) => { @@ -497,6 +500,7 @@ export class VoxaApp { }) .fromPairs() .value(); + transition = _.merge({}, transition, replyTransition); } diff --git a/src/VoxaEvent.ts b/src/VoxaEvent.ts index 0a0a57f8..230986b2 100644 --- a/src/VoxaEvent.ts +++ b/src/VoxaEvent.ts @@ -39,10 +39,29 @@ export interface IVoxaRequest { } export interface IVoxaEventClass { - new(rawEvent: any, logOptions: LambdaLogOptions, context: any): IVoxaEvent; + new (rawEvent: any, logOptions: LambdaLogOptions, context: any): IVoxaEvent; } -export abstract class IVoxaEvent { +export interface IVoxaIntentEvent extends IVoxaEvent { + intent: IVoxaIntent; +} + +export interface IVoxaEvent { + rawEvent: any; // the raw event as sent by the service + session: IVoxaSession; + intent?: IVoxaIntent; + request: IVoxaRequest; + model: Model; + t: i18n.TranslationFunction; + log: LambdaLog; + renderer: Renderer; + user: IVoxaUser; + platform: VoxaPlatform; + supportedInterfaces: string[]; + executionContext?: AWSLambdaContext | AzureContext; +} + +export abstract class VoxaEvent implements IVoxaEvent { public abstract get supportedInterfaces(): string[]; public rawEvent: any; // the raw event as sent by the service public session!: IVoxaSession; diff --git a/src/errors/UnhandledState.ts b/src/errors/UnhandledState.ts index 89c09854..9aab9937 100644 --- a/src/errors/UnhandledState.ts +++ b/src/errors/UnhandledState.ts @@ -20,14 +20,14 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { IVoxaEvent } from "../VoxaEvent"; +import { IVoxaIntentEvent } from "../VoxaEvent"; export class UnhandledState extends Error { - public voxaEvent: IVoxaEvent; + public voxaEvent: IVoxaIntentEvent; public fromState: string; public transition: any; - constructor(voxaEvent: IVoxaEvent, transition: any, fromState: string) { + constructor(voxaEvent: IVoxaIntentEvent, transition: any, fromState: string) { let message: string; if (voxaEvent.intent) { message = `${voxaEvent.intent.name} went unhandled on ${fromState} state`; diff --git a/src/index.ts b/src/index.ts index d0552543..96e60e29 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ export { Tell, Say, SayP, Ask, Reprompt } from "./directives"; export { VoxaPlatform } from "./platforms"; -export { IVoxaEvent, IVoxaIntent } from "./VoxaEvent"; +export { IVoxaEvent, IVoxaIntentEvent, IVoxaIntent } from "./VoxaEvent"; export { IVoxaReply } from "./VoxaReply"; export { AlexaReply, diff --git a/src/platforms/alexa/AlexaEvent.ts b/src/platforms/alexa/AlexaEvent.ts index 1965b6c8..5927d75c 100644 --- a/src/platforms/alexa/AlexaEvent.ts +++ b/src/platforms/alexa/AlexaEvent.ts @@ -20,12 +20,17 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { RequestEnvelope, User as IAlexaUser } from "ask-sdk-model"; +import { + canfulfill, + IntentRequest, + RequestEnvelope, + User as IAlexaUser, +} from "ask-sdk-model"; import { Context as AWSLambdaContext } from "aws-lambda"; import { Context as AzureContext } from "azure-functions-ts-essentials"; import { LambdaLogOptions } from "lambda-log"; import * as _ from "lodash"; -import { IVoxaEvent, IVoxaIntent, IVoxaSession } from "../../VoxaEvent"; +import { IVoxaIntent, VoxaEvent } from "../../VoxaEvent"; import { AlexaIntent } from "./AlexaIntent"; import { CustomerContact, @@ -35,8 +40,9 @@ import { Lists, } from "./apis"; -export class AlexaEvent extends IVoxaEvent { - public intent!: IVoxaIntent; +export class AlexaEvent extends VoxaEvent { + public intent?: IVoxaIntent; + public rawEvent!: RequestEnvelope; public alexa!: { customerContact: CustomerContact; deviceAddress: DeviceAddress; @@ -46,25 +52,10 @@ export class AlexaEvent extends IVoxaEvent { }; public requestToIntent: any = { - "AlexaSkillEvent.SkillDisabled": "AlexaSkillEvent.SkillDisabled", - "AlexaSkillEvent.SkillEnabled": "AlexaSkillEvent.SkillEnabled", - "AudioPlayer.PlaybackFailed": "AudioPlayer.PlaybackFailed", - "AudioPlayer.PlaybackFinished": "AudioPlayer.PlaybackFinished", - "AudioPlayer.PlaybackNearlyFinished": "AudioPlayer.PlaybackNearlyFinished", - "AudioPlayer.PlaybackStarted": "AudioPlayer.PlaybackStarted", - "AudioPlayer.PlaybackStopped": "AudioPlayer.PlaybackStopped", "Connections.Response": "Connections.Response", "Display.ElementSelected": "Display.ElementSelected", "GameEngine.InputHandlerEvent": "GameEngine.InputHandlerEvent", "LaunchRequest": "LaunchIntent", - "PlaybackController.NextCommandIssued": - "PlaybackController.NextCommandIssued", - "PlaybackController.PauseCommandIssued": - "PlaybackController.PauseCommandIssued", - "PlaybackController.PlayCommandIssued": - "PlaybackController.PlayCommandIssued", - "PlaybackController.PreviousCommandIssued": - "PlaybackController.PreviousCommandIssued", }; constructor( @@ -134,13 +125,18 @@ export class AlexaEvent extends IVoxaEvent { } protected initIntents() { - if ( - _.includes( - ["IntentRequest", "CanFulfillIntentRequest"], - this.request.type, - ) - ) { - this.intent = new AlexaIntent(this.rawEvent.request.intent); + const { request } = this.rawEvent; + if (isIntentRequest(request)) { + this.intent = new AlexaIntent(request.intent); } } } + +function isIntentRequest( + request: any, +): request is IntentRequest | canfulfill.CanFulfillIntentRequest { + return ( + request.type === "IntentRequest" || + request.type === "CanFulfillIntentRequest" + ); +} diff --git a/src/platforms/alexa/AlexaPlatform.ts b/src/platforms/alexa/AlexaPlatform.ts index 5a7d8085..e7574846 100644 --- a/src/platforms/alexa/AlexaPlatform.ts +++ b/src/platforms/alexa/AlexaPlatform.ts @@ -26,7 +26,7 @@ import { Context as AzureContext } from "azure-functions-ts-essentials"; import * as _ from "lodash"; import { OnSessionEndedError } from "../../errors"; import { VoxaApp } from "../../VoxaApp"; -import { IVoxaEvent } from "../../VoxaEvent"; +import { IVoxaEvent, IVoxaIntentEvent } from "../../VoxaEvent"; import { IVoxaReply } from "../../VoxaReply"; import { IVoxaPlatformConfig, VoxaPlatform } from "../VoxaPlatform"; import { AlexaEvent } from "./AlexaEvent"; @@ -86,7 +86,7 @@ export class AlexaPlatform extends VoxaPlatform { this.config = config; this.app.onCanFulfillIntentRequest( - (event: AlexaEvent, reply: AlexaReply) => { + (event: IVoxaIntentEvent, reply: AlexaReply) => { if (_.includes(this.config.defaultFulfillIntents, event.intent.name)) { reply.fulfillIntent("YES"); @@ -141,13 +141,9 @@ export class AlexaPlatform extends VoxaPlatform { } protected checkSessionEndedRequest(alexaEvent: AlexaEvent): void { - if ( - alexaEvent.request.type === "SessionEndedRequest" && - alexaEvent.rawEvent.request.reason === "ERROR" - ) { - throw new OnSessionEndedError( - _.get(alexaEvent.rawEvent, "request.error"), - ); + const { request } = alexaEvent.rawEvent; + if (request.type === "SessionEndedRequest" && request.reason === "ERROR") { + throw new OnSessionEndedError(request.error); } } diff --git a/src/platforms/botframework/BotFrameworkEvent.ts b/src/platforms/botframework/BotFrameworkEvent.ts index f78fe11b..b4902aa9 100644 --- a/src/platforms/botframework/BotFrameworkEvent.ts +++ b/src/platforms/botframework/BotFrameworkEvent.ts @@ -9,7 +9,7 @@ import { } from "botbuilder"; import { LambdaLogOptions } from "lambda-log"; import * as _ from "lodash"; -import { ITypeMap, IVoxaEvent, IVoxaIntent, IVoxaUser } from "../../VoxaEvent"; +import { ITypeMap, IVoxaIntent, IVoxaUser, VoxaEvent } from "../../VoxaEvent"; const MicrosoftCortanaIntents: ITypeMap = { "Microsoft.Launch": "LaunchIntent", @@ -23,7 +23,7 @@ export interface IBotframeworkPayload { intent?: IVoxaIntent; } -export class BotFrameworkEvent extends IVoxaEvent { +export class BotFrameworkEvent extends VoxaEvent { public rawEvent!: IBotframeworkPayload; public requestToRequest: ITypeMap = { diff --git a/src/platforms/dialogflow/DialogFlowEvent.ts b/src/platforms/dialogflow/DialogFlowEvent.ts index 6f53fddc..c36af05e 100644 --- a/src/platforms/dialogflow/DialogFlowEvent.ts +++ b/src/platforms/dialogflow/DialogFlowEvent.ts @@ -29,11 +29,11 @@ import { Context as AzureContext } from "azure-functions-ts-essentials"; import { LambdaLogOptions } from "lambda-log"; import * as _ from "lodash"; import { v1 } from "uuid"; -import { IVoxaEvent } from "../../VoxaEvent"; +import { VoxaEvent } from "../../VoxaEvent"; import { DialogFlowIntent } from "./DialogFlowIntent"; import { DialogFlowSession } from "./DialogFlowSession"; -export class DialogFlowEvent extends IVoxaEvent { +export class DialogFlowEvent extends VoxaEvent { public rawEvent!: GoogleCloudDialogflowV2WebhookRequest; public session!: DialogFlowSession; public google!: { conv: DialogflowConversation }; diff --git a/test/StateMachine.spec.ts b/test/StateMachine.spec.ts index 2ed631a6..dfe7e2cd 100644 --- a/test/StateMachine.spec.ts +++ b/test/StateMachine.spec.ts @@ -1,7 +1,13 @@ "use strict"; import { expect } from "chai"; import * as simple from "simple-mock"; -import { AlexaEvent, AlexaPlatform, AlexaReply, VoxaApp } from "../src"; +import { + AlexaEvent, + AlexaPlatform, + AlexaReply, + IVoxaIntentEvent, + VoxaApp, +} from "../src"; import { isState, StateMachine } from "../src/StateMachine"; import { AlexaRequestBuilder } from "./tools"; import { views } from "./views"; @@ -10,7 +16,7 @@ const rb = new AlexaRequestBuilder(); describe("StateMachine", () => { let states: any; - let voxaEvent: AlexaEvent; + let voxaEvent: IVoxaIntentEvent; let reply: AlexaReply; let app: VoxaApp; let skill: AlexaPlatform; @@ -18,7 +24,9 @@ describe("StateMachine", () => { beforeEach(() => { app = new VoxaApp({ views }); skill = new AlexaPlatform(app); - voxaEvent = new AlexaEvent(rb.getIntentRequest("AMAZON.YesIntent")); + voxaEvent = new AlexaEvent( + rb.getIntentRequest("AMAZON.YesIntent"), + ) as IVoxaIntentEvent; voxaEvent.platform = skill; reply = new AlexaReply(); @@ -162,7 +170,7 @@ describe("StateMachine", () => { const stateMachine = new StateMachine({ states }); const launchIntent = new AlexaEvent( rb.getIntentRequest("LaunchIntent"), - ); + ) as IVoxaIntentEvent; launchIntent.platform = skill; stateMachine.runTransition("entry", launchIntent, reply).then( diff --git a/test/VoxaPlatform.spec.ts b/test/VoxaPlatform.spec.ts index dc244763..ecad61e9 100644 --- a/test/VoxaPlatform.spec.ts +++ b/test/VoxaPlatform.spec.ts @@ -102,7 +102,6 @@ describe("VoxaPlatform", () => { tmpError?: Error | null | string, tmpResult?: any, ): void => { - console.log({ counter, tmpError, tmpResult }); counter += 1; error = tmpError; result = tmpResult; diff --git a/test/alexa/AlexaEvent.spec.ts b/test/alexa/AlexaEvent.spec.ts index 261bb7e9..0fc07158 100644 --- a/test/alexa/AlexaEvent.spec.ts +++ b/test/alexa/AlexaEvent.spec.ts @@ -1,6 +1,6 @@ import { expect } from "chai"; import * as _ from "lodash"; -import { AlexaEvent } from "../../src/platforms/alexa/AlexaEvent"; +import { AlexaEvent, IVoxaIntentEvent } from "../../src/"; import { AlexaRequestBuilder } from "../tools"; describe("AlexaEvent", () => { @@ -15,7 +15,7 @@ describe("AlexaEvent", () => { const rawEvent = rb.getIntentRequest("SomeIntent", { Dish: "Fried Chicken", }); - const alexaEvent = new AlexaEvent(rawEvent); + const alexaEvent = new AlexaEvent(rawEvent) as IVoxaIntentEvent; expect(alexaEvent.intent.params).to.deep.equal({ Dish: "Fried Chicken" }); }); @@ -65,7 +65,7 @@ describe("AlexaEvent", () => { const rawEvent = rb.getDisplayElementSelectedRequest( "SleepSingleIntent@2018-09-13T00:40:16.047Z", ); - const alexaEvent = new AlexaEvent(rawEvent); + const alexaEvent = new AlexaEvent(rawEvent) as IVoxaIntentEvent; expect(alexaEvent.intent.params).to.be.ok; }); }); diff --git a/test/alexa/InSkillPurchase.spec.ts b/test/alexa/InSkillPurchase.spec.ts index 7f7a685e..8082e875 100644 --- a/test/alexa/InSkillPurchase.spec.ts +++ b/test/alexa/InSkillPurchase.spec.ts @@ -2,8 +2,7 @@ import { expect } from "chai"; import * as _ from "lodash"; import * as nock from "nock"; -import { AlexaPlatform } from "../../src/platforms/alexa/AlexaPlatform"; -import { VoxaApp } from "../../src/VoxaApp"; +import { AlexaPlatform, VoxaApp } from "../../src"; import { AlexaRequestBuilder, isAlexaEvent } from "./../tools"; import { variables } from "./../variables"; import { views } from "./../views"; @@ -200,10 +199,6 @@ describe("InSkillPurchase", () => { status, ); - app.onSessionStarted((voxaEvent: any) => { - voxaEvent.model.flag = 1; - }); - alexaSkill.onState("firstState", () => ({})); alexaSkill.onIntent("Connections.Response", (voxaEvent) => { if (voxaEvent.rawEvent.request.payload.purchaseResult === "ACCEPTED") { @@ -216,6 +211,7 @@ describe("InSkillPurchase", () => { }); const reply = await alexaSkill.execute(event); + console.log(reply); expect(_.get(reply, "response.outputSpeech.ssml")).to.include( "Thanks for buying this product, do you want to try it out?", @@ -223,7 +219,7 @@ describe("InSkillPurchase", () => { expect(_.get(reply, "response.reprompt.outputSpeech.ssml")).to.include( "Do you want to try it out?", ); - expect(_.get(reply, "sessionAttributes.model.flag")).to.equal(1); + expect(_.get(reply, "sessionAttributes.state")).to.equal("firstState"); expect(reply.response.shouldEndSession).to.equal(false); }); @@ -247,10 +243,6 @@ describe("InSkillPurchase", () => { status, ); - app.onSessionStarted((voxaEvent: any) => { - voxaEvent.model.flag = 1; - }); - alexaSkill.onIntent("Connections.Response", (voxaEvent) => { if (voxaEvent.rawEvent.request.payload.purchaseResult === "ACCEPTED") { return { ask: "ISP.ProductBought" }; @@ -265,7 +257,6 @@ describe("InSkillPurchase", () => { "Thanks for your interest", ); expect(reply.response.reprompt).to.be.undefined; - expect(_.get(reply, "sessionAttributes.model.flag")).to.equal(1); expect(_.get(reply, "sessionAttributes.state")).to.equal("die"); expect(reply.response.shouldEndSession).to.equal(true); }); diff --git a/test/botframework/BotFrameworkPlatform.spec.ts b/test/botframework/BotFrameworkPlatform.spec.ts index cd6d1bd7..fb22133c 100644 --- a/test/botframework/BotFrameworkPlatform.spec.ts +++ b/test/botframework/BotFrameworkPlatform.spec.ts @@ -118,7 +118,6 @@ describe("BotFrameworkPlatform", () => { throw err; } - console.log(result); expect(result).to.be.ok; }; const context = getLambdaContext(callback);