├── 10 ├── README.md ├── ask-resources.json ├── lambda │ └── custom │ │ ├── constants.js │ │ ├── documents │ │ ├── launchSampleDatasource.json │ │ ├── launchSampleDatasource_fr_CA.json │ │ ├── launchSampleDatasource_fr_FR.json │ │ ├── launchSampleDatasource_it.json │ │ ├── launchScreen.json │ │ ├── listSampleDatasource.json │ │ ├── listSampleDatasource_fr_CA.json │ │ ├── listSampleDatasource_fr_FR.json │ │ ├── listSampleDatasource_it.json │ │ ├── listScreen.json │ │ └── sampleBirthdaysResponse.json │ │ ├── handlers.js │ │ ├── index.js │ │ ├── interceptors.js │ │ ├── localisation.js │ │ ├── logic.js │ │ ├── package.json │ │ └── util.js ├── media │ ├── LICENSE.txt │ ├── cake_1024x600.png │ ├── cake_1280x800.png │ ├── cake_1920x1080.png │ ├── cake_480x480.png │ ├── cake_960x480.png │ ├── confetti_1024x600.png │ ├── confetti_1280x800.png │ ├── confetti_1920x1080.png │ ├── confetti_480x480.png │ ├── confetti_960x480.png │ ├── full_icon_108.png │ ├── full_icon_512.png │ ├── garlands_1024x600.png │ ├── garlands_1280x800.png │ ├── garlands_1920x1080.png │ ├── garlands_480x480.png │ ├── garlands_960x480.png │ ├── happy_birthday.mp3 │ ├── icon_108.png │ ├── icon_512.png │ ├── lights_1024x600.png │ ├── lights_1280x800.png │ ├── lights_1920x1080.png │ ├── lights_480x480.png │ ├── lights_960x480.png │ ├── papers_1024x600.png │ ├── papers_1280x800.png │ ├── papers_1920x1080.png │ ├── papers_480x480.png │ ├── papers_960x480.png │ ├── straws_1024x600.png │ ├── straws_1280x800.png │ ├── straws_1920x1080.png │ ├── straws_480x480.png │ └── straws_960x480.png └── skill-package │ ├── interactionModels │ └── custom │ │ ├── en-AU.json │ │ ├── en-CA.json │ │ ├── en-GB.json │ │ ├── en-IN.json │ │ ├── en-US.json │ │ ├── es-ES.json │ │ ├── es-MX.json │ │ ├── es-US.json │ │ ├── fr-CA.json │ │ ├── fr-FR.json │ │ └── it-IT.json │ └── skill.json ├── .gitignore ├── 01 ├── README.md ├── lambda │ ├── index.js │ ├── package.json │ └── util.js └── models │ └── en-US.json ├── 02 ├── README.md ├── lambda │ ├── index.js │ ├── package.json │ └── util.js └── models │ ├── en-AU.json │ ├── en-CA.json │ ├── en-GB.json │ ├── en-IN.json │ ├── en-US.json │ ├── es-ES.json │ ├── es-MX.json │ ├── es-US.json │ ├── fr-CA.json │ ├── fr-FR.json │ └── it-IT.json ├── 03 ├── README.md ├── lambda │ ├── index.js │ ├── package.json │ └── util.js └── models │ ├── en-AU.json │ ├── en-CA.json │ ├── en-GB.json │ ├── en-IN.json │ ├── en-US.json │ ├── es-ES.json │ ├── es-MX.json │ ├── es-US.json │ ├── fr-CA.json │ ├── fr-FR.json │ └── it-IT.json ├── 04 ├── README.md ├── lambda │ ├── index.js │ ├── localisation.js │ ├── package.json │ └── util.js └── models │ ├── en-AU.json │ ├── en-CA.json │ ├── en-GB.json │ ├── en-IN.json │ ├── en-US.json │ ├── es-ES.json │ ├── es-MX.json │ ├── es-US.json │ ├── fr-CA.json │ ├── fr-FR.json │ └── it-IT.json ├── 05 ├── README.md ├── ask-resources.json ├── lambda │ ├── index.js │ ├── interceptors.js │ ├── localisation.js │ ├── package.json │ └── util.js └── skill-package │ ├── interactionModels │ └── custom │ │ ├── en-AU.json │ │ ├── en-CA.json │ │ ├── en-GB.json │ │ ├── en-IN.json │ │ ├── en-US.json │ │ ├── es-ES.json │ │ ├── es-MX.json │ │ ├── es-US.json │ │ ├── fr-CA.json │ │ ├── fr-FR.json │ │ └── it-IT.json │ └── skill.json ├── 05_intent_chaining ├── README.md ├── ask-resources.json ├── lambda │ └── custom │ │ ├── index.js │ │ ├── interceptors.js │ │ ├── localisation.js │ │ ├── package.json │ │ └── util.js └── skill-package │ ├── interactionModels │ └── custom │ │ ├── en-AU.json │ │ ├── en-CA.json │ │ ├── en-GB.json │ │ ├── en-IN.json │ │ ├── en-US.json │ │ ├── es-ES.json │ │ ├── es-MX.json │ │ └── es-US.json │ └── skill.json ├── 06 ├── README.md ├── ask-resources.json ├── lambda │ ├── constants.js │ ├── index.js │ ├── interceptors.js │ ├── localisation.js │ ├── logic.js │ ├── package.json │ └── util.js └── skill-package │ ├── interactionModels │ └── custom │ │ ├── en-AU.json │ │ ├── en-CA.json │ │ ├── en-GB.json │ │ ├── en-IN.json │ │ ├── en-US.json │ │ ├── es-ES.json │ │ ├── es-MX.json │ │ ├── es-US.json │ │ ├── fr-CA.json │ │ ├── fr-FR.json │ │ └── it-IT.json │ └── skill.json ├── 07 ├── README.md ├── ask-resources.json ├── lambda │ ├── constants.js │ ├── index.js │ ├── interceptors.js │ ├── localisation.js │ ├── logic.js │ ├── package.json │ └── util.js └── skill-package │ ├── interactionModels │ └── custom │ │ ├── en-AU.json │ │ ├── en-CA.json │ │ ├── en-GB.json │ │ ├── en-IN.json │ │ ├── en-US.json │ │ ├── es-ES.json │ │ ├── es-MX.json │ │ ├── es-US.json │ │ ├── fr-CA.json │ │ ├── fr-FR.json │ │ └── it-IT.json │ └── skill.json ├── 08 ├── README.md ├── ask-resources.json ├── lambda │ ├── constants.js │ ├── documents │ │ ├── images │ │ │ ├── LICENSE.txt │ │ │ ├── cake_1024x600.png │ │ │ ├── cake_1280x800.png │ │ │ ├── cake_1920x1080.png │ │ │ ├── cake_480x480.png │ │ │ ├── cake_960x480.png │ │ │ ├── confetti_1024x600.png │ │ │ ├── confetti_1280x800.png │ │ │ ├── confetti_1920x1080.png │ │ │ ├── confetti_480x480.png │ │ │ ├── confetti_960x480.png │ │ │ ├── full_icon_108.png │ │ │ ├── full_icon_512.png │ │ │ ├── garlands_1024x600.png │ │ │ ├── garlands_1280x800.png │ │ │ ├── garlands_1920x1080.png │ │ │ ├── garlands_480x480.png │ │ │ ├── garlands_960x480.png │ │ │ ├── icon_108.png │ │ │ ├── icon_512.png │ │ │ ├── lights_1024x600.png │ │ │ ├── lights_1280x800.png │ │ │ ├── lights_1920x1080.png │ │ │ ├── lights_480x480.png │ │ │ ├── lights_960x480.png │ │ │ ├── papers_1024x600.png │ │ │ ├── papers_1280x800.png │ │ │ ├── papers_1920x1080.png │ │ │ ├── papers_480x480.png │ │ │ ├── papers_960x480.png │ │ │ ├── straws_1024x600.png │ │ │ ├── straws_1280x800.png │ │ │ ├── straws_1920x1080.png │ │ │ ├── straws_480x480.png │ │ │ └── straws_960x480.png │ │ ├── launchSampleDatasource.json │ │ ├── launchSampleDatasource_fr_CA.json │ │ ├── launchSampleDatasource_fr_FR.json │ │ ├── launchSampleDatasource_it.json │ │ └── launchScreen.json │ ├── handlers.js │ ├── index.js │ ├── interceptors.js │ ├── localisation.js │ ├── logic.js │ ├── package.json │ └── util.js └── skill-package │ ├── interactionModels │ └── custom │ │ ├── en-AU.json │ │ ├── en-CA.json │ │ ├── en-GB.json │ │ ├── en-IN.json │ │ ├── en-US.json │ │ ├── es-ES.json │ │ ├── es-MX.json │ │ ├── es-US.json │ │ ├── fr-CA.json │ │ ├── fr-FR.json │ │ └── it-IT.json │ └── skill.json ├── 09 ├── README.md ├── ask-resources.json ├── lambda │ ├── constants.js │ ├── documents │ │ ├── images │ │ │ ├── LICENSE.txt │ │ │ ├── cake_1024x600.png │ │ │ ├── cake_1280x800.png │ │ │ ├── cake_1920x1080.png │ │ │ ├── cake_480x480.png │ │ │ ├── cake_960x480.png │ │ │ ├── confetti_1024x600.png │ │ │ ├── confetti_1280x800.png │ │ │ ├── confetti_1920x1080.png │ │ │ ├── confetti_480x480.png │ │ │ ├── confetti_960x480.png │ │ │ ├── full_icon_108.png │ │ │ ├── full_icon_512.png │ │ │ ├── garlands_1024x600.png │ │ │ ├── garlands_1280x800.png │ │ │ ├── garlands_1920x1080.png │ │ │ ├── garlands_480x480.png │ │ │ ├── garlands_960x480.png │ │ │ ├── icon_108.png │ │ │ ├── icon_512.png │ │ │ ├── lights_1024x600.png │ │ │ ├── lights_1280x800.png │ │ │ ├── lights_1920x1080.png │ │ │ ├── lights_480x480.png │ │ │ ├── lights_960x480.png │ │ │ ├── papers_1024x600.png │ │ │ ├── papers_1280x800.png │ │ │ ├── papers_1920x1080.png │ │ │ ├── papers_480x480.png │ │ │ ├── papers_960x480.png │ │ │ ├── straws_1024x600.png │ │ │ ├── straws_1280x800.png │ │ │ ├── straws_1920x1080.png │ │ │ ├── straws_480x480.png │ │ │ └── straws_960x480.png │ │ ├── launchSampleDatasource.json │ │ ├── launchSampleDatasource_fr_CA.json │ │ ├── launchSampleDatasource_fr_FR.json │ │ ├── launchSampleDatasource_it.json │ │ ├── launchScreen.json │ │ ├── listSampleDatasource.json │ │ ├── listSampleDatasource_fr_CA.json │ │ ├── listSampleDatasource_fr_FR.json │ │ ├── listSampleDatasource_it.json │ │ ├── listScreen.json │ │ └── sampleBirthdaysResponse.json │ ├── handlers.js │ ├── index.js │ ├── interceptors.js │ ├── localisation.js │ ├── logic.js │ ├── package.json │ └── util.js └── skill-package │ ├── interactionModels │ └── custom │ │ ├── en-AU.json │ │ ├── en-CA.json │ │ ├── en-GB.json │ │ ├── en-IN.json │ │ ├── en-US.json │ │ ├── es-ES.json │ │ ├── es-MX.json │ │ ├── es-US.json │ │ ├── fr-CA.json │ │ ├── fr-FR.json │ │ └── it-IT.json │ └── skill.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md └── README_ES.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | package-lock.json 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | -------------------------------------------------------------------------------- /01/README.md: -------------------------------------------------------------------------------- 1 | # Part 1 - Alexa-Hosted Skills (preset - Hello World) 2 | 3 | In this module we introduce some theory about ASK and then we build an Alexa Hosted Skill from scratch in no time (Hello World template) with no localization (out-of-the-box experience). When using the console we introduce the basic tabs (Build, Code, Test, etc) 4 | 5 | ## Milestones 6 | 7 | 1. **Developer Console**: Build, Code and Test tabs. Types of skills that you can create (focus on Custom) 8 | 2. **Build tab**: Invocation name, Built-In and Custom Intents and Utterances in VIM. VIM as JSON (JSON editor) 9 | 3. **Code Tab**: Dependencies (require), package.json, Save/Deploy/Promote to Live in AHS (branches), basic Handler Structure (canHandle/handle), Handler Input, Response Builder, Speak/Reprompt, Skill Builder + handler registration, Error handler, Session ended request 10 | 4. **Code Tab**: Intent Reflector & FallbackIntent handler (*) 11 | 5. **Test Tab**: Custom Skill invocation, JSON in/out, LaunchRequest, IntentRequest, Help/Stop/Cancel, Session ended, Test on real device (same account) 12 | 13 | ## Concepts 14 | 15 | 1. Development and Production Lambda Stages (AHS branches) 16 | 2. Lambda Dependencies (package.json, requires in code) 17 | 3. Handler as processor of incoming requests. Handler structure 18 | 4. Request Types (LaunchRequest, IntentRequest, SessionEndedRequest) 19 | 5. Skill Builder (custom vs standard) and its functions 20 | 6. Reflector (catch all intent handler) 21 | 7. Out-of-domain utterances (*) 22 | 23 | * (only in locales that support AMAZON.FallbackIntent) otherwise explain that including the handler on the back-end side does not affect the operation (we can keep it but should clarify to avoid confusion) 24 | 25 | ## Videos 26 | 27 | [EN](https://alexa.design/zerotohero1)/[DE](https://alexa.design/de_zerotohero1)/[FR](https://alexa.design/fr_zerotohero1)/[IT](https://alexa.design/it_zerotohero1)/[ES](../README_ES.md) 28 | (EN version includes the following subtitles: EN, DE, PT) -------------------------------------------------------------------------------- /01/lambda/index.js: -------------------------------------------------------------------------------- 1 | // This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2). 2 | // Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management, 3 | // session persistence, api calls, and more. 4 | const Alexa = require('ask-sdk-core'); 5 | 6 | const LaunchRequestHandler = { 7 | canHandle(handlerInput) { 8 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest'; 9 | }, 10 | handle(handlerInput) { 11 | const speakOutput = 'Welcome, you can say Hello or Help. Which would you like to try?'; 12 | return handlerInput.responseBuilder 13 | .speak(speakOutput) 14 | .reprompt(speakOutput) 15 | .getResponse(); 16 | } 17 | }; 18 | const HelloWorldIntentHandler = { 19 | canHandle(handlerInput) { 20 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' 21 | && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldIntent'; 22 | }, 23 | handle(handlerInput) { 24 | const speakOutput = 'Hello World!'; 25 | return handlerInput.responseBuilder 26 | .speak(speakOutput) 27 | //.reprompt('add a reprompt if you want to keep the session open for the user to respond') 28 | .getResponse(); 29 | } 30 | }; 31 | const HelpIntentHandler = { 32 | canHandle(handlerInput) { 33 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' 34 | && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent'; 35 | }, 36 | handle(handlerInput) { 37 | const speakOutput = 'You can say hello to me! How can I help?'; 38 | 39 | return handlerInput.responseBuilder 40 | .speak(speakOutput) 41 | .reprompt(speakOutput) 42 | .getResponse(); 43 | } 44 | }; 45 | const CancelAndStopIntentHandler = { 46 | canHandle(handlerInput) { 47 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' 48 | && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent' 49 | || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent'); 50 | }, 51 | handle(handlerInput) { 52 | const speakOutput = 'Goodbye!'; 53 | return handlerInput.responseBuilder 54 | .speak(speakOutput) 55 | .getResponse(); 56 | } 57 | }; 58 | const SessionEndedRequestHandler = { 59 | canHandle(handlerInput) { 60 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest'; 61 | }, 62 | handle(handlerInput) { 63 | // Any cleanup logic goes here. 64 | return handlerInput.responseBuilder.getResponse(); 65 | } 66 | }; 67 | 68 | // The intent reflector is used for interaction model testing and debugging. 69 | // It will simply repeat the intent the user said. You can create custom handlers 70 | // for your intents by defining them above, then also adding them to the request 71 | // handler chain below. 72 | const IntentReflectorHandler = { 73 | canHandle(handlerInput) { 74 | return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'; 75 | }, 76 | handle(handlerInput) { 77 | const intentName = Alexa.getIntentName(handlerInput.requestEnvelope); 78 | const speakOutput = `You just triggered ${intentName}`; 79 | 80 | return handlerInput.responseBuilder 81 | .speak(speakOutput) 82 | //.reprompt('add a reprompt if you want to keep the session open for the user to respond') 83 | .getResponse(); 84 | } 85 | }; 86 | 87 | // Generic error handling to capture any syntax or routing errors. If you receive an error 88 | // stating the request handler chain is not found, you have not implemented a handler for 89 | // the intent being invoked or included it in the skill builder below. 90 | const ErrorHandler = { 91 | canHandle() { 92 | return true; 93 | }, 94 | handle(handlerInput, error) { 95 | console.log(`~~~~ Error handled: ${error.stack}`); 96 | const speakOutput = `Sorry, I had trouble doing what you asked. Please try again.`; 97 | 98 | return handlerInput.responseBuilder 99 | .speak(speakOutput) 100 | .reprompt(speakOutput) 101 | .getResponse(); 102 | } 103 | }; 104 | 105 | // The SkillBuilder acts as the entry point for your skill, routing all request and response 106 | // payloads to the handlers above. Make sure any new handlers or interceptors you've 107 | // defined are included below. The order matters - they're processed top to bottom. 108 | exports.handler = Alexa.SkillBuilders.custom() 109 | .addRequestHandlers( 110 | LaunchRequestHandler, 111 | HelloWorldIntentHandler, 112 | HelpIntentHandler, 113 | CancelAndStopIntentHandler, 114 | SessionEndedRequestHandler, 115 | IntentReflectorHandler, // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers 116 | ) 117 | .addErrorHandlers( 118 | ErrorHandler, 119 | ) 120 | .lambda(); 121 | -------------------------------------------------------------------------------- /01/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "1.1.0", 4 | "description": "alexa utility for quickly building skills", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon Alexa", 10 | "license": "ISC", 11 | "dependencies": { 12 | "ask-sdk-core": "^2.6.0", 13 | "ask-sdk-model": "^1.18.0", 14 | "aws-sdk": "^2.326.0" 15 | } 16 | } -------------------------------------------------------------------------------- /01/lambda/util.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const s3SigV4Client = new AWS.S3({ 4 | signatureVersion: 'v4' 5 | }); 6 | 7 | module.exports.getS3PreSignedUrl = function getS3PreSignedUrl(s3ObjectKey) { 8 | 9 | const bucketName = process.env.S3_PERSISTENCE_BUCKET; 10 | const s3PreSignedUrl = s3SigV4Client.getSignedUrl('getObject', { 11 | Bucket: bucketName, 12 | Key: s3ObjectKey, 13 | Expires: 60*1 // the Expires is capped for 1 minute 14 | }); 15 | console.log(`Util.s3PreSignedUrl: ${s3ObjectKey} URL ${s3PreSignedUrl}`); 16 | return s3PreSignedUrl; 17 | 18 | } -------------------------------------------------------------------------------- /01/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": "HelloWorldIntent", 20 | "slots": [], 21 | "samples": [ 22 | "hello", 23 | "how are you", 24 | "say hi world", 25 | "say hi", 26 | "hi", 27 | "say hello world", 28 | "say hello" 29 | ] 30 | }, 31 | { 32 | "name": "AMAZON.NavigateHomeIntent", 33 | "samples": [] 34 | } 35 | ], 36 | "types": [] 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /02/README.md: -------------------------------------------------------------------------------- 1 | # Part 2 - Skill Internationalization (i18n), Interceptors and Error Handler 2 | 3 | In this module we explain how to use i18next to do the i18n of your skill. We got rid of the sprintf module and now we're using plain i18next for string replacement We also introduce interceptors and attributtes. Start with brief intro to i18n. In this module we generate a locale specific Hello World (eg. Spanish, French, etc) 4 | 5 | ## Milestones 6 | 7 | 1. **Developer Console**: adding an extra locale (xx-XX (your own locale), departing from the en-US model) 8 | 2. **Code Tab**: i18next dependency, string replacement with {{}}, languageStrings (embedded for now) 9 | 3. **Code Tab**: Request and Response Interceptors (loggers), Localisation Interceptor (simplest possible for now, no arrays) 10 | 4. **Code Tab**: handlerInput.t and handlerInput.t with parameters (string replacement example in Reflector message) 11 | 12 | ## Concepts 13 | 14 | 1. Multiple models per locale 15 | 2. Key/value string resources for i18n 16 | 3. Enriching handlerInput with t function via interceptor 17 | 4. Attribute manager as key/value store 18 | 5. High level attribute types (session(short term), persistent(long term)) 19 | 6. Changing locale on Build tab and on Test tab (test both locales) 20 | 21 | ## Diff 22 | 23 | 1. *lambda/custom/package.json*: add i18next dependency, we also update all existing dependencies to latest versions 24 | 2. *lambda/custom/index.js*: add i18next require, add languageStrings, get localisation strings via function handlerInput.t, add 3 interceptors (log request, log response and localization interceptor), add them to the exports via Alexa skill builder (bottom of file). Replace all hard coded speech string with handlerIput.t(STRING_KEY). Add detection of locale to i18n interceptor via ASK SDK Utilities (Alexa.getLocale()) 25 | 3. *models/xx-XX.json*: add this file copied from en-US.json and translate (xx-XX is your own locale) 26 | 27 | ## Videos 28 | 29 | [EN](https://alexa.design/zerotohero2)/[DE](https://alexa.design/de_zerotohero2)/[FR](https://alexa.design/fr_zerotohero2)/[IT](https://alexa.design/it_zerotohero2)/[ES](../README_ES.md) 30 | (EN version includes the following subtitles: EN, DE, PT) -------------------------------------------------------------------------------- /02/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "0.2.0", 4 | "description": "alexa utility for quickly building skills", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon Alexa", 10 | "dependencies": { 11 | "ask-sdk-core": "^2.7.0", 12 | "ask-sdk-model": "^1.18.0", 13 | "i18next": "^15.0.5" 14 | } 15 | } -------------------------------------------------------------------------------- /02/lambda/util.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const s3SigV4Client = new AWS.S3({ 4 | signatureVersion: 'v4' 5 | }); 6 | 7 | module.exports.getS3PreSignedUrl = function getS3PreSignedUrl(s3ObjectKey) { 8 | const bucketName = process.env.S3_PERSISTENCE_BUCKET; 9 | const s3PreSignedUrl = s3SigV4Client.getSignedUrl('getObject', { 10 | Bucket: bucketName, 11 | Key: s3ObjectKey, 12 | Expires: 60*1 // the Expires is capped for 1 minute 13 | }); 14 | console.log(`Util.s3PreSignedUrl: ${s3ObjectKey} URL ${s3PreSignedUrl}`); 15 | return s3PreSignedUrl; 16 | } -------------------------------------------------------------------------------- /02/models/en-AU.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": "HelloWorldIntent", 20 | "slots": [], 21 | "samples": [ 22 | "hello", 23 | "how are you", 24 | "say hi world", 25 | "say hi", 26 | "hi", 27 | "say hello world", 28 | "say hello" 29 | ] 30 | }, 31 | { 32 | "name": "AMAZON.NavigateHomeIntent", 33 | "samples": [] 34 | }, 35 | { 36 | "name": "AMAZON.FallbackIntent", 37 | "samples": [] 38 | } 39 | ], 40 | "types": [] 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /02/models/en-CA.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": "HelloWorldIntent", 20 | "slots": [], 21 | "samples": [ 22 | "hello", 23 | "how are you", 24 | "say hi world", 25 | "say hi", 26 | "hi", 27 | "say hello world", 28 | "say hello" 29 | ] 30 | }, 31 | { 32 | "name": "AMAZON.NavigateHomeIntent", 33 | "samples": [] 34 | }, 35 | { 36 | "name": "AMAZON.FallbackIntent", 37 | "samples": [] 38 | } 39 | ], 40 | "types": [] 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /02/models/en-GB.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": "HelloWorldIntent", 20 | "slots": [], 21 | "samples": [ 22 | "hello", 23 | "how are you", 24 | "say hi world", 25 | "say hi", 26 | "hi", 27 | "say hello world", 28 | "say hello" 29 | ] 30 | }, 31 | { 32 | "name": "AMAZON.NavigateHomeIntent", 33 | "samples": [] 34 | }, 35 | { 36 | "name": "AMAZON.FallbackIntent", 37 | "samples": [] 38 | } 39 | ], 40 | "types": [] 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /02/models/en-IN.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": "HelloWorldIntent", 20 | "slots": [], 21 | "samples": [ 22 | "hello", 23 | "how are you", 24 | "say hi world", 25 | "say hi", 26 | "hi", 27 | "say hello world", 28 | "say hello" 29 | ] 30 | }, 31 | { 32 | "name": "AMAZON.NavigateHomeIntent", 33 | "samples": [] 34 | }, 35 | { 36 | "name": "AMAZON.FallbackIntent", 37 | "samples": [] 38 | } 39 | ], 40 | "types": [] 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /02/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": "HelloWorldIntent", 20 | "slots": [], 21 | "samples": [ 22 | "hello", 23 | "how are you", 24 | "say hi world", 25 | "say hi", 26 | "hi", 27 | "say hello world", 28 | "say hello" 29 | ] 30 | }, 31 | { 32 | "name": "AMAZON.NavigateHomeIntent", 33 | "samples": [] 34 | }, 35 | { 36 | "name": "AMAZON.FallbackIntent", 37 | "samples": [] 38 | } 39 | ], 40 | "types": [] 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /02/models/es-ES.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "hola mundo", 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": "HelloWorldIntent", 20 | "slots": [], 21 | "samples": [ 22 | "hola", 23 | "como estás", 24 | "di hola mundo", 25 | "di hola", 26 | "hola mundo" 27 | ] 28 | }, 29 | { 30 | "name": "AMAZON.NavigateHomeIntent", 31 | "samples": [] 32 | } 33 | ], 34 | "types": [] 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /02/models/es-MX.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "hola mundo", 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": "HelloWorldIntent", 20 | "slots": [], 21 | "samples": [ 22 | "hola", 23 | "como estás", 24 | "di hola mundo", 25 | "di hola", 26 | "hola mundo" 27 | ] 28 | }, 29 | { 30 | "name": "AMAZON.NavigateHomeIntent", 31 | "samples": [] 32 | } 33 | ], 34 | "types": [] 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /02/models/es-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "hola mundo", 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": "HelloWorldIntent", 20 | "slots": [], 21 | "samples": [ 22 | "hola", 23 | "como estás", 24 | "di hola mundo", 25 | "di hola", 26 | "hola mundo" 27 | ] 28 | }, 29 | { 30 | "name": "AMAZON.NavigateHomeIntent", 31 | "samples": [] 32 | } 33 | ], 34 | "types": [] 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /02/models/fr-CA.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "le génie des salutations", 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": "HelloWorldIntent", 20 | "slots": [], 21 | "samples": [ 22 | "bon matin", 23 | "bonjour", 24 | "coucou", 25 | "salut", 26 | "me dire bonjour", 27 | "me dire bon matin" 28 | ] 29 | }, 30 | { 31 | "name": "AMAZON.NavigateHomeIntent", 32 | "samples": [] 33 | } 34 | ], 35 | "types": [] 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /02/models/fr-FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "le génie des salutations", 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": "HelloWorldIntent", 20 | "slots": [], 21 | "samples": [ 22 | "bonjour", 23 | "coucou", 24 | "salut", 25 | "me dire bonjour" 26 | ] 27 | }, 28 | { 29 | "name": "AMAZON.NavigateHomeIntent", 30 | "samples": [] 31 | } 32 | ], 33 | "types": [] 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /02/models/it-IT.json: -------------------------------------------------------------------------------- 1 | { 2 | "interactionModel": { 3 | "languageModel": { 4 | "invocationName": "ciao mondo", 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": "HelloWorldIntent", 20 | "slots": [], 21 | "samples": [ 22 | "ciao", 23 | "come stai", 24 | "dì ciao", 25 | "salutami", 26 | "salutarmi" 27 | ] 28 | }, 29 | { 30 | "name": "AMAZON.NavigateHomeIntent", 31 | "samples": [] 32 | } 33 | ], 34 | "types": [] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /03/README.md: -------------------------------------------------------------------------------- 1 | # Part 3 - Slots, Slot Validation and Automatic Dialog Delegation 2 | 3 | In this module we depart from the previous project and overwrite HelloWorldIntent. We show slots (built-in and custom), synonyms, slot validation and dialogs & prompts with AutoDelegate. Here we also introduce intent chaining in LaunchRequest 4 | 5 | ## Milestones 6 | 7 | 1. **Build Tab**: Change invocation name to "happy birthday" in en-* and to the equivalent translation in your foreign locale 8 | 2. **Build Tab**: rename HelloWorldIntent to RegisterBirthdayIntent. Auto delegate should be already enabled when the skill was created 9 | 3. **Build Tab**: create slot day, slot type is AMAZON.NUMBER in your foreign locale and AMAZON.Ordinal in en-*. Show json 10 | 4. **Build Tab**: create slot month, create slot type MonthType with a list of months. MonthType is a custom slot (for illustration purposes on how to create custom slots we're not using AMAZON.Month). If you want compare solution to using AMAZON.Date 11 | 5. **Build Tab**: create slot year, type is AMAZON.Four_Digit. 12 | 6. **Build Tab**: create synonyms in month type (eg. "first month of the year", "the first one"). Not used just for illustration purposes 13 | 7. **Build Tab**: create utterances that cover collection of slots plus some slot-less utterances (eg. "register my birthday") 14 | 8. **Build Tab**: mark day as required, add prompts (including prompt that use the other slots) 15 | 9. **Build Tab**: add slot validation to day (eg >= 1 & <= 31 for AMAZON.Number and constrain on set of 1st to 31st for AMAZON.Ordinal) 16 | 10. **Build Tab**: mark month as required, add prompts (including prompts that refer to other slots) 17 | 11. **Build Tab**: add slot validation to month (show constrain on slot type values and synonyms, mention this is not possible on built-in list slots (eg. AMAZON.Actor)) 18 | 12. **Build Tab**: mark year as required, add prompts (including prompts that refer to other slots) 19 | 13. **Build Tab**: add an arbitrary slot validation to year (eg > 1900 & < 2014) to catch people too young or too old 20 | 14. **Build Tab**: explain dialog delegation strategy (fallback to skill, enable, disable) in intent now that prompts have been created. explain conditional validation requires a mixed slot validation approach not covered here: https://developer.amazon.com/docs/custom-skills/delegate-dialog-to-alexa.html#auto-delegate-json (validation for coffee <> validation for tea) 21 | 15. **Build Tab**: show Utterance Profiler (how slots are collected) 22 | 16. **Code Tab**: Modify help and welcome messages to guide for new utterances (not hello world) 23 | 17. **Code Tab**: add intent chaining to birthday registration intent in launch request 24 | 18. **Code Tab**: add intent confirmation to register birthday intent 25 | 26 | ## Concepts 27 | 28 | 1. Slots explanation 29 | 2. Built in and custom slot types 30 | 3. Synonyms (minimal, we're not using synonyms, eg. January -> first month) 31 | 4. Required Slots & Prompts 32 | 5. Slot Validation 33 | 6. Auto-Delegate, Dialog Delegation Strategy 34 | 7. Utterance Profiler 35 | 8. Intent Confirmation 36 | 9. Basic Intent Chaining 37 | 38 | ## Diff 39 | 40 | 1. *lambda/custom/package.json*: change name and description of project to a happy birthday theme (no change in dependencies) 41 | 2. *models/xx-XX.json*: change invocation name to happy birthday theme (eg. happy birthday). Keep fallback intent if the locale supports it (otherwise remove it!). Change HelloWorldIntent to RegisterBirthdayIntent. Add slots: day (AMAZON.Ordinal or AMAZON.NUMBER), month (custom type with 2 digits as month ids) and year (AMAZON.FOUR_DIGIT_NUMBER). In en-* the slot day is AMAZON.Ordinal while in other locales it's AMAZON.NUMBER. Add samples utterances to collect each slot in isolation plus together. Also add utterances that do not collect slots (e.g. register my birthday). Make all 3 slots mandatory. Add prompts and enable dialog management via auto-delegate. Add validations for all 3 slots(day between 1 and 31 if NUMBER or in set of 31 days if Ordinal. Month: validate as member of the custom value set. Year: validate arbitraily as too old or too young (you set the cut years)). 42 | 3. *lambda/custom/index.js*: in languageStrings, replace welcome, hello (becomes register) and help messages with new string. Rename HelloWorldIntentHandler to RegisterBirthdayIntentHandler (and also update on module.exports at the bottom). In that same handler change intent name to RegisterBirthdayIntent and then access the itent to fetch all slots (day, month id, month value and year). Say the birthday back to the user (echo the values). Added intent chaining to birthday registration intent in launch request 43 | 44 | ## Videos 45 | 46 | [EN](https://alexa.design/zerotohero3)/[DE](https://alexa.design/de_zerotohero3)/[FR](https://alexa.design/fr_zerotohero3)/[IT](https://alexa.design/it_zerotohero3)/[ES](../README_ES.md) 47 | (EN version includes the following subtitles: EN, DE, PT) -------------------------------------------------------------------------------- /03/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happy-birthday", 3 | "version": "0.3.0", 4 | "description": "happy birthday skill", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon Alexa", 10 | "dependencies": { 11 | "ask-sdk-core": "^2.7.0", 12 | "ask-sdk-model": "^1.18.0", 13 | "i18next": "^15.0.5" 14 | } 15 | } -------------------------------------------------------------------------------- /03/lambda/util.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const s3SigV4Client = new AWS.S3({ 4 | signatureVersion: 'v4' 5 | }); 6 | 7 | module.exports.getS3PreSignedUrl = function getS3PreSignedUrl(s3ObjectKey) { 8 | const bucketName = process.env.S3_PERSISTENCE_BUCKET; 9 | const s3PreSignedUrl = s3SigV4Client.getSignedUrl('getObject', { 10 | Bucket: bucketName, 11 | Key: s3ObjectKey, 12 | Expires: 60*1 // the Expires is capped for 1 minute 13 | }); 14 | console.log(`Util.s3PreSignedUrl: ${s3ObjectKey} URL ${s3PreSignedUrl}`); 15 | return s3PreSignedUrl; 16 | } -------------------------------------------------------------------------------- /04/README.md: -------------------------------------------------------------------------------- 1 | # Part 4 - Persistence 2 | 3 | In this module we show how to store data that lives across sessions (persistence). We use the S3 persistence adapter available in Alexa Hosted Skills. 4 | We show session attributes and create interceptors that help make them persistent automatically. 5 | 6 | ## Milestones 7 | 8 | 1. **Build Tab**: add new intent SayBirthdayIntent with utterances to ask for remaining time until birthday (no slots) 9 | 2. **Code Tab**: remove languageStrings and put then in separate localisation file 10 | 3. **Code Tab**: add DynamoDBand S3 adapter dependency in package.json, also moment.js dependency. Add requires in index.js 11 | 4. **Code Tab**: create getPersistenceAdapter() function that returns the adapter 12 | 5. **Code Tab**: add persistence adapter to skill builder 13 | 6. **Code Tab**: in RegisterBirthdayIntent get attributes manager and put slot values in session attributes 14 | 7. **Code Tab**: load persistent attributes to session attributes via interceptor, add interceptor to skill builder 15 | 8. **Code Tab**: save session attributes to persistent attributes via interceptor, add interceptor to skill builder 16 | 9. **Code Tab**: in SayBirthdayLaunchRequestHandler load session attributes and if slot values are not present call the registration itent using intent chaining. Call this handler from LaunchRequest and RegisterBirthdayIntent handler, use Moments.js for the date math, send one of two responses: remaining days until birthday or happy birthday greeting. 17 | 18 | ## Concepts 19 | 20 | 1. Session attributes 21 | 2. Persistent attributes 22 | 3. Persistence adapters (S3 and DynamoDB) / detect if lambda is Alexa hosted 23 | 4. Copy session attributes to and from persistent attributes via interceptors 24 | 5. Async/await 25 | 6. Session counter (to say eg. "welcome back") 26 | 27 | ## Diff 28 | 29 | 1. *models/xx-XX.json*: add SayBirthdayIntent with slot-less utterances 30 | 2. *lambda/custom/package.json*: add dependencies for S3 and DynamoDB persistence adapters and for the moment-timezone library 31 | 3. *lambda/custom/localisation.js*: create this file and put languageStrings here 32 | 4. *lambda/custom/index.js*: add function to get/initialize the persistence adapter - getPersistenceAdapter(). Add require for moment-timezone library. Change languageString to require the localisation.js file. Create getPersistenceAdapter() function. In launch request handler get the session attributes and read day, month, monthId and year (if present call the say intent handler, otherwise call the register handler via intent chaining). In register birthday intent handler get all slot values and put them on session attributes (then call say intent handler). Add say birthday intent handler and get day, month id and year from session attributes. If these attributes are present calculate remaining days to birthday with moments-timezone. If it's the user birthday say happy birthday. If the attributes are not present use intent chaining to point to register birthday. Add request interceptor to load the session attributes and a response interceptor to save them. Add to the skill builder: say birthday intent handler, the new interceptors and the persistence adapter 33 | 34 | ## Videos 35 | 36 | [EN](https://alexa.design/zerotohero4)/[DE](https://alexa.design/de_zerotohero4)/[FR](https://alexa.design/fr_zerotohero4)/[IT](https://alexa.design/it_zerotohero4)/[ES](../README_ES.md) 37 | (EN version includes the following subtitles: EN, DE, PT) -------------------------------------------------------------------------------- /04/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happy-birthday", 3 | "version": "0.4.0", 4 | "description": "happy birthday skill", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon Alexa", 10 | "dependencies": { 11 | "ask-sdk-core": "^2.7.0", 12 | "ask-sdk-model": "^1.18.0", 13 | "i18next": "^15.0.5", 14 | "ask-sdk-s3-persistence-adapter": "^2.7.0", 15 | "ask-sdk-dynamodb-persistence-adapter": "^2.7.0", 16 | "moment-timezone": "^0.5.23" 17 | } 18 | } -------------------------------------------------------------------------------- /04/lambda/util.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const s3SigV4Client = new AWS.S3({ 4 | signatureVersion: 'v4' 5 | }); 6 | 7 | module.exports.getS3PreSignedUrl = function getS3PreSignedUrl(s3ObjectKey) { 8 | const bucketName = process.env.S3_PERSISTENCE_BUCKET; 9 | const s3PreSignedUrl = s3SigV4Client.getSignedUrl('getObject', { 10 | Bucket: bucketName, 11 | Key: s3ObjectKey, 12 | Expires: 60*1 // the Expires is capped for 1 minute 13 | }); 14 | console.log(`Util.s3PreSignedUrl: ${s3ObjectKey} URL ${s3PreSignedUrl}`); 15 | return s3PreSignedUrl; 16 | } -------------------------------------------------------------------------------- /05/README.md: -------------------------------------------------------------------------------- 1 | # Part 5 - Accessing ASK APIs 2 | 3 | In this module we show how to access internal ASK APIs (in this case the User Profile API to fetch the given name of the user and the Settings API to fetch the timezone of the device). 4 | We also intriduce some SSML magic to use speechcons and audio files. 5 | 6 | ## Milestones 7 | 8 | 1. **Build Tab**: enable Given Name permissions in the skill 9 | 2. **Code Tab**: create interceptors file 10 | 3. **Code Tab**: move interceptors to separate file (interceptors.js), move persistence adapter to util.js 11 | 4. **Code Tab**: add given name retrieval interceptor in interceptors.js, add it to exports in index.js 12 | 5. **Code Tab**: add timezone retrieval interceptor in interceptors.js, add it to exports in index.js 13 | 6. **Code Tab**: add speechcon and sound from audio bank to localization file 14 | 7. **Code Tab**: add extra variable on localization strings to pass given name 15 | 8. **Code Tab**: add API client to exports (because we're using a custom builder). See: https://developer.amazon.com/blogs/alexa/post/a47f25e9-3e87-4afd-b632-ff3b86febcd4/skill-builder-objects-to-customize-or-not-to-customize 16 | 17 | ## Concepts 18 | 19 | 1. Service API (User Profile API - given name) 20 | 2. Settings API (timezone) 21 | 3. SSML (speechcons and audio files) 22 | 4. Array capable localisation interceptor 23 | 5. String replacement with plurals support 24 | 25 | ## Diff 26 | 27 | 1. *lambda/custom/interceptors.js*: add this file and move all interceptors here (plus several require()s, check the top of interceptors.js). Add interceptors to fetch the user first name and timezone and put the values in session attributes. Add constant for given name permission 28 | 2. *skill.json*: we add this file so we show we require given name permission but you don't need it in an AHS 29 | 4. *lambda/custom/localisation.js*: modify messages to include given name as replacement string. Modify goodbye message to include an array of goodbyes. Add message to say timezone could not be retrieved. Add sound and speechcon to birthday greeting message. 30 | 5. *lambda/custom/index.js*: add requires for persistence.js and interceptors.js. Remove requires for localisation and i18next. Remove all interceptors above skill builder (they all go to interceptors.js) 31 | 6. *lambda/custom/util.js*: move getPersistanceAdapter() function here from index.js 32 | 33 | ## Videos 34 | 35 | [EN](https://alexa.design/zerotohero5)/[DE](https://alexa.design/de_zerotohero5)/[FR](https://alexa.design/fr_zerotohero5)/[IT](https://alexa.design/it_zerotohero5)/[ES](../README_ES.md) 36 | (EN version includes the following subtitles: EN, DE, PT) -------------------------------------------------------------------------------- /05/ask-resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "askcliResourcesVersion": "2020-03-31", 3 | "profiles": { 4 | "default": { 5 | "skillMetadata": { 6 | "src": "./skill-package" 7 | }, 8 | "code": { 9 | "default": { 10 | "src": "./lambda" 11 | } 12 | }, 13 | "skillInfrastructure": { 14 | "userConfig": { 15 | "runtime": "nodejs10.x", 16 | "handler": "index.handler", 17 | "awsRegion": "us-east-1" 18 | }, 19 | "type": "@ask-cli/lambda-deployer" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /05/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happy-birthday", 3 | "version": "0.5.0", 4 | "description": "happy birthday skill", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon Alexa", 10 | "dependencies": { 11 | "ask-sdk-core": "^2.7.0", 12 | "ask-sdk-model": "^1.18.0", 13 | "i18next": "^15.0.5", 14 | "ask-sdk-s3-persistence-adapter": "^2.7.0", 15 | "ask-sdk-dynamodb-persistence-adapter": "^2.7.0", 16 | "moment-timezone": "^0.5.23" 17 | } 18 | } -------------------------------------------------------------------------------- /05/lambda/util.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const s3SigV4Client = new AWS.S3({ 4 | signatureVersion: 'v4' 5 | }); 6 | 7 | module.exports = { 8 | getS3PreSignedUrl(s3ObjectKey) { 9 | const bucketName = process.env.S3_PERSISTENCE_BUCKET; 10 | const s3PreSignedUrl = s3SigV4Client.getSignedUrl('getObject', { 11 | Bucket: bucketName, 12 | Key: s3ObjectKey, 13 | Expires: 60*1 // the Expires is capped for 1 minute 14 | }); 15 | console.log(`Util.s3PreSignedUrl: ${s3ObjectKey} URL ${s3PreSignedUrl}`); 16 | return s3PreSignedUrl; 17 | }, 18 | getPersistenceAdapter(tableName) { 19 | // This function is an indirect way to detect if this is part of an Alexa-Hosted skill 20 | function isAlexaHosted() { 21 | return process.env.S3_PERSISTENCE_BUCKET; 22 | } 23 | if (isAlexaHosted()) { 24 | const {S3PersistenceAdapter} = require('ask-sdk-s3-persistence-adapter'); 25 | return new S3PersistenceAdapter({ 26 | bucketName: process.env.S3_PERSISTENCE_BUCKET 27 | }); 28 | } else { 29 | // IMPORTANT: don't forget to give DynamoDB access to the role you're using to run this lambda (via IAM policy) 30 | const {DynamoDbPersistenceAdapter} = require('ask-sdk-dynamodb-persistence-adapter'); 31 | return new DynamoDbPersistenceAdapter({ 32 | tableName: tableName || 'happy_birthday', 33 | createTable: true 34 | }); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /05/skill-package/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "es-ES": { 6 | "name": "Feliz Cumpleaños" 7 | }, 8 | "es-MX": { 9 | "name": "Feliz Cumpleaños" 10 | }, 11 | "es-US": { 12 | "name": "Feliz Cumpleaños" 13 | }, 14 | "en-US": { 15 | "name": "Happy Birthday" 16 | }, 17 | "en-AU": { 18 | "name": "Happy Birthday" 19 | }, 20 | "en-CA": { 21 | "name": "Happy Birthday" 22 | }, 23 | "en-GB": { 24 | "name": "Happy Birthday" 25 | }, 26 | "en-IN": { 27 | "name": "Happy Birthday" 28 | }, 29 | "fr-FR": { 30 | "name": "Joyeux Anniversaire" 31 | }, 32 | "fr-CA": { 33 | "name": "Bonne Fête" 34 | }, 35 | "it-IT": { 36 | "name": "Buon Compleanno" 37 | } 38 | } 39 | }, 40 | "apis": { 41 | "custom": {} 42 | }, 43 | "manifestVersion": "1.0", 44 | "permissions": [ 45 | { 46 | "name": "alexa::profile:given_name:read" 47 | } 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /05_intent_chaining/README.md: -------------------------------------------------------------------------------- 1 | # Part 5 - Accessing ASK APIs 2 | 3 | In this module we show how to access internal ASK APIs (in this case the User Profile API to fetch the given name of the user and the Settings API to fetch the timezone of the device). 4 | We also intriduce some SSML magic to use speechcons and audio files. 5 | 6 | ## Milestones 7 | 8 | 1. **Build Tab**: enable Given Name permissions in the skill 9 | 2. **Code Tab**: create interceptors file 10 | 3. **Code Tab**: move interceptors to separate file (interceptors.js), move persistence adapter to util.js 11 | 4. **Code Tab**: add given name retrieval interceptor in interceptors.js, add it to exports in index.js 12 | 5. **Code Tab**: add timezone retrieval interceptor in interceptors.js, add it to exports in index.js 13 | 6. **Code Tab**: add speechcon and sound from audio bank to localization file 14 | 7. **Code Tab**: add extra variable on localization strings to pass given name 15 | 8. **Code Tab**: add API client to exports (because we're using a custom builder). See: https://developer.amazon.com/blogs/alexa/post/a47f25e9-3e87-4afd-b632-ff3b86febcd4/skill-builder-objects-to-customize-or-not-to-customize 16 | 17 | ## Concepts 18 | 19 | 1. Service API (User Profile API - given name) 20 | 2. Settings API (timezone) 21 | 3. SSML (speechcons and audio files) 22 | 4. Array capable localisation interceptor 23 | 5. String replacement with plurals support 24 | 25 | ## Diff 26 | 27 | 1. *lambda/custom/interceptors.js*: add this file and move all interceptors here (plus several require()s, check the top of interceptors.js). Add interceptors to fetch the user first name and timezone and put the values in session attributes. Add constant for given name permission 28 | 2. *skill.json*: we add this file so we show we require given name permission but you don't need it in an AHS 29 | 4. *lambda/custom/localisation.js*: modify messages to include given name as replacement string. Modify goodbye message to include an array of goodbyes. Add message to say timezone could not be retrieved. Add sound and speechcon to birthday greeting message. 30 | 5. *lambda/custom/index.js*: add requires for persistence.js and interceptors.js. Remove requires for localisation and i18next. Remove all interceptors above skill builder (they all go to interceptors.js) 31 | 6. *lambda/custom/util.js*: move getPersistanceAdapter() function here from index.js 32 | 33 | ## Videos 34 | 35 | [ES](https://alexa.design/es_zerotohero11) 36 | -------------------------------------------------------------------------------- /05_intent_chaining/ask-resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "askcliResourcesVersion": "2020-03-31", 3 | "profiles": { 4 | "default": { 5 | "skillMetadata": { 6 | "src": "./skill-package" 7 | }, 8 | "code": { 9 | "default": { 10 | "src": "lambda/custom" 11 | } 12 | }, 13 | "skillInfrastructure": { 14 | "userConfig": { 15 | "runtime": "nodejs10.x", 16 | "handler": "index.handler", 17 | "awsRegion": "us-east-1" 18 | }, 19 | "type": "@ask-cli/lambda-deployer" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /05_intent_chaining/lambda/custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happy-birthday", 3 | "version": "0.5.1", 4 | "description": "happy birthday skill", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon Alexa", 10 | "dependencies": { 11 | "ask-sdk-core": "^2.7.0", 12 | "ask-sdk-model": "^1.18.0", 13 | "i18next": "^15.0.5", 14 | "ask-sdk-s3-persistence-adapter": "^2.7.0", 15 | "ask-sdk-dynamodb-persistence-adapter": "^2.7.0", 16 | "moment-timezone": "^0.5.23" 17 | } 18 | } -------------------------------------------------------------------------------- /05_intent_chaining/lambda/custom/util.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const s3SigV4Client = new AWS.S3({ 4 | signatureVersion: 'v4' 5 | }); 6 | 7 | module.exports = { 8 | getS3PreSignedUrl(s3ObjectKey) { 9 | const bucketName = process.env.S3_PERSISTENCE_BUCKET; 10 | const s3PreSignedUrl = s3SigV4Client.getSignedUrl('getObject', { 11 | Bucket: bucketName, 12 | Key: s3ObjectKey, 13 | Expires: 60*1 // the Expires is capped for 1 minute 14 | }); 15 | console.log(`Util.s3PreSignedUrl: ${s3ObjectKey} URL ${s3PreSignedUrl}`); 16 | return s3PreSignedUrl; 17 | }, 18 | getPersistenceAdapter(tableName) { 19 | // This function is an indirect way to detect if this is part of an Alexa-Hosted skill 20 | function isAlexaHosted() { 21 | return process.env.S3_PERSISTENCE_BUCKET; 22 | } 23 | if (isAlexaHosted()) { 24 | const {S3PersistenceAdapter} = require('ask-sdk-s3-persistence-adapter'); 25 | return new S3PersistenceAdapter({ 26 | bucketName: process.env.S3_PERSISTENCE_BUCKET 27 | }); 28 | } else { 29 | // IMPORTANT: don't forget to give DynamoDB access to the role you're using to run this lambda (via IAM policy) 30 | const {DynamoDbPersistenceAdapter} = require('ask-sdk-dynamodb-persistence-adapter'); 31 | return new DynamoDbPersistenceAdapter({ 32 | tableName: tableName || 'happy_birthday', 33 | createTable: true 34 | }); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /05_intent_chaining/skill-package/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "es-ES": { 6 | "name": "Feliz Cumpleaños" 7 | }, 8 | "es-MX": { 9 | "name": "Feliz Cumpleaños" 10 | }, 11 | "es-US": { 12 | "name": "Feliz Cumpleaños" 13 | }, 14 | "en-US": { 15 | "name": "Happy Birthday" 16 | }, 17 | "en-AU": { 18 | "name": "Happy Birthday" 19 | }, 20 | "en-CA": { 21 | "name": "Happy Birthday" 22 | }, 23 | "en-GB": { 24 | "name": "Happy Birthday" 25 | }, 26 | "en-IN": { 27 | "name": "Happy Birthday" 28 | } 29 | } 30 | }, 31 | "apis": { 32 | "custom": {} 33 | }, 34 | "manifestVersion": "1.0", 35 | "permissions": [ 36 | { 37 | "name": "alexa::profile:given_name:read" 38 | } 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /06/README.md: -------------------------------------------------------------------------------- 1 | # Part 6 - Reminders API 2 | 3 | Here we show you how to use the Reminders API to have Alexa say a message on a specific point in time. The user has to grant permission for this so we also show how to send a card to the Alexa app to get this permission. 4 | Additionally we show you how to use a slot of type AMAZON.SearchQuery to capture arbitrary sentences that the user might say (no predefined slot type). 5 | 6 | ## Milestones 7 | 8 | 1. **Build Tab**: enable Reminders in Permissions 9 | 2. **Build Tab**: add reminder birthday intent and collect reminder message via SearchQuery (make slot required) 10 | 3. **Code Tab**: create constants and logic files 11 | 4. **Code Tab**: add Reminders permission constant to constants.js 12 | 5. **Code Tab**: create Reminders structure (use SCHEDULE_ABSOLUTE) 13 | 6. **Code Tab**: add remind birthday intent handler with support for confirmation 14 | 7. **Code Tab**: add new handler to skill builder 15 | 16 | ## Concepts 17 | 18 | 1. Reminders API 19 | 2. AMAZON.SearchQuery 20 | 3. Intent Confirmation (again) 21 | 22 | ## Diff 23 | 24 | 1. *skill.json*: we add reminders permission here but this file is not needed if you use AHS 25 | 2. *lambda/custom/constants.js*: move all constants here including the array of persistent attributes name 26 | 3. *lambda/custom/logic.js*: create this file to encapsulate birthday calculation logic (and reminder structure creation). Move moment-timezone require to this file 27 | 4. *skill.json*: add permission to read/write reminders 28 | 5. *lambda/custom/localisation.js*: add all messages related to reminders. 29 | 6. *models/xx-XX.json*: add remind birthday intent, slot-less utterances and utterances to collect a message slot (SearchQuery). Use auto-delegate to collect the slot if not present. Add prompts. Add intent confirmation. 30 | 7. *lambda/custom/index.js*: replace moment library require with a require for logic.js. Add constant with reminders permission. Replace birthday logic in say birthday intent with calls to logic.js. Add remind birthday intent handler. Verify intent was confirmed. Create reminder using timezone info (put creation of reminder structure in logic.js). Add new handler to skill builder 31 | 8. *lambda/custom/util.js*: add create reminder function 32 | 33 | Warning: the simulator might return inconsistent timezone results such your geographical timezone by API but a different time (not consistent with your timezone). It can also return no time zone at all. In order to see the reminders demo working properly please try it on a physical device 34 | 35 | ## Videos 36 | 37 | [EN](https://alexa.design/zerotohero6)/[DE](https://alexa.design/de_zerotohero6)/[FR](https://alexa.design/fr_zerotohero6)/[IT](https://alexa.design/it_zerotohero6)/[ES](../README_ES.md) 38 | (EN version includes the following subtitles: EN, DE, PT) -------------------------------------------------------------------------------- /06/ask-resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "askcliResourcesVersion": "2020-03-31", 3 | "profiles": { 4 | "default": { 5 | "skillMetadata": { 6 | "src": "./skill-package" 7 | }, 8 | "code": { 9 | "default": { 10 | "src": "./lambda" 11 | } 12 | }, 13 | "skillInfrastructure": { 14 | "userConfig": { 15 | "runtime": "nodejs10.x", 16 | "handler": "index.handler", 17 | "awsRegion": "us-east-1" 18 | }, 19 | "type": "@ask-cli/lambda-deployer" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /06/lambda/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // we now specify which attributes are saved (see the save interceptor below) 3 | PERSISTENT_ATTRIBUTES_NAMES: ['day', 'month', 'monthName', 'year', 'sessionCounter', 'reminderId'], 4 | // these are the permissions needed to fetch the first name 5 | GIVEN_NAME_PERMISSION: ['alexa::profile:given_name:read'], 6 | // these are the permissions needed to send reminders 7 | REMINDERS_PERMISSION: ['alexa::alerts:reminders:skill:readwrite'] 8 | } -------------------------------------------------------------------------------- /06/lambda/logic.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment-timezone'); // will help us do all the dates math while considering the timezone 2 | const util = require('./util'); 3 | 4 | module.exports = { 5 | getBirthdayData(day, month, year, timezone) { 6 | const today = moment().tz(timezone).startOf('day'); 7 | const wasBorn = moment(`${month}/${day}/${year}`, "MM/DD/YYYY").tz(timezone).startOf('day'); 8 | const nextBirthday = moment(`${month}/${day}/${today.year()}`, "MM/DD/YYYY").tz(timezone).startOf('day'); 9 | if (today.isAfter(nextBirthday)) { 10 | nextBirthday.add(1, 'years'); 11 | } 12 | const age = today.diff(wasBorn, 'years'); 13 | const daysAlive = today.diff(wasBorn, 'days'); 14 | const daysUntilBirthday = nextBirthday.startOf('day').diff(today, 'days'); // same day returns 0 15 | 16 | return { 17 | daysAlive: daysAlive, // not used but nice to have :) 18 | daysUntilBirthday: daysUntilBirthday, 19 | age: age //in years 20 | } 21 | }, 22 | createBirthdayReminder(daysUntilBirthday, timezone, locale, message) { 23 | moment.locale(locale); 24 | const createdMoment = moment().tz(timezone); 25 | let triggerMoment = createdMoment.startOf('day').add(daysUntilBirthday, 'days'); 26 | if (daysUntilBirthday === 0) { 27 | triggerMoment = createdMoment.startOf('day').add(1, 'years'); // reminder created on the day of birthday will trigger next year 28 | } 29 | console.log('Reminder schedule: ' + triggerMoment.format('YYYY-MM-DDTHH:mm:00.000')); 30 | 31 | return util.createReminder(createdMoment, triggerMoment, timezone, locale, message); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /06/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happy-birthday", 3 | "version": "0.6.0", 4 | "description": "happy birthday skill", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon Alexa", 10 | "dependencies": { 11 | "ask-sdk-core": "^2.7.0", 12 | "ask-sdk-model": "^1.18.0", 13 | "i18next": "^15.0.5", 14 | "ask-sdk-s3-persistence-adapter": "^2.7.0", 15 | "ask-sdk-dynamodb-persistence-adapter": "^2.7.0", 16 | "moment-timezone": "^0.5.23" 17 | } 18 | } -------------------------------------------------------------------------------- /06/lambda/util.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const s3SigV4Client = new AWS.S3({ 4 | signatureVersion: 'v4' 5 | }); 6 | 7 | module.exports = { 8 | getS3PreSignedUrl(s3ObjectKey) { 9 | const bucketName = process.env.S3_PERSISTENCE_BUCKET; 10 | const s3PreSignedUrl = s3SigV4Client.getSignedUrl('getObject', { 11 | Bucket: bucketName, 12 | Key: s3ObjectKey, 13 | Expires: 60*1 // the Expires is capped for 1 minute 14 | }); 15 | console.log(`Util.s3PreSignedUrl: ${s3ObjectKey} URL ${s3PreSignedUrl}`); 16 | return s3PreSignedUrl; 17 | }, 18 | getPersistenceAdapter(tableName) { 19 | // This function is an indirect way to detect if this is part of an Alexa-Hosted skill 20 | function isAlexaHosted() { 21 | return process.env.S3_PERSISTENCE_BUCKET; 22 | } 23 | if (isAlexaHosted()) { 24 | const {S3PersistenceAdapter} = require('ask-sdk-s3-persistence-adapter'); 25 | return new S3PersistenceAdapter({ 26 | bucketName: process.env.S3_PERSISTENCE_BUCKET 27 | }); 28 | } else { 29 | // IMPORTANT: don't forget to give DynamoDB access to the role you're using to run this lambda (via IAM policy) 30 | const {DynamoDbPersistenceAdapter} = require('ask-sdk-dynamodb-persistence-adapter'); 31 | return new DynamoDbPersistenceAdapter({ 32 | tableName: tableName || 'happy_birthday', 33 | createTable: true 34 | }); 35 | } 36 | }, 37 | createReminder(requestMoment, scheduledMoment, timezone, locale, message) { 38 | return { 39 | requestTime: requestMoment.format('YYYY-MM-DDTHH:mm:00.000'), 40 | trigger: { 41 | type: 'SCHEDULED_ABSOLUTE', 42 | scheduledTime: scheduledMoment.format('YYYY-MM-DDTHH:mm:00.000'), 43 | timeZoneId: timezone 44 | }, 45 | alertInfo: { 46 | spokenInfo: { 47 | content: [{ 48 | locale: locale, 49 | text: message 50 | }] 51 | } 52 | }, 53 | pushNotification: { 54 | status: 'ENABLED' 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /06/skill-package/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "es-ES": { 6 | "name": "Feliz Cumpleaños" 7 | }, 8 | "es-MX": { 9 | "name": "Feliz Cumpleaños" 10 | }, 11 | "es-US": { 12 | "name": "Feliz Cumpleaños" 13 | }, 14 | "en-US": { 15 | "name": "Happy Birthday" 16 | }, 17 | "en-AU": { 18 | "name": "Happy Birthday" 19 | }, 20 | "en-CA": { 21 | "name": "Happy Birthday" 22 | }, 23 | "en-GB": { 24 | "name": "Happy Birthday" 25 | }, 26 | "en-IN": { 27 | "name": "Happy Birthday" 28 | }, 29 | "fr-FR": { 30 | "name": "Joyeux Anniversaire" 31 | }, 32 | "fr-CA": { 33 | "name": "Bonne Fête" 34 | }, 35 | "it-IT": { 36 | "name": "Buon Compleanno" 37 | } 38 | } 39 | }, 40 | "apis": { 41 | "custom": {} 42 | }, 43 | "manifestVersion": "1.0", 44 | "permissions": [ 45 | { 46 | "name": "alexa::profile:given_name:read" 47 | }, 48 | { 49 | "name": "alexa::alerts:reminders:skill:readwrite" 50 | } 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /07/README.md: -------------------------------------------------------------------------------- 1 | # Part 7 - Accessing External APIs 2 | 3 | In this module we show how to access an external API (3rd party REST service) with the Axios library (you might use other libraries similarly, like e.g. node-fetch). We use async/await to wait for a response from the service. 4 | We set up a connection timeout so the call comes back in time before we run out of time during an Alexa conversation turn (typically 8 seconds). 5 | We use the Progressive Response API before doing the call so Alexa gives the user a nice waiting message if the connection takes a few seconds to return. 6 | 7 | ## Milestones 8 | 9 | 1. **Build Tab**: add celebrity birthdays intent with utterances to ask for today's birthdays (no slots) 10 | 2. **Code Tab**: add axios dependency to package.json 11 | 3. **Code Tab**: add code to access external API (with async/await), axios allows to set a timeout (use 6500 ms) 12 | 4. **Code Tab**: add progressive response directive and attach to external API call 13 | 5. **Code Tab**: add celebrity birthdays intent handler 14 | 6. **Code Tab**: add new handler to skill builder 15 | 16 | ## Concepts 17 | 18 | 1. Fetch external API (async/await) 19 | 2. Progressive Response 20 | 21 | ## Diff 22 | 23 | 1. *lambda/custom/constants.js*: add max birthdays to this file 24 | 2. *lambda/custom/package.json*: add axios dependency 25 | 3. *lambda/custom/localisation.js*: add all messages related to API access 26 | 4. *models/xx-XX.json*: add celebrity birthdays intent with utterances to ask for today's birthdays 27 | 5. *lambda/custom/logic.js*: add functions to return current date info and to call external API 28 | 6. *lambda/custom/util*: add function to create progressive response directive (callDirectiveService) 29 | 7. *lambda/custom/index.js*: Extend birthday greeting to mention celebrity birthdays. Add celebrity birthdays intent handler and fetch the birthdays from the API. Add new handler to skill builder 30 | 31 | ## Videos 32 | 33 | [EN](https://alexa.design/zerotohero7)/[DE](https://alexa.design/de_zerotohero7)/[FR](https://alexa.design/fr_zerotohero7)/[IT](https://alexa.design/it_zerotohero7)/[ES](../README_ES.md) 34 | (EN version includes the following subtitles: EN, DE, PT) -------------------------------------------------------------------------------- /07/ask-resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "askcliResourcesVersion": "2020-03-31", 3 | "profiles": { 4 | "default": { 5 | "skillMetadata": { 6 | "src": "./skill-package" 7 | }, 8 | "code": { 9 | "default": { 10 | "src": "./lambda" 11 | } 12 | }, 13 | "skillInfrastructure": { 14 | "userConfig": { 15 | "runtime": "nodejs10.x", 16 | "handler": "index.handler", 17 | "awsRegion": "us-east-1" 18 | }, 19 | "type": "@ask-cli/lambda-deployer" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /07/lambda/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // we now specify which attributes are saved (see the save interceptor below) 3 | PERSISTENT_ATTRIBUTES_NAMES: ['day', 'month', 'monthName', 'year', 'sessionCounter', 'reminderId'], 4 | // these are the permissions needed to fetch the first name 5 | GIVEN_NAME_PERMISSION: ['alexa::profile:given_name:read'], 6 | // these are the permissions needed to send reminders 7 | REMINDERS_PERMISSION: ['alexa::alerts:reminders:skill:readwrite'], 8 | // max number of entries to fetch from the external API 9 | MAX_BIRTHDAYS: 5 10 | } 11 | -------------------------------------------------------------------------------- /07/lambda/logic.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment-timezone'); // will help us do all the dates math while considering the timezone 2 | const util = require('./util'); 3 | const axios = require('axios'); 4 | 5 | module.exports = { 6 | getBirthdayData(day, month, year, timezone) { 7 | const today = moment().tz(timezone).startOf('day'); 8 | const wasBorn = moment(`${month}/${day}/${year}`, "MM/DD/YYYY").tz(timezone).startOf('day'); 9 | const nextBirthday = moment(`${month}/${day}/${today.year()}`, "MM/DD/YYYY").tz(timezone).startOf('day'); 10 | if (today.isAfter(nextBirthday)) { 11 | nextBirthday.add(1, 'years'); 12 | } 13 | const age = today.diff(wasBorn, 'years'); 14 | const daysAlive = today.diff(wasBorn, 'days'); 15 | const daysUntilBirthday = nextBirthday.startOf('day').diff(today, 'days'); // same day returns 0 16 | 17 | return { 18 | daysAlive: daysAlive, // not used but nice to have :) 19 | daysUntilBirthday: daysUntilBirthday, 20 | age: age //in years 21 | } 22 | }, 23 | createBirthdayReminder(daysUntilBirthday, timezone, locale, message) { 24 | moment.locale(locale); 25 | const createdMoment = moment().tz(timezone); 26 | let triggerMoment = createdMoment.startOf('day').add(daysUntilBirthday, 'days'); 27 | if (daysUntilBirthday === 0) { 28 | triggerMoment = createdMoment.startOf('day').add(1, 'years'); // reminder created on the day of birthday will trigger next year 29 | } 30 | console.log('Reminder schedule: ' + triggerMoment.format('YYYY-MM-DDTHH:mm:00.000')); 31 | 32 | return util.createReminder(createdMoment, triggerMoment, timezone, locale, message); 33 | }, 34 | getAdjustedDate(timezone) { 35 | const today = moment().tz(timezone).startOf('day'); 36 | 37 | return { 38 | day: today.date(), 39 | month: today.month() + 1 40 | } 41 | }, 42 | fetchBirthdays(day, month, limit){ 43 | const endpoint = 'https://query.wikidata.org/sparql'; 44 | // List of actors with pictures and date of birth for a given day and month 45 | const sparqlQuery = 46 | `SELECT DISTINCT ?human ?humanLabel ?picture ?date_of_birth ?place_of_birthLabel WHERE { 47 | ?human wdt:P31 wd:Q5; 48 | wdt:P106 wd:Q33999; 49 | wdt:P18 ?picture. 50 | FILTER((DATATYPE(?date_of_birth)) = xsd:dateTime) 51 | FILTER((MONTH(?date_of_birth)) = ${month}) 52 | FILTER((DAY(?date_of_birth)) = ${day}) 53 | FILTER (bound(?place_of_birth)) 54 | SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } 55 | OPTIONAL { ?human wdt:P569 ?date_of_birth. } 56 | OPTIONAL { ?human wdt:P19 ?place_of_birth. } 57 | } 58 | LIMIT ${limit}`; 59 | const url = endpoint + '?query=' + encodeURIComponent(sparqlQuery); 60 | console.log(url); // in case you want to try the query in a web browser 61 | 62 | var config = { 63 | timeout: 6500, // timeout api call before we reach Alexa's 8 sec timeout, or set globally via axios.defaults.timeout 64 | headers: {'Accept': 'application/sparql-results+json'} 65 | }; 66 | 67 | async function getJsonResponse(url, config){ 68 | const res = await axios.get(url, config); 69 | return res.data; 70 | } 71 | 72 | return getJsonResponse(url, config).then((result) => { 73 | return result; 74 | }).catch((error) => { 75 | return null; 76 | }); 77 | }, 78 | convertBirthdaysResponse(handlerInput, response, withAge, timezone){ 79 | let speechResponse = ''; 80 | // if the API call failed we just don't append today's birthdays to the response 81 | if (!response || !response.results || !response.results.bindings || !Object.keys(response.results.bindings).length > 0) 82 | return speechResponse; 83 | const results = response.results.bindings; 84 | speechResponse += handlerInput.t('ALSO_TODAY_MSG'); 85 | results.forEach((person, index) => { 86 | console.log(person); 87 | speechResponse += person.humanLabel.value; 88 | if (withAge && timezone && person.date_of_birth.value) { 89 | const age = module.exports.convertBirthdateToYearsOld(person, timezone); 90 | speechResponse += handlerInput.t('TURNING_YO_MSG', {count: age}); 91 | } 92 | if (index === Object.keys(results).length - 2) 93 | speechResponse += handlerInput.t('CONJUNCTION_MSG'); 94 | else 95 | speechResponse += '. '; 96 | }); 97 | 98 | return speechResponse; 99 | }, 100 | convertBirthdateToYearsOld(person, timezone) { 101 | const today = moment().tz(timezone).startOf('day'); 102 | const wasBorn = moment(person.date_of_birth.value).tz(timezone).startOf('day'); 103 | return today.diff(wasBorn, 'years'); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /07/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happy-birthday", 3 | "version": "0.7.0", 4 | "description": "happy birthday skill", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon Alexa", 10 | "dependencies": { 11 | "ask-sdk-core": "^2.7.0", 12 | "ask-sdk-model": "^1.18.0", 13 | "i18next": "^15.0.5", 14 | "ask-sdk-s3-persistence-adapter": "^2.7.0", 15 | "ask-sdk-dynamodb-persistence-adapter": "^2.7.0", 16 | "moment-timezone": "^0.5.23", 17 | "axios": ">=0.21.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /07/lambda/util.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const s3SigV4Client = new AWS.S3({ 4 | signatureVersion: 'v4' 5 | }); 6 | 7 | module.exports = { 8 | getS3PreSignedUrl(s3ObjectKey) { 9 | const bucketName = process.env.S3_PERSISTENCE_BUCKET; 10 | const s3PreSignedUrl = s3SigV4Client.getSignedUrl('getObject', { 11 | Bucket: bucketName, 12 | Key: s3ObjectKey, 13 | Expires: 60*1 // the Expires is capped for 1 minute 14 | }); 15 | console.log(`Util.s3PreSignedUrl: ${s3ObjectKey} URL ${s3PreSignedUrl}`); 16 | return s3PreSignedUrl; 17 | }, 18 | getPersistenceAdapter(tableName) { 19 | // This function is an indirect way to detect if this is part of an Alexa-Hosted skill 20 | function isAlexaHosted() { 21 | return process.env.S3_PERSISTENCE_BUCKET; 22 | } 23 | if (isAlexaHosted()) { 24 | const {S3PersistenceAdapter} = require('ask-sdk-s3-persistence-adapter'); 25 | return new S3PersistenceAdapter({ 26 | bucketName: process.env.S3_PERSISTENCE_BUCKET 27 | }); 28 | } else { 29 | // IMPORTANT: don't forget to give DynamoDB access to the role you're using to run this lambda (via IAM policy) 30 | const {DynamoDbPersistenceAdapter} = require('ask-sdk-dynamodb-persistence-adapter'); 31 | return new DynamoDbPersistenceAdapter({ 32 | tableName: tableName || 'happy_birthday', 33 | createTable: true 34 | }); 35 | } 36 | }, 37 | createReminder(requestMoment, scheduledMoment, timezone, locale, message) { 38 | return { 39 | requestTime: requestMoment.format('YYYY-MM-DDTHH:mm:00.000'), 40 | trigger: { 41 | type: 'SCHEDULED_ABSOLUTE', 42 | scheduledTime: scheduledMoment.format('YYYY-MM-DDTHH:mm:00.000'), 43 | timeZoneId: timezone 44 | }, 45 | alertInfo: { 46 | spokenInfo: { 47 | content: [{ 48 | locale: locale, 49 | text: message 50 | }] 51 | } 52 | }, 53 | pushNotification: { 54 | status: 'ENABLED' 55 | } 56 | } 57 | }, 58 | callDirectiveService(handlerInput, msg) { 59 | // Call Alexa Directive Service. 60 | const {requestEnvelope} = handlerInput; 61 | const directiveServiceClient = handlerInput.serviceClientFactory.getDirectiveServiceClient(); 62 | const requestId = requestEnvelope.request.requestId; 63 | const {apiEndpoint, apiAccessToken} = requestEnvelope.context.System; 64 | // build the progressive response directive 65 | const directive = { 66 | header: { 67 | requestId 68 | }, 69 | directive:{ 70 | type: 'VoicePlayer.Speak', 71 | speech: msg 72 | } 73 | }; 74 | // send directive 75 | return directiveServiceClient.enqueue(directive, apiEndpoint, apiAccessToken); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /07/skill-package/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "es-ES": { 6 | "name": "Feliz Cumpleaños" 7 | }, 8 | "es-MX": { 9 | "name": "Feliz Cumpleaños" 10 | }, 11 | "es-US": { 12 | "name": "Feliz Cumpleaños" 13 | }, 14 | "en-US": { 15 | "name": "Happy Birthday" 16 | }, 17 | "en-AU": { 18 | "name": "Happy Birthday" 19 | }, 20 | "en-CA": { 21 | "name": "Happy Birthday" 22 | }, 23 | "en-GB": { 24 | "name": "Happy Birthday" 25 | }, 26 | "en-IN": { 27 | "name": "Happy Birthday" 28 | }, 29 | "fr-FR": { 30 | "name": "Joyeux Anniversaire" 31 | }, 32 | "fr-CA": { 33 | "name": "Bonne Fête" 34 | }, 35 | "it-IT": { 36 | "name": "Buon Compleanno" 37 | } 38 | } 39 | }, 40 | "apis": { 41 | "custom": {} 42 | }, 43 | "manifestVersion": "1.0", 44 | "permissions": [ 45 | { 46 | "name": "alexa::profile:given_name:read" 47 | }, 48 | { 49 | "name": "alexa::alerts:reminders:skill:readwrite" 50 | } 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /08/README.md: -------------------------------------------------------------------------------- 1 | # Part 8 - Alexa Presentation Language (I) 2 | 3 | This is where we introduce APL, the Alexa Prersentation Language. In this module we create a simple splash screen APL document that we reuse across several intents. 4 | We also introduce Home Cards (these cards are typically visble in the Alexa app) and media storage via S3 in your Alexa Hosted Skill space to store the images we need to render. 5 | The APL document shows a tailored hint (thanks to the testToHint transformer) 6 | 7 | ## Milestones 8 | 9 | 1. *Build Tab*: enable APL interface 10 | 2. *Code Tab*: create documents folder. Go to S3 Media folder and add all image files in the project's documents/images 11 | 3. *Code Tab*: in documents folder create launchScreen.json and leave empty 12 | 4. *Code Tab*: add handlers.js file, move all handlers here, index.js becomes very small 13 | 4. *Display Tab*: open APL authoring tool to explain the basics. Paste launchScreen.json as doc and launchSampleDatasource.json as datasource and explain/play with it. 14 | 5. *Code Tab*: fill documents/launchScreen.json with result from APL authoring tool (better just copy paste the original) 15 | 6. *Code Tab*: locate RemindBirthdayIntentHandler and add APL directive via util. Add standard card to response builder 16 | 7. *Code Tab*: locate SayBirthdayIntentHandler and add APL directive via util. Add standard card to response builder 17 | 8. *Code Tab*: locate CelebrityBirthdaysIntentHandler and add APL directive via util. Add standard card to response builder 18 | 19 | ## Concepts 20 | 21 | 1. APL RenderDocument and APL Directive 22 | 2. APL Databinding and APL Authoring Tool 23 | 3. APL Styles, Layouts and ViewPorts 24 | 4. APL Transformers (Text to Hint) 25 | 5. Home Cards 26 | 6. Media storage in Alexa-hosted Skills 27 | 28 | ## Diff 29 | 30 | 1. **handlers.js**: create file, put handlers here. For SayBirthdayIntentHandler, RemindBirthdayIntentHandler and CelebrityVirthdaysIntenteHandler: Add APL directive, use util.js APL helper and AHS url helper to pass background urls. Add card responses too. 31 | 2. **skill.json**: insert APL interface definition for reference (not used in the project) 32 | 3. **util.js**: insert supportsAPL() function 33 | 4. **documents**: create folder and create inside launchScreen.json and launchSampleDatasource.json (for the APL authoring tool) 34 | 5. **constants.js**: create APLDoc structure pointing to to launchScreen.json (for now) 35 | 6. **localisation.js**: add strings for launch header and launch hint 36 | 7. **index.js**: index is now shorter as it gets rid of the handlers 37 | 38 | ## Videos 39 | 40 | [EN](https://alexa.design/zerotohero8)/[DE](https://alexa.design/de_zerotohero8)/[FR](https://alexa.design/fr_zerotohero8)/[IT](https://alexa.design/it_zerotohero8)/[ES](../README_ES.md) 41 | (EN version includes the following subtitles: EN, DE, PT) -------------------------------------------------------------------------------- /08/ask-resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "askcliResourcesVersion": "2020-03-31", 3 | "profiles": { 4 | "default": { 5 | "skillMetadata": { 6 | "src": "./skill-package" 7 | }, 8 | "code": { 9 | "default": { 10 | "src": "./lambda" 11 | } 12 | }, 13 | "skillInfrastructure": { 14 | "userConfig": { 15 | "runtime": "nodejs10.x", 16 | "handler": "index.handler", 17 | "awsRegion": "us-east-1" 18 | }, 19 | "type": "@ask-cli/lambda-deployer" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /08/lambda/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // we now specify which attributes are saved (see the save interceptor below) 3 | PERSISTENT_ATTRIBUTES_NAMES: ['day', 'month', 'monthName', 'year', 'sessionCounter', 'reminderId'], 4 | // these are the permissions needed to fetch the first name 5 | GIVEN_NAME_PERMISSION: ['alexa::profile:given_name:read'], 6 | // these are the permissions needed to send reminders 7 | REMINDERS_PERMISSION: ['alexa::alerts:reminders:skill:readwrite'], 8 | // max number of entries to fetch from the external API 9 | MAX_BIRTHDAYS: 5, 10 | // APL documents 11 | APL: { 12 | launchDoc: require('./documents/launchScreen.json') 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /08/lambda/documents/images/LICENSE.txt: -------------------------------------------------------------------------------- 1 | https://unsplash.com/license 2 | 3 | All photos published on Unsplash can be used for free. You can use them for commercial and noncommercial purposes. You do not need to ask permission from or provide credit to the photographer or Unsplash, although it is appreciated when possible. 4 | 5 | More precisely, Unsplash grants you an irrevocable, nonexclusive, worldwide copyright license to download, copy, modify, distribute, perform, and use photos from Unsplash for free, including for commercial purposes, without permission from or attributing the photographer or Unsplash. This license does not include the right to compile photos from Unsplash to replicate a similar or competing service. -------------------------------------------------------------------------------- /08/lambda/documents/images/cake_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/cake_1024x600.png -------------------------------------------------------------------------------- /08/lambda/documents/images/cake_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/cake_1280x800.png -------------------------------------------------------------------------------- /08/lambda/documents/images/cake_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/cake_1920x1080.png -------------------------------------------------------------------------------- /08/lambda/documents/images/cake_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/cake_480x480.png -------------------------------------------------------------------------------- /08/lambda/documents/images/cake_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/cake_960x480.png -------------------------------------------------------------------------------- /08/lambda/documents/images/confetti_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/confetti_1024x600.png -------------------------------------------------------------------------------- /08/lambda/documents/images/confetti_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/confetti_1280x800.png -------------------------------------------------------------------------------- /08/lambda/documents/images/confetti_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/confetti_1920x1080.png -------------------------------------------------------------------------------- /08/lambda/documents/images/confetti_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/confetti_480x480.png -------------------------------------------------------------------------------- /08/lambda/documents/images/confetti_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/confetti_960x480.png -------------------------------------------------------------------------------- /08/lambda/documents/images/full_icon_108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/full_icon_108.png -------------------------------------------------------------------------------- /08/lambda/documents/images/full_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/full_icon_512.png -------------------------------------------------------------------------------- /08/lambda/documents/images/garlands_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/garlands_1024x600.png -------------------------------------------------------------------------------- /08/lambda/documents/images/garlands_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/garlands_1280x800.png -------------------------------------------------------------------------------- /08/lambda/documents/images/garlands_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/garlands_1920x1080.png -------------------------------------------------------------------------------- /08/lambda/documents/images/garlands_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/garlands_480x480.png -------------------------------------------------------------------------------- /08/lambda/documents/images/garlands_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/garlands_960x480.png -------------------------------------------------------------------------------- /08/lambda/documents/images/icon_108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/icon_108.png -------------------------------------------------------------------------------- /08/lambda/documents/images/icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/icon_512.png -------------------------------------------------------------------------------- /08/lambda/documents/images/lights_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/lights_1024x600.png -------------------------------------------------------------------------------- /08/lambda/documents/images/lights_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/lights_1280x800.png -------------------------------------------------------------------------------- /08/lambda/documents/images/lights_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/lights_1920x1080.png -------------------------------------------------------------------------------- /08/lambda/documents/images/lights_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/lights_480x480.png -------------------------------------------------------------------------------- /08/lambda/documents/images/lights_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/lights_960x480.png -------------------------------------------------------------------------------- /08/lambda/documents/images/papers_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/papers_1024x600.png -------------------------------------------------------------------------------- /08/lambda/documents/images/papers_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/papers_1280x800.png -------------------------------------------------------------------------------- /08/lambda/documents/images/papers_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/papers_1920x1080.png -------------------------------------------------------------------------------- /08/lambda/documents/images/papers_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/papers_480x480.png -------------------------------------------------------------------------------- /08/lambda/documents/images/papers_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/papers_960x480.png -------------------------------------------------------------------------------- /08/lambda/documents/images/straws_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/straws_1024x600.png -------------------------------------------------------------------------------- /08/lambda/documents/images/straws_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/straws_1280x800.png -------------------------------------------------------------------------------- /08/lambda/documents/images/straws_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/straws_1920x1080.png -------------------------------------------------------------------------------- /08/lambda/documents/images/straws_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/straws_480x480.png -------------------------------------------------------------------------------- /08/lambda/documents/images/straws_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/08/lambda/documents/images/straws_960x480.png -------------------------------------------------------------------------------- /08/lambda/documents/launchSampleDatasource.json: -------------------------------------------------------------------------------- 1 | { 2 | "launchData": { 3 | "properties": { 4 | "headerTitle": "Happy Birthday", 5 | "mainText": "When is your birthday?", 6 | "hintString": "Alexa, register my birthday", 7 | "logoImage": "https://i.imgur.com/ZJJtcRT.png", 8 | "backgroundImage": "https://i.imgur.com/Y21B9Y5.jpg", 9 | "backgroundOpacity": "0.5" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /08/lambda/documents/launchSampleDatasource_fr_CA.json: -------------------------------------------------------------------------------- 1 | { 2 | "launchData": { 3 | "properties": { 4 | "headerTitle": "Bonne Fête", 5 | "mainText": "Vous ne n'avez pas encore dit votre date de naissance", 6 | "hintString": "quand est ma fête ?", 7 | "logoImage": "https://i.imgur.com/ZJJtcRT.png", 8 | "backgroundImage": "https://i.imgur.com/Y21B9Y5.jpg", 9 | "backgroundOpacity": "0.5" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /08/lambda/documents/launchSampleDatasource_fr_FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "launchData": { 3 | "properties": { 4 | "headerTitle": "Joyeux Anniversaire", 5 | "mainText": "Vous ne n'avez pas encore dit votre date de naissance", 6 | "hintString": "quand est mon anniversaire ?", 7 | "logoImage": "https://i.imgur.com/ZJJtcRT.png", 8 | "backgroundImage": "https://i.imgur.com/Y21B9Y5.jpg", 9 | "backgroundOpacity": "0.5" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /08/lambda/documents/launchSampleDatasource_it.json: -------------------------------------------------------------------------------- 1 | { 2 | "launchData": { 3 | "properties": { 4 | "headerTitle": "Buon Compleanno", 5 | "mainText": "Quando è il tuo compleanno?", 6 | "hintString": "Alexa, chi compie gli anni oggi?", 7 | "logoImage": "https://i.imgur.com/ZJJtcRT.png", 8 | "backgroundImage": "https://i.imgur.com/Y21B9Y5.jpg", 9 | "backgroundOpacity": "0.5" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /08/lambda/documents/launchScreen.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "APL", 3 | "version": "1.1", 4 | "theme": "dark", 5 | "import": [ 6 | { 7 | "name": "alexa-viewport-profiles", 8 | "version": "1.0.0" 9 | }, 10 | { 11 | "name": "alexa-layouts", 12 | "version": "1.0.0" 13 | }, 14 | { 15 | "name": "alexa-styles", 16 | "version": "1.0.0" 17 | } 18 | ], 19 | "layouts": { 20 | "LaunchScreen": { 21 | "description": "A basic launch screen with a text and logo", 22 | "parameters": [ 23 | { 24 | "name": "mainText", 25 | "type": "string" 26 | }, 27 | { 28 | "name": "logo", 29 | "type": "string" 30 | } 31 | ], 32 | "items": [ 33 | { 34 | "type": "Container", 35 | "width": "100%", 36 | "height": "100%", 37 | "justifyContent": "center", 38 | "alignItems": "center", 39 | "item": [ 40 | { 41 | "type": "Image", 42 | "source": "${logo}", 43 | "width": "20vw", 44 | "height": "20vw", 45 | "scale": "best-fill" 46 | }, 47 | { 48 | "type": "Text", 49 | "text": "${mainText}", 50 | "style": "textStyleDisplay5", 51 | "textAlign": "center", 52 | "paddingTop": "30dp", 53 | "color": "white" 54 | } 55 | ] 56 | } 57 | ] 58 | } 59 | }, 60 | "mainTemplate": { 61 | "parameters": [ 62 | "payload" 63 | ], 64 | "items": [ 65 | { 66 | "type": "Container", 67 | "direction": "column", 68 | "items": [ 69 | { 70 | "type": "Image", 71 | "source": "${payload.launchData.properties.backgroundImage}", 72 | "scale": "best-fill", 73 | "width": "100vw", 74 | "height": "100vh", 75 | "opacity": "${payload.launchData.properties.backgroundOpacity}" 76 | }, 77 | { 78 | "type": "Container", 79 | "position": "absolute", 80 | "width": "100vw", 81 | "height": "100vh", 82 | "direction": "column", 83 | "items": [ 84 | { 85 | "headerTitle": "${payload.launchData.properties.headerTitle}", 86 | "type": "AlexaHeader" 87 | }, 88 | { 89 | "when": "${@viewportProfile == @hubRoundSmall}", 90 | "type": "Container", 91 | "width": "100vw", 92 | "height": "60vh", 93 | "position": "relative", 94 | "alignItems": "center", 95 | "justifyContent": "center", 96 | "direction": "column", 97 | "items": [ 98 | { 99 | "type": "LaunchScreen", 100 | "mainText": "${payload.launchData.properties.mainText}", 101 | "logo": "${payload.launchData.properties.logoImage}" 102 | } 103 | ] 104 | }, 105 | { 106 | "when": "${@viewportProfile == @hubLandscapeSmall || @viewportProfile == @hubLandscapeMedium || @viewportProfile == @hubLandscapeLarge || @viewportProfile == @tvLandscapeXLarge}", 107 | "type": "Container", 108 | "width": "100vw", 109 | "height": "70vh", 110 | "direction": "column", 111 | "alignItems": "center", 112 | "justifyContent": "center", 113 | "items": [ 114 | { 115 | "type": "LaunchScreen", 116 | "mainText": "${payload.launchData.properties.mainText}", 117 | "logo": "${payload.launchData.properties.logoImage}" 118 | } 119 | ] 120 | }, 121 | { 122 | "footerHint": "${payload.launchData.properties.hintString}", 123 | "type": "AlexaFooter", 124 | "when": "${@viewportProfile == @hubLandscapeSmall || @viewportProfile == @hubLandscapeMedium || @viewportProfile == @hubLandscapeLarge || @viewportProfile == @tvLandscapeXLarge}" 125 | } 126 | ] 127 | } 128 | ] 129 | } 130 | ] 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /08/lambda/index.js: -------------------------------------------------------------------------------- 1 | const Alexa = require('ask-sdk-core'); 2 | const util = require('./util'); 3 | const interceptors = require('./interceptors'); 4 | const handlers = require('./handlers'); 5 | /** 6 | * This handler acts as the entry point for your skill, routing all request and response 7 | * payloads to the handlers above. Make sure any new handlers or interceptors you've 8 | * defined are included below. The order matters - they're processed top to bottom 9 | * */ 10 | exports.handler = Alexa.SkillBuilders.custom() 11 | .addRequestHandlers( 12 | handlers.LaunchRequestHandler, 13 | handlers.RegisterBirthdayIntentHandler, 14 | handlers.SayBirthdayIntentHandler, 15 | handlers.RemindBirthdayIntentHandler, 16 | handlers.CelebrityBirthdaysIntentHandler, 17 | handlers.HelpIntentHandler, 18 | handlers.CancelAndStopIntentHandler, 19 | handlers.FallbackIntentHandler, 20 | handlers.SessionEndedRequestHandler, 21 | handlers.IntentReflectorHandler) 22 | .addErrorHandlers( 23 | handlers.ErrorHandler) 24 | .addRequestInterceptors( 25 | interceptors.LoadAttributesRequestInterceptor, 26 | interceptors.LocalisationRequestInterceptor, 27 | interceptors.LoggingRequestInterceptor, 28 | interceptors.LoadNameRequestInterceptor, 29 | interceptors.LoadTimezoneRequestInterceptor) 30 | .addResponseInterceptors( 31 | interceptors.LoggingResponseInterceptor, 32 | interceptors.SaveAttributesResponseInterceptor) 33 | .withPersistenceAdapter(util.getPersistenceAdapter()) 34 | .withApiClient(new Alexa.DefaultApiClient()) 35 | .withCustomUserAgent('sample/happy-birthday/mod8') 36 | .lambda(); 37 | -------------------------------------------------------------------------------- /08/lambda/logic.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment-timezone'); // will help us do all the dates math while considering the timezone 2 | const util = require('./util'); 3 | const axios = require('axios'); 4 | 5 | module.exports = { 6 | getBirthdayData(day, month, year, timezone) { 7 | const today = moment().tz(timezone).startOf('day'); 8 | const wasBorn = moment(`${month}/${day}/${year}`, "MM/DD/YYYY").tz(timezone).startOf('day'); 9 | const nextBirthday = moment(`${month}/${day}/${today.year()}`, "MM/DD/YYYY").tz(timezone).startOf('day'); 10 | if (today.isAfter(nextBirthday)) { 11 | nextBirthday.add(1, 'years'); 12 | } 13 | const age = today.diff(wasBorn, 'years'); 14 | const daysAlive = today.diff(wasBorn, 'days'); 15 | const daysUntilBirthday = nextBirthday.startOf('day').diff(today, 'days'); // same day returns 0 16 | 17 | return { 18 | daysAlive: daysAlive, // not used but nice to have :) 19 | daysUntilBirthday: daysUntilBirthday, 20 | age: age //in years 21 | } 22 | }, 23 | createBirthdayReminder(daysUntilBirthday, timezone, locale, message) { 24 | moment.locale(locale); 25 | const createdMoment = moment().tz(timezone); 26 | let triggerMoment = createdMoment.startOf('day').add(daysUntilBirthday, 'days'); 27 | if (daysUntilBirthday === 0) { 28 | triggerMoment = createdMoment.startOf('day').add(1, 'years'); // reminder created on the day of birthday will trigger next year 29 | } 30 | console.log('Reminder schedule: ' + triggerMoment.format('YYYY-MM-DDTHH:mm:00.000')); 31 | 32 | return util.createReminder(createdMoment, triggerMoment, timezone, locale, message); 33 | }, 34 | getAdjustedDate(timezone) { 35 | const today = moment().tz(timezone).startOf('day'); 36 | 37 | return { 38 | day: today.date(), 39 | month: today.month() + 1 40 | } 41 | }, 42 | fetchBirthdays(day, month, limit){ 43 | const endpoint = 'https://query.wikidata.org/sparql'; 44 | // List of actors with pictures and date of birth for a given day and month 45 | const sparqlQuery = 46 | `SELECT DISTINCT ?human ?humanLabel ?picture ?date_of_birth ?place_of_birthLabel WHERE { 47 | ?human wdt:P31 wd:Q5; 48 | wdt:P106 wd:Q33999; 49 | wdt:P18 ?picture. 50 | FILTER((DATATYPE(?date_of_birth)) = xsd:dateTime) 51 | FILTER((MONTH(?date_of_birth)) = ${month}) 52 | FILTER((DAY(?date_of_birth)) = ${day}) 53 | FILTER (bound(?place_of_birth)) 54 | SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } 55 | OPTIONAL { ?human wdt:P569 ?date_of_birth. } 56 | OPTIONAL { ?human wdt:P19 ?place_of_birth. } 57 | } 58 | LIMIT ${limit}`; 59 | const url = endpoint + '?query=' + encodeURIComponent(sparqlQuery); 60 | console.log(url); // in case you want to try the query in a web browser 61 | 62 | var config = { 63 | timeout: 6500, // timeout api call before we reach Alexa's 8 sec timeout, or set globally via axios.defaults.timeout 64 | headers: {'Accept': 'application/sparql-results+json'} 65 | }; 66 | 67 | async function getJsonResponse(url, config){ 68 | const res = await axios.get(url, config); 69 | return res.data; 70 | } 71 | 72 | return getJsonResponse(url, config).then((result) => { 73 | return result; 74 | }).catch((error) => { 75 | return null; 76 | }); 77 | }, 78 | convertBirthdaysResponse(handlerInput, response, withAge, timezone){ 79 | let speechResponse = ''; 80 | // if the API call failed we just don't append today's birthdays to the response 81 | if (!response || !response.results || !response.results.bindings || !Object.keys(response.results.bindings).length > 0) 82 | return speechResponse; 83 | const results = response.results.bindings; 84 | speechResponse += handlerInput.t('ALSO_TODAY_MSG'); 85 | results.forEach((person, index) => { 86 | console.log(person); 87 | speechResponse += person.humanLabel.value; 88 | if (withAge && timezone && person.date_of_birth.value) { 89 | const age = module.exports.convertBirthdateToYearsOld(person, timezone); 90 | speechResponse += handlerInput.t('TURNING_YO_MSG', {count: age}); 91 | } 92 | if (index === Object.keys(results).length - 2) 93 | speechResponse += handlerInput.t('CONJUNCTION_MSG'); 94 | else 95 | speechResponse += '. '; 96 | }); 97 | 98 | return speechResponse; 99 | }, 100 | convertBirthdateToYearsOld(person, timezone) { 101 | const today = moment().tz(timezone).startOf('day'); 102 | const wasBorn = moment(person.date_of_birth.value).tz(timezone).startOf('day'); 103 | return today.diff(wasBorn, 'years'); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /08/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happy-birthday", 3 | "version": "0.8.0", 4 | "description": "happy birthday skill", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon Alexa", 10 | "dependencies": { 11 | "ask-sdk-core": "^2.7.0", 12 | "ask-sdk-model": "^1.18.0", 13 | "i18next": "^15.0.5", 14 | "ask-sdk-s3-persistence-adapter": "^2.7.0", 15 | "ask-sdk-dynamodb-persistence-adapter": "^2.7.0", 16 | "moment-timezone": "^0.5.23", 17 | "axios": ">=0.21.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /08/lambda/util.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const s3SigV4Client = new AWS.S3({ 4 | signatureVersion: 'v4' 5 | }); 6 | 7 | module.exports = { 8 | getS3PreSignedUrl(s3ObjectKey) { 9 | const bucketName = process.env.S3_PERSISTENCE_BUCKET; 10 | const s3PreSignedUrl = s3SigV4Client.getSignedUrl('getObject', { 11 | Bucket: bucketName, 12 | Key: s3ObjectKey, 13 | Expires: 60*1 // the Expires is capped for 1 minute 14 | }); 15 | console.log(`Util.s3PreSignedUrl: ${s3ObjectKey} URL ${s3PreSignedUrl}`); 16 | return s3PreSignedUrl; 17 | }, 18 | getPersistenceAdapter(tableName) { 19 | // This function is an indirect way to detect if this is part of an Alexa-Hosted skill 20 | function isAlexaHosted() { 21 | return process.env.S3_PERSISTENCE_BUCKET; 22 | } 23 | if (isAlexaHosted()) { 24 | const {S3PersistenceAdapter} = require('ask-sdk-s3-persistence-adapter'); 25 | return new S3PersistenceAdapter({ 26 | bucketName: process.env.S3_PERSISTENCE_BUCKET 27 | }); 28 | } else { 29 | // IMPORTANT: don't forget to give DynamoDB access to the role you're using to run this lambda (via IAM policy) 30 | const {DynamoDbPersistenceAdapter} = require('ask-sdk-dynamodb-persistence-adapter'); 31 | return new DynamoDbPersistenceAdapter({ 32 | tableName: tableName || 'happy_birthday', 33 | createTable: true 34 | }); 35 | } 36 | }, 37 | createReminder(requestMoment, scheduledMoment, timezone, locale, message) { 38 | return { 39 | requestTime: requestMoment.format('YYYY-MM-DDTHH:mm:00.000'), 40 | trigger: { 41 | type: 'SCHEDULED_ABSOLUTE', 42 | scheduledTime: scheduledMoment.format('YYYY-MM-DDTHH:mm:00.000'), 43 | timeZoneId: timezone 44 | }, 45 | alertInfo: { 46 | spokenInfo: { 47 | content: [{ 48 | locale: locale, 49 | text: message 50 | }] 51 | } 52 | }, 53 | pushNotification: { 54 | status: 'ENABLED' 55 | } 56 | } 57 | }, 58 | callDirectiveService(handlerInput, msg) { 59 | // Call Alexa Directive Service. 60 | const {requestEnvelope} = handlerInput; 61 | const directiveServiceClient = handlerInput.serviceClientFactory.getDirectiveServiceClient(); 62 | const requestId = requestEnvelope.request.requestId; 63 | const {apiEndpoint, apiAccessToken} = requestEnvelope.context.System; 64 | // build the progressive response directive 65 | const directive = { 66 | header: { 67 | requestId 68 | }, 69 | directive:{ 70 | type: 'VoicePlayer.Speak', 71 | speech: msg 72 | } 73 | }; 74 | // send directive 75 | return directiveServiceClient.enqueue(directive, apiEndpoint, apiAccessToken); 76 | }, 77 | supportsAPL(handlerInput) { 78 | const {supportedInterfaces} = handlerInput.requestEnvelope.context.System.device; 79 | return !!supportedInterfaces['Alexa.Presentation.APL']; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /08/skill-package/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "es-ES": { 6 | "name": "Feliz Cumpleaños" 7 | }, 8 | "es-MX": { 9 | "name": "Feliz Cumpleaños" 10 | }, 11 | "es-US": { 12 | "name": "Feliz Cumpleaños" 13 | }, 14 | "en-US": { 15 | "name": "Happy Birthday" 16 | }, 17 | "en-AU": { 18 | "name": "Happy Birthday" 19 | }, 20 | "en-CA": { 21 | "name": "Happy Birthday" 22 | }, 23 | "en-GB": { 24 | "name": "Happy Birthday" 25 | }, 26 | "en-IN": { 27 | "name": "Happy Birthday" 28 | }, 29 | "fr-FR": { 30 | "name": "Joyeux Anniversaire" 31 | }, 32 | "fr-CA": { 33 | "name": "Bonne Fête" 34 | }, 35 | "it-IT": { 36 | "name": "Buon Compleanno" 37 | } 38 | } 39 | }, 40 | "apis": { 41 | "custom": { 42 | "interfaces": [ 43 | { 44 | "type": "ALEXA_PRESENTATION_APL" 45 | } 46 | ] 47 | } 48 | }, 49 | "manifestVersion": "1.0", 50 | "permissions": [ 51 | { 52 | "name": "alexa::profile:given_name:read" 53 | }, 54 | { 55 | "name": "alexa::alerts:reminders:skill:readwrite" 56 | } 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /09/README.md: -------------------------------------------------------------------------------- 1 | # Part 9 - Alexa Presentation Language (II) 2 | 3 | In this second APL module we show a slightly more complex APL document. We use the results of the external API (celebrity birthdays) to render a list on screen. We enable each item shown in the list with touch capability. 4 | 5 | ## Milestones 6 | 7 | 1. *Code Tab*: do the steps in the Diff section up to 5 8 | 2. *VS Code*: show sample data from API (sampleBirthdayResponse) and APL doc (listScreen.json) 9 | 3. *Display Designer*: explain Designer. copy listSampleDatasource.son to datasources and listScreen.json to doc tab. Play with it 10 | 4. *Code Tab*: do the remaining steps in the Diff section 11 | 12 | ## Concepts 13 | 14 | 1. APL Authoring Tool 15 | 2. APL Layouts & Sequences 16 | 3. APL Transformers (Text to Hint) 17 | 4. APL Touch Wrapper 18 | 19 | ## Diff 20 | 21 | 1. **index.js**: add TouchIntentHandler to exports 22 | 2. **documents/listScreen.json**: create, paste doc and show on APL authoring tool, explain it 23 | 3. **constants.js**: add reference to listScreen doc (the APL doc with the celebrity list) 24 | 4. **handles.js**: Change APL doc to listScreen in CelebrityBirthdaysIntent handler. Add TouchIntentHandler code and add it to module.exports. [TODO: Add celebrities as dynamic entities + voice navigation] 25 | 5. **index.js**: add handlers.TouchIntentHandler to handlers 26 | 6. **localisation.js**: add LIST_* related strings at the bottom of the file 27 | 7. **logic.js**: add convertBirthdateToYearsOld() function. in convertBirthdaysResponse function where you can see the calculation of age in results loop you need to add a line to put age in actor's list response (we modify the API response format to better suit our APL doc) 28 | 8. **documents/listSampleDatasource.json**: add (not used, just to test in the APL authoring tool) 29 | 9. **documents/sampleBirthdayResponse.json**: added only as reference to show what an API response looks like (not used) 30 | 31 | ## Videos 32 | 33 | [EN](https://alexa.design/zerotohero9)/[DE](https://alexa.design/de_zerotohero9)/[FR](https://alexa.design/fr_zerotohero9)/[IT](https://alexa.design/it_zerotohero9)/[ES](../README_ES.md) 34 | (EN version includes the following subtitles: EN, DE, PT) 35 | -------------------------------------------------------------------------------- /09/ask-resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "askcliResourcesVersion": "2020-03-31", 3 | "profiles": { 4 | "default": { 5 | "skillMetadata": { 6 | "src": "./skill-package" 7 | }, 8 | "code": { 9 | "default": { 10 | "src": "./lambda" 11 | } 12 | }, 13 | "skillInfrastructure": { 14 | "userConfig": { 15 | "runtime": "nodejs10.x", 16 | "handler": "index.handler", 17 | "awsRegion": "us-east-1" 18 | }, 19 | "type": "@ask-cli/lambda-deployer" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /09/lambda/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // we now specify which attributes are saved (see the save interceptor below) 3 | PERSISTENT_ATTRIBUTES_NAMES: ['day', 'month', 'monthName', 'year', 'sessionCounter', 'reminderId'], 4 | // these are the permissions needed to fetch the first name 5 | GIVEN_NAME_PERMISSION: ['alexa::profile:given_name:read'], 6 | // these are the permissions needed to send reminders 7 | REMINDERS_PERMISSION: ['alexa::alerts:reminders:skill:readwrite'], 8 | // max number of entries to fetch from the external API 9 | MAX_BIRTHDAYS: 5, 10 | // APL documents 11 | APL: { 12 | launchDoc: require('./documents/launchScreen.json'), 13 | listDoc: require('./documents/listScreen.json') 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /09/lambda/documents/images/LICENSE.txt: -------------------------------------------------------------------------------- 1 | https://unsplash.com/license 2 | 3 | All photos published on Unsplash can be used for free. You can use them for commercial and noncommercial purposes. You do not need to ask permission from or provide credit to the photographer or Unsplash, although it is appreciated when possible. 4 | 5 | More precisely, Unsplash grants you an irrevocable, nonexclusive, worldwide copyright license to download, copy, modify, distribute, perform, and use photos from Unsplash for free, including for commercial purposes, without permission from or attributing the photographer or Unsplash. This license does not include the right to compile photos from Unsplash to replicate a similar or competing service. -------------------------------------------------------------------------------- /09/lambda/documents/images/cake_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/cake_1024x600.png -------------------------------------------------------------------------------- /09/lambda/documents/images/cake_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/cake_1280x800.png -------------------------------------------------------------------------------- /09/lambda/documents/images/cake_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/cake_1920x1080.png -------------------------------------------------------------------------------- /09/lambda/documents/images/cake_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/cake_480x480.png -------------------------------------------------------------------------------- /09/lambda/documents/images/cake_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/cake_960x480.png -------------------------------------------------------------------------------- /09/lambda/documents/images/confetti_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/confetti_1024x600.png -------------------------------------------------------------------------------- /09/lambda/documents/images/confetti_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/confetti_1280x800.png -------------------------------------------------------------------------------- /09/lambda/documents/images/confetti_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/confetti_1920x1080.png -------------------------------------------------------------------------------- /09/lambda/documents/images/confetti_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/confetti_480x480.png -------------------------------------------------------------------------------- /09/lambda/documents/images/confetti_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/confetti_960x480.png -------------------------------------------------------------------------------- /09/lambda/documents/images/full_icon_108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/full_icon_108.png -------------------------------------------------------------------------------- /09/lambda/documents/images/full_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/full_icon_512.png -------------------------------------------------------------------------------- /09/lambda/documents/images/garlands_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/garlands_1024x600.png -------------------------------------------------------------------------------- /09/lambda/documents/images/garlands_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/garlands_1280x800.png -------------------------------------------------------------------------------- /09/lambda/documents/images/garlands_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/garlands_1920x1080.png -------------------------------------------------------------------------------- /09/lambda/documents/images/garlands_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/garlands_480x480.png -------------------------------------------------------------------------------- /09/lambda/documents/images/garlands_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/garlands_960x480.png -------------------------------------------------------------------------------- /09/lambda/documents/images/icon_108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/icon_108.png -------------------------------------------------------------------------------- /09/lambda/documents/images/icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/icon_512.png -------------------------------------------------------------------------------- /09/lambda/documents/images/lights_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/lights_1024x600.png -------------------------------------------------------------------------------- /09/lambda/documents/images/lights_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/lights_1280x800.png -------------------------------------------------------------------------------- /09/lambda/documents/images/lights_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/lights_1920x1080.png -------------------------------------------------------------------------------- /09/lambda/documents/images/lights_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/lights_480x480.png -------------------------------------------------------------------------------- /09/lambda/documents/images/lights_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/lights_960x480.png -------------------------------------------------------------------------------- /09/lambda/documents/images/papers_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/papers_1024x600.png -------------------------------------------------------------------------------- /09/lambda/documents/images/papers_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/papers_1280x800.png -------------------------------------------------------------------------------- /09/lambda/documents/images/papers_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/papers_1920x1080.png -------------------------------------------------------------------------------- /09/lambda/documents/images/papers_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/papers_480x480.png -------------------------------------------------------------------------------- /09/lambda/documents/images/papers_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/papers_960x480.png -------------------------------------------------------------------------------- /09/lambda/documents/images/straws_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/straws_1024x600.png -------------------------------------------------------------------------------- /09/lambda/documents/images/straws_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/straws_1280x800.png -------------------------------------------------------------------------------- /09/lambda/documents/images/straws_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/straws_1920x1080.png -------------------------------------------------------------------------------- /09/lambda/documents/images/straws_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/straws_480x480.png -------------------------------------------------------------------------------- /09/lambda/documents/images/straws_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/09/lambda/documents/images/straws_960x480.png -------------------------------------------------------------------------------- /09/lambda/documents/launchSampleDatasource.json: -------------------------------------------------------------------------------- 1 | { 2 | "launchData": { 3 | "properties": { 4 | "headerTitle": "Happy Birthday", 5 | "mainText": "When is your birthday?", 6 | "hintString": "Alexa, register my birthday", 7 | "logoImage": "https://i.imgur.com/ZJJtcRT.png", 8 | "backgroundImage": "https://i.imgur.com/Y21B9Y5.jpg", 9 | "backgroundOpacity": "0.5" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /09/lambda/documents/launchSampleDatasource_fr_CA.json: -------------------------------------------------------------------------------- 1 | { 2 | "launchData": { 3 | "properties": { 4 | "headerTitle": "Bonne Fête", 5 | "mainText": "Vous ne n'avez pas encore dit votre date de naissance", 6 | "hintString": "quand est ma fête ?", 7 | "logoImage": "https://i.imgur.com/ZJJtcRT.png", 8 | "backgroundImage": "https://i.imgur.com/Y21B9Y5.jpg", 9 | "backgroundOpacity": "0.5" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /09/lambda/documents/launchSampleDatasource_fr_FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "launchData": { 3 | "properties": { 4 | "headerTitle": "Joyeux Anniversaire", 5 | "mainText": "Vous ne n'avez pas encore dit votre date de naissance", 6 | "hintString": "quand est mon anniversaire ?", 7 | "logoImage": "https://i.imgur.com/ZJJtcRT.png", 8 | "backgroundImage": "https://i.imgur.com/Y21B9Y5.jpg", 9 | "backgroundOpacity": "0.5" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /09/lambda/documents/launchSampleDatasource_it.json: -------------------------------------------------------------------------------- 1 | { 2 | "launchData": { 3 | "properties": { 4 | "headerTitle": "Buon Compleanno", 5 | "mainText": "Quando è il tuo compleanno?", 6 | "hintString": "Alexa, chi compie gli anni oggi?", 7 | "logoImage": "https://i.imgur.com/ZJJtcRT.png", 8 | "backgroundImage": "https://i.imgur.com/Y21B9Y5.jpg", 9 | "backgroundOpacity": "0.5" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /09/lambda/documents/launchScreen.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "APL", 3 | "version": "1.1", 4 | "theme": "dark", 5 | "import": [ 6 | { 7 | "name": "alexa-viewport-profiles", 8 | "version": "1.0.0" 9 | }, 10 | { 11 | "name": "alexa-layouts", 12 | "version": "1.0.0" 13 | }, 14 | { 15 | "name": "alexa-styles", 16 | "version": "1.0.0" 17 | } 18 | ], 19 | "layouts": { 20 | "LaunchScreen": { 21 | "description": "A basic launch screen with a text and logo", 22 | "parameters": [ 23 | { 24 | "name": "mainText", 25 | "type": "string" 26 | }, 27 | { 28 | "name": "logo", 29 | "type": "string" 30 | } 31 | ], 32 | "items": [ 33 | { 34 | "type": "Container", 35 | "width": "100%", 36 | "height": "100%", 37 | "justifyContent": "center", 38 | "alignItems": "center", 39 | "item": [ 40 | { 41 | "type": "Image", 42 | "source": "${logo}", 43 | "width": "20vw", 44 | "height": "20vw", 45 | "scale": "best-fill" 46 | }, 47 | { 48 | "type": "Text", 49 | "text": "${mainText}", 50 | "style": "textStyleDisplay5", 51 | "textAlign": "center", 52 | "paddingTop": "30dp", 53 | "color": "white" 54 | } 55 | ] 56 | } 57 | ] 58 | } 59 | }, 60 | "mainTemplate": { 61 | "parameters": [ 62 | "payload" 63 | ], 64 | "items": [ 65 | { 66 | "type": "Container", 67 | "direction": "column", 68 | "items": [ 69 | { 70 | "type": "Image", 71 | "source": "${payload.launchData.properties.backgroundImage}", 72 | "scale": "best-fill", 73 | "width": "100vw", 74 | "height": "100vh", 75 | "opacity": "${payload.launchData.properties.backgroundOpacity}" 76 | }, 77 | { 78 | "type": "Container", 79 | "position": "absolute", 80 | "width": "100vw", 81 | "height": "100vh", 82 | "direction": "column", 83 | "items": [ 84 | { 85 | "headerTitle": "${payload.launchData.properties.headerTitle}", 86 | "type": "AlexaHeader" 87 | }, 88 | { 89 | "when": "${@viewportProfile == @hubRoundSmall}", 90 | "type": "Container", 91 | "width": "100vw", 92 | "height": "60vh", 93 | "position": "relative", 94 | "alignItems": "center", 95 | "justifyContent": "center", 96 | "direction": "column", 97 | "items": [ 98 | { 99 | "type": "LaunchScreen", 100 | "mainText": "${payload.launchData.properties.mainText}", 101 | "logo": "${payload.launchData.properties.logoImage}" 102 | } 103 | ] 104 | }, 105 | { 106 | "when": "${@viewportProfile == @hubLandscapeSmall || @viewportProfile == @hubLandscapeMedium || @viewportProfile == @hubLandscapeLarge || @viewportProfile == @tvLandscapeXLarge}", 107 | "type": "Container", 108 | "width": "100vw", 109 | "height": "70vh", 110 | "direction": "column", 111 | "alignItems": "center", 112 | "justifyContent": "center", 113 | "items": [ 114 | { 115 | "type": "LaunchScreen", 116 | "mainText": "${payload.launchData.properties.mainText}", 117 | "logo": "${payload.launchData.properties.logoImage}" 118 | } 119 | ] 120 | }, 121 | { 122 | "footerHint": "${payload.launchData.properties.hintString}", 123 | "type": "AlexaFooter", 124 | "when": "${@viewportProfile == @hubLandscapeSmall || @viewportProfile == @hubLandscapeMedium || @viewportProfile == @hubLandscapeLarge || @viewportProfile == @tvLandscapeXLarge}" 125 | } 126 | ] 127 | } 128 | ] 129 | } 130 | ] 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /09/lambda/documents/listSampleDatasource_fr_CA.json: -------------------------------------------------------------------------------- 1 | { 2 | "listData": { 3 | "type": "object", 4 | "properties": { 5 | "config": { 6 | "backgroundImage": "https://i.imgur.com/Y21B9Y5.jpg", 7 | "title": "Fêtes du jour", 8 | "skillIcon" : "https://i.imgur.com/ZJJtcRT.png", 9 | "hintText": "Alexa, quand a lieu ma fête ?" 10 | }, 11 | "list": { 12 | "listItems": [ 13 | { 14 | "human": { 15 | "type": "uri", 16 | "value": "http://www.wikidata.org/entity/Q17319618" 17 | }, 18 | "picture": { 19 | "type": "uri", 20 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/%D0%9D%D0%B8%D0%BA%D0%B8%D1%82%D0%B0%20%D0%9F%D1%80%D0%B5%D1%81%D0%BD%D1%8F%D0%BA%D0%BE%D0%B2%20MULTIVERSE%20.jpg" 21 | }, 22 | "date_of_birth": { 23 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 24 | "type": "literal", 25 | "value": "1991-05-21T00:00:00Z" 26 | }, 27 | "humanLabel": { 28 | "xml:lang": "en", 29 | "type": "literal", 30 | "value": "Nikita Presnyakov" 31 | }, 32 | "place_of_birthLabel": { 33 | "xml:lang": "en", 34 | "type": "literal", 35 | "value": "London" 36 | } 37 | }, 38 | { 39 | "human": { 40 | "type": "uri", 41 | "value": "http://www.wikidata.org/entity/Q1896173" 42 | }, 43 | "picture": { 44 | "type": "uri", 45 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/Maria%20Minzenti%20-%20Nov%201922%20Shadowland.jpg" 46 | }, 47 | "date_of_birth": { 48 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 49 | "type": "literal", 50 | "value": "1898-05-21T00:00:00Z" 51 | }, 52 | "humanLabel": { 53 | "xml:lang": "en", 54 | "type": "literal", 55 | "value": "Maria Mindzenti" 56 | }, 57 | "place_of_birthLabel": { 58 | "xml:lang": "en", 59 | "type": "literal", 60 | "value": "Vienna" 61 | } 62 | }, 63 | { 64 | "human": { 65 | "type": "uri", 66 | "value": "http://www.wikidata.org/entity/Q1646159" 67 | }, 68 | "picture": { 69 | "type": "uri", 70 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/Lola%20Lane%20in%20Four%20Daughters%20trailer.jpg" 71 | }, 72 | "date_of_birth": { 73 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 74 | "type": "literal", 75 | "value": "1906-05-21T00:00:00Z" 76 | }, 77 | "humanLabel": { 78 | "xml:lang": "en", 79 | "type": "literal", 80 | "value": "Lola Lane" 81 | }, 82 | "place_of_birthLabel": { 83 | "xml:lang": "en", 84 | "type": "literal", 85 | "value": "Indiana" 86 | } 87 | }, 88 | { 89 | "human": { 90 | "type": "uri", 91 | "value": "http://www.wikidata.org/entity/Q6048832" 92 | }, 93 | "picture": { 94 | "type": "uri", 95 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/Ayse%20kokcu.jpg" 96 | }, 97 | "date_of_birth": { 98 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 99 | "type": "literal", 100 | "value": "1955-05-21T00:00:00Z" 101 | }, 102 | "humanLabel": { 103 | "xml:lang": "en", 104 | "type": "literal", 105 | "value": "Ayse Kökçü" 106 | }, 107 | "place_of_birthLabel": { 108 | "xml:lang": "en", 109 | "type": "literal", 110 | "value": "Istanbul" 111 | } 112 | }, 113 | { 114 | "human": { 115 | "type": "uri", 116 | "value": "http://www.wikidata.org/entity/Q589048" 117 | }, 118 | "picture": { 119 | "type": "uri", 120 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/Fabriano-Civita02.jpg" 121 | }, 122 | "date_of_birth": { 123 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 124 | "type": "literal", 125 | "value": "1744-05-21T00:00:00Z" 126 | }, 127 | "humanLabel": { 128 | "xml:lang": "en", 129 | "type": "literal", 130 | "value": "Gaspare Pacchierotti" 131 | }, 132 | "place_of_birthLabel": { 133 | "xml:lang": "en", 134 | "type": "literal", 135 | "value": "Fabriano" 136 | } 137 | } 138 | ] 139 | } 140 | }, 141 | "transformers": [{ 142 | "inputPath": "config.hintText", 143 | "transformer": "textToHint" 144 | }] 145 | } 146 | } -------------------------------------------------------------------------------- /09/lambda/documents/sampleBirthdaysResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "head": { 3 | "vars": [ 4 | "human", 5 | "humanLabel", 6 | "picture", 7 | "date_of_birth", 8 | "place_of_birthLabel" 9 | ] 10 | }, 11 | "results": { 12 | "bindings": [ 13 | { 14 | "human": { 15 | "type": "uri", 16 | "value": "http://www.wikidata.org/entity/Q17319618" 17 | }, 18 | "picture": { 19 | "type": "uri", 20 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/%D0%9D%D0%B8%D0%BA%D0%B8%D1%82%D0%B0%20%D0%9F%D1%80%D0%B5%D1%81%D0%BD%D1%8F%D0%BA%D0%BE%D0%B2%20MULTIVERSE%20.jpg" 21 | }, 22 | "date_of_birth": { 23 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 24 | "type": "literal", 25 | "value": "1991-05-21T00:00:00Z" 26 | }, 27 | "humanLabel": { 28 | "xml:lang": "en", 29 | "type": "literal", 30 | "value": "Nikita Presnyakov" 31 | }, 32 | "place_of_birthLabel": { 33 | "xml:lang": "en", 34 | "type": "literal", 35 | "value": "London" 36 | } 37 | }, 38 | { 39 | "human": { 40 | "type": "uri", 41 | "value": "http://www.wikidata.org/entity/Q1896173" 42 | }, 43 | "picture": { 44 | "type": "uri", 45 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/Maria%20Minzenti%20-%20Nov%201922%20Shadowland.jpg" 46 | }, 47 | "date_of_birth": { 48 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 49 | "type": "literal", 50 | "value": "1898-05-21T00:00:00Z" 51 | }, 52 | "humanLabel": { 53 | "xml:lang": "en", 54 | "type": "literal", 55 | "value": "Maria Mindzenti" 56 | }, 57 | "place_of_birthLabel": { 58 | "xml:lang": "en", 59 | "type": "literal", 60 | "value": "Vienna" 61 | } 62 | }, 63 | { 64 | "human": { 65 | "type": "uri", 66 | "value": "http://www.wikidata.org/entity/Q1646159" 67 | }, 68 | "picture": { 69 | "type": "uri", 70 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/Lola%20Lane%20in%20Four%20Daughters%20trailer.jpg" 71 | }, 72 | "date_of_birth": { 73 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 74 | "type": "literal", 75 | "value": "1906-05-21T00:00:00Z" 76 | }, 77 | "humanLabel": { 78 | "xml:lang": "en", 79 | "type": "literal", 80 | "value": "Lola Lane" 81 | }, 82 | "place_of_birthLabel": { 83 | "xml:lang": "en", 84 | "type": "literal", 85 | "value": "Indiana" 86 | } 87 | }, 88 | { 89 | "human": { 90 | "type": "uri", 91 | "value": "http://www.wikidata.org/entity/Q6048832" 92 | }, 93 | "picture": { 94 | "type": "uri", 95 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/Ayse%20kokcu.jpg" 96 | }, 97 | "date_of_birth": { 98 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 99 | "type": "literal", 100 | "value": "1955-05-21T00:00:00Z" 101 | }, 102 | "humanLabel": { 103 | "xml:lang": "en", 104 | "type": "literal", 105 | "value": "Ayse Kökçü" 106 | }, 107 | "place_of_birthLabel": { 108 | "xml:lang": "en", 109 | "type": "literal", 110 | "value": "Istanbul" 111 | } 112 | }, 113 | { 114 | "human": { 115 | "type": "uri", 116 | "value": "http://www.wikidata.org/entity/Q589048" 117 | }, 118 | "picture": { 119 | "type": "uri", 120 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/Fabriano-Civita02.jpg" 121 | }, 122 | "date_of_birth": { 123 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 124 | "type": "literal", 125 | "value": "1744-05-21T00:00:00Z" 126 | }, 127 | "humanLabel": { 128 | "xml:lang": "en", 129 | "type": "literal", 130 | "value": "Gaspare Pacchierotti" 131 | }, 132 | "place_of_birthLabel": { 133 | "xml:lang": "en", 134 | "type": "literal", 135 | "value": "Fabriano" 136 | } 137 | } 138 | ] 139 | } 140 | } -------------------------------------------------------------------------------- /09/lambda/index.js: -------------------------------------------------------------------------------- 1 | const Alexa = require('ask-sdk-core'); 2 | const util = require('./util'); 3 | const interceptors = require('./interceptors'); 4 | const handlers = require('./handlers'); 5 | /** 6 | * This handler acts as the entry point for your skill, routing all request and response 7 | * payloads to the handlers above. Make sure any new handlers or interceptors you've 8 | * defined are included below. The order matters - they're processed top to bottom 9 | * */ 10 | exports.handler = Alexa.SkillBuilders.custom() 11 | .addRequestHandlers( 12 | handlers.LaunchRequestHandler, 13 | handlers.RegisterBirthdayIntentHandler, 14 | handlers.SayBirthdayIntentHandler, 15 | handlers.RemindBirthdayIntentHandler, 16 | handlers.CelebrityBirthdaysIntentHandler, 17 | handlers.TouchIntentHandler, 18 | handlers.HelpIntentHandler, 19 | handlers.CancelAndStopIntentHandler, 20 | handlers.FallbackIntentHandler, 21 | handlers.SessionEndedRequestHandler, 22 | handlers.IntentReflectorHandler) 23 | .addErrorHandlers( 24 | handlers.ErrorHandler) 25 | .addRequestInterceptors( 26 | interceptors.LoadAttributesRequestInterceptor, 27 | interceptors.LocalisationRequestInterceptor, 28 | interceptors.LoggingRequestInterceptor, 29 | interceptors.LoadNameRequestInterceptor, 30 | interceptors.LoadTimezoneRequestInterceptor) 31 | .addResponseInterceptors( 32 | interceptors.LoggingResponseInterceptor, 33 | interceptors.SaveAttributesResponseInterceptor) 34 | .withPersistenceAdapter(util.getPersistenceAdapter()) 35 | .withApiClient(new Alexa.DefaultApiClient()) 36 | .withCustomUserAgent('sample/happy-birthday/mod9') 37 | .lambda(); 38 | -------------------------------------------------------------------------------- /09/lambda/logic.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment-timezone'); // will help us do all the dates math while considering the timezone 2 | const util = require('./util'); 3 | const axios = require('axios'); 4 | 5 | module.exports = { 6 | getBirthdayData(day, month, year, timezone) { 7 | const today = moment().tz(timezone).startOf('day'); 8 | const wasBorn = moment(`${month}/${day}/${year}`, "MM/DD/YYYY").tz(timezone).startOf('day'); 9 | const nextBirthday = moment(`${month}/${day}/${today.year()}`, "MM/DD/YYYY").tz(timezone).startOf('day'); 10 | if (today.isAfter(nextBirthday)) { 11 | nextBirthday.add(1, 'years'); 12 | } 13 | const age = today.diff(wasBorn, 'years'); 14 | const daysAlive = today.diff(wasBorn, 'days'); 15 | const daysUntilBirthday = nextBirthday.startOf('day').diff(today, 'days'); // same day returns 0 16 | 17 | return { 18 | daysAlive: daysAlive, // not used but nice to have :) 19 | daysUntilBirthday: daysUntilBirthday, 20 | age: age //in years 21 | } 22 | }, 23 | createBirthdayReminder(daysUntilBirthday, timezone, locale, message) { 24 | moment.locale(locale); 25 | const createdMoment = moment().tz(timezone); 26 | let triggerMoment = createdMoment.startOf('day').add(daysUntilBirthday, 'days'); 27 | if (daysUntilBirthday === 0) { 28 | triggerMoment = createdMoment.startOf('day').add(1, 'years'); // reminder created on the day of birthday will trigger next year 29 | } 30 | console.log('Reminder schedule: ' + triggerMoment.format('YYYY-MM-DDTHH:mm:00.000')); 31 | 32 | return util.createReminder(createdMoment, triggerMoment, timezone, locale, message); 33 | }, 34 | getAdjustedDate(timezone) { 35 | const today = moment().tz(timezone).startOf('day'); 36 | 37 | return { 38 | day: today.date(), 39 | month: today.month() + 1 40 | } 41 | }, 42 | fetchBirthdays(day, month, limit){ 43 | const endpoint = 'https://query.wikidata.org/sparql'; 44 | // List of actors with pictures and date of birth for a given day and month 45 | const sparqlQuery = 46 | `SELECT DISTINCT ?human ?humanLabel ?picture ?date_of_birth ?place_of_birthLabel WHERE { 47 | ?human wdt:P31 wd:Q5; 48 | wdt:P106 wd:Q33999; 49 | wdt:P18 ?picture. 50 | FILTER((DATATYPE(?date_of_birth)) = xsd:dateTime) 51 | FILTER((MONTH(?date_of_birth)) = ${month}) 52 | FILTER((DAY(?date_of_birth)) = ${day}) 53 | FILTER (bound(?place_of_birth)) 54 | SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } 55 | OPTIONAL { ?human wdt:P569 ?date_of_birth. } 56 | OPTIONAL { ?human wdt:P19 ?place_of_birth. } 57 | } 58 | LIMIT ${limit}`; 59 | const url = endpoint + '?query=' + encodeURIComponent(sparqlQuery); 60 | console.log(url); // in case you want to try the query in a web browser 61 | 62 | var config = { 63 | timeout: 6500, // timeout api call before we reach Alexa's 8 sec timeout, or set globally via axios.defaults.timeout 64 | headers: {'Accept': 'application/sparql-results+json'} 65 | }; 66 | 67 | async function getJsonResponse(url, config){ 68 | const res = await axios.get(url, config); 69 | return res.data; 70 | } 71 | 72 | return getJsonResponse(url, config).then((result) => { 73 | return result; 74 | }).catch((error) => { 75 | return null; 76 | }); 77 | }, 78 | convertBirthdaysResponse(handlerInput, response, withAge, timezone){ 79 | let speechResponse = ''; 80 | // if the API call failed we just don't append today's birthdays to the response 81 | if (!response || !response.results || !response.results.bindings || !Object.keys(response.results.bindings).length > 0) 82 | return speechResponse; 83 | const results = response.results.bindings; 84 | speechResponse += handlerInput.t('ALSO_TODAY_MSG'); 85 | results.forEach((person, index) => { 86 | console.log(person); 87 | speechResponse += person.humanLabel.value; 88 | if (withAge && timezone && person.date_of_birth.value) { 89 | const age = module.exports.convertBirthdateToYearsOld(person, timezone); 90 | speechResponse += handlerInput.t('TURNING_YO_MSG', {count: age}); 91 | person.date_of_birth.value = handlerInput.t('LIST_YO_ABBREV_MSG', {count: age}); 92 | } 93 | if (index === Object.keys(results).length - 2) 94 | speechResponse += handlerInput.t('CONJUNCTION_MSG'); 95 | else 96 | speechResponse += '. '; 97 | }); 98 | 99 | return speechResponse; 100 | }, 101 | convertBirthdateToYearsOld(person, timezone) { 102 | const today = moment().tz(timezone).startOf('day'); 103 | const wasBorn = moment(person.date_of_birth.value).tz(timezone).startOf('day'); 104 | return today.diff(wasBorn, 'years'); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /09/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happy-birthday", 3 | "version": "0.9.0", 4 | "description": "happy birthday skill", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon Alexa", 10 | "dependencies": { 11 | "ask-sdk-core": "^2.7.0", 12 | "ask-sdk-model": "^1.18.0", 13 | "i18next": "^15.0.5", 14 | "ask-sdk-s3-persistence-adapter": "^2.7.0", 15 | "ask-sdk-dynamodb-persistence-adapter": "^2.7.0", 16 | "moment-timezone": "^0.5.23", 17 | "axios": ">=0.21.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /09/lambda/util.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | 3 | const s3SigV4Client = new AWS.S3({ 4 | signatureVersion: 'v4' 5 | }); 6 | 7 | module.exports = { 8 | getS3PreSignedUrl(s3ObjectKey) { 9 | const bucketName = process.env.S3_PERSISTENCE_BUCKET; 10 | const s3PreSignedUrl = s3SigV4Client.getSignedUrl('getObject', { 11 | Bucket: bucketName, 12 | Key: s3ObjectKey, 13 | Expires: 60*1 // the Expires is capped for 1 minute 14 | }); 15 | console.log(`Util.s3PreSignedUrl: ${s3ObjectKey} URL ${s3PreSignedUrl}`); 16 | return s3PreSignedUrl; 17 | }, 18 | getPersistenceAdapter(tableName) { 19 | // This function is an indirect way to detect if this is part of an Alexa-Hosted skill 20 | function isAlexaHosted() { 21 | return process.env.S3_PERSISTENCE_BUCKET; 22 | } 23 | if (isAlexaHosted()) { 24 | const {S3PersistenceAdapter} = require('ask-sdk-s3-persistence-adapter'); 25 | return new S3PersistenceAdapter({ 26 | bucketName: process.env.S3_PERSISTENCE_BUCKET 27 | }); 28 | } else { 29 | // IMPORTANT: don't forget to give DynamoDB access to the role you're using to run this lambda (via IAM policy) 30 | const {DynamoDbPersistenceAdapter} = require('ask-sdk-dynamodb-persistence-adapter'); 31 | return new DynamoDbPersistenceAdapter({ 32 | tableName: tableName || 'happy_birthday', 33 | createTable: true 34 | }); 35 | } 36 | }, 37 | createReminder(requestMoment, scheduledMoment, timezone, locale, message) { 38 | return { 39 | requestTime: requestMoment.format('YYYY-MM-DDTHH:mm:00.000'), 40 | trigger: { 41 | type: 'SCHEDULED_ABSOLUTE', 42 | scheduledTime: scheduledMoment.format('YYYY-MM-DDTHH:mm:00.000'), 43 | timeZoneId: timezone 44 | }, 45 | alertInfo: { 46 | spokenInfo: { 47 | content: [{ 48 | locale: locale, 49 | text: message 50 | }] 51 | } 52 | }, 53 | pushNotification: { 54 | status: 'ENABLED' 55 | } 56 | } 57 | }, 58 | callDirectiveService(handlerInput, msg) { 59 | // Call Alexa Directive Service. 60 | const {requestEnvelope} = handlerInput; 61 | const directiveServiceClient = handlerInput.serviceClientFactory.getDirectiveServiceClient(); 62 | const requestId = requestEnvelope.request.requestId; 63 | const {apiEndpoint, apiAccessToken} = requestEnvelope.context.System; 64 | // build the progressive response directive 65 | const directive = { 66 | header: { 67 | requestId 68 | }, 69 | directive:{ 70 | type: 'VoicePlayer.Speak', 71 | speech: msg 72 | } 73 | }; 74 | // send directive 75 | return directiveServiceClient.enqueue(directive, apiEndpoint, apiAccessToken); 76 | }, 77 | supportsAPL(handlerInput) { 78 | const {supportedInterfaces} = handlerInput.requestEnvelope.context.System.device; 79 | return !!supportedInterfaces['Alexa.Presentation.APL']; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /09/skill-package/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "es-ES": { 6 | "name": "Feliz Cumpleaños" 7 | }, 8 | "es-MX": { 9 | "name": "Feliz Cumpleaños" 10 | }, 11 | "es-US": { 12 | "name": "Feliz Cumpleaños" 13 | }, 14 | "en-US": { 15 | "name": "Happy Birthday" 16 | }, 17 | "en-AU": { 18 | "name": "Happy Birthday" 19 | }, 20 | "en-CA": { 21 | "name": "Happy Birthday" 22 | }, 23 | "en-GB": { 24 | "name": "Happy Birthday" 25 | }, 26 | "en-IN": { 27 | "name": "Happy Birthday" 28 | }, 29 | "fr-FR": { 30 | "name": "Joyeux Anniversaire" 31 | }, 32 | "fr-CA": { 33 | "name": "Bonne Fête" 34 | }, 35 | "it-IT": { 36 | "name": "Buon Compleanno" 37 | } 38 | } 39 | }, 40 | "apis": { 41 | "custom": { 42 | "interfaces": [ 43 | { 44 | "type": "ALEXA_PRESENTATION_APL" 45 | } 46 | ] 47 | } 48 | }, 49 | "manifestVersion": "1.0", 50 | "permissions": [ 51 | { 52 | "name": "alexa::profile:given_name:read" 53 | }, 54 | { 55 | "name": "alexa::alerts:reminders:skill:readwrite" 56 | } 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /10/README.md: -------------------------------------------------------------------------------- 1 | # Part 10 - ASK CLI 2 | 3 | # # Milestones 4 | 5 | 1. *CLI*: Install ASL CLI via NPM 6 | 2. *CLI*: Do an "ask init" 7 | 3. *CLI*: Try "ask clone" 8 | 4. *CLI*: Explain dev cycle in AHS -> git commit & ask deploy (with VS Code plugin) 9 | 10 | ## Concepts 11 | 12 | 1. Configuring the ASK-CLI with personal AWS account support 13 | 2. Cloning an Alexa Hosted Skill 14 | 3. Editing a skill in code and deploying the skill 15 | 16 | ## Diff 17 | 18 | 1. **media/**: moved media files directory to top level. Make sure yo upload all media files 19 | 2. **.ask/config**: this new file now includes deployment data 20 | 3. **skill.json**: this file now includes deployment data (+uri) 21 | 22 | ## Videos 23 | 24 | [EN](https://alexa.design/zerotohero10)/[DE](https://alexa.design/de_zerotohero10)/[FR](https://alexa.design/fr_zerotohero10)/[IT](https://alexa.design/it_zerotohero10)/[ES](../README_ES.md) 25 | (EN version includes the following subtitles: EN, DE, PT) 26 | 27 | -------------------------------------------------------------------------------- /10/ask-resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "askcliResourcesVersion": "2020-03-31", 3 | "profiles": { 4 | "default": { 5 | "skillMetadata": { 6 | "src": "./skill-package" 7 | }, 8 | "code": { 9 | "default": { 10 | "src": "./lambda" 11 | } 12 | }, 13 | "skillInfrastructure": { 14 | "userConfig": { 15 | "runtime": "nodejs10.x", 16 | "handler": "index.handler", 17 | "awsRegion": "us-east-1" 18 | }, 19 | "type": "@ask-cli/lambda-deployer" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /10/lambda/custom/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // we now specify which attributes are saved (see the save interceptor below) 3 | PERSISTENT_ATTRIBUTES_NAMES: ['day', 'month', 'monthName', 'year', 'sessionCounter', 'reminderId'], 4 | // these are the permissions needed to fetch the first name 5 | GIVEN_NAME_PERMISSION: ['alexa::profile:given_name:read'], 6 | // these are the permissions needed to send reminders 7 | REMINDERS_PERMISSION: ['alexa::alerts:reminders:skill:readwrite'], 8 | // max number of entries to fetch from the external API 9 | MAX_BIRTHDAYS: 5, 10 | // APL documents 11 | APL: { 12 | launchDoc: require('./documents/launchScreen.json'), 13 | listDoc: require('./documents/listScreen.json') 14 | }, 15 | // only necessary if you didn't deploy this project as an Alexa Hosted Skill 16 | BASE_S3_URL: 'Replace this with your S3 base url with images (skip the Media directory here, end it in .com)' 17 | } 18 | -------------------------------------------------------------------------------- /10/lambda/custom/documents/launchSampleDatasource.json: -------------------------------------------------------------------------------- 1 | { 2 | "launchData": { 3 | "properties": { 4 | "headerTitle": "Happy Birthday", 5 | "mainText": "When is your birthday?", 6 | "hintString": "Alexa, register my birthday", 7 | "logoImage": "https://i.imgur.com/ZJJtcRT.png", 8 | "backgroundImage": "https://i.imgur.com/Y21B9Y5.jpg", 9 | "backgroundOpacity": "0.5" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /10/lambda/custom/documents/launchSampleDatasource_fr_CA.json: -------------------------------------------------------------------------------- 1 | { 2 | "launchData": { 3 | "properties": { 4 | "headerTitle": "Bonne Fête", 5 | "mainText": "Vous ne n'avez pas encore dit votre date de naissance", 6 | "hintString": "quand est ma fête ?", 7 | "logoImage": "https://i.imgur.com/ZJJtcRT.png", 8 | "backgroundImage": "https://i.imgur.com/Y21B9Y5.jpg", 9 | "backgroundOpacity": "0.5" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /10/lambda/custom/documents/launchSampleDatasource_fr_FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "launchData": { 3 | "properties": { 4 | "headerTitle": "Joyeux Anniversaire", 5 | "mainText": "Vous ne n'avez pas encore dit votre date de naissance", 6 | "hintString": "quand est mon anniversaire ?", 7 | "logoImage": "https://i.imgur.com/ZJJtcRT.png", 8 | "backgroundImage": "https://i.imgur.com/Y21B9Y5.jpg", 9 | "backgroundOpacity": "0.5" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /10/lambda/custom/documents/launchSampleDatasource_it.json: -------------------------------------------------------------------------------- 1 | { 2 | "launchData": { 3 | "properties": { 4 | "headerTitle": "Buon Compleanno", 5 | "mainText": "Quando è il tuo compleanno?", 6 | "hintString": "Alexa, chi compie gli anni oggi?", 7 | "logoImage": "https://i.imgur.com/ZJJtcRT.png", 8 | "backgroundImage": "https://i.imgur.com/Y21B9Y5.jpg", 9 | "backgroundOpacity": "0.5" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /10/lambda/custom/documents/launchScreen.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "APL", 3 | "version": "1.1", 4 | "theme": "dark", 5 | "import": [ 6 | { 7 | "name": "alexa-viewport-profiles", 8 | "version": "1.0.0" 9 | }, 10 | { 11 | "name": "alexa-layouts", 12 | "version": "1.0.0" 13 | }, 14 | { 15 | "name": "alexa-styles", 16 | "version": "1.0.0" 17 | } 18 | ], 19 | "layouts": { 20 | "LaunchScreen": { 21 | "description": "A basic launch screen with a text and logo", 22 | "parameters": [ 23 | { 24 | "name": "mainText", 25 | "type": "string" 26 | }, 27 | { 28 | "name": "logo", 29 | "type": "string" 30 | } 31 | ], 32 | "items": [ 33 | { 34 | "type": "Container", 35 | "width": "100%", 36 | "height": "100%", 37 | "justifyContent": "center", 38 | "alignItems": "center", 39 | "item": [ 40 | { 41 | "type": "Image", 42 | "source": "${logo}", 43 | "width": "20vw", 44 | "height": "20vw", 45 | "scale": "best-fill" 46 | }, 47 | { 48 | "type": "Text", 49 | "text": "${mainText}", 50 | "style": "textStyleDisplay5", 51 | "textAlign": "center", 52 | "paddingTop": "30dp", 53 | "color": "white" 54 | } 55 | ] 56 | } 57 | ] 58 | } 59 | }, 60 | "mainTemplate": { 61 | "parameters": [ 62 | "payload" 63 | ], 64 | "items": [ 65 | { 66 | "type": "Container", 67 | "direction": "column", 68 | "items": [ 69 | { 70 | "type": "Image", 71 | "source": "${payload.launchData.properties.backgroundImage}", 72 | "scale": "best-fill", 73 | "width": "100vw", 74 | "height": "100vh", 75 | "opacity": "${payload.launchData.properties.backgroundOpacity}" 76 | }, 77 | { 78 | "type": "Container", 79 | "position": "absolute", 80 | "width": "100vw", 81 | "height": "100vh", 82 | "direction": "column", 83 | "items": [ 84 | { 85 | "headerTitle": "${payload.launchData.properties.headerTitle}", 86 | "type": "AlexaHeader" 87 | }, 88 | { 89 | "when": "${@viewportProfile == @hubRoundSmall}", 90 | "type": "Container", 91 | "width": "100vw", 92 | "height": "60vh", 93 | "position": "relative", 94 | "alignItems": "center", 95 | "justifyContent": "center", 96 | "direction": "column", 97 | "items": [ 98 | { 99 | "type": "LaunchScreen", 100 | "mainText": "${payload.launchData.properties.mainText}", 101 | "logo": "${payload.launchData.properties.logoImage}" 102 | } 103 | ] 104 | }, 105 | { 106 | "when": "${@viewportProfile == @hubLandscapeSmall || @viewportProfile == @hubLandscapeMedium || @viewportProfile == @hubLandscapeLarge || @viewportProfile == @tvLandscapeXLarge}", 107 | "type": "Container", 108 | "width": "100vw", 109 | "height": "70vh", 110 | "direction": "column", 111 | "alignItems": "center", 112 | "justifyContent": "center", 113 | "items": [ 114 | { 115 | "type": "LaunchScreen", 116 | "mainText": "${payload.launchData.properties.mainText}", 117 | "logo": "${payload.launchData.properties.logoImage}" 118 | } 119 | ] 120 | }, 121 | { 122 | "footerHint": "${payload.launchData.properties.hintString}", 123 | "type": "AlexaFooter", 124 | "when": "${@viewportProfile == @hubLandscapeSmall || @viewportProfile == @hubLandscapeMedium || @viewportProfile == @hubLandscapeLarge || @viewportProfile == @tvLandscapeXLarge}" 125 | } 126 | ] 127 | } 128 | ] 129 | } 130 | ] 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /10/lambda/custom/documents/sampleBirthdaysResponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "head": { 3 | "vars": [ 4 | "human", 5 | "humanLabel", 6 | "picture", 7 | "date_of_birth", 8 | "place_of_birthLabel" 9 | ] 10 | }, 11 | "results": { 12 | "bindings": [ 13 | { 14 | "human": { 15 | "type": "uri", 16 | "value": "http://www.wikidata.org/entity/Q17319618" 17 | }, 18 | "picture": { 19 | "type": "uri", 20 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/%D0%9D%D0%B8%D0%BA%D0%B8%D1%82%D0%B0%20%D0%9F%D1%80%D0%B5%D1%81%D0%BD%D1%8F%D0%BA%D0%BE%D0%B2%20MULTIVERSE%20.jpg" 21 | }, 22 | "date_of_birth": { 23 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 24 | "type": "literal", 25 | "value": "1991-05-21T00:00:00Z" 26 | }, 27 | "humanLabel": { 28 | "xml:lang": "en", 29 | "type": "literal", 30 | "value": "Nikita Presnyakov" 31 | }, 32 | "place_of_birthLabel": { 33 | "xml:lang": "en", 34 | "type": "literal", 35 | "value": "London" 36 | } 37 | }, 38 | { 39 | "human": { 40 | "type": "uri", 41 | "value": "http://www.wikidata.org/entity/Q1896173" 42 | }, 43 | "picture": { 44 | "type": "uri", 45 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/Maria%20Minzenti%20-%20Nov%201922%20Shadowland.jpg" 46 | }, 47 | "date_of_birth": { 48 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 49 | "type": "literal", 50 | "value": "1898-05-21T00:00:00Z" 51 | }, 52 | "humanLabel": { 53 | "xml:lang": "en", 54 | "type": "literal", 55 | "value": "Maria Mindzenti" 56 | }, 57 | "place_of_birthLabel": { 58 | "xml:lang": "en", 59 | "type": "literal", 60 | "value": "Vienna" 61 | } 62 | }, 63 | { 64 | "human": { 65 | "type": "uri", 66 | "value": "http://www.wikidata.org/entity/Q1646159" 67 | }, 68 | "picture": { 69 | "type": "uri", 70 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/Lola%20Lane%20in%20Four%20Daughters%20trailer.jpg" 71 | }, 72 | "date_of_birth": { 73 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 74 | "type": "literal", 75 | "value": "1906-05-21T00:00:00Z" 76 | }, 77 | "humanLabel": { 78 | "xml:lang": "en", 79 | "type": "literal", 80 | "value": "Lola Lane" 81 | }, 82 | "place_of_birthLabel": { 83 | "xml:lang": "en", 84 | "type": "literal", 85 | "value": "Indiana" 86 | } 87 | }, 88 | { 89 | "human": { 90 | "type": "uri", 91 | "value": "http://www.wikidata.org/entity/Q6048832" 92 | }, 93 | "picture": { 94 | "type": "uri", 95 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/Ayse%20kokcu.jpg" 96 | }, 97 | "date_of_birth": { 98 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 99 | "type": "literal", 100 | "value": "1955-05-21T00:00:00Z" 101 | }, 102 | "humanLabel": { 103 | "xml:lang": "en", 104 | "type": "literal", 105 | "value": "Ayse Kökçü" 106 | }, 107 | "place_of_birthLabel": { 108 | "xml:lang": "en", 109 | "type": "literal", 110 | "value": "Istanbul" 111 | } 112 | }, 113 | { 114 | "human": { 115 | "type": "uri", 116 | "value": "http://www.wikidata.org/entity/Q589048" 117 | }, 118 | "picture": { 119 | "type": "uri", 120 | "value": "http://commons.wikimedia.org/wiki/Special:FilePath/Fabriano-Civita02.jpg" 121 | }, 122 | "date_of_birth": { 123 | "datatype": "http://www.w3.org/2001/XMLSchema#dateTime", 124 | "type": "literal", 125 | "value": "1744-05-21T00:00:00Z" 126 | }, 127 | "humanLabel": { 128 | "xml:lang": "en", 129 | "type": "literal", 130 | "value": "Gaspare Pacchierotti" 131 | }, 132 | "place_of_birthLabel": { 133 | "xml:lang": "en", 134 | "type": "literal", 135 | "value": "Fabriano" 136 | } 137 | } 138 | ] 139 | } 140 | } -------------------------------------------------------------------------------- /10/lambda/custom/index.js: -------------------------------------------------------------------------------- 1 | const Alexa = require('ask-sdk-core'); 2 | const util = require('./util'); 3 | const interceptors = require('./interceptors'); 4 | const handlers = require('./handlers'); 5 | /** 6 | * This handler acts as the entry point for your skill, routing all request and response 7 | * payloads to the handlers above. Make sure any new handlers or interceptors you've 8 | * defined are included below. The order matters - they're processed top to bottom 9 | * */ 10 | exports.handler = Alexa.SkillBuilders.custom() 11 | .addRequestHandlers( 12 | handlers.LaunchRequestHandler, 13 | handlers.RegisterBirthdayIntentHandler, 14 | handlers.SayBirthdayIntentHandler, 15 | handlers.RemindBirthdayIntentHandler, 16 | handlers.CelebrityBirthdaysIntentHandler, 17 | handlers.TouchIntentHandler, 18 | handlers.HelpIntentHandler, 19 | handlers.CancelAndStopIntentHandler, 20 | handlers.FallbackIntentHandler, 21 | handlers.SessionEndedRequestHandler, 22 | handlers.IntentReflectorHandler) 23 | .addErrorHandlers( 24 | handlers.ErrorHandler) 25 | .addRequestInterceptors( 26 | interceptors.LoadAttributesRequestInterceptor, 27 | interceptors.LocalisationRequestInterceptor, 28 | interceptors.LoggingRequestInterceptor, 29 | interceptors.LoadNameRequestInterceptor, 30 | interceptors.LoadTimezoneRequestInterceptor) 31 | .addResponseInterceptors( 32 | interceptors.LoggingResponseInterceptor, 33 | interceptors.SaveAttributesResponseInterceptor) 34 | .withPersistenceAdapter(util.getPersistenceAdapter()) 35 | .withApiClient(new Alexa.DefaultApiClient()) 36 | .withCustomUserAgent('sample/happy-birthday/mod10') 37 | .lambda(); 38 | -------------------------------------------------------------------------------- /10/lambda/custom/logic.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment-timezone'); // will help us do all the dates math while considering the timezone 2 | const util = require('./util'); 3 | const axios = require('axios'); 4 | 5 | module.exports = { 6 | getBirthdayData(day, month, year, timezone) { 7 | const today = moment().tz(timezone).startOf('day'); 8 | const wasBorn = moment(`${month}/${day}/${year}`, "MM/DD/YYYY").tz(timezone).startOf('day'); 9 | const nextBirthday = moment(`${month}/${day}/${today.year()}`, "MM/DD/YYYY").tz(timezone).startOf('day'); 10 | if (today.isAfter(nextBirthday)) { 11 | nextBirthday.add(1, 'years'); 12 | } 13 | const age = today.diff(wasBorn, 'years'); 14 | const daysAlive = today.diff(wasBorn, 'days'); 15 | const daysUntilBirthday = nextBirthday.startOf('day').diff(today, 'days'); // same day returns 0 16 | 17 | return { 18 | daysAlive: daysAlive, // not used but nice to have :) 19 | daysUntilBirthday: daysUntilBirthday, 20 | age: age //in years 21 | } 22 | }, 23 | createBirthdayReminder(daysUntilBirthday, timezone, locale, message) { 24 | moment.locale(locale); 25 | const createdMoment = moment().tz(timezone); 26 | let triggerMoment = createdMoment.startOf('day').add(daysUntilBirthday, 'days'); 27 | if (daysUntilBirthday === 0) { 28 | triggerMoment = createdMoment.startOf('day').add(1, 'years'); // reminder created on the day of birthday will trigger next year 29 | } 30 | console.log('Reminder schedule: ' + triggerMoment.format('YYYY-MM-DDTHH:mm:00.000')); 31 | 32 | return util.createReminder(createdMoment, triggerMoment, timezone, locale, message); 33 | }, 34 | getAdjustedDate(timezone) { 35 | const today = moment().tz(timezone).startOf('day'); 36 | 37 | return { 38 | day: today.date(), 39 | month: today.month() + 1 40 | } 41 | }, 42 | fetchBirthdays(day, month, limit){ 43 | const endpoint = 'https://query.wikidata.org/sparql'; 44 | // List of actors with pictures and date of birth for a given day and month 45 | const sparqlQuery = 46 | `SELECT DISTINCT ?human ?humanLabel ?picture ?date_of_birth ?place_of_birthLabel WHERE { 47 | ?human wdt:P31 wd:Q5; 48 | wdt:P106 wd:Q33999; 49 | wdt:P18 ?picture. 50 | FILTER((DATATYPE(?date_of_birth)) = xsd:dateTime) 51 | FILTER((MONTH(?date_of_birth)) = ${month}) 52 | FILTER((DAY(?date_of_birth)) = ${day}) 53 | FILTER (bound(?place_of_birth)) 54 | SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } 55 | OPTIONAL { ?human wdt:P569 ?date_of_birth. } 56 | OPTIONAL { ?human wdt:P19 ?place_of_birth. } 57 | } 58 | LIMIT ${limit}`; 59 | const url = endpoint + '?query=' + encodeURIComponent(sparqlQuery); 60 | console.log(url); // in case you want to try the query in a web browser 61 | 62 | var config = { 63 | timeout: 6500, // timeout api call before we reach Alexa's 8 sec timeout, or set globally via axios.defaults.timeout 64 | headers: {'Accept': 'application/sparql-results+json'} 65 | }; 66 | 67 | async function getJsonResponse(url, config){ 68 | const res = await axios.get(url, config); 69 | return res.data; 70 | } 71 | 72 | return getJsonResponse(url, config).then((result) => { 73 | return result; 74 | }).catch((error) => { 75 | return null; 76 | }); 77 | }, 78 | convertBirthdaysResponse(handlerInput, response, withAge, timezone){ 79 | let speechResponse = ''; 80 | // if the API call failed we just don't append today's birthdays to the response 81 | if (!response || !response.results || !response.results.bindings || !Object.keys(response.results.bindings).length > 0) 82 | return speechResponse; 83 | const results = response.results.bindings; 84 | speechResponse += handlerInput.t('ALSO_TODAY_MSG'); 85 | results.forEach((person, index) => { 86 | console.log(person); 87 | speechResponse += person.humanLabel.value; 88 | if (withAge && timezone && person.date_of_birth.value) { 89 | const age = module.exports.convertBirthdateToYearsOld(person, timezone); 90 | speechResponse += handlerInput.t('TURNING_YO_MSG', {count: age}); 91 | person.date_of_birth.value = handlerInput.t('LIST_YO_ABBREV_MSG', {count: age}); 92 | } 93 | if (index === Object.keys(results).length - 2) 94 | speechResponse += handlerInput.t('CONJUNCTION_MSG'); 95 | else 96 | speechResponse += '. '; 97 | }); 98 | 99 | return speechResponse; 100 | }, 101 | convertBirthdateToYearsOld(person, timezone) { 102 | const today = moment().tz(timezone).startOf('day'); 103 | const wasBorn = moment(person.date_of_birth.value).tz(timezone).startOf('day'); 104 | return today.diff(wasBorn, 'years'); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /10/lambda/custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "happy-birthday", 3 | "version": "1.0.0", 4 | "description": "happy birthday skill", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Amazon Alexa", 10 | "dependencies": { 11 | "ask-sdk-core": "^2.7.0", 12 | "ask-sdk-model": "^1.18.0", 13 | "i18next": "^15.0.5", 14 | "ask-sdk-s3-persistence-adapter": "^2.7.0", 15 | "ask-sdk-dynamodb-persistence-adapter": "^2.7.0", 16 | "moment-timezone": "^0.5.23", 17 | "axios": ">=0.21.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /10/lambda/custom/util.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const constants = require('./constants'); // constants such as specific service permissions go here 3 | 4 | const s3SigV4Client = new AWS.S3({ 5 | signatureVersion: 'v4' 6 | }); 7 | 8 | module.exports = { 9 | getS3PreSignedUrl(s3ObjectKey) { 10 | const bucketName = process.env.S3_PERSISTENCE_BUCKET; 11 | if(!bucketName){ // Not an Alexa Hosted Skill 12 | // Make sure you have uploaded the project's image 13 | // to a public S3 bucket and inside a Media directory 14 | // Also make sure to provide the S3 base url in constants.js 15 | const fixedUrl = constants.BASE_S3_URL + '/' + s3ObjectKey; 16 | console.log(`util.s3PreSignedUrl: Fixed_Url URL ${fixedUrl}`); 17 | return fixedUrl; 18 | } 19 | const s3PreSignedUrl = s3SigV4Client.getSignedUrl('getObject', { 20 | Bucket: bucketName, 21 | Key: s3ObjectKey, 22 | Expires: 60*1 // the Expires is capped for 1 minute (no effect changing this) 23 | }); 24 | console.log(`util.s3PreSignedUrl: ${s3ObjectKey} URL ${s3PreSignedUrl}`); 25 | return s3PreSignedUrl; 26 | }, 27 | getPersistenceAdapter(tableName) { 28 | // This function is an indirect way to detect if this is part of an Alexa-Hosted skill 29 | function isAlexaHosted() { 30 | return process.env.S3_PERSISTENCE_BUCKET; 31 | } 32 | if (isAlexaHosted()) { 33 | const {S3PersistenceAdapter} = require('ask-sdk-s3-persistence-adapter'); 34 | return new S3PersistenceAdapter({ 35 | bucketName: process.env.S3_PERSISTENCE_BUCKET 36 | }); 37 | } else { 38 | // IMPORTANT: don't forget to give DynamoDB access to the role you're using to run this lambda (via IAM policy) 39 | const {DynamoDbPersistenceAdapter} = require('ask-sdk-dynamodb-persistence-adapter'); 40 | return new DynamoDbPersistenceAdapter({ 41 | tableName: tableName || 'happy_birthday', 42 | createTable: true 43 | }); 44 | } 45 | }, 46 | createReminder(requestMoment, scheduledMoment, timezone, locale, message) { 47 | return { 48 | requestTime: requestMoment.format('YYYY-MM-DDTHH:mm:00.000'), 49 | trigger: { 50 | type: 'SCHEDULED_ABSOLUTE', 51 | scheduledTime: scheduledMoment.format('YYYY-MM-DDTHH:mm:00.000'), 52 | timeZoneId: timezone 53 | }, 54 | alertInfo: { 55 | spokenInfo: { 56 | content: [{ 57 | locale: locale, 58 | text: message 59 | }] 60 | } 61 | }, 62 | pushNotification: { 63 | status: 'ENABLED' 64 | } 65 | } 66 | }, 67 | callDirectiveService(handlerInput, msg) { 68 | // Call Alexa Directive Service. 69 | const {requestEnvelope} = handlerInput; 70 | const directiveServiceClient = handlerInput.serviceClientFactory.getDirectiveServiceClient(); 71 | const requestId = requestEnvelope.request.requestId; 72 | const {apiEndpoint, apiAccessToken} = requestEnvelope.context.System; 73 | // build the progressive response directive 74 | const directive = { 75 | header: { 76 | requestId 77 | }, 78 | directive:{ 79 | type: 'VoicePlayer.Speak', 80 | speech: msg 81 | } 82 | }; 83 | // send directive 84 | return directiveServiceClient.enqueue(directive, apiEndpoint, apiAccessToken); 85 | }, 86 | supportsAPL(handlerInput) { 87 | const {supportedInterfaces} = handlerInput.requestEnvelope.context.System.device; 88 | return !!supportedInterfaces['Alexa.Presentation.APL']; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /10/media/LICENSE.txt: -------------------------------------------------------------------------------- 1 | https://unsplash.com/license 2 | 3 | All photos published on Unsplash can be used for free. You can use them for commercial and noncommercial purposes. You do not need to ask permission from or provide credit to the photographer or Unsplash, although it is appreciated when possible. 4 | 5 | More precisely, Unsplash grants you an irrevocable, nonexclusive, worldwide copyright license to download, copy, modify, distribute, perform, and use photos from Unsplash for free, including for commercial purposes, without permission from or attributing the photographer or Unsplash. This license does not include the right to compile photos from Unsplash to replicate a similar or competing service. -------------------------------------------------------------------------------- /10/media/cake_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/cake_1024x600.png -------------------------------------------------------------------------------- /10/media/cake_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/cake_1280x800.png -------------------------------------------------------------------------------- /10/media/cake_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/cake_1920x1080.png -------------------------------------------------------------------------------- /10/media/cake_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/cake_480x480.png -------------------------------------------------------------------------------- /10/media/cake_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/cake_960x480.png -------------------------------------------------------------------------------- /10/media/confetti_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/confetti_1024x600.png -------------------------------------------------------------------------------- /10/media/confetti_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/confetti_1280x800.png -------------------------------------------------------------------------------- /10/media/confetti_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/confetti_1920x1080.png -------------------------------------------------------------------------------- /10/media/confetti_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/confetti_480x480.png -------------------------------------------------------------------------------- /10/media/confetti_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/confetti_960x480.png -------------------------------------------------------------------------------- /10/media/full_icon_108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/full_icon_108.png -------------------------------------------------------------------------------- /10/media/full_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/full_icon_512.png -------------------------------------------------------------------------------- /10/media/garlands_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/garlands_1024x600.png -------------------------------------------------------------------------------- /10/media/garlands_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/garlands_1280x800.png -------------------------------------------------------------------------------- /10/media/garlands_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/garlands_1920x1080.png -------------------------------------------------------------------------------- /10/media/garlands_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/garlands_480x480.png -------------------------------------------------------------------------------- /10/media/garlands_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/garlands_960x480.png -------------------------------------------------------------------------------- /10/media/happy_birthday.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/happy_birthday.mp3 -------------------------------------------------------------------------------- /10/media/icon_108.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/icon_108.png -------------------------------------------------------------------------------- /10/media/icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/icon_512.png -------------------------------------------------------------------------------- /10/media/lights_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/lights_1024x600.png -------------------------------------------------------------------------------- /10/media/lights_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/lights_1280x800.png -------------------------------------------------------------------------------- /10/media/lights_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/lights_1920x1080.png -------------------------------------------------------------------------------- /10/media/lights_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/lights_480x480.png -------------------------------------------------------------------------------- /10/media/lights_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/lights_960x480.png -------------------------------------------------------------------------------- /10/media/papers_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/papers_1024x600.png -------------------------------------------------------------------------------- /10/media/papers_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/papers_1280x800.png -------------------------------------------------------------------------------- /10/media/papers_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/papers_1920x1080.png -------------------------------------------------------------------------------- /10/media/papers_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/papers_480x480.png -------------------------------------------------------------------------------- /10/media/papers_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/papers_960x480.png -------------------------------------------------------------------------------- /10/media/straws_1024x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/straws_1024x600.png -------------------------------------------------------------------------------- /10/media/straws_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/straws_1280x800.png -------------------------------------------------------------------------------- /10/media/straws_1920x1080.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/straws_1920x1080.png -------------------------------------------------------------------------------- /10/media/straws_480x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/straws_480x480.png -------------------------------------------------------------------------------- /10/media/straws_960x480.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexa-samples/skill-sample-nodejs-zero-to-hero/d1882204e7da5269cadcd039a05c5fe119ca9e4c/10/media/straws_960x480.png -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/germanviscuso/ASKVideoSeries/issues), or [recently closed](https://github.com/germanviscuso/ASKVideoSeries/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/germanviscuso/ASKVideoSeries/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/germanviscuso/ASKVideoSeries/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Amazon Software License 1.0 2 | 3 | This Amazon Software License ("License") governs your use, reproduction, and 4 | distribution of the accompanying software as specified below. 5 | 6 | 1. Definitions 7 | 8 | "Licensor" means any person or entity that distributes its Work. 9 | 10 | "Software" means the original work of authorship made available under this 11 | License. 12 | 13 | "Work" means the Software and any additions to or derivative works of the 14 | Software that are made available under this License. 15 | 16 | The terms "reproduce," "reproduction," "derivative works," and 17 | "distribution" have the meaning as provided under U.S. copyright law; 18 | provided, however, that for the purposes of this License, derivative works 19 | shall not include works that remain separable from, or merely link (or bind 20 | by name) to the interfaces of, the Work. 21 | 22 | Works, including the Software, are "made available" under this License by 23 | including in or with the Work either (a) a copyright notice referencing the 24 | applicability of this License to the Work, or (b) a copy of this License. 25 | 26 | 2. License Grants 27 | 28 | 2.1 Copyright Grant. Subject to the terms and conditions of this License, 29 | each Licensor grants to you a perpetual, worldwide, non-exclusive, 30 | royalty-free, copyright license to reproduce, prepare derivative works of, 31 | publicly display, publicly perform, sublicense and distribute its Work and 32 | any resulting derivative works in any form. 33 | 34 | 2.2 Patent Grant. Subject to the terms and conditions of this License, each 35 | Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free 36 | patent license to make, have made, use, sell, offer for sale, import, and 37 | otherwise transfer its Work, in whole or in part. The foregoing license 38 | applies only to the patent claims licensable by Licensor that would be 39 | infringed by Licensor's Work (or portion thereof) individually and 40 | excluding any combinations with any other materials or technology. 41 | 42 | 3. Limitations 43 | 44 | 3.1 Redistribution. You may reproduce or distribute the Work only if 45 | (a) you do so under this License, (b) you include a complete copy of this 46 | License with your distribution, and (c) you retain without modification 47 | any copyright, patent, trademark, or attribution notices that are present 48 | in the Work. 49 | 50 | 3.2 Derivative Works. You may specify that additional or different terms 51 | apply to the use, reproduction, and distribution of your derivative works 52 | of the Work ("Your Terms") only if (a) Your Terms provide that the use 53 | limitation in Section 3.3 applies to your derivative works, and (b) you 54 | identify the specific derivative works that are subject to Your Terms. 55 | Notwithstanding Your Terms, this License (including the redistribution 56 | requirements in Section 3.1) will continue to apply to the Work itself. 57 | 58 | 3.3 Use Limitation. The Work and any derivative works thereof only may be 59 | used or intended for use with the web services, computing platforms or 60 | applications provided by Amazon.com, Inc. or its affiliates, including 61 | Amazon Web Services, Inc. 62 | 63 | 3.4 Patent Claims. If you bring or threaten to bring a patent claim against 64 | any Licensor (including any claim, cross-claim or counterclaim in a 65 | lawsuit) to enforce any patents that you allege are infringed by any Work, 66 | then your rights under this License from such Licensor (including the 67 | grants in Sections 2.1 and 2.2) will terminate immediately. 68 | 69 | 3.5 Trademarks. This License does not grant any rights to use any 70 | Licensor's or its affiliates' names, logos, or trademarks, except as 71 | necessary to reproduce the notices described in this License. 72 | 73 | 3.6 Termination. If you violate any term of this License, then your rights 74 | under this License (including the grants in Sections 2.1 and 2.2) will 75 | terminate immediately. 76 | 77 | 4. Disclaimer of Warranty. 78 | 79 | THE WORK IS PROVIDED "AS IS" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 80 | EITHER EXPRESS OR IMPLIED, INCLUDING WARRANTIES OR CONDITIONS OF 81 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR 82 | NON-INFRINGEMENT. YOU BEAR THE RISK OF UNDERTAKING ANY ACTIVITIES UNDER 83 | THIS LICENSE. SOME STATES' CONSUMER LAWS DO NOT ALLOW EXCLUSION OF AN 84 | IMPLIED WARRANTY, SO THIS DISCLAIMER MAY NOT APPLY TO YOU. 85 | 86 | 5. Limitation of Liability. 87 | 88 | EXCEPT AS PROHIBITED BY APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL 89 | THEORY, WHETHER IN TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE 90 | SHALL ANY LICENSOR BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, 91 | INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR 92 | RELATED TO THIS LICENSE, THE USE OR INABILITY TO USE THE WORK (INCLUDING 93 | BUT NOT LIMITED TO LOSS OF GOODWILL, BUSINESS INTERRUPTION, LOST PROFITS 94 | OR DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY OTHER COMM ERCIAL DAMAGES 95 | OR LOSSES), EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF 96 | SUCH DAMAGES. -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Skill Sample: Feliz Cumpleaños 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zero to Hero: A comprehensive course to building an Alexa Skill 2 | 3 | Support code for "Zero to Hero: A comprehensive course to building an Alexa Skill", an ASK training course in multiple languages. This is progressive code that starts from scratch and adds features on each module. The Learn more links below are suitable for learning (and teaching) how to code Alexa skills (see the README file on each module for video links and instructions on how to evolve the code departing from the previous module). 4 | 5 | ## Zero to Hero, Part 1: Alexa Skills Kit Overview 6 | 7 | 1. Development and Production Lambda Stages (AHS branches) 8 | 2. Lambda Dependencies (package.json, requires in code) 9 | 3. Handler as processor of incoming requests. Handler structure 10 | 4. Request Types (LaunchRequest, IntentRequest, SessionEndedRequest) 11 | 5. Skill Builder (custom vs standard) and its functions 12 | 6. Reflector (catch all intent handler) 13 | 7. Out-of-domain utterances (*) 14 | 15 | [Learn more](./01) 16 | 17 | ## Zero to Hero, Part 2: Skill Internationalization (i18n), Interceptors & Error Handling 18 | 19 | 1. Multiple models per locale 20 | 2. Key/value string resources for i18n 21 | 3. Enriching handlerInput with t function via interceptor 22 | 4. Attribute manager as key/value store 23 | 5. High level attribute types (session(short term), persistent(long term)) 24 | 6. Changing locale on Build tab and on Test tab (test both locales) 25 | 26 | [Learn more](./02) 27 | 28 | ## Zero to Hero, Part 3: Slots, Slot Validation & Automatic Dialog Delegation 29 | 30 | 1. Slots explanation 31 | 2. Built in and custom slot types 32 | 3. Synonyms (minimal, we're not using synonyms, eg. January -> first month) 33 | 4. Required Slots & Prompts 34 | 5. Slot Validation 35 | 6. Auto-Delegate, Dialog Delegation Strategy 36 | 7. Utterance Profiler 37 | 8. Intent Confirmation 38 | 9. Basic Intent Chaining 39 | 40 | [Learn more](./03) 41 | 42 | ## Zero to Hero, Part 4: Persistence 43 | 44 | 1. Session attributes 45 | 2. Persistent attributes 46 | 3. Persistence adapters (S3 and DynamoDB) / detect if lambda is Alexa hosted 47 | 4. Copy session attributes to and from persistent attributes via interceptors 48 | 5. Async/await 49 | 6. Session counter (to say eg. "welcome back") 50 | 51 | [Learn more](./04) 52 | 53 | ## Zero to Hero, Part 5: Accessing ASK APIs 54 | 55 | 1. Service API (User Profile API - given name) 56 | 2. Settings API (timezone) 57 | 3. SSML (speechcons and audio files) 58 | 4. Array capable localisation interceptor 59 | 5. String replacement with plurals support 60 | 61 | [Learn more](./05) 62 | 63 | ## Zero to Hero, Part 6: Reminders API 64 | 65 | 1. Reminders API 66 | 2. AMAZON.SearchQuery 67 | 3. Intent Confirmation (again) 68 | 69 | [Learn more](./06) 70 | 71 | ## Zero to Hero, Part 7: Accessing External APIs 72 | 73 | 1. Fetch external API (async/await) 74 | 2. Progressive Response 75 | 76 | [Learn more](./07) 77 | 78 | ## Zero to Hero, Part 8: Alexa Presentation Language (APL), Part 1 79 | 80 | 1. APL RenderDocument and APL Directive 81 | 2. APL Databinding and APL Authoring Tool 82 | 3. APL Styles, Layouts and ViewPorts 83 | 4. APL Transformers (Text to Hint) 84 | 5. Home Cards 85 | 6. Media storage in Alexa-hosted Skills 86 | 87 | [Learn more](./08) 88 | 89 | ## Zero to Hero, Part 9: Alexa Presentation Language (APL), Part 2 90 | 91 | 1. APL Authoring Tool 92 | 2. APL Layouts & Sequences 93 | 3. APL Transformers (Text to Hint) 94 | 4. APL Touch Wrapper 95 | 96 | [Learn more](./09) 97 | 98 | ## Zero to Hero, Part 10: The ASK Command Line Interface (CLI) 99 | 100 | 1. Configuring the ASK-CLI with personal AWS account support 101 | 2. Cloning an Alexa Hosted Skill 102 | 3. Editing a skill in code and deploying the skill 103 | 104 | [Learn more](./10) 105 | 106 | ## Zero to Hero, Part 11: Publishing 107 | 108 | 1. Distribution tab in developer console 109 | 2. Skill metadata for publication 110 | 3. Submitting a skill for certification 111 | 112 | [EN](https://alexa.design/zerotohero11)/[DE](https://alexa.design/de_zerotohero11)/[FR](https://alexa.design/fr_zerotohero11)/[IT](https://alexa.design/it_zerotohero11) 113 | (EN version includes the following subtitles: EN, DE, PT) 114 | 115 | ## License 116 | 117 | This library is licensed under the Amazon Software License 118 | -------------------------------------------------------------------------------- /README_ES.md: -------------------------------------------------------------------------------- 1 | # De Cero a Héroe: Construyendo Skills Alexa 2 | 3 | Este repositiorio contiene el código de soporte para el curso "De Cero a Héroe: Construyendo Skills Alexa" disponible [aquí](https://alexa.design/es_zerotohero) 4 | 5 | Como el código ha ido evolucionando y ha sido restructurado, a continuación proveemos los enlaces al estado del código fuente del momento justo en que se grabó cada video. 6 | 7 | ## De Cero a Héroe, Parte 1 8 | ### Introducción a ASK y Alexa-Hosted Skills 9 | 10 | [Código](https://alexa.design/ES_Z2H1) / [Video](https://alexa.design/es_zerotohero1) 11 | 12 | ## De Cero a Héroe, Parte 2 13 | ### Internacionalización de Skills, Interceptores y Manejo de Errores 14 | 15 | [Código](https://alexa.design/ES_Z2H2) / [Video](https://alexa.design/es_zerotohero2) 16 | 17 | ## De Cero a Héroe, Parte 3 18 | ### Manejo de Diálogos y Validación de Slots 19 | 20 | [Código](https://alexa.design/ES_Z2H3) / [Video](https://alexa.design/es_zerotohero3) 21 | 22 | ## De Cero a Héroe, Parte 4 23 | ### Persistencia 24 | 25 | [Código](https://alexa.design/ES_Z2H4) / [Video](https://alexa.design/es_zerotohero4) 26 | 27 | ## De Cero a Héroe, Parte 5 28 | ### Acceso a APIs de ASK 29 | 30 | [Código](https://alexa.design/ES_Z2H5) / [Video](https://alexa.design/es_zerotohero5) 31 | 32 | ## De Cero a Héroe, Parte 6 33 | ### API de Recordatorios 34 | 35 | [Código](https://alexa.design/ES_Z2H6) / [Video](https://alexa.design/es_zerotohero6) 36 | 37 | ## De Cero a Héroe, Parte 7 38 | ### Acceso a APIs Externas y Respuesta Progresiva 39 | 40 | [Código](https://alexa.design/ES_Z2H7) / [Video](https://alexa.design/es_zerotohero7) 41 | 42 | ## De Cero a Héroe, Parte 8 43 | ### Introducción a APL: Alexa Presentation Language 44 | 45 | [Código](https://alexa.design/ES_Z2H8) / [Video](https://alexa.design/es_zerotohero8) 46 | 47 | ## De Cero a Héroe, Parte 9 48 | ### Profundizando con APL: Alexa Presentation Language 49 | 50 | [Código](https://alexa.design/ES_Z2H9) / [Video](https://alexa.design/es_zerotohero9) 51 | 52 | ## De Cero a Héroe, Parte 10 53 | ### Desarrollo Rápido con el ASK-CLI 54 | 55 | [Código](https://alexa.design/ES_Z2H10) / [Video](https://alexa.design/es_zerotohero10) 56 | 57 | Enlaces a archivos descargables con el estado exacto en el video de cada proyecto también disponible [aquí](https://github.com/alexa/skill-sample-nodejs-zero-to-hero/tags) 58 | 59 | # Bonus 60 | 61 | Estos 4 módulos complementan el curso en español con algunos temas más avanzados. 62 | 63 | ## De Cero a Héroe, Parte 11 64 | ### Intent Chaining 65 | 66 | [Código](https://alexa.design/ES_Z2H11) / [Video](https://alexa.design/es_zerotohero11) 67 | 68 | ## De Cero a Héroe, Parte 12 69 | ### In-Skill Purchasing 70 | 71 | [Código](https://alexa.design/ES_Z2H12) / [Video](https://alexa.design/es_zerotohero12) 72 | 73 | ## De Cero a Héroe, Parte 13 74 | ### Proactive Events Notifications 75 | 76 | [Código](https://alexa.design/ES_Z2H13) / [Video](https://alexa.design/es_zerotohero13) 77 | 78 | ## De Cero a Héroe, Parte 14 79 | ### Dynamic Entities 80 | 81 | [Código](https://alexa.design/ES_Z2H14) / [Video](https://alexa.design/es_zerotohero14) 82 | 83 | # Licencia 84 | 85 | Este proyecto tiene licencia Amazon Software License 86 | --------------------------------------------------------------------------------