├── lambda ├── custom │ ├── intents │ │ ├── hello │ │ │ ├── index.ts │ │ │ └── HelloWorld.ts │ │ ├── index.ts │ │ ├── Debug.ts │ │ ├── SessionEnded.ts │ │ ├── Help.ts │ │ ├── Launch.ts │ │ ├── SystemExceptionEncountered.ts │ │ ├── Fallback.ts │ │ └── Stop.ts │ ├── errors │ │ ├── index.ts │ │ ├── Unknown.ts │ │ └── Unexpected.ts │ ├── interceptors │ │ ├── index.ts │ │ ├── Slots.ts │ │ └── Localization.ts │ ├── package.json │ ├── lib │ │ ├── config.ts │ │ ├── strings.ts │ │ ├── constants.ts │ │ └── helpers.ts │ ├── index.ts │ └── typings │ │ └── index.d.ts └── local │ └── index.ts ├── .editorconfig ├── nodemon.json ├── .gitignore ├── gulpfile.js ├── .ask └── config ├── tsconfig.json ├── package.json ├── models └── en-US.json ├── skill.json ├── hooks ├── post_new_hook.sh ├── pre_deploy_hook.sh ├── post_new_hook.ps1 └── pre_deploy_hook.ps1 ├── gulpfile-base.js └── README.md /lambda/custom/intents/hello/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./HelloWorld"; 2 | -------------------------------------------------------------------------------- /lambda/custom/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Unknown"; 2 | export * from "./Unexpected"; 3 | -------------------------------------------------------------------------------- /lambda/custom/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Localization"; 2 | export * from "./Slots"; 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /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 | } 11 | -------------------------------------------------------------------------------- /.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* 15 | -------------------------------------------------------------------------------- /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/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/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": {}, 7 | "author": "", 8 | "license": "ISC", 9 | "dependencies": { 10 | "ask-sdk-core": "2.7.0", 11 | "ask-sdk-model": "1.19.0", 12 | "i18next": "17.0.6", 13 | "i18next-sprintf-postprocessor": "0.2.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const gulp = require("gulp"); 4 | const gulp_base = require("./gulpfile-base"); 5 | 6 | gulp.task("tsc", gulp_base.tsc); 7 | 8 | gulp.task("clean", gulp_base.clean); 9 | 10 | gulp.task("copyFiles", gulp_base.copyFiles); 11 | 12 | gulp.task("models", gulp_base.models); 13 | 14 | gulp.task("default", gulp.series("clean", gulp.parallel(["tsc", "copyFiles"]))); 15 | gulp.task("release", gulp.series("default")); 16 | -------------------------------------------------------------------------------- /.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-skill-template-default" 12 | } 13 | } 14 | } 15 | } 16 | }, 17 | "in_skill_products": [] 18 | } 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/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 | .getResponse(); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /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.AmazonHelp); 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 | .getResponse(); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /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 | .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/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.AmazonFallback); 8 | }, 9 | handle(handlerInput) { 10 | const { t } = GetRequestAttributes(handlerInput); 11 | 12 | const speechText = t(Strings.FALLBACK_MSG); 13 | 14 | return handlerInput.responseBuilder 15 | .speak(speechText) 16 | .reprompt(t(Strings.FALLBACK_REPROMPT)) 17 | .getResponse(); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /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.AmazonStop, IntentTypes.AmazonCancel); 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 | .withShouldEndSession(true) 17 | .getResponse(); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /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 | ] 27 | } 28 | -------------------------------------------------------------------------------- /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.error(error); 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/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.error(error); 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/interceptors/Slots.ts: -------------------------------------------------------------------------------- 1 | import { RequestInterceptor } from "ask-sdk-core"; 2 | import { RequestAttributes } from "../typings"; 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/lib/config.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | 3 | } 4 | 5 | export enum EnvironmentTypes { 6 | Local = "local", 7 | Development = "dev", 8 | Production = "prod", 9 | } 10 | 11 | export function GetConfig(): Config { 12 | const env = process.env.ENV as EnvironmentTypes; 13 | if (env) { 14 | switch (env) { 15 | case EnvironmentTypes.Local: 16 | return { 17 | 18 | }; 19 | case EnvironmentTypes.Development: 20 | return { 21 | 22 | }; 23 | case EnvironmentTypes.Production: 24 | return { 25 | 26 | }; 27 | default: 28 | throw new Error(`Unknown ENV value ${env}`); 29 | } 30 | } 31 | throw new Error("ENV environment variable not set"); 32 | } 33 | 34 | export const config = GetConfig(); 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexa-typescript-template", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "ENV=local nodemon", 7 | "build": "gulp" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "ask-sdk-core": "2.7.0", 13 | "ask-sdk-model": "1.19.0", 14 | "i18next": "17.0.6", 15 | "i18next-sprintf-postprocessor": "0.2.2" 16 | }, 17 | "devDependencies": { 18 | "@types/express": "4.17.0", 19 | "@types/i18next": "12.1.0", 20 | "@types/i18next-sprintf-postprocessor": "0.0.29", 21 | "express": "4.17.1", 22 | "gulp": "4.0.2", 23 | "del": "5.1.0", 24 | "ts-node": "5.0.1", 25 | "typescript": "3.6.3", 26 | "nodemon": "1.19.2", 27 | "tslint": "5.20.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /models/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "hello world", 5 | "intents": [ 6 | { 7 | "name": "AMAZON.CancelIntent", 8 | "samples": [] 9 | }, 10 | { 11 | "name": "AMAZON.HelpIntent", 12 | "samples": [] 13 | }, 14 | { 15 | "name": "AMAZON.StopIntent", 16 | "samples": [] 17 | }, 18 | { 19 | "name": "AMAZON.FallbackIntent", 20 | "samples": [] 21 | }, 22 | { 23 | "name": "HelloWorldIntent", 24 | "slots": [], 25 | "samples": [ 26 | "hello", 27 | "say hello", 28 | "say hello world" 29 | ] 30 | } 31 | ], 32 | "types": [] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "en-US": { 6 | "summary": "Sample Short Description", 7 | "examplePhrases": [ 8 | "Alexa open hello world", 9 | "Alexa tell hello world hello", 10 | "Alexa ask hello world 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 | } 31 | -------------------------------------------------------------------------------- /lambda/custom/lib/strings.ts: -------------------------------------------------------------------------------- 1 | import { Strings, LocaleTypes } from "./constants"; 2 | import { Resource } from "../typings"; 3 | 4 | interface IStrings { 5 | [Strings.WELCOME_MSG]: string; 6 | [Strings.GOODBYE_MSG]: string; 7 | [Strings.HELLO_MSG]: string; 8 | [Strings.HELP_MSG]: string; 9 | [Strings.ERROR_MSG]: string; 10 | [Strings.ERROR_UNEXPECTED_MSG]: string; 11 | } 12 | 13 | export const strings: Resource = { 14 | [LocaleTypes.enUS]: { 15 | translation: { 16 | WELCOME_MSG: "Welcome to the Alexa Skills Kit, you can say hello!", 17 | GOODBYE_MSG: "Goodbye!", 18 | HELP_MSG: "You can say hello to me!", 19 | ERROR_MSG: "Sorry, I can't understand the command. Please say again.", 20 | ERROR_UNEXPECTED_MSG: "Sorry, an unexpected error has occured. Please try again later.", 21 | FALLBACK_MSG: "This skill can't help you with that.", 22 | FALLBACK_REPROMPT: "What can I help you with?", 23 | HELLO_MSG: "Hello world!", 24 | } as IStrings, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /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 } 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(handler)); 32 | -------------------------------------------------------------------------------- /lambda/custom/interceptors/Localization.ts: -------------------------------------------------------------------------------- 1 | import { RequestInterceptor } from "ask-sdk-core"; 2 | import * as i18next from "i18next"; 3 | // temporary fix until the typings are correctly made by the author 4 | // https://github.com/i18next/i18next/issues/1271 5 | const i18n: i18next.default.i18n = i18next as any; 6 | import * as sprintf from "i18next-sprintf-postprocessor"; 7 | import { strings } from "../lib/strings"; 8 | import { RequestAttributes } from "../typings"; 9 | 10 | /** 11 | * Adds translation functions to the RequestAttributes. 12 | */ 13 | export const Localization: RequestInterceptor = { 14 | process(handlerInput) { 15 | i18n.use(sprintf).init({ 16 | lng: handlerInput.requestEnvelope.request.locale, 17 | overloadTranslationOptionHandler: sprintf.overloadTranslationOptionHandler, 18 | resources: strings, 19 | returnObjects: true, 20 | }); 21 | 22 | function t(...args: any[]) { 23 | const value = (i18n.t as any)(...args); 24 | 25 | if (Array.isArray(value)) { 26 | return value[Math.floor(Math.random() * value.length)]; 27 | } 28 | 29 | return value; 30 | }; 31 | 32 | const attributes = handlerInput.attributesManager.getRequestAttributes() as RequestAttributes; 33 | attributes.t = t; 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /hooks/post_new_hook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Shell script for ask-cli post-new hook for Node.js 3 | # Script Usage: post_new_hook.sh 4 | 5 | # SKILL_NAME is the preformatted name passed from the CLI, after removing special characters. 6 | # DO_DEBUG is boolean value for debug logging 7 | 8 | # Run this script one level outside of the skill root folder 9 | 10 | # The script does the following: 11 | # - Run "npm install" in each sourceDir in skill.json 12 | 13 | SKILL_NAME=$1 14 | DO_DEBUG=${2:-false} 15 | 16 | if [ $DO_DEBUG == false ] 17 | then 18 | exec > /dev/null 2>&1 19 | fi 20 | 21 | install_dependencies() { 22 | npm install --prefix "$SKILL_NAME/$1" >/dev/null 2>&1 23 | return $? 24 | } 25 | 26 | echo "###########################" 27 | echo "###### post-new hook ######" 28 | echo "###########################" 29 | 30 | grep "sourceDir" $SKILL_NAME/skill.json | cut -d: -f2 | sed 's/"//g' | sed 's/,//g' | while read -r SOURCE_DIR; do 31 | if install_dependencies $SOURCE_DIR; then 32 | echo "Codebase ($SOURCE_DIR) built successfully." 33 | else 34 | echo "There was a problem installing dependencies for ($SOURCE_DIR)." 35 | exit 1 36 | fi 37 | done 38 | echo "###########################" 39 | 40 | exit 0 41 | 42 | 43 | -------------------------------------------------------------------------------- /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 | AmazonHelp = "AMAZON.HelpIntent", 10 | AmazonStop = "AMAZON.StopIntent", 11 | AmazonCancel = "AMAZON.CancelIntent", 12 | AmazonFallback = "AMAZON.FallbackIntent", 13 | 14 | HelloWorld = "HelloWorldIntent", 15 | } 16 | 17 | export enum ErrorTypes { 18 | Unknown = "UnknownError", 19 | Unexpected = "UnexpectedError", 20 | } 21 | 22 | export enum SlotTypes { 23 | } 24 | 25 | export enum LocaleTypes { 26 | deDE = "de-DE", 27 | enAU = "en-AU", 28 | enCA = "en-CA", 29 | enGB = "en-GB", 30 | enIN = "en-IN", 31 | enUS = "en-US", 32 | esES = "es-ES", 33 | esMX = "es-MX", 34 | esUS = "es-US", 35 | frCA = "fr-CA", 36 | frFR = "fr-FR", 37 | itIT = "it-IT", 38 | jaJP = "ja-JP", 39 | ptBR = "pt-BR", 40 | } 41 | 42 | export enum Strings { 43 | WELCOME_MSG = "WELCOME_MSG", 44 | GOODBYE_MSG = "GOODBYE_MSG", 45 | HELP_MSG = "HELP_MSG", 46 | ERROR_MSG = "ERROR_MSG", 47 | ERROR_UNEXPECTED_MSG = "ERROR_UNEXPECTED_MSG", 48 | FALLBACK_MSG = "FALLBACK_MSG", 49 | FALLBACK_REPROMPT = "FALLBACK_REPROMPT", 50 | HELLO_MSG = "HELLO_MSG", 51 | } 52 | -------------------------------------------------------------------------------- /hooks/pre_deploy_hook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Shell script for ask-cli pre-deploy hook for Node.js 3 | # Script Usage: pre_deploy_hook.sh 4 | 5 | # SKILL_NAME is the preformatted name passed from the CLI, after removing special characters. 6 | # DO_DEBUG is boolean value for debug logging 7 | # TARGET is the deploy TARGET provided to the CLI. (eg: all, skill, lambda etc.) 8 | 9 | # Run this script under skill root folder 10 | 11 | # The script does the following: 12 | # - Run "npm install" in each sourceDir in skill.json 13 | 14 | SKILL_NAME=$1 15 | DO_DEBUG=${2:-false} 16 | TARGET=${3:-"all"} 17 | 18 | if [ $DO_DEBUG == false ] 19 | then 20 | exec > /dev/null 2>&1 21 | fi 22 | 23 | install_dependencies() { 24 | npm install --prefix "$1" >/dev/null 2>&1 25 | return $? 26 | } 27 | 28 | echo "###########################" 29 | echo "##### pre-deploy hook #####" 30 | echo "###########################" 31 | 32 | if [[ $TARGET == "all" || $TARGET == "lambda" ]]; then 33 | grep "sourceDir" ./skill.json | cut -d: -f2 | sed 's/"//g' | sed 's/,//g' | while read -r SOURCE_DIR; do 34 | if install_dependencies $SOURCE_DIR; then 35 | echo "Codebase ($SOURCE_DIR) built successfully." 36 | else 37 | echo "There was a problem installing dependencies for ($SOURCE_DIR)." 38 | exit 1 39 | fi 40 | done 41 | echo "###########################" 42 | fi 43 | 44 | exit 0 45 | 46 | -------------------------------------------------------------------------------- /hooks/post_new_hook.ps1: -------------------------------------------------------------------------------- 1 | # Powershell script for ask-cli post-new hook for Node.js 2 | # Script Usage: post_new_hook.ps1 3 | 4 | # SKILL_NAME is the preformatted name passed from the CLI, after removing special characters. 5 | # DO_DEBUG is boolean value for debug logging 6 | 7 | # Run this script one level outside of the skill root folder 8 | 9 | # The script does the following: 10 | # - Run "npm install" in each sourceDir in skill.json 11 | 12 | param( 13 | [string] $SKILL_NAME, 14 | [bool] $DO_DEBUG = $False 15 | ) 16 | 17 | if ($DO_DEBUG) { 18 | Write-Output "###########################" 19 | Write-Output "###### post-new hook ######" 20 | Write-Output "###########################" 21 | } 22 | 23 | function install_dependencies ($CWD, $SOURCE_DIR) { 24 | $INSTALL_PATH = $SKILL_NAME + "\" +$SOURCE_DIR 25 | Set-Location $INSTALL_PATH 26 | Invoke-Expression "npm install" 2>&1 | Out-Null 27 | $EXEC_RESULT = $? 28 | Set-Location $CWD 29 | return $EXEC_RESULT 30 | } 31 | 32 | $SKILL_FILE_PATH = $SKILL_NAME + "\skill.json" 33 | $ALL_SOURCE_DIRS = Get-Content -Path $SKILL_FILE_PATH | select-string -Pattern "sourceDir" -CaseSensitive 34 | Foreach ($SOURCE_DIR in $ALL_SOURCE_DIRS) { 35 | $FILTER_SOURCE_DIR = $SOURCE_DIR -replace "`"", "" -replace "\s", "" -replace ",","" -replace "sourceDir:", "" 36 | $CWD = (Get-Location).Path 37 | if (install_dependencies $CWD $FILTER_SOURCE_DIR) { 38 | if ($DO_DEBUG) { 39 | Write-Output "Codebase ($FILTER_SOURCE_DIR) built successfully." 40 | } 41 | } else { 42 | if ($DO_DEBUG) { 43 | Write-Output "There was a problem installing dependencies for ($FILTER_SOURCE_DIR)." 44 | } 45 | exit 1 46 | } 47 | } 48 | 49 | if ($DO_DEBUG) { 50 | Write-Output "###########################" 51 | } 52 | 53 | exit 0 54 | -------------------------------------------------------------------------------- /hooks/pre_deploy_hook.ps1: -------------------------------------------------------------------------------- 1 | # Powershell script for ask-cli pre-deploy hook for Node.js 2 | # Script Usage: pre_deploy_hook.ps1 3 | 4 | # SKILL_NAME is the preformatted name passed from the CLI, after removing special characters. 5 | # DO_DEBUG is boolean value for debug logging 6 | # TARGET is the deploy TARGET provided to the CLI. (eg: all, skill, lambda etc.) 7 | 8 | # Run this script under the skill root folder 9 | 10 | # The script does the following: 11 | # - Run "npm install" in each sourceDir in skill.json 12 | 13 | param( 14 | [string] $SKILL_NAME, 15 | [bool] $DO_DEBUG = $False, 16 | [string] $TARGET = "all" 17 | ) 18 | 19 | function install_dependencies ($CWD, $SOURCE_DIR) { 20 | Set-Location $SOURCE_DIR 21 | Invoke-Expression "npm install" 2>&1 | Out-Null 22 | $EXEC_RESULT = $? 23 | Set-Location $CWD 24 | return $EXEC_RESULT 25 | } 26 | 27 | if ($DO_DEBUG) { 28 | Write-Output "###########################" 29 | Write-Output "##### pre-deploy hook #####" 30 | Write-Output "###########################" 31 | } 32 | 33 | if ($TARGET -eq "all" -Or $TARGET -eq "lambda") { 34 | $ALL_SOURCE_DIRS = Get-Content -Path "skill.json" | select-string -Pattern "sourceDir" -CaseSensitive 35 | Foreach ($SOURCE_DIR in $ALL_SOURCE_DIRS) { 36 | $FILTER_SOURCE_DIR = $SOURCE_DIR -replace "`"", "" -replace "\s", "" -replace ",","" -replace "sourceDir:", "" 37 | $CWD = (Get-Location).Path 38 | if (install_dependencies $CWD $FILTER_SOURCE_DIR) { 39 | if ($DO_DEBUG) { 40 | Write-Output "Codebase ($FILTER_SOURCE_DIR) built successfully." 41 | } 42 | } else { 43 | if ($DO_DEBUG) { 44 | Write-Output "There was a problem installing dependencies for ($FILTER_SOURCE_DIR)." 45 | } 46 | exit 1 47 | } 48 | } 49 | if ($DO_DEBUG) { 50 | Write-Output "###########################" 51 | } 52 | } 53 | 54 | exit 0 55 | -------------------------------------------------------------------------------- /lambda/custom/typings/index.d.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 | * The slot values for the current request. 15 | */ 16 | slots: SlotValues; 17 | 18 | [key: string]: any; 19 | } 20 | 21 | export interface SessionAttributes { 22 | [key: string]: any; 23 | } 24 | 25 | export type Slots = { [key: string]: Slot }; 26 | 27 | /** 28 | * A matched slot value (if `status.code` = "ER_SUCCESS_MATCH"). 29 | */ 30 | export interface MatchedSlotValue { 31 | /** 32 | * Name of the slot. 33 | */ 34 | name: string; 35 | 36 | /** 37 | * Value that the user said (unresolved). 38 | */ 39 | value: string; 40 | 41 | /** 42 | * `statis.code` = "ER_SUCCESS_MATCH" 43 | */ 44 | isMatch: true; 45 | 46 | /** 47 | * The first resolved value. 48 | */ 49 | resolved: string; 50 | 51 | /** 52 | * The first resolved id. 53 | */ 54 | id: string; 55 | 56 | /** 57 | * `True` if there are multiple resolved values. 58 | */ 59 | isAmbiguous: boolean; 60 | 61 | /** 62 | * All resolved values. If there are multiple values, `isAmbiguous` will be `true`. 63 | */ 64 | values: slu.entityresolution.Value[]; 65 | 66 | /** 67 | * Whether the user has explicitly confirmed or denied the value of this slot. 68 | */ 69 | confirmationStatus: SlotConfirmationStatus; 70 | } 71 | 72 | /** 73 | * An unmatched slot value (if `status.code` != "ER_SUCCESS_MATCH"). 74 | */ 75 | export interface UnmatchedSlotValue { 76 | /** 77 | * Name of the slot. 78 | */ 79 | name: string; 80 | 81 | /** 82 | * Value that the user said (unresolved). 83 | */ 84 | value: string | undefined; 85 | 86 | /** 87 | * `statis.code` != "ER_SUCCESS_MATCH" 88 | */ 89 | isMatch: false; 90 | 91 | /** 92 | * Whether the user has explicitly confirmed or denied the value of this slot. 93 | */ 94 | confirmationStatus: SlotConfirmationStatus; 95 | } 96 | 97 | export interface SlotValues { 98 | [key: string]: MatchedSlotValue | UnmatchedSlotValue | undefined; 99 | } 100 | 101 | export interface Resource { 102 | [language: string]: ResourceLanguage; 103 | } 104 | 105 | interface ResourceLanguage { 106 | [namespace: string]: ResourceKey; 107 | } 108 | 109 | type ResourceKey = 110 | | string 111 | | { 112 | [key: string]: any; 113 | }; 114 | -------------------------------------------------------------------------------- /gulpfile-base.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const gulp = require("gulp"); 4 | const path = require("path"); 5 | const child_process = require("child_process"); 6 | 7 | const OUT_DIR = "dist"; 8 | const IN_DIR = "lambda"; 9 | 10 | module.exports = { 11 | 12 | /** 13 | * Compiles the project. 14 | */ 15 | tsc: (done) => { 16 | const tscPath = path.normalize("./node_modules/.bin/tsc"); 17 | const command = `${tscPath} -p tsconfig.json`; 18 | 19 | exec(command, done); 20 | }, 21 | 22 | /** 23 | * Cleans the project. 24 | */ 25 | clean: () => { 26 | const del = require("del"); 27 | 28 | return del(["dist"]); 29 | }, 30 | 31 | /** 32 | * Copies the files that are required for the project to run but are not automatically 33 | * included by `tsc`. 34 | */ 35 | copyFiles: () => { 36 | return gulp.src(IN_DIR + "/**/*.json").pipe(gulp.dest(OUT_DIR)); 37 | }, 38 | 39 | /** 40 | * Updates the invocation name of the models for the current environment. 41 | * 42 | * This allows you to have different invocation names for different environments 43 | * without having to manually change it every time. You can hook this into the 44 | * deploy script to do it automatically. 45 | */ 46 | models: (done) => { 47 | const fs = require("fs"); 48 | 49 | /** 50 | * Reads the model for the given locale and returns the parsed JSON. 51 | * 52 | * @param {string} locale 53 | */ 54 | function readModel(locale) { 55 | const model = fs.readFileSync(`${__dirname}/models/${locale}.json`, "utf-8"); 56 | return JSON.parse(model); 57 | } 58 | 59 | /** 60 | * Writes the given model to the file. 61 | * 62 | * @param {object} model 63 | * @param {string} locale 64 | */ 65 | function writeModel(model, locale) { 66 | const json = JSON.stringify(model, null, 2); 67 | fs.writeFileSync(`${__dirname}/models/${locale}.json`, json); 68 | } 69 | 70 | // add more locales if needed 71 | const Locales = { 72 | enUS: "en-US", 73 | }; 74 | 75 | const Environments = { 76 | Dev: "dev", 77 | Prod: "prod", 78 | Local: "local", 79 | }; 80 | 81 | const invocations = { 82 | [Environments.Local]: { 83 | [Locales.enUS]: "hello world local", 84 | }, 85 | [Environments.Dev]: { 86 | [Locales.enUS]: "hello world development", 87 | }, 88 | [Environments.Prod]: { 89 | [Locales.enUS]: "hello world", 90 | }, 91 | }; 92 | 93 | // make sure we have the environment set 94 | if (!process.env.ENV) { 95 | throw new Error("ENV environment variable not set"); 96 | } 97 | 98 | // get the current environment 99 | const env = process.env.ENV; 100 | 101 | // make sure the env is valid 102 | if (env !== Environments.Local 103 | && env !== Environments.Dev 104 | && env !== Environments.Prod) { 105 | throw new Error("Invalid ENV environment variable: " + env); 106 | } 107 | 108 | /** 109 | * Updates the invocation name of the model for the given environment and locale. 110 | * 111 | * @param {*} env 112 | * @param {*} locale 113 | */ 114 | function updateModelInvocationName(env, locale) { 115 | // read the model 116 | const model = readModel(locale); 117 | 118 | // set the invocation name 119 | model.interactionModel.languageModel.invocationName = invocations[env][locale]; 120 | 121 | // write the model back to the file 122 | writeModel(model, locale); 123 | } 124 | 125 | // update the invocation names 126 | updateModelInvocationName(env, Locales.enUS); 127 | 128 | done(); 129 | }, 130 | 131 | }; 132 | 133 | function exec(command, callback) { 134 | child_process.exec(command, function (err, stdout, stderr) { 135 | if (stdout) { 136 | console.log(stdout); 137 | } 138 | 139 | if (stderr) { 140 | console.log(stderr); 141 | } 142 | 143 | callback(err); 144 | }); 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alexa Skill starter project using AWS Lambda and Typescript 2 | 3 | This is a simple starter project for Alexa skills using Typescript. 4 | 5 | ## What is included in the project 6 | 7 | - Default request handlers 8 | 9 | | Name | 10 | | --- | 11 | | `LaunchRequest` | 12 | | `SessionEndedRequest` | 13 | | `System.ExceptionEncountered` | 14 | | `AMAZON.HelpIntent` | 15 | | `AMAZON.StopIntent` and `AMAZON.CancelIntent` | 16 | | `AMAZON.FallbackIntent` | 17 | 18 | - Extra handlers 19 | 20 | | Name | Description | 21 | | --- | --- | 22 | | `HelloWorld` | Triggered when the user says "hello", will answer back with "hello". | 23 | | `Debug` | Can be placed at the beginning of the request handlers stack and it will print the `handlerInput`. Useful for debugging. | 24 | 25 | - Error handlers 26 | 27 | | Name | Description | 28 | | --- | --- | 29 | | `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. | 30 | | `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). | 31 | 32 | - Request interceptors 33 | 34 | | Name | Description | 35 | | --- | --- | 36 | | `Localization` | Adds `i18next` localization functions to the `RequestAttributes`. | 37 | | `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. | 38 | 39 | - Localization strings 40 | 41 | Check `lambda/custom/lib/strings.ts`. 42 | 43 | - Constants 44 | 45 | Including the String keys, so you can have type safety everywhere. 46 | 47 | Check `lambda/custom/lib/constants.ts`. 48 | 49 | - Helper functions 50 | 51 | Many helper functions which should reduce code duplication, and reduce the code needed to do common tasks. 52 | 53 | Check `lambda/custom/lib/helpers.ts`. 54 | 55 | - Local development 56 | 57 | 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. 58 | 59 | ## Pre-requisites 60 | 61 | - Node.js 62 | - Register for an [AWS Account](https://aws.amazon.com/) 63 | - Register for an [Amazon Developer Account](https://developer.amazon.com/) 64 | - Install and Setup [ASK CLI](https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html) 65 | 66 | ## Installation 67 | 68 | 1. **Make sure** you are running the latest version of the CLI 69 | 70 | ```bash 71 | npm update -g ask-cli 72 | ``` 73 | 74 | 2. **Clone** the repository and navigate into the skill's root directory. 75 | 76 | ```bash 77 | ask new --url https://github.com/Xzya/alexa-typescript-skill-template.git --skill-name hello-world 78 | cd hello-world 79 | ``` 80 | 81 | 3. Install npm dependencies 82 | 83 | ```bash 84 | npm install 85 | ``` 86 | 87 | 4. To start the skill locally, run the following 88 | 89 | ```bash 90 | npm start 91 | ``` 92 | 93 | ## Deployment 94 | 95 | **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. 96 | 97 | 1. Navigate to the project's root directory. you should see a file named 'skill.json' there. 98 | 99 | 2. Deploy the skill and the Lambda function in one step by running the following command: 100 | 101 | ```bash 102 | $ ask deploy 103 | ``` 104 | 105 | ## Local development 106 | 107 | 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/). 108 | 109 | ### Using servo.net 110 | 111 | This is the easiest to setup 112 | 113 | 1. You need to have an SSH client installed, then simply run 114 | 115 | ```bash 116 | $ ssh -R 80:localhost:3980 serveo.net 117 | Forwarding HTTP traffic from [https://YOUR_URL] 118 | Press g to start a GUI session and ctrl-c to quit. 119 | ``` 120 | 121 | 2. Once you see the URL, copy it and go to your Skill console. 122 | 123 | 3. Open the `Endpoint` menu and select `HTTPS` 124 | 125 | 4. Under `Default Region` paste the previous URL you copied. 126 | 127 | 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`. 128 | 129 | 6. You are done! Just run `npm start` to start the local server and begin testing the skill. 130 | 131 | ### Using ngrok.io 132 | 133 | 1. [Install ngrok](https://ngrok.com/download) 134 | 135 | 2. Run `ngrok http 3980` 136 | 137 | 3. Copy the URL and follow the same steps above from 3 to 6. 138 | 139 | ## Developer tasks 140 | 141 | | Command | Description | 142 | | --- | --- | 143 | | `clean` | Deletes the `dist` folder | 144 | | `build` | Builds the lambda function and exports it to the `dist` folder | 145 | | `deploy` | Builds the lambda function and deploys EVERYTHING (skill, model, lambda) | 146 | | `deploy:lambda` | Builds the lambda function and deploys it (just the lambda function) | 147 | | `deploy:local` | Deploys the skill details for the local profile, which will update the HTTPS endpoint | 148 | | `start` | Starts the local `express` server using `nodemon` for local development | 149 | 150 | To see the actual commands, check `package.json`. 151 | 152 | 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`. 153 | 154 | ## Testing 155 | 156 | Taken from [the official hello world project](https://github.com/alexa/skill-sample-nodejs-hello-world/blob/master/instructions/7-cli.md#testing). 157 | 158 | 1. To test, you need to login to Alexa Developer Console, and **enable the "Test" switch on your skill from the "Test" Tab**. 159 | 160 | 2. Simulate verbal interaction with your skill through the command line (this might take a few moments) using the following example: 161 | 162 | ```bash 163 | $ ask simulate -l en-US -t "open hello world" 164 | 165 | ✓ Simulation created for simulation id: 4a7a9ed8-94b2-40c0-b3bd-fb63d9887fa7 166 | ◡ Waiting for simulation response{ 167 | "status": "SUCCESSFUL", 168 | ... 169 | ``` 170 | 171 | 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 : 172 | 173 | ```text 174 | Alexa, start hello world 175 | ``` 176 | 177 | ## Customization 178 | 179 | Taken from [the official hello world project](https://github.com/alexa/skill-sample-nodejs-hello-world/blob/master/instructions/7-cli.md#customization). 180 | 181 | 1. ```./skill.json``` 182 | 183 | Change the skill name, example phrase, icons, testing instructions etc ... 184 | 185 | Remember than many information are locale-specific and must be changed for each locale (e.g. en-US, en-GB, de-DE, etc.) 186 | 187 | See the Skill [Manifest Documentation](https://developer.amazon.com/docs/smapi/skill-manifest.html) for more information. 188 | 189 | 2. ```./lambda/custom/index.ts``` 190 | 191 | Modify messages, and data from the source code to customize the skill. 192 | 193 | 3. ```./models/*.json``` 194 | 195 | 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. 196 | 197 | 4. Remember to re-deploy your skill and Lambda function for your changes to take effect. 198 | 199 | ```bash 200 | $ npm run deploy 201 | ``` 202 | 203 | ## Updating the dependencies 204 | 205 | Note that there are two `package.json` files: 206 | 207 | - `./package.json`, used when running the skill locally. This contains the normal dependencies, as well as some development dependencies and extra scripts 208 | - `./lambda/custom/package.json`, this is used only for production and it just contains the normal dependencies 209 | 210 | If you need to update the normal dependencies (e.g. `ask-sdk-model`), make sure to update it in both `package.json`, otherwise the production build will miss those changes. 211 | -------------------------------------------------------------------------------- /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 "../typings"; 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 as string, 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 as string, 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 | * Returns a VoicePlayer.Speak directive with the given speech. Useful for sending progressive responses. 281 | * 282 | * @param handlerInput 283 | * @param speech 284 | */ 285 | export function VoicePlayerSpeakDirective(handlerInput: HandlerInput, speech?: string): services.directive.SendDirectiveRequest { 286 | const requestId = handlerInput.requestEnvelope.request.requestId; 287 | 288 | return { 289 | directive: { 290 | type: "VoicePlayer.Speak", 291 | speech: speech, 292 | }, 293 | header: { 294 | requestId, 295 | }, 296 | }; 297 | } 298 | --------------------------------------------------------------------------------