├── .ask └── config ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── __tests__ ├── BuiltIn.spec.ts ├── Errors.spec.ts ├── Hello.spec.ts ├── Launch.spec.ts └── helpers.ts ├── gulpfile.js ├── lambda ├── custom │ ├── errors │ │ ├── Unexpected.ts │ │ ├── Unknown.ts │ │ └── index.ts │ ├── index.ts │ ├── intents │ │ ├── Debug.ts │ │ ├── Fallback.ts │ │ ├── Help.ts │ │ ├── Launch.ts │ │ ├── SessionEnded.ts │ │ ├── Stop.ts │ │ ├── SystemExceptionEncountered.ts │ │ ├── hello │ │ │ ├── HelloWorld.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── interceptors │ │ ├── Localization.ts │ │ ├── Slots.ts │ │ └── index.ts │ ├── interfaces.ts │ ├── lib │ │ ├── constants.ts │ │ ├── helpers.ts │ │ └── strings.ts │ ├── package-lock.json │ └── package.json └── local │ └── index.ts ├── models └── en-US.json ├── nodemon.json ├── package-lock.json ├── package.json ├── skill.json └── tsconfig.json /.ask/config: -------------------------------------------------------------------------------- 1 | { 2 | "deploy_settings": { 3 | "default": { 4 | "skill_id": "", 5 | "was_cloned": false, 6 | "merge": { 7 | "manifest": { 8 | "apis": { 9 | "custom": { 10 | "endpoint": { 11 | "uri": "alexa-typescript-starter-default" 12 | } 13 | } 14 | } 15 | } 16 | } 17 | }, 18 | "local": { 19 | "skill_id": "", 20 | "was_cloned": false, 21 | "merge": { 22 | "manifest": { 23 | "apis": { 24 | "custom": { 25 | "endpoint": { 26 | "uri": "https://YOUR_URL.ngrok.io", 27 | "sslCertificateType": "Wildcard" 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build directories 2 | .history 3 | dist 4 | .coverage 5 | 6 | # Dependency directories 7 | /**/node_modules 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8.10" 4 | before_install: 5 | - npm install codecov -g 6 | install: 7 | - npm install 8 | script: 9 | - npm run build 10 | - codecov -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mihail Cristian Dumitru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Xzya/alexa-typescript-starter.svg?branch=master)](https://travis-ci.org/Xzya/alexa-typescript-starter) 2 | [![codecov](https://codecov.io/gh/Xzya/alexa-typescript-starter/branch/master/graph/badge.svg)](https://codecov.io/gh/Xzya/alexa-typescript-starter) 3 | 4 | # Alexa Skill starter project using AWS Lambda and Typescript 5 | 6 | This is a simple starter project for Alexa skills using Typescript. 7 | 8 | ## What is included in the project 9 | 10 | - Default request handlers 11 | 12 | | Name | 13 | | --- | 14 | | `LaunchRequest` | 15 | | `SessionEndedRequest` | 16 | | `System.ExceptionEncountered` | 17 | | `AMAZON.HelpIntent` | 18 | | `AMAZON.StopIntent` and `AMAZON.CancelIntent` | 19 | | `AMAZON.FallbackIntent` | 20 | 21 | - Extra handlers 22 | 23 | | Name | Description | 24 | | --- | --- | 25 | | `HelloWorld` | Triggered when the user says "hello", will answer back with "hello". | 26 | | `Debug` | Can be placed at the beginning of the request handlers stack and it will print the `handlerInput`. Useful for debugging. | 27 | 28 | - Error handlers 29 | 30 | | Name | Description | 31 | | --- | --- | 32 | | `Unexpected` | Catches `ErrorTypes.Unexpected`, which should be thrown when...something unexpected happens. It will tell the user something unexpected happend, and to try again later. | 33 | | `Unknown` | Catches all other errors. It will tell the user it didn't understand the command, and to try saying it again (doesn't end session). | 34 | 35 | - Request interceptors 36 | 37 | | Name | Description | 38 | | --- | --- | 39 | | `Localization` | Adds `i18next` localization functions to the `RequestAttributes`. | 40 | | `Slots` | Parses the slot values, adds additional useful information to them (e.g. if it was an exact match, or if it's ambiguous etc.), and adds them to the `RequestAttributes`. Check the `GetSlotValues` function inside `lambda/custom/lib/helpers.ts` for more details. | 41 | 42 | - Localization strings 43 | 44 | Check `lambda/custom/lib/strings.ts`. 45 | 46 | - Constants 47 | 48 | Including the String keys, so you can have type safety everywhere. 49 | 50 | Check `lambda/custom/lib/constants.ts`. 51 | 52 | - Helper functions 53 | 54 | Many helper functions which should reduce code duplication, and reduce the code needed to do common tasks. 55 | 56 | Check `lambda/custom/lib/helpers.ts`. 57 | 58 | - Local development 59 | 60 | Contains an `http` server using `express`, which you can use with `ngrok` or `servo.net` during local development. Check the [Local development section below](#local-development) for more details. 61 | 62 | - Scripts 63 | 64 | There are a few scripts inside `package.json` for building and deploying your lambda function using the `ask-cli`. Check the [Developer tasks section below](#developer-tasks) for more details. 65 | 66 | - Tests 67 | 68 | The project contains automated tests using [jest](https://jestjs.io/). Check the `__tests__` folder. 69 | 70 | ```bash 71 | $ npm run test 72 | ``` 73 | 74 | If you want to include code coverage, run 75 | 76 | ```bash 77 | $ npm run test:coverage 78 | ``` 79 | 80 | You can also start `jest` in watch mode: 81 | 82 | ```bash 83 | $ npm run test:watch 84 | ``` 85 | 86 | - Travis CI and Codecov integrations 87 | 88 | ## Pre-requisites 89 | 90 | - Node.js 91 | - Register for an [AWS Account](https://aws.amazon.com/) 92 | - Register for an [Amazon Developer Account](https://developer.amazon.com/) 93 | - Install and Setup [ASK CLI](https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html) 94 | 95 | ## Installation 96 | 97 | - Install the dependencies 98 | 99 | ```bash 100 | $ npm install 101 | ``` 102 | 103 | ## Deployment 104 | 105 | **ASK CLI** will create the skill and the Lambda function for you. The Lambda function will be created in `us-east-1 (Northern Virginia)` by default. 106 | 107 | 1. Navigate to the project's root directory. you should see a file named 'skill.json' there. 108 | 109 | 2. Deploy the skill and the Lambda function in one step by running the following command: 110 | 111 | ```bash 112 | $ ask deploy 113 | ``` 114 | 115 | ## Local development 116 | 117 | In order to develop locally and see your changes reflected instantly, you will need to create an SSH tunnel or expose somehow your local development server. There are several services that allow you to do this, for example [ngrok](https://ngrok.com/) or [serveo.net](https://serveo.net/). 118 | 119 | ### Using servo.net 120 | 121 | This is the easiest to setup 122 | 123 | 1. You need to have an SSH client installed, then simply run 124 | 125 | ```bash 126 | $ ssh -R 80:localhost:3980 serveo.net 127 | Forwarding HTTP traffic from [https://YOUR_URL] 128 | Press g to start a GUI session and ctrl-c to quit. 129 | ``` 130 | 131 | 2. Once you see the URL, copy it and go to your Skill console. 132 | 133 | 3. Open the `Endpoint` menu and select `HTTPS` 134 | 135 | 4. Under `Default Region` paste the previous URL you copied. 136 | 137 | 5. On the select box choose: `My development endpoint is a sub-domain of a domain that has a wildcard certificate from a certificate authority`. 138 | 139 | 6. You are done! Just run `npm start` to start the local server and begin testing the skill. 140 | 141 | ### Using ngrok.io 142 | 143 | 1. [Install ngrok](https://ngrok.com/download) 144 | 145 | 2. Run `ngrok http 3980` 146 | 147 | 3. Copy the URL and follow the same steps above from 3 to 6. 148 | 149 | ## Developer tasks 150 | 151 | | Command | Description | 152 | | --- | --- | 153 | | `clean` | Deletes the `dist` folder | 154 | | `build` | Builds the lambda function and exports it to the `dist` folder | 155 | | `deploy` | Builds the lambda function and deploys EVERYTHING (skill, model, lambda) | 156 | | `deploy:lambda` | Builds the lambda function and deploys it (just the lambda function) | 157 | | `deploy:local` | Deploys the skill details for the local profile, which will update the HTTPS endpoint | 158 | | `start` | Starts the local `express` server using `nodemon` for local development | 159 | 160 | To see the actual commands, check `package.json`. 161 | 162 | Also check the [ASK CLI Command Reference](https://developer.amazon.com/docs/smapi/ask-cli-command-reference.html) for more details on using the `ASK CLI`. 163 | 164 | ## Testing 165 | 166 | Taken from [the official hello world project](https://github.com/alexa/skill-sample-nodejs-hello-world/blob/master/instructions/7-cli.md#testing). 167 | 168 | 1. To test, you need to login to Alexa Developer Console, and **enable the "Test" switch on your skill from the "Test" Tab**. 169 | 170 | 2. Simulate verbal interaction with your skill through the command line (this might take a few moments) using the following example: 171 | 172 | ```bash 173 | $ ask simulate -l en-US -t "open greeter" 174 | 175 | ✓ Simulation created for simulation id: 4a7a9ed8-94b2-40c0-b3bd-fb63d9887fa7 176 | ◡ Waiting for simulation response{ 177 | "status": "SUCCESSFUL", 178 | ... 179 | ``` 180 | 181 | 3. Once the "Test" switch is enabled, your skill can be tested on devices associated with the developer account as well. Speak to Alexa from any enabled device, from your browser at [echosim.io](https://echosim.io/welcome), or through your Amazon Mobile App and say : 182 | 183 | ```text 184 | Alexa, start hello world 185 | ``` 186 | 187 | ## Customization 188 | 189 | Taken from [the official hello world project](https://github.com/alexa/skill-sample-nodejs-hello-world/blob/master/instructions/7-cli.md#customization). 190 | 191 | 1. ```./skill.json``` 192 | 193 | Change the skill name, example phrase, icons, testing instructions etc ... 194 | 195 | Remember than many information are locale-specific and must be changed for each locale (e.g. en-US, en-GB, de-DE, etc.) 196 | 197 | See the Skill [Manifest Documentation](https://developer.amazon.com/docs/smapi/skill-manifest.html) for more information. 198 | 199 | 2. ```./lambda/custom/index.ts``` 200 | 201 | Modify messages, and data from the source code to customize the skill. 202 | 203 | 3. ```./models/*.json``` 204 | 205 | Change the model definition to replace the invocation name and the sample phrase for each intent. Repeat the operation for each locale you are planning to support. 206 | 207 | 4. Remember to re-deploy your skill and Lambda function for your changes to take effect. 208 | 209 | ```bash 210 | $ npm run deploy 211 | ``` 212 | 213 | ## License 214 | 215 | Open sourced under the [MIT license](./LICENSE.md). -------------------------------------------------------------------------------- /__tests__/BuiltIn.spec.ts: -------------------------------------------------------------------------------- 1 | import { skill, ssml, RequestWithIntent, RequestWithType } from "./helpers"; 2 | import { IntentTypes, LocaleTypes } from "../lambda/custom/lib/constants"; 3 | 4 | describe("BuiltIn Intents", () => { 5 | it("Help", async () => { 6 | const response = await skill(RequestWithIntent({ 7 | name: IntentTypes.Help, 8 | locale: LocaleTypes.enUS, 9 | })); 10 | expect(response).toMatchObject(ssml(/You can say hello/gi)); 11 | }); 12 | 13 | it("Stop", async () => { 14 | const response = await skill(RequestWithIntent({ 15 | name: IntentTypes.Stop, 16 | locale: LocaleTypes.enUS, 17 | })); 18 | expect(response).toMatchObject(ssml(/Goodbye/gi)); 19 | }); 20 | 21 | it("Cancel", async () => { 22 | const response = await skill(RequestWithIntent({ 23 | name: IntentTypes.Cancel, 24 | locale: LocaleTypes.enUS, 25 | })); 26 | expect(response).toMatchObject(ssml(/Goodbye/gi)); 27 | }); 28 | 29 | it("Fallback", async () => { 30 | const response = await skill(RequestWithIntent({ 31 | name: IntentTypes.Fallback, 32 | locale: LocaleTypes.enUS, 33 | })); 34 | expect(response).toMatchObject(ssml(/Sorry, I can't understand the command/gi)); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /__tests__/Errors.spec.ts: -------------------------------------------------------------------------------- 1 | import { skill, ssml, RequestWithIntent } from "./helpers"; 2 | import { IntentTypes, LocaleTypes } from "../lambda/custom/lib/constants"; 3 | 4 | describe("Errors", () => { 5 | it("Unknown", async () => { 6 | const response = await skill(RequestWithIntent({ 7 | name: "Intent" as IntentTypes, 8 | locale: LocaleTypes.enUS, 9 | })); 10 | expect(response).toMatchObject(ssml(/Sorry, I can't understand the command/gi)); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/Hello.spec.ts: -------------------------------------------------------------------------------- 1 | import { skill, ssml, RequestWithIntent } from "./helpers"; 2 | import { LocaleTypes, IntentTypes } from "../lambda/custom/lib/constants"; 3 | 4 | describe("Hello", () => { 5 | it("should work", async () => { 6 | const response = await skill(RequestWithIntent({ 7 | name: IntentTypes.HelloWorld, 8 | locale: LocaleTypes.enUS, 9 | })); 10 | expect(response).toMatchObject(ssml(/Hello world/gi)); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/Launch.spec.ts: -------------------------------------------------------------------------------- 1 | import { skill, ssml, RequestWithType } from "./helpers"; 2 | import { RequestTypes, LocaleTypes } from "../lambda/custom/lib/constants"; 3 | 4 | describe("Launch", () => { 5 | it("should work", async () => { 6 | const response = await skill(RequestWithType({ 7 | type: RequestTypes.Launch, 8 | locale: LocaleTypes.enUS, 9 | })); 10 | expect(response).toMatchObject(ssml(/Welcome to the Alexa Skills Kit/gi)); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/helpers.ts: -------------------------------------------------------------------------------- 1 | import { RequestEnvelope, ResponseEnvelope, DialogState, SlotConfirmationStatus, slu, Slot } from "ask-sdk-model"; 2 | import { handler } from "../lambda/custom"; 3 | import { RequestTypes, LocaleTypes, IntentTypes } from "../lambda/custom/lib/constants"; 4 | 5 | export type PartialDeep = { 6 | [P in keyof T]?: PartialDeep; 7 | }; 8 | 9 | /** 10 | * Accepts a partial T object and returns it as T. 11 | * Useful when you only need a few fields of T but still want to get type 12 | * completions and to suppress compilation error. 13 | * 14 | * @param value 15 | */ 16 | export function partial(value: PartialDeep): T { 17 | return (value as any) as T; 18 | } 19 | 20 | /** 21 | * Calls the skill handler with the given RequestEnvelope. Returns a promise 22 | * with the response. 23 | * 24 | * @param event 25 | */ 26 | export function skill(event: RequestEnvelope) { 27 | return new Promise((fulfill, reject) => { 28 | return handler(event, null, (err, res) => { 29 | if (err) return reject(err); 30 | return fulfill(res); 31 | }); 32 | }); 33 | } 34 | 35 | /** 36 | * Returns a partial ResponseEnvelope with the given ssml pattern. 37 | * 38 | * @param pattern 39 | */ 40 | export function ssml(pattern: string | RegExp) { 41 | return partial({ 42 | response: { 43 | outputSpeech: { 44 | ssml: expect.stringMatching(pattern) 45 | } 46 | } 47 | }); 48 | } 49 | 50 | /** 51 | * Returns a RequestEnvelope with the given type. 52 | * 53 | * @param options 54 | */ 55 | export function RequestWithType(options: { 56 | type: RequestTypes; 57 | locale: LocaleTypes; 58 | }) { 59 | return partial<{}>({ 60 | "context": { 61 | "System": {} 62 | }, 63 | "request": { 64 | "type": options.type, 65 | "locale": options.locale 66 | } 67 | }) as RequestEnvelope; 68 | } 69 | 70 | /** 71 | * Returns a RequestEnvelope with the given intent. 72 | * 73 | * @param options 74 | */ 75 | export function RequestWithIntent(options: { 76 | name: IntentTypes; 77 | locale: LocaleTypes; 78 | }) { 79 | return partial({ 80 | "context": { 81 | "System": {} 82 | }, 83 | "request": { 84 | "type": "IntentRequest", 85 | "locale": options.locale, 86 | "intent": { 87 | "name": options.name 88 | } 89 | } 90 | }); 91 | } 92 | 93 | /** 94 | * Creates an intent request envelope with the given parameters. 95 | * 96 | * @param options 97 | */ 98 | export function CreateIntentRequest(options: { 99 | name: string; 100 | locale: LocaleTypes; 101 | dialogState?: DialogState; 102 | slots?: { 103 | [key: string]: { 104 | value?: string; 105 | confirmationStatus?: SlotConfirmationStatus; 106 | resolutions?: { 107 | status: slu.entityresolution.StatusCode, 108 | values?: { 109 | name: string; 110 | id?: string; 111 | }[]; 112 | } 113 | } 114 | }, 115 | }) { 116 | return partial({ 117 | "context": { 118 | "System": {} 119 | }, 120 | "request": { 121 | "type": "IntentRequest", 122 | "locale": options.locale, 123 | "intent": { 124 | "name": options.name, 125 | "confirmationStatus": "NONE", 126 | "slots": options.slots ? (() => { 127 | const slots: { 128 | [key: string]: Slot; 129 | } = {}; 130 | 131 | for (let slotName of Object.keys(options.slots)) { 132 | const slot = options.slots[slotName]; 133 | slots[slotName] = { 134 | name: slotName, 135 | value: slot.value ? slot.value : "", 136 | confirmationStatus: slot.confirmationStatus ? slot.confirmationStatus : "NONE", 137 | resolutions: slot.resolutions ? { 138 | resolutionsPerAuthority: [ 139 | { 140 | authority: "", 141 | status: { 142 | code: slot.resolutions.status 143 | }, 144 | values: slot.resolutions.values ? slot.resolutions.values.map((item) => { 145 | return { 146 | value: { 147 | name: item.name, 148 | id: item.id ? item.id : "" 149 | } 150 | }; 151 | }) : [] 152 | } 153 | ] 154 | } : undefined, 155 | } 156 | } 157 | return slots; 158 | })() : undefined, 159 | }, 160 | "dialogState": options.dialogState ? options.dialogState : "STARTED", 161 | } 162 | }); 163 | } 164 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require("gulp"); 2 | var ts = require("gulp-typescript"); 3 | var tsProject = ts.createProject("tsconfig.json"); 4 | 5 | var OUT_DIR = "dist"; 6 | var IN_DIR = "lambda"; 7 | 8 | // compile typescript 9 | gulp.task("compile", function () { 10 | return tsProject.src() 11 | .pipe(tsProject()) 12 | .js.pipe(gulp.dest(OUT_DIR)); 13 | }); 14 | 15 | // copy json files (e.g. localization json) 16 | gulp.task("json", function () { 17 | return gulp.src(IN_DIR + "/**/*.json").pipe(gulp.dest(OUT_DIR)); 18 | }); 19 | 20 | gulp.task("default", gulp.parallel(["compile", "json"])); -------------------------------------------------------------------------------- /lambda/custom/errors/Unexpected.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler } from "ask-sdk-core"; 2 | import { GetRequestAttributes } from "../lib/helpers"; 3 | import { Strings, ErrorTypes } from "../lib/constants"; 4 | 5 | /** 6 | * Handles ErrorTypes.Unexpected errors which should be thrown when something 7 | * unexpected happens. 8 | */ 9 | export const Unexpected: ErrorHandler = { 10 | canHandle(_, error) { 11 | return error.name === ErrorTypes.Unexpected; 12 | }, 13 | handle(handlerInput, error) { 14 | console.log(`Error handled: ${error.message}`); 15 | 16 | const { t } = GetRequestAttributes(handlerInput); 17 | 18 | return handlerInput.responseBuilder 19 | .speak(t(Strings.ERROR_UNEXPECTED_MSG)) 20 | .getResponse(); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /lambda/custom/errors/Unknown.ts: -------------------------------------------------------------------------------- 1 | import { ErrorHandler } from "ask-sdk-core"; 2 | import { GetRequestAttributes } from "../lib/helpers"; 3 | import { Strings } from "../lib/constants"; 4 | 5 | /** 6 | * Handles unknown errors. Should be placed at the end, as it will catch 7 | * all errors. 8 | */ 9 | export const Unknown: ErrorHandler = { 10 | canHandle() { 11 | return true; 12 | }, 13 | handle(handlerInput, error) { 14 | console.log(`Error handled: ${error.message}`); 15 | 16 | const { t } = GetRequestAttributes(handlerInput); 17 | 18 | const speechText = t(Strings.ERROR_MSG); 19 | 20 | return handlerInput.responseBuilder 21 | .speak(speechText) 22 | .reprompt(speechText) 23 | .getResponse(); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /lambda/custom/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Unknown"; 2 | export * from "./Unexpected"; 3 | -------------------------------------------------------------------------------- /lambda/custom/index.ts: -------------------------------------------------------------------------------- 1 | import * as Alexa from "ask-sdk-core"; 2 | import * as Intents from "./intents"; 3 | import * as Errors from "./errors"; 4 | import * as Interceptors from "./interceptors"; 5 | import * as HelloIntents from "./intents/hello"; 6 | 7 | export const handler = Alexa.SkillBuilders.custom() 8 | .addRequestHandlers( 9 | // Intents.Debug, 10 | 11 | // Default intents 12 | Intents.Launch, 13 | Intents.Help, 14 | Intents.Stop, 15 | Intents.SessionEnded, 16 | Intents.SystemExceptionEncountered, 17 | Intents.Fallback, 18 | 19 | // Hello intents 20 | HelloIntents.HelloWorld 21 | ) 22 | .addErrorHandlers( 23 | Errors.Unknown, 24 | Errors.Unexpected 25 | ) 26 | .addRequestInterceptors( 27 | Interceptors.Localization, 28 | Interceptors.Slots 29 | ) 30 | .lambda(); 31 | -------------------------------------------------------------------------------- /lambda/custom/intents/Debug.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "ask-sdk-core"; 2 | 3 | export const Debug: RequestHandler = { 4 | canHandle(handlerInput) { 5 | console.log(JSON.stringify(handlerInput, null, 2)); 6 | 7 | return false; 8 | }, 9 | handle(handlerInput) { 10 | return handlerInput.responseBuilder 11 | .getResponse(); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /lambda/custom/intents/Fallback.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "ask-sdk-core"; 2 | import { IsIntent, GetRequestAttributes } from "../lib/helpers"; 3 | import { IntentTypes, Strings } from "../lib/constants"; 4 | 5 | export const Fallback: RequestHandler = { 6 | canHandle(handlerInput) { 7 | return IsIntent(handlerInput, IntentTypes.Fallback); 8 | }, 9 | handle(handlerInput) { 10 | const { t } = GetRequestAttributes(handlerInput); 11 | 12 | const speechText = t(Strings.ERROR_MSG); 13 | 14 | return handlerInput.responseBuilder 15 | .speak(speechText) 16 | .reprompt(t(Strings.HELP_MSG)) 17 | .getResponse(); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lambda/custom/intents/Help.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "ask-sdk-core"; 2 | import { IsIntent, GetRequestAttributes } from "../lib/helpers"; 3 | import { IntentTypes, Strings } from "../lib/constants"; 4 | 5 | export const Help: RequestHandler = { 6 | canHandle(handlerInput) { 7 | return IsIntent(handlerInput, IntentTypes.Help); 8 | }, 9 | handle(handlerInput) { 10 | const { t } = GetRequestAttributes(handlerInput); 11 | 12 | const speechText = t(Strings.HELP_MSG); 13 | 14 | return handlerInput.responseBuilder 15 | .speak(speechText) 16 | .reprompt(speechText) 17 | .withSimpleCard(t(Strings.SKILL_NAME), speechText) 18 | .getResponse(); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /lambda/custom/intents/Launch.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "ask-sdk-core"; 2 | import { RequestTypes, Strings } from "../lib/constants"; 3 | import { IsType, GetRequestAttributes } from "../lib/helpers"; 4 | 5 | export const Launch: RequestHandler = { 6 | canHandle(handlerInput) { 7 | return IsType(handlerInput, RequestTypes.Launch); 8 | }, 9 | handle(handlerInput) { 10 | const { t } = GetRequestAttributes(handlerInput); 11 | 12 | const speechText = t(Strings.WELCOME_MSG); 13 | 14 | return handlerInput.responseBuilder 15 | .speak(speechText) 16 | .reprompt(speechText) 17 | .withSimpleCard(t(Strings.SKILL_NAME), speechText) 18 | .getResponse(); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /lambda/custom/intents/SessionEnded.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "ask-sdk-core"; 2 | import { IsType } from "../lib/helpers"; 3 | import { RequestTypes } from "../lib/constants"; 4 | 5 | export const SessionEnded: RequestHandler = { 6 | canHandle(handlerInput) { 7 | return IsType(handlerInput, RequestTypes.SessionEnded); 8 | }, 9 | handle(handlerInput) { 10 | //any cleanup logic goes here 11 | return handlerInput.responseBuilder.getResponse(); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /lambda/custom/intents/Stop.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "ask-sdk-core"; 2 | import { IntentTypes, Strings } from "../lib/constants"; 3 | import { IsIntent, GetRequestAttributes } from "../lib/helpers"; 4 | 5 | export const Stop: RequestHandler = { 6 | canHandle(handlerInput) { 7 | return IsIntent(handlerInput, IntentTypes.Stop, IntentTypes.Cancel); 8 | }, 9 | handle(handlerInput) { 10 | const { t } = GetRequestAttributes(handlerInput); 11 | 12 | const speechText = t(Strings.GOODBYE_MSG); 13 | 14 | return handlerInput.responseBuilder 15 | .speak(speechText) 16 | .withSimpleCard(t(Strings.SKILL_NAME), speechText) 17 | .getResponse(); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lambda/custom/intents/SystemExceptionEncountered.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "ask-sdk-core"; 2 | import { RequestTypes } from "../lib/constants"; 3 | import { IsType } from "../lib/helpers"; 4 | 5 | export const SystemExceptionEncountered: RequestHandler = { 6 | canHandle(handlerInput) { 7 | return IsType(handlerInput, RequestTypes.SystemExceptionEncountered); 8 | }, 9 | handle(handlerInput) { 10 | console.log("\n******************* EXCEPTION **********************"); 11 | console.log("\n" + JSON.stringify(handlerInput.requestEnvelope, null, 2)); 12 | 13 | return handlerInput.responseBuilder 14 | .getResponse(); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /lambda/custom/intents/hello/HelloWorld.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "ask-sdk-core"; 2 | import { IsIntent, GetRequestAttributes } from "../../lib/helpers"; 3 | import { IntentTypes, Strings } from "../../lib/constants"; 4 | 5 | export const HelloWorld: RequestHandler = { 6 | canHandle(handlerInput) { 7 | return IsIntent(handlerInput, IntentTypes.HelloWorld); 8 | }, 9 | handle(handlerInput) { 10 | const { t } = GetRequestAttributes(handlerInput); 11 | 12 | const speechText = t(Strings.HELLO_MSG); 13 | 14 | return handlerInput.responseBuilder 15 | .speak(speechText) 16 | .withSimpleCard(t(Strings.SKILL_NAME), speechText) 17 | .getResponse(); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lambda/custom/intents/hello/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./HelloWorld"; 2 | -------------------------------------------------------------------------------- /lambda/custom/intents/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Launch"; 2 | export * from "./Help"; 3 | export * from "./Stop"; 4 | export * from "./SessionEnded"; 5 | export * from "./Fallback"; 6 | export * from "./SystemExceptionEncountered"; 7 | 8 | export * from "./Debug"; 9 | -------------------------------------------------------------------------------- /lambda/custom/interceptors/Localization.ts: -------------------------------------------------------------------------------- 1 | import { RequestInterceptor } from "ask-sdk-core"; 2 | import * as i18n from "i18next"; 3 | import * as sprintf from "i18next-sprintf-postprocessor"; 4 | import { strings } from "../lib/strings"; 5 | import { RequestAttributes } from "../interfaces"; 6 | import { Random } from "../lib/helpers"; 7 | 8 | type TranslationFunction = (...args: any[]) => string; 9 | 10 | /** 11 | * Adds translation functions to the RequestAttributes. 12 | */ 13 | export const Localization: RequestInterceptor = { 14 | process(handlerInput) { 15 | const localizationClient = i18n.use(sprintf).init({ 16 | lng: handlerInput.requestEnvelope.request.locale, 17 | overloadTranslationOptionHandler: sprintf.overloadTranslationOptionHandler, 18 | resources: strings, 19 | returnObjects: true, 20 | }); 21 | 22 | const attributes = handlerInput.attributesManager.getRequestAttributes() as RequestAttributes; 23 | attributes.t = function (...args: any[]) { 24 | return (localizationClient.t as TranslationFunction)(...args); 25 | }; 26 | attributes.tr = function (key: any) { 27 | const result = localizationClient.t(key) as string[]; 28 | 29 | return Random(result); 30 | }; 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /lambda/custom/interceptors/Slots.ts: -------------------------------------------------------------------------------- 1 | import { RequestInterceptor } from "ask-sdk-core"; 2 | import { RequestAttributes } from "../interfaces"; 3 | import { RequestTypes } from "../lib/constants"; 4 | import { GetSlotValues } from "../lib/helpers"; 5 | 6 | /** 7 | * Parses and adds the slot values to the RequestAttributes. 8 | */ 9 | export const Slots: RequestInterceptor = { 10 | process(handlerInput) { 11 | const attributes = handlerInput.attributesManager.getRequestAttributes() as RequestAttributes; 12 | 13 | if (handlerInput.requestEnvelope.request.type === RequestTypes.Intent) { 14 | attributes.slots = GetSlotValues(handlerInput.requestEnvelope.request.intent.slots); 15 | } else { 16 | attributes.slots = {}; 17 | } 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /lambda/custom/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Localization"; 2 | export * from "./Slots"; 3 | -------------------------------------------------------------------------------- /lambda/custom/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Slot, slu, SlotConfirmationStatus } from "ask-sdk-model"; 2 | 3 | export interface RequestAttributes { 4 | /** 5 | * Searches for the translation of the given key, replaces the arguments 6 | * and returns the result. 7 | * 8 | * @param key 9 | * @param args 10 | */ 11 | t(key: string, ...args: any[]): any; 12 | 13 | /** 14 | * Randomly picks a translation for the given key and returns it. 15 | * 16 | * Note: The value for the key must be an array. 17 | * 18 | * @param key 19 | */ 20 | tr(key: string): string; 21 | 22 | /** 23 | * The slot values for the current request. 24 | */ 25 | slots: SlotValues; 26 | 27 | [key: string]: any; 28 | } 29 | 30 | export interface SessionAttributes { 31 | [key: string]: any; 32 | } 33 | 34 | export type Slots = { [key: string]: Slot }; 35 | 36 | /** 37 | * A matched slot value (if `status.code` = "ER_SUCCESS_MATCH"). 38 | */ 39 | export interface MatchedSlotValue { 40 | /** 41 | * Name of the slot. 42 | */ 43 | name: string; 44 | 45 | /** 46 | * Value that the user said (unresolved). 47 | */ 48 | value: string; 49 | 50 | /** 51 | * `statis.code` = "ER_SUCCESS_MATCH" 52 | */ 53 | isMatch: true; 54 | 55 | /** 56 | * The first resolved value. 57 | */ 58 | resolved: string; 59 | 60 | /** 61 | * The first resolved id. 62 | */ 63 | id: string; 64 | 65 | /** 66 | * `True` if there are multiple resolved values. 67 | */ 68 | isAmbiguous: boolean; 69 | 70 | /** 71 | * All resolved values. If there are multiple values, `isAmbiguous` will be `true`. 72 | */ 73 | values: slu.entityresolution.Value[]; 74 | 75 | /** 76 | * Whether the user has explicitly confirmed or denied the value of this slot. 77 | */ 78 | confirmationStatus: SlotConfirmationStatus; 79 | } 80 | 81 | /** 82 | * An unmatched slot value (if `status.code` != "ER_SUCCESS_MATCH"). 83 | */ 84 | export interface UnmatchedSlotValue { 85 | /** 86 | * Name of the slot. 87 | */ 88 | name: string; 89 | 90 | /** 91 | * Value that the user said (unresolved). 92 | */ 93 | value: string; 94 | 95 | /** 96 | * `statis.code` != "ER_SUCCESS_MATCH" 97 | */ 98 | isMatch: false; 99 | 100 | /** 101 | * Whether the user has explicitly confirmed or denied the value of this slot. 102 | */ 103 | confirmationStatus: SlotConfirmationStatus 104 | } 105 | 106 | export interface SlotValues { 107 | [key: string]: MatchedSlotValue | UnmatchedSlotValue | undefined; 108 | } 109 | -------------------------------------------------------------------------------- /lambda/custom/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export enum RequestTypes { 2 | Launch = "LaunchRequest", 3 | Intent = "IntentRequest", 4 | SessionEnded = "SessionEndedRequest", 5 | SystemExceptionEncountered = "System.ExceptionEncountered", 6 | } 7 | 8 | export enum IntentTypes { 9 | Help = "AMAZON.HelpIntent", 10 | Stop = "AMAZON.StopIntent", 11 | Cancel = "AMAZON.CancelIntent", 12 | Fallback = "AMAZON.FallbackIntent", 13 | 14 | HelloWorld = "HelloWorldIntent", 15 | } 16 | 17 | export enum ErrorTypes { 18 | Unknown = "UnknownError", 19 | Unexpected = "UnexpectedError", 20 | } 21 | 22 | export enum LocaleTypes { 23 | deDE = "de-DE", 24 | enAU = "en-AU", 25 | enCA = "en-CA", 26 | enGB = "en-GB", 27 | enIN = "en-IN", 28 | enUS = "en-US", 29 | esES = "es-ES", 30 | frFR = "fr-FR", 31 | itIT = "it-IT", 32 | jaJP = "ja-JP", 33 | } 34 | 35 | export enum Strings { 36 | SKILL_NAME = "SKILL_NAME", 37 | WELCOME_MSG = "WELCOME_MSG", 38 | GOODBYE_MSG = "GOODBYE_MSG", 39 | HELLO_MSG = "HELLO_MSG", 40 | HELP_MSG = "HELP_MSG", 41 | ERROR_MSG = "ERROR_MSG", 42 | ERROR_UNEXPECTED_MSG = "ERROR_UNEXPECTED_MSG", 43 | } 44 | -------------------------------------------------------------------------------- /lambda/custom/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | import { HandlerInput } from "ask-sdk-core"; 2 | import { IntentRequest, services } from "ask-sdk-model"; 3 | import { RequestAttributes, Slots, SlotValues, SessionAttributes } from "../interfaces"; 4 | import { RequestTypes, ErrorTypes } from "./constants"; 5 | 6 | /** 7 | * Checks if the request matches any of the given intents. 8 | * 9 | * @param handlerInput 10 | * @param intents 11 | */ 12 | export function IsIntent(handlerInput: HandlerInput, ...intents: string[]): boolean { 13 | if (handlerInput.requestEnvelope.request.type === RequestTypes.Intent) { 14 | for (let i = 0; i < intents.length; i++) { 15 | if (handlerInput.requestEnvelope.request.intent.name === intents[i]) { 16 | return true; 17 | } 18 | } 19 | } 20 | return false; 21 | } 22 | 23 | /** 24 | * Checks if the request matches any of the given types. 25 | * 26 | * @param handlerInput 27 | * @param types 28 | */ 29 | export function IsType(handlerInput: HandlerInput, ...types: string[]): boolean { 30 | for (let i = 0; i < types.length; i++) { 31 | if (handlerInput.requestEnvelope.request.type === types[i]) { 32 | return true; 33 | } 34 | } 35 | return false; 36 | } 37 | 38 | /** 39 | * Checks if the request matches the given intent and dialogState. 40 | * 41 | * @param handlerInput 42 | * @param intent 43 | * @param state 44 | */ 45 | export function IsIntentWithDialogState(handlerInput: HandlerInput, intent: string, state: string): boolean { 46 | return handlerInput.requestEnvelope.request.type === RequestTypes.Intent 47 | && handlerInput.requestEnvelope.request.intent.name === intent 48 | && handlerInput.requestEnvelope.request.dialogState === state; 49 | } 50 | 51 | /** 52 | * Checks if the request matches the given intent with a non COMPLETED dialogState. 53 | * 54 | * @param handlerInput 55 | * @param intent 56 | */ 57 | export function IsIntentWithIncompleteDialog(handlerInput: HandlerInput, intent: string): boolean { 58 | return handlerInput.requestEnvelope.request.type === RequestTypes.Intent 59 | && handlerInput.requestEnvelope.request.intent.name === intent 60 | && handlerInput.requestEnvelope.request.dialogState !== "COMPLETED"; 61 | } 62 | 63 | /** 64 | * Checks if the request matches the given intent with the COMPLETED dialogState. 65 | * 66 | * @param handlerInput 67 | * @param intent 68 | */ 69 | export function IsIntentWithCompleteDialog(handlerInput: HandlerInput, intent: string): boolean { 70 | return IsIntentWithDialogState(handlerInput, intent, "COMPLETED"); 71 | } 72 | 73 | /** 74 | * Gets the request attributes and casts it to our custom RequestAttributes type. 75 | * 76 | * @param handlerInput 77 | */ 78 | export function GetRequestAttributes(handlerInput: HandlerInput): RequestAttributes { 79 | return handlerInput.attributesManager.getRequestAttributes() as RequestAttributes; 80 | } 81 | 82 | /** 83 | * Gets the session attributes and casts it to our custom SessionAttributes type. 84 | * 85 | * @param handlerInput 86 | */ 87 | export function GetSessionAttributes(handlerInput: HandlerInput): SessionAttributes { 88 | return handlerInput.attributesManager.getSessionAttributes() as SessionAttributes; 89 | } 90 | 91 | /** 92 | * Gets the directive service client. 93 | * 94 | * @param handlerInput 95 | */ 96 | export function GetDirectiveServiceClient(handlerInput: HandlerInput): services.directive.DirectiveServiceClient { 97 | return handlerInput.serviceClientFactory!.getDirectiveServiceClient(); 98 | } 99 | 100 | /** 101 | * Resets the given slot value by setting it to an empty string. 102 | * If the intent is using the Dialog Directive, this will cause Alexa 103 | * to reprompt the user for that slot. 104 | * 105 | * @param request 106 | * @param slotName 107 | */ 108 | export function ResetSlotValue(request: IntentRequest, slotName: string) { 109 | if (request.intent.slots) { 110 | const slot = request.intent.slots[slotName]; 111 | if (slot) { 112 | slot.value = ""; 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * Resets all unmatched slot values by setting them to an empty string. 119 | * If the intent is using the Dialog Directive, this will cause Alexa 120 | * to reprompt the user for those slots. 121 | * 122 | * @param request 123 | */ 124 | export function ResetUnmatchedSlotValues(handlerInput: HandlerInput, slots: SlotValues) { 125 | if (handlerInput.requestEnvelope.request.type === RequestTypes.Intent) { 126 | const request = handlerInput.requestEnvelope.request; 127 | 128 | // reset invalid slots 129 | Object.keys(slots).forEach((key) => { 130 | const slot = slots[key]; 131 | 132 | if (slot && !slot.isMatch) { 133 | ResetSlotValue(request, slot.name); 134 | } 135 | }); 136 | } 137 | } 138 | 139 | /** 140 | * Parses the slot values and returns a new object with additional information, 141 | * e.g. if the value was matched, or if it is ambiguous etc. 142 | * 143 | * Example: 144 | * If we have the following Drink Slot Type: 145 | * { 146 | * "types": [{ 147 | * "values": [{ 148 | * "id": "cocacola", 149 | * "name": { 150 | * "value": "Coca Cola" 151 | * } 152 | * }, 153 | * { 154 | * "id": "cocacolazero", 155 | * "name": { 156 | * "value": "Coca Cola Zero" 157 | * } 158 | * } 159 | * ] 160 | * }] 161 | * } 162 | * 163 | * If the user said "Cola", the following value should be generated: 164 | * { 165 | * "name": "drink", // slot name 166 | * "value": "Cola", // what the user said 167 | * "isMatch": true, // was successfuly matched with our slot type 168 | * "resolved": "Coca Cola", // the first resolved value 169 | * "id": "cocacola", // the first resolved id 170 | * "isAmbiguous": true, // true because we matched multiple possible values 171 | * "values": [{ 172 | * "name": "Coca Cola", 173 | * "id": "cocacola" 174 | * }, 175 | * { 176 | * "name": "Coca Cola Zero", 177 | * "id": "cocacolazero" 178 | * } 179 | * ], 180 | * "confirmationStatus": "NONE" 181 | * } 182 | * 183 | * @param filledSlots 184 | */ 185 | export function GetSlotValues(filledSlots?: Slots): SlotValues { 186 | const slotValues: SlotValues = {}; 187 | 188 | if (filledSlots) { 189 | Object.keys(filledSlots).forEach((item) => { 190 | const name = filledSlots[item].name; 191 | const value = filledSlots[item].value; 192 | const confirmationStatus = filledSlots[item].confirmationStatus; 193 | 194 | if (filledSlots[item] && 195 | filledSlots[item].resolutions && 196 | filledSlots[item].resolutions!.resolutionsPerAuthority && 197 | filledSlots[item].resolutions!.resolutionsPerAuthority![0] && 198 | filledSlots[item].resolutions!.resolutionsPerAuthority![0].status && 199 | filledSlots[item].resolutions!.resolutionsPerAuthority![0].status.code) { 200 | switch (filledSlots[item].resolutions!.resolutionsPerAuthority![0].status.code) { 201 | case "ER_SUCCESS_MATCH": 202 | const valueWrappers = filledSlots[item].resolutions!.resolutionsPerAuthority![0].values; 203 | 204 | if (valueWrappers.length > 1) { 205 | slotValues[name] = { 206 | name: name, 207 | value: value, 208 | isMatch: true, 209 | resolved: valueWrappers[0].value.name, 210 | id: valueWrappers[0].value.id, 211 | isAmbiguous: true, 212 | values: valueWrappers.map((valueWrapper) => valueWrapper.value), 213 | confirmationStatus: confirmationStatus, 214 | }; 215 | break; 216 | } 217 | 218 | slotValues[name] = { 219 | name: name, 220 | value: value, 221 | isMatch: true, 222 | resolved: valueWrappers[0].value.name, 223 | id: valueWrappers[0].value.id, 224 | isAmbiguous: false, 225 | values: [], 226 | confirmationStatus: confirmationStatus, 227 | }; 228 | break; 229 | case "ER_SUCCESS_NO_MATCH": 230 | slotValues[name] = { 231 | name: name, 232 | value: value, 233 | isMatch: false, 234 | confirmationStatus: confirmationStatus, 235 | }; 236 | break; 237 | default: 238 | break; 239 | } 240 | } else { 241 | slotValues[name] = { 242 | name: name, 243 | value: value, 244 | isMatch: false, 245 | confirmationStatus: confirmationStatus, 246 | }; 247 | } 248 | }); 249 | } 250 | 251 | return slotValues; 252 | } 253 | 254 | /** 255 | * Wraps the given string as an interjection. 256 | * 257 | * @param str 258 | */ 259 | export function Interject(str: string): string { 260 | return `${str}`; 261 | } 262 | 263 | /** 264 | * Creates an error with the given message and type. 265 | * 266 | * @param msg 267 | * @param type 268 | */ 269 | export function CreateError( 270 | msg: string = "Something unexpected happened.", 271 | type: string = ErrorTypes.Unknown 272 | ): Error { 273 | const error = new Error(msg); 274 | error.name = type; 275 | 276 | return error; 277 | } 278 | 279 | /** 280 | * Selects a random element from the array; 281 | * 282 | * @param arr 283 | */ 284 | export function Random(arr: T[]): T { 285 | return arr[Math.floor(Math.random() * arr.length)]; 286 | } 287 | 288 | /** 289 | * Returns a VoicePlayer.Speak directive with the given speech. Useful for sending progressive responses. 290 | * 291 | * @param handlerInput 292 | * @param speech 293 | */ 294 | export function VoicePlayerSpeakDirective(handlerInput: HandlerInput, speech?: string): services.directive.SendDirectiveRequest { 295 | const requestId = handlerInput.requestEnvelope.request.requestId; 296 | 297 | return { 298 | directive: { 299 | type: "VoicePlayer.Speak", 300 | speech: speech, 301 | }, 302 | header: { 303 | requestId, 304 | }, 305 | }; 306 | } 307 | -------------------------------------------------------------------------------- /lambda/custom/lib/strings.ts: -------------------------------------------------------------------------------- 1 | import { Resource } from "i18next"; 2 | import { Strings, LocaleTypes } from "./constants"; 3 | 4 | interface IStrings { 5 | [Strings.SKILL_NAME]: string; 6 | [Strings.WELCOME_MSG]: string; 7 | [Strings.GOODBYE_MSG]: string; 8 | [Strings.HELLO_MSG]: string; 9 | [Strings.HELP_MSG]: string; 10 | [Strings.ERROR_MSG]: string; 11 | [Strings.ERROR_UNEXPECTED_MSG]: string; 12 | } 13 | 14 | export const strings: Resource = { 15 | [LocaleTypes.enUS]: { 16 | translation: { 17 | SKILL_NAME: "Hello world", 18 | WELCOME_MSG: "Welcome to the Alexa Skills Kit, you can say hello!", 19 | GOODBYE_MSG: "Goodbye!", 20 | HELLO_MSG: "Hello world!", 21 | HELP_MSG: "You can say hello to me!", 22 | ERROR_MSG: "Sorry, I can't understand the command. Please say again.", 23 | ERROR_UNEXPECTED_MSG: "Sorry, an unexpected error has occured. Please try again later.", 24 | } as IStrings, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /lambda/custom/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ask-sdk-core": { 8 | "version": "2.0.7", 9 | "resolved": "https://registry.npmjs.org/ask-sdk-core/-/ask-sdk-core-2.0.7.tgz", 10 | "integrity": "sha512-L0YoF7ls0iUoo/WYDYj7uDjAFThYZSDjzF8YvLHIEZyzKVgrNNqxetRorUB+odDoctPWW7RV1xcep8F4p7c1jg==" 11 | }, 12 | "ask-sdk-model": { 13 | "version": "1.3.1", 14 | "resolved": "https://registry.npmjs.org/ask-sdk-model/-/ask-sdk-model-1.3.1.tgz", 15 | "integrity": "sha512-ZDcmJ8sDRAzfIPz5WhRpy8HJ8SheBOyjoeHtYIcoVO6ZQEgxXtZ11GJGg8FhRDfwwIWj5Ma8G6m7OCgo4nuJDA==" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lambda/custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "ask-sdk-core": "^2.0.7", 13 | "ask-sdk-model": "^1.3.1", 14 | "i18next": "^11.3.5", 15 | "i18next-sprintf-postprocessor": "^0.2.2" 16 | } 17 | } -------------------------------------------------------------------------------- /lambda/local/index.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import * as bodyParser from "body-parser"; 3 | import { AddressInfo } from "net"; 4 | import { LambdaHandler } from "ask-sdk-core/dist/skill/factory/BaseSkillFactory"; 5 | import { RequestEnvelope } from "ask-sdk-model"; 6 | 7 | import { handler as helloHandler } from "../custom"; 8 | 9 | function CreateHandler(handler: LambdaHandler): express.RequestHandler { 10 | return (req, res) => { 11 | handler(req.body as RequestEnvelope, null, (err, result) => { 12 | if (err) { 13 | return res.status(500).send(err); 14 | } 15 | return res.status(200).json(result); 16 | }); 17 | }; 18 | } 19 | 20 | // create server 21 | const server = express(); 22 | const listener = server.listen(process.env.port || process.env.PORT || 3980, function () { 23 | const { address, port } = listener.address() as AddressInfo; 24 | console.log('%s listening to %s%s', server.name, address, port); 25 | }); 26 | 27 | // parse json 28 | server.use(bodyParser.json()); 29 | 30 | // connect the lambda functions to http 31 | server.post("/", CreateHandler(helloHandler)); 32 | -------------------------------------------------------------------------------- /models/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "greeter", 5 | "intents": [{ 6 | "name": "AMAZON.CancelIntent", 7 | "samples": [] 8 | }, 9 | { 10 | "name": "AMAZON.HelpIntent", 11 | "samples": [] 12 | }, 13 | { 14 | "name": "AMAZON.StopIntent", 15 | "samples": [] 16 | }, 17 | { 18 | "name": "AMAZON.FallbackIntent", 19 | "samples": [] 20 | }, 21 | { 22 | "name": "HelloWorldIntent", 23 | "slots": [], 24 | "samples": [ 25 | "hello", 26 | "say hello", 27 | "say hello world" 28 | ] 29 | } 30 | ], 31 | "types": [] 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "lambda" 4 | ], 5 | "ext": "ts", 6 | "ignore": [ 7 | "lambda/**/*.spec.ts" 8 | ], 9 | "exec": "ts-node ./lambda/local/index.ts" 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexa-typescript-starter", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rimraf dist", 8 | "build": "npm run clean && npm run test:coverage && gulp", 9 | "deploy": "npm run build && ask deploy", 10 | "deploy:lambda": "npm run build && ask deploy --target lambda", 11 | "deploy:local": "ask deploy --target skill --profile local", 12 | "test": "jest", 13 | "test:watch": "jest --watch", 14 | "test:coverage": "jest --coverage", 15 | "start": "nodemon" 16 | }, 17 | "jest": { 18 | "testEnvironment": "node", 19 | "transform": { 20 | "^.+\\.tsx?$": "ts-jest" 21 | }, 22 | "moduleFileExtensions": [ 23 | "ts", 24 | "tsx", 25 | "js" 26 | ], 27 | "testMatch": [ 28 | "**/*.spec.ts" 29 | ], 30 | "coverageDirectory": ".coverage" 31 | }, 32 | "author": "Mihail Cristian Dumitru", 33 | "license": "MIT", 34 | "dependencies": { 35 | "ask-sdk-core": "^2.0.7", 36 | "ask-sdk-model": "^1.3.1", 37 | "i18next": "^11.3.5", 38 | "i18next-sprintf-postprocessor": "^0.2.2" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^10.5.2", 42 | "@types/express": "^4.16.0", 43 | "@types/i18next": "^8.4.3", 44 | "@types/i18next-sprintf-postprocessor": "^0.0.29", 45 | "@types/jest": "^23.3.0", 46 | "express": "^4.16.3", 47 | "gulp": "^4.0.0", 48 | "gulp-typescript": "^4.0.1", 49 | "rimraf": "^2.6.2", 50 | "ts-node": "^5.0.1", 51 | "typescript": "^2.9.2", 52 | "nodemon": "^1.17.5", 53 | "jest": "^23.6.0", 54 | "ts-jest": "^23.0.1" 55 | } 56 | } -------------------------------------------------------------------------------- /skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "en-US": { 6 | "summary": "Sample Short Description", 7 | "examplePhrases": [ 8 | "Alexa open greeter", 9 | "Alexa tell greeter hello", 10 | "Alexa ask greeter to say hello" 11 | ], 12 | "name": "ask-hello", 13 | "description": "Sample Full Description" 14 | } 15 | }, 16 | "isAvailableWorldwide": true, 17 | "testingInstructions": "Sample Testing Instructions.", 18 | "category": "EDUCATION_AND_REFERENCE", 19 | "distributionCountries": [] 20 | }, 21 | "apis": { 22 | "custom": { 23 | "endpoint": { 24 | "sourceDir": "dist/custom" 25 | } 26 | } 27 | }, 28 | "manifestVersion": "1.0" 29 | } 30 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": [ 6 | "es6" 7 | ], 8 | "moduleResolution": "node", 9 | "rootDir": "lambda", 10 | "outDir": "dist", 11 | "sourceMap": false, 12 | "allowJs": false, 13 | "noImplicitAny": true, 14 | "noUnusedLocals": true, 15 | "noImplicitThis": true, 16 | "strictNullChecks": true, 17 | "noImplicitReturns": true, 18 | "preserveConstEnums": true, 19 | "suppressImplicitAnyIndexErrors": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "strict": true 22 | }, 23 | "exclude": [ 24 | "/**/node_modules", 25 | "lambda/local/", 26 | "__tests__", 27 | "**/__mocks__" 28 | ] 29 | } --------------------------------------------------------------------------------