@@ -0,0 +1 @@ | |||
source.zip |
@@ -0,0 +1,21 @@ | |||
MIT License | |||
Copyright (c) 2017 Bin Hong Lee | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is | |||
furnished to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in all | |||
copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
SOFTWARE. |
@@ -0,0 +1,10 @@ | |||
{ | |||
"intents": [ | |||
{ | |||
"intent": "RequestIntent" | |||
}, | |||
{ | |||
"intent": "StopIntent" | |||
} | |||
] | |||
} |
@@ -0,0 +1,3 @@ | |||
RequestIntent give me random hero | |||
RequestIntent another hero | |||
StopIntent stop |
@@ -0,0 +1,49 @@ | |||
'use strict' | |||
const Alexa = require('alexa-sdk') | |||
const XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest | |||
let response = '' | |||
exports.handler = function (event, context, callback) { | |||
var alexa = Alexa.handler(event, context) | |||
alexa.registerHandlers(handlers) | |||
alexa.execute() | |||
} | |||
// Handler for all input | |||
var handlers = { | |||
// LaunchRequest handler | |||
'LaunchRequest': function () { | |||
// Providing users with example of how this is to be used | |||
const say = 'Welcome to Dota2 random skill! ' | |||
// Emit the message | |||
this.emit(':ask', say) | |||
}, | |||
// RequestIntent handler | |||
'RequestIntent': function () { | |||
var xhr = new XMLHttpRequest() | |||
xhr.open('GET', 'https://api.opendota.com/api/heroes', true) | |||
xhr.onreadystatechange = function () { | |||
if (this.readyState === 4) { | |||
response = JSON.parse(xhr.responseText) | |||
} | |||
} | |||
xhr.send() | |||
var speechOutput = 'Output is :' + response[getRandomInt() - 1] + '?' | |||
// Emit the message | |||
this.emit(':ask', speechOutput) | |||
}, | |||
// StopIntent handler | |||
'StopIntent': function () { | |||
// Tell user "Goodbye" | |||
this.emit(':tell', 'Enjoy the game!') | |||
} | |||
} | |||
function getRandomInt () { | |||
return Math.random() * (response.length) | |||
} |
@@ -0,0 +1 @@ | |||
../uuid/bin/uuid |
@@ -0,0 +1 @@ | |||
node_modules |
@@ -0,0 +1,202 @@ | |||
Apache License | |||
Version 2.0, January 2004 | |||
http://www.apache.org/licenses/ | |||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |||
1. Definitions. | |||
"License" shall mean the terms and conditions for use, reproduction, | |||
and distribution as defined by Sections 1 through 9 of this document. | |||
"Licensor" shall mean the copyright owner or entity authorized by | |||
the copyright owner that is granting the License. | |||
"Legal Entity" shall mean the union of the acting entity and all | |||
other entities that control, are controlled by, or are under common | |||
control with that entity. For the purposes of this definition, | |||
"control" means (i) the power, direct or indirect, to cause the | |||
direction or management of such entity, whether by contract or | |||
otherwise, or (ii) ownership of fifty percent (50%) or more of the | |||
outstanding shares, or (iii) beneficial ownership of such entity. | |||
"You" (or "Your") shall mean an individual or Legal Entity | |||
exercising permissions granted by this License. | |||
"Source" form shall mean the preferred form for making modifications, | |||
including but not limited to software source code, documentation | |||
source, and configuration files. | |||
"Object" form shall mean any form resulting from mechanical | |||
transformation or translation of a Source form, including but | |||
not limited to compiled object code, generated documentation, | |||
and conversions to other media types. | |||
"Work" shall mean the work of authorship, whether in Source or | |||
Object form, made available under the License, as indicated by a | |||
copyright notice that is included in or attached to the work | |||
(an example is provided in the Appendix below). | |||
"Derivative Works" shall mean any work, whether in Source or Object | |||
form, that is based on (or derived from) the Work and for which the | |||
editorial revisions, annotations, elaborations, or other modifications | |||
represent, as a whole, an original work of authorship. For the purposes | |||
of this License, Derivative Works shall not include works that remain | |||
separable from, or merely link (or bind by name) to the interfaces of, | |||
the Work and Derivative Works thereof. | |||
"Contribution" shall mean any work of authorship, including | |||
the original version of the Work and any modifications or additions | |||
to that Work or Derivative Works thereof, that is intentionally | |||
submitted to Licensor for inclusion in the Work by the copyright owner | |||
or by an individual or Legal Entity authorized to submit on behalf of | |||
the copyright owner. For the purposes of this definition, "submitted" | |||
means any form of electronic, verbal, or written communication sent | |||
to the Licensor or its representatives, including but not limited to | |||
communication on electronic mailing lists, source code control systems, | |||
and issue tracking systems that are managed by, or on behalf of, the | |||
Licensor for the purpose of discussing and improving the Work, but | |||
excluding communication that is conspicuously marked or otherwise | |||
designated in writing by the copyright owner as "Not a Contribution." | |||
"Contributor" shall mean Licensor and any individual or Legal Entity | |||
on behalf of whom a Contribution has been received by Licensor and | |||
subsequently incorporated within the Work. | |||
2. Grant of Copyright License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
copyright license to reproduce, prepare Derivative Works of, | |||
publicly display, publicly perform, sublicense, and distribute the | |||
Work and such Derivative Works in Source or Object form. | |||
3. Grant of Patent License. Subject to the terms and conditions of | |||
this License, each Contributor hereby grants to You a perpetual, | |||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
(except as stated in this section) patent license to make, have made, | |||
use, offer to sell, sell, import, and otherwise transfer the Work, | |||
where such license applies only to those patent claims licensable | |||
by such Contributor that are necessarily infringed by their | |||
Contribution(s) alone or by combination of their Contribution(s) | |||
with the Work to which such Contribution(s) was submitted. If You | |||
institute patent litigation against any entity (including a | |||
cross-claim or counterclaim in a lawsuit) alleging that the Work | |||
or a Contribution incorporated within the Work constitutes direct | |||
or contributory patent infringement, then any patent licenses | |||
granted to You under this License for that Work shall terminate | |||
as of the date such litigation is filed. | |||
4. Redistribution. You may reproduce and distribute copies of the | |||
Work or Derivative Works thereof in any medium, with or without | |||
modifications, and in Source or Object form, provided that You | |||
meet the following conditions: | |||
(a) You must give any other recipients of the Work or | |||
Derivative Works a copy of this License; and | |||
(b) You must cause any modified files to carry prominent notices | |||
stating that You changed the files; and | |||
(c) You must retain, in the Source form of any Derivative Works | |||
that You distribute, all copyright, patent, trademark, and | |||
attribution notices from the Source form of the Work, | |||
excluding those notices that do not pertain to any part of | |||
the Derivative Works; and | |||
(d) If the Work includes a "NOTICE" text file as part of its | |||
distribution, then any Derivative Works that You distribute must | |||
include a readable copy of the attribution notices contained | |||
within such NOTICE file, excluding those notices that do not | |||
pertain to any part of the Derivative Works, in at least one | |||
of the following places: within a NOTICE text file distributed | |||
as part of the Derivative Works; within the Source form or | |||
documentation, if provided along with the Derivative Works; or, | |||
within a display generated by the Derivative Works, if and | |||
wherever such third-party notices normally appear. The contents | |||
of the NOTICE file are for informational purposes only and | |||
do not modify the License. You may add Your own attribution | |||
notices within Derivative Works that You distribute, alongside | |||
or as an addendum to the NOTICE text from the Work, provided | |||
that such additional attribution notices cannot be construed | |||
as modifying the License. | |||
You may add Your own copyright statement to Your modifications and | |||
may provide additional or different license terms and conditions | |||
for use, reproduction, or distribution of Your modifications, or | |||
for any such Derivative Works as a whole, provided Your use, | |||
reproduction, and distribution of the Work otherwise complies with | |||
the conditions stated in this License. | |||
5. Submission of Contributions. Unless You explicitly state otherwise, | |||
any Contribution intentionally submitted for inclusion in the Work | |||
by You to the Licensor shall be under the terms and conditions of | |||
this License, without any additional terms or conditions. | |||
Notwithstanding the above, nothing herein shall supersede or modify | |||
the terms of any separate license agreement you may have executed | |||
with Licensor regarding such Contributions. | |||
6. Trademarks. This License does not grant permission to use the trade | |||
names, trademarks, service marks, or product names of the Licensor, | |||
except as required for reasonable and customary use in describing the | |||
origin of the Work and reproducing the content of the NOTICE file. | |||
7. Disclaimer of Warranty. Unless required by applicable law or | |||
agreed to in writing, Licensor provides the Work (and each | |||
Contributor provides its Contributions) on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||
implied, including, without limitation, any warranties or conditions | |||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |||
PARTICULAR PURPOSE. You are solely responsible for determining the | |||
appropriateness of using or redistributing the Work and assume any | |||
risks associated with Your exercise of permissions under this License. | |||
8. Limitation of Liability. In no event and under no legal theory, | |||
whether in tort (including negligence), contract, or otherwise, | |||
unless required by applicable law (such as deliberate and grossly | |||
negligent acts) or agreed to in writing, shall any Contributor be | |||
liable to You for damages, including any direct, indirect, special, | |||
incidental, or consequential damages of any character arising as a | |||
result of this License or out of the use or inability to use the | |||
Work (including but not limited to damages for loss of goodwill, | |||
work stoppage, computer failure or malfunction, or any and all | |||
other commercial damages or losses), even if such Contributor | |||
has been advised of the possibility of such damages. | |||
9. Accepting Warranty or Additional Liability. While redistributing | |||
the Work or Derivative Works thereof, You may choose to offer, | |||
and charge a fee for, acceptance of support, warranty, indemnity, | |||
or other liability obligations and/or rights consistent with this | |||
License. However, in accepting such obligations, You may act only | |||
on Your own behalf and on Your sole responsibility, not on behalf | |||
of any other Contributor, and only if You agree to indemnify, | |||
defend, and hold each Contributor harmless for any liability | |||
incurred by, or claims asserted against, such Contributor by reason | |||
of your accepting any such warranty or additional liability. | |||
END OF TERMS AND CONDITIONS | |||
APPENDIX: How to apply the Apache License to your work. | |||
To apply the Apache License to your work, attach the following | |||
boilerplate notice, with the fields enclosed by brackets "[]" | |||
replaced with your own identifying information. (Don't include | |||
the brackets!) The text should be enclosed in the appropriate | |||
comment syntax for the file format. We also recommend that a | |||
file or class name and description of purpose be included on the | |||
same "printed page" as the copyright notice for easier | |||
identification within third-party archives. | |||
Copyright [yyyy] [name of copyright owner] | |||
Licensed under the Apache License, Version 2.0 (the "License"); | |||
you may not use this file except in compliance with the License. | |||
You may obtain a copy of the License at | |||
http://www.apache.org/licenses/LICENSE-2.0 | |||
Unless required by applicable law or agreed to in writing, software | |||
distributed under the License is distributed on an "AS IS" BASIS, | |||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
See the License for the specific language governing permissions and | |||
limitations under the License. |
@@ -0,0 +1,2 @@ | |||
Alexa SDK for JavaScript | |||
Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. |
@@ -0,0 +1,543 @@ | |||
# Alexa Skills Kit SDK for Node.js | |||
Today we're happy to announce the new [alexa-sdk](https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs) for Node.js to help you build skills faster and with less complexity. | |||
Creating an Alexa skill using the [Alexa Skills Kit](http://developer.amazon.com/ask), [Node.js](https://nodejs.org/en/) and [AWS Lambda](https://aws.amazon.com/lambda/) has become one of the most popular ways we see skills created today. The event-driven, non-blocking I/O model of Node.js is well suited for an Alexa skill and Node.js is one of the largest ecosystems of open source libraries in the world. Plus, AWS Lambda is free for the first one million calls per month, which is enough for most developers. Also, when using AWS Lambda you don't need to manage any SSL certificates since the Alexa Skills Kit is a trusted trigger. | |||
While setting up an Alexa skill using AWS Lambda, Node.js and the Alexa Skills Kit has been a simple process, the actual amount of code you have had to write has not. We have seen a large amount of time spent in Alexa skills on handling session attributes, skill state persistence, response building and behavior modeling. With that in mind the Alexa team set out to build an Alexa Skills Kit SDK specifically for Node.js that will help you avoid common hang-ups and focus on your skill's logic instead of boilerplate code. | |||
### Enabling Faster Alexa Skill Development with the Alexa Skills Kit SDK for Node.js (alexa-sdk) | |||
With the new alexa-sdk, our goal is to help you build skills faster while allowing you to avoid unneeded complexity. Today, we are launching the SDK with the following capabilities: | |||
- Hosted as an NPM package allowing simple deployment to any Node.js environment | |||
- Ability to build Alexa responses using built-in events | |||
- Helper events for new sessions and unhandled events that can act as a 'catch-all' events | |||
- Helper functions to build state-machine based Intent handling | |||
- This makes it possible to define different event handlers based on the current state of the skill | |||
- Simple configuration to enable attribute persistence with DynamoDB | |||
- All speech output is automatically wrapped as SSML | |||
- Lambda event and context objects are fully available via `this.event` and `this.context` | |||
- Ability to override built-in functions giving you more flexibility on how you manage state or build responses. For example, saving state attributes to AWS S3. | |||
### Installing and Working with the Alexa Skills Kit SDK for Node.js (alexa-sdk) | |||
The alexa-sdk is immediately available on [github](https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs) and can be deployed as a node package using the following command from within your Node.js environment: | |||
```bash | |||
npm install --save alexa-sdk | |||
``` | |||
In order to start using the alexa-sdk first import the library. To do this within your own project simply create a file named index.js and add the following to it: | |||
```javascript | |||
var Alexa = require('alexa-sdk'); | |||
exports.handler = function(event, context, callback){ | |||
var alexa = Alexa.handler(event, context, callback); | |||
}; | |||
``` | |||
This will import alexa-sdk and set up an `Alexa` object for us to work with. Next, we need to handle the intents for our skill. Alexa-sdk makes it simple to have a function fire an Intent. For example, to create a handler for 'HelloWorldIntent' we simply add the following: | |||
```javascript | |||
var handlers = { | |||
'HelloWorldIntent': function () { | |||
this.emit(':tell', 'Hello World!'); | |||
} | |||
}; | |||
``` | |||
Notice the new syntax above for ':tell'? Alexa-sdk follows a tell/ask response methodology for generating your [outputSpeech response objects](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference#Response%20Format). To ask the user for information we would instead use an `:ask` action. The difference between `:ask` and `:tell` is that after a `:tell` action, the session is ended without waiting for the user to provide more input. | |||
```javascript | |||
this.emit(':ask', 'What would you like to do?', 'Please say that again?'); | |||
``` | |||
In fact, many of the responses follow this same syntax! Here are some additional examples for common skill responses: | |||
```javascript | |||
var speechOutput = 'Hello world!'; | |||
var repromptSpeech = 'Hello again!'; | |||
this.emit(':tell', speechOutput); | |||
this.emit(':ask', speechOutput, repromptSpeech); | |||
var cardTitle = 'Hello World Card'; | |||
var cardContent = 'This text will be displayed in the companion app card.'; | |||
var imageObj = { | |||
smallImageUrl: 'https://imgs.xkcd.com/comics/standards.png', | |||
largeImageUrl: 'https://imgs.xkcd.com/comics/standards.png' | |||
}; | |||
var permissionArray = ['read::alexa:device:all:address']; | |||
var updatedIntent = this.event.request.intent; | |||
var slotToElicit = "Slot to elicit"; | |||
var slotToConfirm = "Slot to confirm"; | |||
this.emit(':askWithCard', speechOutput, repromptSpeech, cardTitle, cardContent, imageObj); | |||
this.emit(':tellWithCard', speechOutput, cardTitle, cardContent, imageObj); | |||
this.emit(':tellWithLinkAccountCard', speechOutput); | |||
this.emit(':askWithLinkAccountCard', speechOutput); | |||
this.emit(':tellWithPermissionCard', speechOutput, permissionArray); | |||
this.emit(':delegate', updatedIntent); | |||
this.emit(':elicitSlot', slotToElicit, speechOutput, repromptSpeech, updatedIntent); | |||
this.emit(':elicitSlotWithCard', slotToElicit, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj); | |||
this.emit(':confirmSlot', slotToConfirm, speechOutput, repromptSpeech, updatedIntent); | |||
this.emit(':confirmSlotWithCard', slotToConfirm, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj); | |||
this.emit(':confirmIntent', speechOutput, repromptSpeech, updatedIntent); | |||
this.emit(':confirmIntentWithCard', speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj); | |||
this.emit(':responseReady'); // Called after the response is built but before it is returned to the Alexa service. Calls :saveState. Can be overridden. | |||
this.emit(':saveState', false); // Handles saving the contents of this.attributes and the current handler state to DynamoDB and then sends the previously built response to the Alexa service. Override if you wish to use a different persistence provider. The second attribute is optional and can be set to 'true' to force saving. | |||
this.emit(':saveStateError'); // Called if there is an error while saving state. Override to handle any errors yourself. | |||
``` | |||
Once we have set up our event handlers we need to register them using the registerHandlers function of the alexa object we just created. | |||
```javascript | |||
exports.handler = function(event, context, callback) { | |||
var alexa = Alexa.handler(event, context, callback); | |||
alexa.registerHandlers(handlers); | |||
}; | |||
``` | |||
You can also register multiple handler objects at once: | |||
```javascript | |||
alexa.registerHandlers(handlers, handlers2, handlers3, ...); | |||
``` | |||
The handlers you define can call each other, making it possible to ensure your responses are uniform. Here is an example where our LaunchRequest and IntentRequest (of HelloWorldIntent) both return the same 'Hello World' message. | |||
```javascript | |||
var handlers = { | |||
'LaunchRequest': function () { | |||
this.emit('HelloWorldIntent'); | |||
}, | |||
'HelloWorldIntent': function () { | |||
this.emit(':tell', 'Hello World!'); | |||
} | |||
}; | |||
``` | |||
Once you are done registering all of your intent handler functions, you simply use the execute function from the alexa object to run your skill's logic. The final line would look like this: | |||
```javascript | |||
exports.handler = function(event, context, callback) { | |||
var alexa = Alexa.handler(event, context, callback); | |||
alexa.registerHandlers(handlers); | |||
alexa.execute(); | |||
}; | |||
``` | |||
You can download a full working sample off github. We have also updated the following Node.js sample skills to work with the alexa-sdk: [Fact](https://github.com/alexa/skill-sample-nodejs-fact), [HelloWorld](https://github.com/alexa/skill-sample-nodejs-hello-world), [HighLow](https://github.com/alexa/skill-sample-nodejs-highlowgame), [HowTo](https://github.com/alexa/skill-sample-nodejs-howto) and [Trivia](https://github.com/alexa/skill-sample-nodejs-trivia). | |||
Note: for specifications regarding the ```imgObj``` please see [here](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/providing-home-cards-for-the-amazon-alexa-app). | |||
### Making Skill State Management Simpler | |||
Alexa-sdk will route incoming intents to the correct function handler based on state. State is stored as a string in your session attributes indicating the current state of the skill. You can emulate the built-in intent routing by appending the state string to the intent name when defining your intent handlers, but alexa-sdk helps do that for you. | |||
For example, let's create a simple number-guessing game with 'start' and 'guess' states based on our previous example of handling a `NewSession` event. | |||
```javascript | |||
var states = { | |||
GUESSMODE: '_GUESSMODE', // User is trying to guess the number. | |||
STARTMODE: '_STARTMODE' // Prompt the user to start or restart the game. | |||
}; | |||
var newSessionHandlers = { | |||
// This will short-cut any incoming intent or launch requests and route them to this handler. | |||
'NewSession': function() { | |||
if(Object.keys(this.attributes).length === 0) { // Check if it's the first time the skill has been invoked | |||
this.attributes['endedSessionCount'] = 0; | |||
this.attributes['gamesPlayed'] = 0; | |||
} | |||
this.handler.state = states.STARTMODE; | |||
this.emit(':ask', 'Welcome to High Low guessing game. You have played ' | |||
+ this.attributes['gamesPlayed'].toString() + ' times. Would you like to play?', | |||
'Say yes to start the game or no to quit.'); | |||
} | |||
}; | |||
``` | |||
Notice that when a new session is created we simply set the state of our skill into `STARTMODE` using this.handler.state. The skills state will automatically be persisted in your skill's session attributes, and will be optionally persisted across sessions if you set a DynamoDB table. | |||
It is also important point out that `NewSession` is a great catch-all behavior and a good entry point but it is not required. `NewSession` will only be invoked if a handler with that name is defined. Each state you define can have its own `NewSession` handler which will be invoked if you are using the built-in persistence. In the above example we could define different `NewSession` behavior for both `states.STARTMODE` and `states.GUESSMODE` giving us added flexibility. | |||
In order to define intents that will respond to the different states of our skill, we need to use the `Alexa.CreateStateHandler` function. Any intent handlers defined here will only work when the skill is in a specific state, giving us even greater flexibility! | |||
For example, if we are in the `GUESSMODE` state we defined above we want to handle a user responding to a question. This can be done using StateHandlers like this: | |||
```javascript | |||
var guessModeHandlers = Alexa.CreateStateHandler(states.GUESSMODE, { | |||
'NewSession': function () { | |||
this.handler.state = ''; | |||
this.emitWithState('NewSession'); // Equivalent to the Start Mode NewSession handler | |||
}, | |||
'NumberGuessIntent': function() { | |||
var guessNum = parseInt(this.event.request.intent.slots.number.value); | |||
var targetNum = this.attributes['guessNumber']; | |||
console.log('user guessed: ' + guessNum); | |||
if(guessNum > targetNum){ | |||
this.emit('TooHigh', guessNum); | |||
} else if( guessNum < targetNum){ | |||
this.emit('TooLow', guessNum); | |||
} else if (guessNum === targetNum){ | |||
// With a callback, use the arrow function to preserve the correct 'this' context | |||
this.emit('JustRight', () => { | |||
this.emit(':ask', guessNum.toString() + 'is correct! Would you like to play a new game?', | |||
'Say yes to start a new game, or no to end the game.'); | |||
}); | |||
} else { | |||
this.emit('NotANum'); | |||
} | |||
}, | |||
'AMAZON.HelpIntent': function() { | |||
this.emit(':ask', 'I am thinking of a number between zero and one hundred, try to guess and I will tell you' + | |||
' if it is higher or lower.', 'Try saying a number.'); | |||
}, | |||
'SessionEndedRequest': function () { | |||
console.log('session ended!'); | |||
this.attributes['endedSessionCount'] += 1; | |||
this.emit(':saveState', true); // Be sure to call :saveState to persist your session attributes in DynamoDB | |||
}, | |||
'Unhandled': function() { | |||
this.emit(':ask', 'Sorry, I didn\'t get that. Try saying a number.', 'Try saying a number.'); | |||
} | |||
}); | |||
``` | |||
On the flip side, if I am in `STARTMODE` I can define my `StateHandlers` to be the following: | |||
```javascript | |||
var startGameHandlers = Alexa.CreateStateHandler(states.STARTMODE, { | |||
'NewSession': function () { | |||
this.emit('NewSession'); // Uses the handler in newSessionHandlers | |||
}, | |||
'AMAZON.HelpIntent': function() { | |||
var message = 'I will think of a number between zero and one hundred, try to guess and I will tell you if it' + | |||
' is higher or lower. Do you want to start the game?'; | |||
this.emit(':ask', message, message); | |||
}, | |||
'AMAZON.YesIntent': function() { | |||
this.attributes['guessNumber'] = Math.floor(Math.random() * 100); | |||
this.handler.state = states.GUESSMODE; | |||
this.emit(':ask', 'Great! ' + 'Try saying a number to start the game.', 'Try saying a number.'); | |||
}, | |||
'AMAZON.NoIntent': function() { | |||
this.emit(':tell', 'Ok, see you next time!'); | |||
}, | |||
'SessionEndedRequest': function () { | |||
console.log('session ended!'); | |||
this.attributes['endedSessionCount'] += 1; | |||
this.emit(':saveState', true); | |||
}, | |||
'Unhandled': function() { | |||
var message = 'Say yes to continue, or no to end the game.'; | |||
this.emit(':ask', message, message); | |||
} | |||
}); | |||
``` | |||
Take a look at how `AMAZON.YesIntent` and `AMAZON.NoIntent` are not defined in the `guessModeHandlers` object, since it doesn't make sense for a 'yes' or 'no' response in this state. Those intents will be caught by the `Unhandled` handler. | |||
Also, notice the different behavior for `NewSession` and `Unhandled` across both states? In this game, we 'reset' the state by calling a `NewSession` handler defined in the `newSessionHandlers` object. You can also skip defining it and alexa-sdk will call the intent handler for the current state. Just remember to register your State Handlers before you call `alexa.execute()` or they will not be found. | |||
Your attributes will be automatically saved when you end the session, but if the user ends the session you have to emit the `:saveState` event (`this.emit(':saveState', true)`) to force a save. You should do this in your `SessionEndedRequest` handler which is called when the user ends the session by saying 'quit' or timing out. Take a look at the example above. | |||
We have wrapped up the above example into a high/low number guessing game skill you can [download here](https://github.com/alexa/skill-sample-nodejs-highlowgame). | |||
### Persisting Skill Attributes through DynamoDB | |||
Many of you would like to persist your session attribute values into storage for further use. Alexa-sdk integrates directly with [Amazon DynamoDB](https://aws.amazon.com/dynamodb/) (a NoSQL database service) to enable you to do this with a single line of code. | |||
Simply set the name of the DynamoDB table on your alexa object before you call alexa.execute. | |||
```javascript | |||
exports.handler = function (event, context, callback) { | |||
var alexa = Alexa.handler(event, context, callback); | |||
alexa.appId = appId; | |||
alexa.dynamoDBTableName = 'YourTableName'; // That's it! | |||
alexa.registerHandlers(State1Handlers, State2Handlers); | |||
alexa.execute(); | |||
}; | |||
``` | |||
Then later on to set a value you simply need to call into the attributes property of the alexa object. No more separate `put` and `get` functions! | |||
```javascript | |||
this.attributes['yourAttribute'] = 'value'; | |||
``` | |||
You can [create the table manually](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/SampleData.CreateTables.html) beforehand or simply give your Lambda function DynamoDB [create table permissions](http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_CreateTable.html) and it will happen automatically. Just remember it can take a minute or so for the table to be created on the first invocation. If you create the table manually, the Primary Key must be a string value called "userId". | |||
### Generating your own responses | |||
Normally emitting a response event like `this.emit(':tell', speechOutput, repromptSpeech)` will set up the response and send it to Alexa for you, using any speech or card values you pass it. If you want to manually create your own responses, you can use `this.response` to help. `this.response` contains a series of functions, that you can use to set the different properties of the response. This allows you to take advantage of the Alexa Skills Kit's built-in audio player support. Once you've set up your response, you can just call `this.emit(':responseReady')` to send your response to Alexa. The functions within `this.response` are also chainable, so you can use as many as you want in a row. | |||
For example, the below code is equivalent to `this.emit(':ask', 'foo', 'bar');` | |||
```javascript | |||
this.response.speak('foo').listen('bar'); | |||
this.emit(':responseReady'); | |||
``` | |||
Here's the API for using `this.response`: | |||
- `this.response.speak(outputSpeech)`: sets the first speech output of the response to `outputSpeech`. | |||
- `this.response.listen(repromptSpeech)`: sets the reprompt speech of the response to `repromptSpeech` and `shouldEndSession` to false. Unless this function is called, `this.response` will set `shouldEndSession` to true. | |||
- `this.response.cardRenderer(cardTitle, cardContent, cardImage)`: sets the card in the response to have the title `cardTitle`, the content `cardContent`, and the image `cardImage`. `cardImage` can be excluded, but if it's included it must be of the correct image object format, detailed above. | |||
- `this.response.linkAccountCard()`: sets the type of the card to a 'Link Account' card. | |||
- `this.response.audioPlayer(directiveType, behavior, url, token, expectedPreviousToken, offsetInMilliseconds)`: sets the audioPlayer directive using the provided parameters. See the [audioPlayer interface reference](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/custom-audioplayer-interface-reference) for more details. Options for `directiveType` are `'play'`, `'stop'` and `'clearqueue'`. Inputting any other value is equivalent to `'clearqueue'`. | |||
- If you use `'play'`, you need to include all the parameters after: `behavior, url, token, expectedPreviousToken, offsetInMilliseconds`. | |||
- If you use `'stop'`, no further parameters are needed. | |||
- If you use `'clearQueue'`, you only need to include the `behaviour` parameter. | |||
- `this.response.audioPlayerPlay(behavior, url, token, expectedPreviousToken, offsetInMilliseconds)`: sets the audioPlayer directive using the provided parameters, and `AudioPlayer.Play` as the directive type. This will make the audio player play an audio file at a requested URL. | |||
- `this.response.audioPlayerStop()` sets the directive type to `AudioPlayer.Stop`. This will make the audio player stop. | |||
- `this.response.audioPlayerClearQueue(clearBehaviour)` sets the directive type to `AudioPlayer.ClearQueue` and sets the clear behaviour of the directive. Options for this value are `'CLEAR_ENQUEUED'` and `'CLEAR_ALL'`. This will either clear the queue and continue the current stream, or clear the queue and stop the current stream. | |||
When you've set up your response, simply call `this.emit(':responseReady');` to send your response off. | |||
### Tips | |||
- When any of the response events are emitted `:ask`, `:tell`, `:askWithCard`, etc. The lambda context.succeed() method is called, which immediately stops processing of any further background tasks. Any asynchronous jobs that are still will not be completed and any lines of code below the response emit statement will not be executed. This is not the case for non responding events like `:saveState`. | |||
- In order to "transfer" a call from one state handler to another, `this.handler.state` needs to be set to the name of the target state. If the target state is "", then `this.emit("TargetHandlerName")` should be called. For any other states, `this.emitWithState("TargetHandlerName")` must be called instead. | |||
- The contents of the prompt and repompt values get wrapped in SSML tags. This means that any special XML characters within the value need to be escape coded. For example, this.emit(":ask", "I like M&M's") will cause a failure because the `&` character needs to be encoded as `&`. Other characters that need to be encoded include: `<` -> `<`, and `>` -> `>`. | |||
### Adding Multi-Language Support for Skill | |||
Let's take the Hello World example here. Define all user-facing language strings in the following format. | |||
```javascript | |||
var languageStrings = { | |||
'en-GB': { | |||
'translation': { | |||
'SAY_HELLO_MESSAGE' : 'Hello World!' | |||
} | |||
}, | |||
'en-US': { | |||
'translation': { | |||
'SAY_HELLO_MESSAGE' : 'Hello World!' | |||
} | |||
}, | |||
'de-DE': { | |||
'translation': { | |||
'SAY_HELLO_MESSAGE' : 'Hallo Welt!' | |||
} | |||
} | |||
}; | |||
``` | |||
To enable string internationalization features in Alexa-sdk, set resources to the object we created above. | |||
```javascript | |||
exports.handler = function(event, context, callback) { | |||
var alexa = Alexa.handler(event, context); | |||
alexa.appId = appId; | |||
// To enable string internationalization (i18n) features, set a resources object. | |||
alexa.resources = languageStrings; | |||
alexa.registerHandlers(handlers); | |||
alexa.execute(); | |||
}; | |||
``` | |||
Once you are done defining and enabling language strings, you can access these strings using the this.t() function. Strings will be rendered in the language that matches the locale of the incoming request. | |||
```javascript | |||
var handlers = { | |||
'LaunchRequest': function () { | |||
this.emit('SayHello'); | |||
}, | |||
'HelloWorldIntent': function () { | |||
this.emit('SayHello'); | |||
}, | |||
'SayHello': function () { | |||
this.emit(':tell', this.t('SAY_HELLO_MESSAGE')); | |||
} | |||
}; | |||
``` | |||
For more infomation about developing and deploying skills in multiple languages, please go [here](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/developing-skills-in-multiple-languages). | |||
### Device ID Support | |||
When a customer enables your Alexa skill, your skill can obtain the customerโs permission to use address data associated with the customerโs Alexa device. You can then use this address data to provide key functionality for the skill, or to enhance the customer experience. | |||
The `deviceId` is now exposed through the context object in each request and can be accessed in any intent handler through `this.event.context.System.device.deviceId`. See the [Address API sample skill](https://github.com/alexa/skill-sample-node-device-address-api) to see how we leveraged the deviceId and the Address API to use a user's device address in a skill. | |||
### Speechcons (Interjections) | |||
[Speechcons](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/speechcon-reference) are special words and phrases that Alexa pronounces more expressively. In order to use them you can just include the SSML markup in the text to emit. | |||
* `this.emit(':tell', 'Sometimes when I look at the Alexa skills you have all taught me, I just have to say, <say-as interpret-as="interjection">Bazinga.</say-as> ');` | |||
* `this.emit(':tell', '<say-as interpret-as="interjection">Oh boy</say-as><break time="1s"/> this is just an example.');` | |||
_Speechcons are supported for English (US), English (UK), and German._ | |||
### Dialog Management Support for Skill | |||
The `Dialog` interface provides directives for managing a multi-turn conversation between your skill and the user. You can use the directives to ask the user for the information you need to fulfill their request. See the [Dialog Interface](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/dialog-interface-reference) and [Skill Editor](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/ask-define-the-vui-with-gui) documentation for more information on how to use dialog directives. | |||
You can use `this.event.request.dialogState` to access current `dialogState`. | |||
#### Delegate Directive | |||
Sends Alexa a command to handle the next turn in the dialog with the user. You can use this directive if the skill has a dialog model and the current status of the dialog (`dialogState`) is either `STARTED` or `IN_PROGRESS`. You cannot emit this directive if the `dialogState` is `COMPLETED`. | |||
You can use `this.emit(':delegate')` to send delegate directive response. | |||
```javascript | |||
var handlers = { | |||
'BookFlightIntent': function () { | |||
if (this.event.request.dialogState === 'STARTED') { | |||
var updatedIntent = this.event.request.intent; | |||
// Pre-fill slots: update the intent object with slot values for which | |||
// you have defaults, then emit :delegate with this updated intent. | |||
updatedIntent.slots.SlotName.value = 'DefaultValue'; | |||
this.emit(':delegate', updatedIntent); | |||
} else if (this.event.request.dialogState !== 'COMPLETED'){ | |||
this.emit(':delegate'); | |||
} else { | |||
// All the slots are filled (And confirmed if you choose to confirm slot/intent) | |||
handlePlanMyTripIntent(); | |||
} | |||
} | |||
}; | |||
``` | |||
#### Elicit Slot Directive | |||
Sends Alexa a command to ask the user for the value of a specific slot. Specify the name of the slot to elicit in the `slotToElicit`. Provide a prompt to ask the user for the slot value in `speechOutput`. | |||
You can use `this.emit(':elicitSlot', slotToElicit, speechOutput, repromptSpeech, updatedIntent)` or `this.emit(':elicitSlotWithCard', slotToElicit, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj)` to send elicit slot directive response. | |||
When using `this.emit(':elicitSlotWithCard', slotToElicit, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj)`, `updatedIntent` and `imageObj` are optional parameters. You can set them to `null` or not pass them. | |||
```javascript | |||
var handlers = { | |||
'BookFlightIntent': function () { | |||
var intentObj = this.event.request.intent; | |||
if (!intentObj.slots.Source.value) { | |||
var slotToElicit = 'Source'; | |||
var speechOutput = 'Where would you like to fly from?'; | |||
var repromptSpeech = speechOutput; | |||
this.emit(':elicitSlot', slotToElicit, speechOutput, repromptSpeech); | |||
} else if (!intentObj.slots.Destination.value) { | |||
var slotToElicit = 'Destination'; | |||
var speechOutput = 'Where would you like to fly to?'; | |||
var repromptSpeech = speechOutput; | |||
var cardContent = 'What is the destination?'; | |||
var cardTitle = 'Destination'; | |||
var updatedIntent = intentObj; | |||
// An intent object representing the intent sent to your skill. | |||
// You can use this property set or change slot values and confirmation status if necessary. | |||
var imageObj = { | |||
smallImageUrl: 'https://imgs.xkcd.com/comics/standards.png', | |||
largeImageUrl: 'https://imgs.xkcd.com/comics/standards.png' | |||
}; | |||
this.emit(':elicitSlotWithCard', slotToElicit, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj); | |||
} else { | |||
handlePlanMyTripIntentAllSlotsAreFilled(); | |||
} | |||
} | |||
}; | |||
``` | |||
#### Confirm Slot Directive | |||
Sends Alexa a command to confirm the value of a specific slot before continuing with the dialog. Specify the name of the slot to confirm in the `slotToConfirm`. Provide a prompt to ask the user for confirmation in `speechOutput`. | |||
You can use `this.emit(':confirmSlot', slotToConfirm, speechOutput, repromptSpeech, updatedIntent)` or `this.emit(':confirmSlotWithCard', slotToConfirm, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj)` to send confirm slot directive response. | |||
When using `this.emit(':confirmSlotWithCard', slotToConfirm, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj)`, `updatedIntent` and `imageObj` are optional parameters. You can set them to `null` or not pass them. | |||
```javascript | |||
var handlers = { | |||
'BookFlightIntent': function () { | |||
var intentObj = this.event.request.intent; | |||
if (intentObj.slots.Source.confirmationStatus !== 'CONFIRMED') { | |||
if (intentObj.slots.Source.confirmationStatus !== 'DENIED') { | |||
// Slot value is not confirmed | |||
var slotToConfirm = 'Source'; | |||
var speechOutput = 'You want to fly from ' + intentObj.slots.Source.value + ', is that correct?'; | |||
var repromptSpeech = speechOutput; | |||
this.emit(':confirmSlot', slotToConfirm, speechOutput, repromptSpeech); | |||
} else { | |||
// Users denies the confirmation of slot value | |||
var slotToElicit = 'Source'; | |||
var speechOutput = 'Okay, Where would you like to fly from?'; | |||
this.emit(':elicitSlot', slotToElicit, speechOutput, speechOutput); | |||
} | |||
} else if (intentObj.slots.Destination.confirmationStatus !== 'CONFIRMED') { | |||
if (intentObj.slots.Destination.confirmationStatus !== 'DENIED') { | |||
var slotToConfirm = 'Destination'; | |||
var speechOutput = 'You would like to fly to ' + intentObj.slots.Destination.value + ', is that correct?'; | |||
var repromptSpeech = speechOutput; | |||
var cardContent = speechOutput; | |||
var cardTitle = 'Confirm Destination'; | |||
this.emit(':confirmSlotWithCard', slotToConfirm, speechOutput, repromptSpeech, cardTitle, cardContent); | |||
} else { | |||
var slotToElicit = 'Destination'; | |||
var speechOutput = 'Okay, Where would you like to fly to?'; | |||
var repromptSpeech = speechOutput; | |||
this.emit(':elicitSlot', slotToElicit, speechOutput, repromptSpeech); | |||
} | |||
} else { | |||
handlePlanMyTripIntentAllSlotsAreConfirmed(); | |||
} | |||
} | |||
}; | |||
``` | |||
#### Confirm Intent Directive | |||
Sends Alexa a command to confirm the all the information the user has provided for the intent before the skill takes action. Provide a prompt to ask the user for confirmation in `speechOutput`. Be sure to repeat back all the values the user needs to confirm in the prompt. | |||
You can use `this.emit(':confirmIntent', speechOutput, repromptSpeech, updatedIntent)` or `this.emit(':confirmIntentWithCard', speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj)` to send confirm intent directive response. | |||
When using `this.emit(':confirmIntentWithCard', speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj)`, `updatedIntent` and `imageObj` are optional parameters. You can set them to `null` or not pass them. | |||
```javascript | |||
var handlers = { | |||
'BookFlightIntent': function () { | |||
var intentObj = this.event.request.intent; | |||
if (intentObj.confirmationStatus !== 'CONFIRMED') { | |||
if (intentObj.confirmationStatus !== 'DENIED') { | |||
// Intent is not confirmed | |||
var speechOutput = 'You would like to book flight from ' + intentObj.slots.Source.value + ' to ' + | |||
intentObj.slots.Destination.value + ', is that correct?'; | |||
var cardTitle = 'Booking Summary'; | |||
var repromptSpeech = speechOutput; | |||
var cardContent = speechOutput; | |||
this.emit(':confirmIntentWithCard', speechOutput, repromptSpeech, cardTitle, cardContent); | |||
} else { | |||
// Users denies the confirmation of intent. May be value of the slots are not correct. | |||
handleIntentConfimationDenial(); | |||
} | |||
} else { | |||
handlePlanMyTripIntentAllSlotsAndIntentAreConfirmed(); | |||
} | |||
} | |||
}; | |||
``` | |||
### Next Steps | |||
Try extending the HighLow game: | |||
- Have it store your average number of guesses per game | |||
- Add [sound effects](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/speech-synthesis-markup-language-ssml-reference#audio) | |||
- Give the player a limited amount of guesses | |||
For more information about getting started with the Alexa Skills Kit, check out the following additional assets: | |||
[Alexa Dev Chat Podcast](http://bit.ly/alexadevchat) | |||
[Alexa Training with Big Nerd Ranch](https://developer.amazon.com/public/community/blog/tag/Big+Nerd+Ranch) | |||
[Alexa Skills Kit (ASK)](https://developer.amazon.com/ask) | |||
[Alexa Developer Forums](https://forums.developer.amazon.com/forums/category.jspa?categoryID=48) | |||
[Training for the Alexa Skills Kit](https://developer.amazon.com/alexa-skills-kit/alexa-skills-developer-training) | |||
-Dave ( [@TheDaveDev](http://twitter.com/thedavedev)) |
@@ -0,0 +1,96 @@ | |||
The bundled package of the Alexa SDK for JavaScript is available under the | |||
Apache License, Version 2.0: | |||
Copyright 2012-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. | |||
Licensed under the Apache License, Version 2.0 (the "License"). You | |||
may not use this file except in compliance with the License. A copy of | |||
the License is located at | |||
http://aws.amazon.com/apache2.0/ | |||
or in the "license" file accompanying this file. This file is | |||
distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF | |||
ANY KIND, either express or implied. See the License for the specific | |||
language governing permissions and limitations under the License. | |||
This product bundles browserify, which is available under a | |||
"3-clause BSD" license: | |||
Copyright Joyent, Inc. and other Node contributors. | |||
Permission is hereby granted, free of charge, to any person obtaining a | |||
copy of this software and associated documentation files (the | |||
"Software"), to deal in the Software without restriction, including | |||
without limitation the rights to use, copy, modify, merge, publish, | |||
distribute, sublicense, and/or sell copies of the Software, and to permit | |||
persons to whom the Software is furnished to do so, subject to the | |||
following conditions: | |||
The above copyright notice and this permission notice shall be included | |||
in all copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN | |||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, | |||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | |||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE | |||
USE OR OTHER DEALINGS IN THE SOFTWARE. | |||
This product bundles crypto-browserify, which is available under | |||
the MIT license: | |||
Copyright (c) 2013 Dominic Tarr | |||
Permission is hereby granted, free of charge, | |||
to any person obtaining a copy of this software and | |||
associated documentation files (the "Software"), to | |||
deal in the Software without restriction, including | |||
without limitation the rights to use, copy, modify, | |||
merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom | |||
the Software is furnished to do so, | |||
subject to the following conditions: | |||
The above copyright notice and this permission notice | |||
shall be included in all copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | |||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR | |||
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |||
This product bundles MD5, SHA-1, and SHA-256 hashing algorithm components, | |||
which are available under a BSD license: | |||
Copyright (c) 1998 - 2009, Paul Johnston & Contributors | |||
All rights reserved. | |||
Redistribution and use in source and binary forms, with or without | |||
modification, are permitted provided that the following conditions are met: | |||
Redistributions of source code must retain the above copyrightnotice, | |||
this list of conditions and the following disclaimer. Redistributions | |||
in binary form must reproduce the above copyright notice, this list of | |||
conditions and the following disclaimer in the documentation and/or | |||
other materials provided with the distribution. | |||
Neither the name of the author nor the names of its contributors may | |||
be used to endorse or promote products derived from this software | |||
without specific prior written permission. | |||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE | |||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF | |||
THE POSSIBILITY OF SUCH DAMAGE. |
@@ -0,0 +1,5 @@ | |||
var AlexaLambdaHandler = require('./lib/alexa'); | |||
module.exports.handler = AlexaLambdaHandler.LambdaHandler; | |||
module.exports.CreateStateHandler = AlexaLambdaHandler.CreateStateHandler; | |||
module.exports.StateString = AlexaLambdaHandler.StateString; |
@@ -0,0 +1,99 @@ | |||
'use strict'; | |||
var aws = require('aws-sdk'); | |||
var doc; | |||
module.exports = (function() { | |||
return { | |||
get: function(table, userId, callback) { | |||
if(!table) { | |||
callback('DynamoDB Table name is not set.', null); | |||
} | |||
if(!doc) { | |||
doc = new aws.DynamoDB.DocumentClient({apiVersion: '2012-08-10'}); | |||
} | |||
var params = { | |||
Key: { | |||
userId: userId | |||
}, | |||
TableName: table, | |||
ConsistentRead: true | |||
}; | |||
doc.get(params, function(err, data){ | |||
if(err) { | |||
console.log('get error: ' + JSON.stringify(err, null, 4)); | |||
if(err.code === 'ResourceNotFoundException') { | |||
var dynamoClient = new aws.DynamoDB(); | |||
newTableParams['TableName'] = table; | |||
dynamoClient.createTable(newTableParams, function (err, data) { | |||
if(err) { | |||
console.log('Error creating table: ' + JSON.stringify(err, null, 4)); | |||
} | |||
console.log('Creating table ' + table + ':\n' + JSON.stringify(data)); | |||
callback(err, {}); | |||
}); | |||
} else { | |||
callback(err, null); | |||
} | |||
} else { | |||
if(isEmptyObject(data)) { | |||
callback(null, {}); | |||
} else { | |||
callback(null, data.Item['mapAttr']); | |||
} | |||
} | |||
}); | |||
}, | |||
set: function(table, userId, data, callback) { | |||
if(!table) { | |||
callback('DynamoDB Table name is not set.', null); | |||
} | |||
if(!doc) { | |||
doc = new aws.DynamoDB.DocumentClient({apiVersion: '2012-08-10'}); | |||
} | |||
var params = { | |||
Item: { | |||
userId: userId, | |||
mapAttr: data | |||
}, | |||
TableName: table | |||
}; | |||
doc.put(params, function(err, data) { | |||
if(err) { | |||
console.log('Error during DynamoDB put:' + err); | |||
} | |||
callback(err, data); | |||
}); | |||
} | |||
}; | |||
})(); | |||
function isEmptyObject(obj) { | |||
return !Object.keys(obj).length; | |||
} | |||
var newTableParams = { | |||
AttributeDefinitions: [ | |||
{ | |||
AttributeName: 'userId', | |||
AttributeType: 'S' | |||
} | |||
], | |||
KeySchema: [ | |||
{ | |||
AttributeName: 'userId', | |||
KeyType: 'HASH' | |||
} | |||
], | |||
ProvisionedThroughput: { | |||
ReadCapacityUnits: 5, | |||
WriteCapacityUnits: 5 | |||
} | |||
}; |
@@ -0,0 +1,437 @@ | |||
'use strict'; | |||
var EventEmitter = require('events').EventEmitter; | |||
var util = require('util'); | |||
var i18n = require('i18next'); | |||
var sprintf = require('i18next-sprintf-postprocessor'); | |||
var attributesHelper = require('./DynamoAttributesHelper'); | |||
var responseHandlers = require('./response'); | |||
var _StateString = 'STATE'; | |||
function AlexaRequestEmitter() { | |||
EventEmitter.call(this); | |||
} | |||
util.inherits(AlexaRequestEmitter, EventEmitter); | |||
function alexaRequestHandler(event, context, callback) { | |||
if (!event.session) { | |||
event.session = { 'attributes': {} }; | |||
} else if (!event.session['attributes']) { | |||
event.session['attributes'] = {}; | |||
} | |||
var handler = new AlexaRequestEmitter(); | |||
handler.setMaxListeners(Infinity); | |||
Object.defineProperty(handler, '_event', { | |||
value: event, | |||
writable: false | |||
}); | |||
Object.defineProperty(handler, '_context', { | |||
value: context, | |||
writable: false | |||
}); | |||
Object.defineProperty(handler, '_callback', { | |||
value: callback, | |||
writable: false | |||
}); | |||
Object.defineProperty(handler, 'state', { | |||
value: null, | |||
writable: true, | |||
configurable: true | |||
}); | |||
Object.defineProperty(handler, 'appId', { | |||
value: null, | |||
writable: true | |||
}); | |||
Object.defineProperty(handler, 'response', { | |||
value: {}, | |||
writable: true | |||
}); | |||
Object.defineProperty(handler, 'dynamoDBTableName', { | |||
value: null, | |||
writable: true | |||
}); | |||
Object.defineProperty(handler, 'saveBeforeResponse', { | |||
value: false, | |||
writable: true | |||
}); | |||
Object.defineProperty(handler, 'i18n', { | |||
value: i18n, | |||
writable: true | |||
}); | |||
Object.defineProperty(handler, 'locale', { | |||
value: undefined, | |||
writable: true | |||
}); | |||
Object.defineProperty(handler, 'resources', { | |||
value: undefined, | |||
writable: true | |||
}); | |||
Object.defineProperty(handler, 'registerHandlers', { | |||
value: function() { | |||
RegisterHandlers.apply(handler, arguments); | |||
}, | |||
writable: false | |||
}); | |||
Object.defineProperty(handler, 'execute', { | |||
value: function() { | |||
HandleLambdaEvent.call(handler); | |||
}, | |||
writable: false | |||
}); | |||
handler.registerHandlers(responseHandlers); | |||
return handler; | |||
} | |||
function HandleLambdaEvent() { | |||
this.locale = this._event.request.locale; | |||
if(this.resources) { | |||
this.i18n.use(sprintf).init({ | |||
overloadTranslationOptionHandler: sprintf.overloadTranslationOptionHandler, | |||
returnObjects: true, | |||
lng: this.locale, | |||
resources: this.resources | |||
}, (err, t) => { | |||
if(err) { | |||
throw new Error('Error initializing i18next: ' + err); | |||
} | |||
ValidateRequest.call(this); | |||
}); | |||
} else { | |||
ValidateRequest.call(this); | |||
} | |||
} | |||
function ValidateRequest() { | |||
var event = this._event; | |||
var context = this._context; | |||
var callback = this._callback; | |||
var handlerAppId = this.appId; | |||
var requestAppId = ''; | |||
var userId = ''; | |||
// Long-form audio enabled skills use event.context | |||
if (event.context) { | |||
requestAppId = event.context.System.application.applicationId; | |||
userId = event.context.System.user.userId; | |||
} else if (event.session) { | |||
requestAppId = event.session.application.applicationId; | |||
userId = event.session.user.userId; | |||
} | |||
if(!handlerAppId){ | |||
console.log('Warning: Application ID is not set'); | |||
} | |||
try { | |||
// Validate that this request originated from authorized source. | |||
if (handlerAppId && (requestAppId !== handlerAppId)) { | |||
console.log(`The applicationIds don\'t match: ${requestAppId} and ${handlerAppId}`); | |||
var error = new Error('Invalid ApplicationId: ' + handlerAppId) | |||
if(typeof callback === 'undefined') { | |||
return context.fail(error) | |||
} else { | |||
return callback(error); | |||
} | |||
} | |||
if(this.dynamoDBTableName && (!event.session.sessionId || event.session['new']) ) { | |||
attributesHelper.get(this.dynamoDBTableName, userId, (err, data) => { | |||
if(err) { | |||
var error = new Error('Error fetching user state: ' + err) | |||
if(typeof callback === 'undefined') { | |||
return context.fail(error) | |||
} else { | |||
return callback(error); | |||
} | |||
} | |||
Object.assign(this._event.session.attributes, data); | |||
EmitEvent.call(this); | |||
}); | |||
} else { | |||
EmitEvent.call(this); | |||
} | |||
} catch (e) { | |||
console.log(`Unexpected exception '${e}':\n${e.stack}`); | |||
if(typeof callback === 'undefined') { | |||
return context.fail(e) | |||
} else { | |||
return callback(e); | |||
} | |||
} | |||
} | |||
function EmitEvent() { | |||
this.state = this._event.session.attributes[_StateString] || ''; | |||
var eventString = ''; | |||
if (this._event.session['new'] && this.listenerCount('NewSession' + this.state) === 1) { | |||
eventString = 'NewSession'; | |||
} else if(this._event.request.type === 'LaunchRequest') { | |||
eventString = 'LaunchRequest'; | |||
} else if(this._event.request.type === 'IntentRequest') { | |||
eventString = this._event.request.intent.name; | |||
} else if (this._event.request.type === 'SessionEndedRequest'){ | |||
eventString = 'SessionEndedRequest'; | |||
} else if (this._event.request.type.substring(0,11) === 'AudioPlayer') { | |||
eventString = this._event.request.type.substring(12); | |||
} else if (this._event.request.type.substring(0,18) === 'PlaybackController') { | |||
eventString = this._event.request.type.substring(19); | |||
} else if (this._event.request.type === 'Display.ElementSelected') { | |||
eventString = 'ElementSelected'; | |||
} | |||
eventString += this.state; | |||
if(this.listenerCount(eventString) < 1) { | |||
eventString = 'Unhandled' + this.state; | |||
} | |||
if(this.listenerCount(eventString) < 1){ | |||
throw new Error(`No 'Unhandled' function defined for event: ${eventString}`); | |||
} | |||
this.emit(eventString); | |||
} | |||
function RegisterHandlers() { | |||
for(var arg = 0; arg < arguments.length; arg++) { | |||
var handlerObject = arguments[arg]; | |||
if(!isObject(handlerObject)) { | |||
throw new Error(`Argument #${arg} was not an Object`); | |||
} | |||
var eventNames = Object.keys(handlerObject); | |||
for(var i = 0; i < eventNames.length; i++) { | |||
if(typeof(handlerObject[eventNames[i]]) !== 'function') { | |||
throw new Error(`Event handler for '${eventNames[i]}' was not a function`); | |||
} | |||
var eventName = eventNames[i]; | |||
if(handlerObject[_StateString]) { | |||
eventName += handlerObject[_StateString]; | |||
} | |||
var localize = function() { | |||
return this.i18n.t.apply(this.i18n, arguments); | |||
}; | |||
var handlerContext = { | |||
on: this.on.bind(this), | |||
emit: this.emit.bind(this), | |||
emitWithState: EmitWithState.bind(this), | |||
state: this.state, | |||
handler: this, | |||
i18n: this.i18n, | |||
locale: this.locale, | |||
t : localize, | |||
event: this._event, | |||
attributes: this._event.session.attributes, | |||
context: this._context, | |||
callback : this._callback, | |||
name: eventName, | |||
isOverridden: IsOverridden.bind(this, eventName), | |||
response: ResponseBuilder(this) | |||
}; | |||
this.on(eventName, handlerObject[eventNames[i]].bind(handlerContext)); | |||
} | |||
} | |||
} | |||
function isObject(obj) { | |||
return (!!obj) && (obj.constructor === Object); | |||
} | |||
function IsOverridden(name) { | |||
return this.listenerCount(name) > 1; | |||
} | |||
function ResponseBuilder(self) { | |||
var responseObject = self.response; | |||
responseObject.version = '1.0'; | |||
responseObject.response = { | |||
shouldEndSession: true | |||
}; | |||
responseObject.sessionAttributes = self._event.session.attributes; | |||
return (function () { | |||
return { | |||
'speak': function (speechOutput) { | |||
responseObject.response.outputSpeech = createSSMLSpeechObject(speechOutput); | |||
return this; | |||
}, | |||
'listen': function (repromptSpeech) { | |||
responseObject.response.reprompt = { | |||
outputSpeech: createSSMLSpeechObject(repromptSpeech) | |||
}; | |||
responseObject.response.shouldEndSession = false; | |||
return this; | |||
}, | |||
'cardRenderer': function (cardTitle, cardContent, cardImage) { | |||
var card = { | |||
type: 'Simple', | |||
title: cardTitle, | |||
content: cardContent | |||
}; | |||
if(cardImage && (cardImage.smallImageUrl || cardImage.largeImageUrl)) { | |||
card.type = 'Standard'; | |||
card['image'] = {}; | |||
delete card.content; | |||
card.text = cardContent; | |||
if(cardImage.smallImageUrl) { | |||
card.image['smallImageUrl'] = cardImage.smallImageUrl; | |||
} | |||
if(cardImage.largeImageUrl) { | |||
card.image['largeImageUrl'] = cardImage.largeImageUrl; | |||
} | |||
} | |||
responseObject.response.card = card; | |||
return this; | |||
}, | |||
'linkAccountCard': function () { | |||
responseObject.response.card = { | |||
type: 'LinkAccount' | |||
}; | |||
return this; | |||
}, | |||
'audioPlayer': function (directiveType, behavior, url, token, expectedPreviousToken, offsetInMilliseconds) { | |||
var audioPlayerDirective; | |||
if (directiveType === 'play') { | |||
audioPlayerDirective = { | |||
"type": "AudioPlayer.Play", | |||
"playBehavior": behavior, | |||
"audioItem": { | |||
"stream": { | |||
"url": url, | |||
"token": token, | |||
"expectedPreviousToken": expectedPreviousToken, | |||
"offsetInMilliseconds": offsetInMilliseconds | |||
} | |||
} | |||
}; | |||
} else if (directiveType === 'stop') { | |||
audioPlayerDirective = { | |||
"type": "AudioPlayer.Stop" | |||
}; | |||
} else { | |||
audioPlayerDirective = { | |||
"type": "AudioPlayer.ClearQueue", | |||
"clearBehavior": behavior | |||
}; | |||
} | |||
responseObject.response.directives = [audioPlayerDirective]; | |||
return this; | |||
}, | |||
'audioPlayerPlay': function (behavior, url, token, expectedPreviousToken, offsetInMilliseconds) { | |||
var audioPlayerDirective = { | |||
"type": "AudioPlayer.Play", | |||
"playBehavior": behavior, | |||
"audioItem": { | |||
"stream": { | |||
"url": url, | |||
"token": token, | |||
"expectedPreviousToken": expectedPreviousToken, | |||
"offsetInMilliseconds": offsetInMilliseconds | |||
} | |||
} | |||
}; | |||
responseObject.response.directives = [audioPlayerDirective]; | |||
return this; | |||
}, | |||
'audioPlayerStop': function () { | |||
var audioPlayerDirective = { | |||
"type": "AudioPlayer.Stop" | |||
}; | |||
responseObject.response.directives = [audioPlayerDirective]; | |||
return this; | |||
}, | |||
'audioPlayerClearQueue': function (clearBehavior) { | |||
var audioPlayerDirective = { | |||
"type": "AudioPlayer.ClearQueue", | |||
"clearBehavior": clearBehavior | |||
}; | |||
responseObject.response.directives = [audioPlayerDirective]; | |||
return this; | |||
} | |||
} | |||
})(); | |||
} | |||
function createSSMLSpeechObject(message) { | |||
return { | |||
type: 'SSML', | |||
ssml: `<speak> ${message} </speak>` | |||
}; | |||
} | |||
function createStateHandler(state, obj){ | |||
if(!obj) { | |||
obj = {}; | |||
} | |||
Object.defineProperty(obj, _StateString, { | |||
value: state || '' | |||
}); | |||
return obj; | |||
} | |||
function EmitWithState() { | |||
if(arguments.length === 0) { | |||
throw new Error('EmitWithState called without arguments'); | |||
} | |||
arguments[0] = arguments[0] + this.state; | |||
if (this.listenerCount(arguments[0]) < 1) { | |||
arguments[0] = 'Unhandled' + this.state; | |||
} | |||
if (this.listenerCount(arguments[0]) < 1) { | |||
throw new Error(`No 'Unhandled' function defined for event: ${arguments[0]}`); | |||
} | |||
this.emit.apply(this, arguments); | |||
} | |||
process.on('uncaughtException', function(err) { | |||
console.log(`Uncaught exception: ${err}\n${err.stack}`); | |||
throw err; | |||
}); | |||
module.exports.LambdaHandler = alexaRequestHandler; | |||
module.exports.CreateStateHandler = createStateHandler; | |||
module.exports.StateString = _StateString; |
@@ -0,0 +1,364 @@ | |||
'use strict'; | |||
var attributesHelper = require('./DynamoAttributesHelper'); | |||
module.exports = (function () { | |||
return { | |||
':tell': function (speechOutput) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
this.handler.response = buildSpeechletResponse({ | |||
sessionAttributes: this.attributes, | |||
output: getSSMLResponse(speechOutput), | |||
shouldEndSession: true | |||
}); | |||
this.emit(':responseReady'); | |||
}, | |||
':ask': function (speechOutput, repromptSpeech) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
this.handler.response = buildSpeechletResponse({ | |||
sessionAttributes: this.attributes, | |||
output: getSSMLResponse(speechOutput), | |||
reprompt: getSSMLResponse(repromptSpeech), | |||
shouldEndSession: false | |||
}); | |||
this.emit(':responseReady'); | |||
}, | |||
':askWithCard': function(speechOutput, repromptSpeech, cardTitle, cardContent, imageObj) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
this.handler.response = buildSpeechletResponse({ | |||
sessionAttributes: this.attributes, | |||
output: getSSMLResponse(speechOutput), | |||
reprompt: getSSMLResponse(repromptSpeech), | |||
cardTitle: cardTitle, | |||
cardContent: cardContent, | |||
cardImage: imageObj, | |||
shouldEndSession: false | |||
}); | |||
this.emit(':responseReady'); | |||
}, | |||
':tellWithCard': function(speechOutput, cardTitle, cardContent, imageObj) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
this.handler.response = buildSpeechletResponse({ | |||
sessionAttributes: this.attributes, | |||
output: getSSMLResponse(speechOutput), | |||
cardTitle: cardTitle, | |||
cardContent: cardContent, | |||
cardImage: imageObj, | |||
shouldEndSession: true | |||
}); | |||
this.emit(':responseReady'); | |||
}, | |||
':tellWithLinkAccountCard': function(speechOutput) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
this.handler.response = buildSpeechletResponse({ | |||
sessionAttributes: this.attributes, | |||
output: getSSMLResponse(speechOutput), | |||
cardType: 'LinkAccount', | |||
shouldEndSession: true | |||
}); | |||
this.emit(':responseReady'); | |||
}, | |||
':askWithLinkAccountCard': function(speechOutput, repromptSpeech) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
this.handler.response = buildSpeechletResponse({ | |||
sessionAttributes: this.attributes, | |||
output: getSSMLResponse(speechOutput), | |||
reprompt: getSSMLResponse(repromptSpeech), | |||
cardType: 'LinkAccount', | |||
shouldEndSession: false | |||
}); | |||
this.emit(':responseReady'); | |||
}, | |||
':tellWithPermissionCard': function(speechOutput, permissions) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
this.handler.response = buildSpeechletResponse({ | |||
sessionAttributes: this.attributes, | |||
output: getSSMLResponse(speechOutput), | |||
permissions: permissions, | |||
cardType: 'AskForPermissionsConsent', | |||
shouldEndSession: true | |||
}); | |||
this.emit(':responseReady'); | |||
}, | |||
':delegate': function(updatedIntent) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
this.handler.response = buildSpeechletResponse({ | |||
sessionAttributes: this.attributes, | |||
directives: getDialogDirectives('Dialog.Delegate', updatedIntent, null), | |||
shouldEndSession: false | |||
}); | |||
this.emit(':responseReady'); | |||
}, | |||
':elicitSlot': function (slotName, speechOutput, repromptSpeech, updatedIntent) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
this.handler.response = buildSpeechletResponse({ | |||
sessionAttributes: this.attributes, | |||
output: getSSMLResponse(speechOutput), | |||
reprompt: getSSMLResponse(repromptSpeech), | |||
directives: getDialogDirectives('Dialog.ElicitSlot', updatedIntent, slotName), | |||
shouldEndSession: false | |||
}); | |||
this.emit(':responseReady'); | |||
}, | |||
':elicitSlotWithCard': function (slotName, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
this.handler.response = buildSpeechletResponse({ | |||
sessionAttributes: this.attributes, | |||
output: getSSMLResponse(speechOutput), | |||
reprompt: getSSMLResponse(repromptSpeech), | |||
cardTitle: cardTitle, | |||
cardContent: cardContent, | |||
cardImage: imageObj, | |||
directives: getDialogDirectives('Dialog.ElicitSlot', updatedIntent, slotName), | |||
shouldEndSession: false | |||
}); | |||
this.emit(':responseReady'); | |||
}, | |||
':confirmSlot': function (slotName, speechOutput, repromptSpeech, updatedIntent) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
this.handler.response = buildSpeechletResponse({ | |||
sessionAttributes: this.attributes, | |||
output: getSSMLResponse(speechOutput), | |||
reprompt: getSSMLResponse(repromptSpeech), | |||
directives: getDialogDirectives('Dialog.ConfirmSlot', updatedIntent, slotName), | |||
shouldEndSession: false | |||
}); | |||
this.emit(':responseReady'); | |||
}, | |||
':confirmSlotWithCard': function (slotName, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
this.handler.response = buildSpeechletResponse({ | |||
sessionAttributes: this.attributes, | |||
output: getSSMLResponse(speechOutput), | |||
reprompt: getSSMLResponse(repromptSpeech), | |||
cardTitle: cardTitle, | |||
cardContent: cardContent, | |||
cardImage: imageObj, | |||
directives: getDialogDirectives('Dialog.ConfirmSlot', updatedIntent, slotName), | |||
shouldEndSession: false | |||
}); | |||
this.emit(':responseReady'); | |||
}, | |||
':confirmIntent': function (speechOutput, repromptSpeech, updatedIntent) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
this.handler.response = buildSpeechletResponse({ | |||
sessionAttributes: this.attributes, | |||
output: getSSMLResponse(speechOutput), | |||
reprompt: getSSMLResponse(repromptSpeech), | |||
directives: getDialogDirectives('Dialog.ConfirmIntent', updatedIntent, null), | |||
shouldEndSession: false | |||
}); | |||
this.emit(':responseReady'); | |||
}, | |||
':confirmIntentWithCard': function (speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
this.handler.response = buildSpeechletResponse({ | |||
sessionAttributes: this.attributes, | |||
output: getSSMLResponse(speechOutput), | |||
reprompt: getSSMLResponse(repromptSpeech), | |||
cardTitle: cardTitle, | |||
cardContent: cardContent, | |||
cardImage: imageObj, | |||
directives: getDialogDirectives('Dialog.ConfirmIntent', updatedIntent, null), | |||
shouldEndSession: false | |||
}); | |||
this.emit(':responseReady'); | |||
}, | |||
':responseReady': function () { | |||
if (this.isOverridden()) { | |||
return; | |||
} | |||
if(this.handler.state) { | |||
this.handler.response.sessionAttributes['STATE'] = this.handler.state; | |||
} | |||
if (this.handler.dynamoDBTableName) { | |||
this.emit(':saveState'); | |||
} | |||
if(typeof this.callback === 'undefined') { | |||
this.context.succeed(this.handler.response); | |||
} else { | |||
this.callback(null, this.handler.response); | |||
} | |||
}, | |||
':saveState': function(forceSave) { | |||
if (this.isOverridden()) { | |||
return; | |||
} | |||
if(forceSave && this.handler.state){ | |||
this.attributes['STATE'] = this.handler.state; | |||
} | |||
var userId = ''; | |||
// Long-form audio enabled skills use event.context | |||
if (this.event.context) { | |||
userId = this.event.context.System.user.userId; | |||
} else if (this.event.session) { | |||
userId = this.event.session.user.userId; | |||
} | |||
if(this.handler.saveBeforeResponse || forceSave || this.handler.response.response.shouldEndSession) { | |||
attributesHelper.set(this.handler.dynamoDBTableName, userId, this.attributes, (err) => { | |||
if(err) { | |||
return this.emit(':saveStateError', err); | |||
} | |||
}); | |||
} | |||
}, | |||
':saveStateError': function(err) { | |||
if(this.isOverridden()) { | |||
return; | |||
} | |||
console.log(`Error saving state: ${err}\n${err.stack}`); | |||
if(typeof this.callback === 'undefined') { | |||
this.context.fail(err); | |||
} else { | |||
this.callback(err); | |||
} | |||
} | |||
}; | |||
})(); | |||
function createSpeechObject(optionsParam) { | |||
if (optionsParam && optionsParam.type === 'SSML') { | |||
return { | |||
type: optionsParam.type, | |||
ssml: optionsParam['speech'] | |||
}; | |||
} else { | |||
return { | |||
type: optionsParam.type || 'PlainText', | |||
text: optionsParam['speech'] || optionsParam | |||
}; | |||
} | |||
} | |||
function buildSpeechletResponse(options) { | |||
var alexaResponse = { | |||
shouldEndSession: options.shouldEndSession | |||
}; | |||
if (options.output) { | |||
alexaResponse.outputSpeech = createSpeechObject(options.output); | |||
} | |||
if (options.reprompt) { | |||
alexaResponse.reprompt = { | |||
outputSpeech: createSpeechObject(options.reprompt) | |||
}; | |||
} | |||
if (options.directives) { | |||
alexaResponse.directives = options.directives; | |||
} | |||
if (options.cardTitle && options.cardContent) { | |||
alexaResponse.card = { | |||
type: 'Simple', | |||
title: options.cardTitle, | |||
content: options.cardContent | |||
}; | |||
if(options.cardImage && (options.cardImage.smallImageUrl || options.cardImage.largeImageUrl)) { | |||
alexaResponse.card.type = 'Standard'; | |||
alexaResponse.card['image'] = {}; | |||
delete alexaResponse.card.content; | |||
alexaResponse.card.text = options.cardContent; | |||
if(options.cardImage.smallImageUrl) { | |||
alexaResponse.card.image['smallImageUrl'] = options.cardImage.smallImageUrl; | |||
} | |||
if(options.cardImage.largeImageUrl) { | |||
alexaResponse.card.image['largeImageUrl'] = options.cardImage.largeImageUrl; | |||
} | |||
} | |||
} else if (options.cardType === 'LinkAccount') { | |||
alexaResponse.card = { | |||
type: 'LinkAccount' | |||
}; | |||
} else if (options.cardType === 'AskForPermissionsConsent') { | |||
alexaResponse.card = { | |||
type: 'AskForPermissionsConsent', | |||
permissions: options.permissions | |||
}; | |||
} | |||
var returnResult = { | |||
version: '1.0', | |||
response: alexaResponse | |||
}; | |||
if (options.sessionAttributes) { | |||
returnResult.sessionAttributes = options.sessionAttributes; | |||
} | |||
return returnResult; | |||
} | |||
// TODO: check for ssml content in card | |||
function getSSMLResponse(message) { | |||
if (message == null) { | |||
return null; | |||
} else { | |||
return { | |||
type: 'SSML', | |||
speech: `<speak> ${message} </speak>` | |||
}; | |||
} | |||
} | |||
function getDialogDirectives(dialogType, updatedIntent, slotName) { | |||
let directive = { | |||
type: dialogType | |||
}; | |||
if (dialogType === 'Dialog.ElicitSlot') { | |||
directive.slotToElicit = slotName; | |||
} else if (dialogType === 'Dialog.ConfirmSlot') { | |||
directive.slotToConfirm = slotName; | |||
} | |||
if (updatedIntent) { | |||
directive.updatedIntent = updatedIntent; | |||
} | |||
return [directive]; | |||
} |
@@ -0,0 +1,110 @@ | |||
{ | |||
"_args": [ | |||
[ | |||
"alexa-sdk@^1.0.6", | |||
"/home/binhong/dota2-alexa-skill/src" | |||
] | |||
], | |||
"_from": "alexa-sdk@>=1.0.6 <2.0.0", | |||
"_id": "alexa-sdk@1.0.11", | |||
"_inCache": true, | |||
"_installable": true, | |||
"_location": "/alexa-sdk", | |||
"_nodeVersion": "6.9.1", | |||
"_npmOperationalInternal": { | |||
"host": "s3://npm-registry-packages", | |||
"tmp": "tmp/alexa-sdk-1.0.11.tgz_1498871748051_0.3228772508446127" | |||
}, | |||
"_npmUser": { | |||
"email": "alexa-lantern-team@amazon.com", | |||
"name": "alexalantern" | |||
}, | |||
"_npmVersion": "3.10.8", | |||
"_phantomChildren": {}, | |||
"_requested": { | |||
"name": "alexa-sdk", | |||
"raw": "alexa-sdk@^1.0.6", | |||
"rawSpec": "^1.0.6", | |||
"scope": null, | |||
"spec": ">=1.0.6 <2.0.0", | |||
"type": "range" | |||
}, | |||
"_requiredBy": [ | |||
"/" | |||
], | |||
"_resolved": "https://registry.npmjs.org/alexa-sdk/-/alexa-sdk-1.0.11.tgz", | |||
"_shasum": "bbc5d95c9878bc76c4b48f7909ad620c81044995", | |||
"_shrinkwrap": null, | |||
"_spec": "alexa-sdk@^1.0.6", | |||
"_where": "/home/binhong/dota2-alexa-skill/src", | |||
"author": { | |||
"name": "Amazon.com" | |||
}, | |||
"bugs": { | |||
"url": "https://github.com/alexa/alexa-skill-sdk-for-nodejs/issues" | |||
}, | |||
"contributors": [ | |||
{ | |||
"name": "Diego Benitez", | |||
"email": "diegoben@amazon.com" | |||
}, | |||
{ | |||
"name": "Yang Zhang", | |||
"email": "ygzg@amazon.com" | |||
}, | |||
{ | |||
"name": "Brendan Clement", | |||
"email": "clebrend@amazon.com" | |||
}, | |||
{ | |||
"name": "Leandro Pascual", | |||
"email": "leandrop@amazon.com" | |||
}, | |||
{ | |||
"name": "Krish Furia", | |||
"email": "krishf@amazon.com" | |||
} | |||
], | |||
"dependencies": { | |||
"aws-sdk": "^2.4.7", | |||
"i18next": "^3.4.1", | |||
"i18next-sprintf-postprocessor": "^0.2.2" | |||
}, | |||
"description": "Alexa Skill SDK for Node.js", | |||
"devDependencies": {}, | |||
"directories": {}, | |||
"dist": { | |||
"shasum": "bbc5d95c9878bc76c4b48f7909ad620c81044995", | |||
"tarball": "https://registry.npmjs.org/alexa-sdk/-/alexa-sdk-1.0.11.tgz" | |||
}, | |||
"gitHead": "3c91133e99ab404d7aeb6765cb01ecb2100494da", | |||
"homepage": "https://github.com/alexa/alexa-skill-sdk-for-nodejs#readme", | |||
"keywords": [ | |||
"Alexa", | |||
"sdk", | |||
"skill" | |||
], | |||
"license": "Apache-2.0", | |||
"main": "index.js", | |||
"maintainers": [ | |||
{ | |||
"name": "alexalantern", | |||
"email": "alexa-lantern-team@amazon.com" | |||
}, | |||
{ | |||
"name": "diegoben", | |||
"email": "alexa-npm-admin@amazon.com" | |||
} | |||
], | |||
"name": "alexa-sdk", | |||
"optionalDependencies": {}, | |||
"readme": "ERROR: No README data found!", | |||
"repository": { | |||
"type": "git", | |||
"url": "git+https://github.com/alexa/alexa-skill-sdk-for-nodejs.git" | |||
}, | |||
"scripts": { | |||
"test": "echo \"Error: no test specified\" && exit 1" | |||
}, | |||
"version": "1.0.11" | |||
} |
@@ -0,0 +1,17 @@ | |||
[ | |||
{ | |||
"type": "bugfix", | |||
"category": "JSON", | |||
"description": "Fixes issue caused when trying to unmarshall null binary shapes." | |||
}, | |||
{ | |||