├── .ask
└── config
├── .github
└── PULL_REQUEST_TEMPLATE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.txt
├── NOTICE.txt
├── README.md
├── hooks
├── post_new_hook.ps1
├── post_new_hook.sh
├── pre_deploy_hook.ps1
└── pre_deploy_hook.sh
├── instructions
├── 0-intro.md
├── 1-voice-user-interface.md
├── 2-lambda-function.md
├── 3-connect-vui-to-code.md
├── 4-testing.md
├── 5-customization.md
└── 6-publication.md
├── lambda
├── bin
│ └── deploy.js
└── custom
│ ├── configuration.js
│ ├── constants.js
│ ├── eventHandlers.js
│ ├── feedHelper.js
│ ├── index.js
│ ├── intentHandlers.js
│ ├── logHelper.js
│ ├── package.json
│ ├── s3Helper.js
│ ├── speechHandlers.js
│ └── stateHandlers.js
├── models
└── en-US.json
└── skill.json
/.ask/config:
--------------------------------------------------------------------------------
1 | {
2 | "deploy_settings": {
3 | "default": {
4 | "skill_id": "",
5 | "was_cloned": false,
6 | "merge": {}
7 | }
8 | }
9 | }
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | *Issue #, if available:*
2 |
3 | *Description of changes:*
4 |
5 |
6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
7 |
--------------------------------------------------------------------------------
/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.
5 |
--------------------------------------------------------------------------------
/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/alexa/skill-sample-nodejs-feed/issues), or [recently closed](https://github.com/alexa/skill-sample-nodejs-feed/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/alexa/skill-sample-nodejs-feed/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/alexa/skill-sample-nodejs-feed/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.
62 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Amazon Software License
3 |
4 | This Amazon Software License (“License”) governs your use, reproduction, and distribution of the accompanying software as specified below.
5 | 1. Definitions
6 |
7 | “Licensor” means any person or entity that distributes its Work.
8 |
9 | “Software” means the original work of authorship made available under this License.
10 |
11 | “Work” means the Software and any additions to or derivative works of the Software that are made available under this License.
12 |
13 | The terms “reproduce,” “reproduction,” “derivative works,” and “distribution” have the meaning as provided under U.S. copyright law; provided, however, that 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.
14 |
15 | Works, including the Software, are “made available” under this License by including in or with the Work either (a) a copyright notice referencing the applicability of this License to the Work, or (b) a copy of this License.
16 | 2. License Grants
17 |
18 | 2.1 Copyright Grant. Subject to the terms and conditions of this License, each Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free, copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense and distribute its Work and any resulting derivative works in any form.
19 |
20 | 2.2 Patent Grant. Subject to the terms and conditions of this License, each Licensor grants to you a perpetual, worldwide, non-exclusive, royalty-free patent license to make, have made, use, sell, offer for sale, import, and otherwise transfer its Work, in whole or in part. The foregoing license applies only to the patent claims licensable by Licensor that would be infringed by Licensor’s Work (or portion thereof) individually and excluding any combinations with any other materials or technology.
21 | 3. Limitations
22 |
23 | 3.1 Redistribution. You may reproduce or distribute the Work only if (a) you do so under this License, (b) you include a complete copy of this License with your distribution, and (c) you retain without modification any copyright, patent, trademark, or attribution notices that are present in the Work.
24 |
25 | 3.2 Derivative Works. You may specify that additional or different terms apply to the use, reproduction, and distribution of your derivative works of the Work (“Your Terms”) only if (a) Your Terms provide that the use limitation in Section 3.3 applies to your derivative works, and (b) you identify the specific derivative works that are subject to Your Terms. Notwithstanding Your Terms, this License (including the redistribution requirements in Section 3.1) will continue to apply to the Work itself.
26 |
27 | 3.3 Use Limitation. The Work and any derivative works thereof only may be used or intended for use with the web services, computing platforms or applications provided by Amazon.com, Inc. or its affiliates, including Amazon Web Services, Inc.
28 |
29 | 3.4 Patent Claims. If you bring or threaten to bring a patent claim against any Licensor (including any claim, cross-claim or counterclaim in a lawsuit) to enforce any patents that you allege are infringed by any Work, then your rights under this License from such Licensor (including the grants in Sections 2.1 and 2.2) will terminate immediately.
30 |
31 | 3.5 Trademarks. This License does not grant any rights to use any Licensor’s or its affiliates’ names, logos, or trademarks, except as necessary to reproduce the notices described in this License.
32 |
33 | 3.6 Termination. If you violate any term of this License, then your rights under this License (including the grants in Sections 2.1 and 2.2) will terminate immediately.
34 | 4. Disclaimer of Warranty.
35 |
36 | THE WORK IS PROVIDED “AS IS” WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WARRANTIES OR CONDITIONS OF M ERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT. YOU BEAR THE RISK OF UNDERTAKING ANY ACTIVITIES UNDER THIS LICENSE. SOME STATES’ CONSUMER LAWS DO NOT ALLOW EXCLUSION OF AN IMPLIED WARRANTY, SO THIS DISCLAIMER MAY NOT APPLY TO YOU.
37 | 5. Limitation of Liability.
38 |
39 | EXCEPT AS PROHIBITED BY APPLICABLE LAW, IN NO EVENT AND UNDER NO LEGAL THEORY, WHETHER IN TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE SHALL ANY LICENSOR BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR RELATED TO THIS LICENSE, THE USE OR INABILITY TO USE THE WORK (INCLUDING BUT NOT LIMITED TO LOSS OF GOODWILL, BUSINESS INTERRUPTION, LOST PROFITS OR DATA, COMPUTER FAILURE OR MALFUNCTION, OR ANY OTHER COMM ERCIAL DAMAGES OR LOSSES), EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
--------------------------------------------------------------------------------
/NOTICE.txt:
--------------------------------------------------------------------------------
1 | Skill Sample Nodejs Feed
2 | Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Build An Alexa Feed Reader Skill
2 |
3 |
4 | [](./instructions/1-voice-user-interface.md)[](./instructions/2-lambda-function.md)[](./instructions/3-connect-vui-to-code.md)[](./instructions/4-testing.md)[](./instructions/5-customization.md)[](./instructions/6-publication.md)
5 |
6 | # Build a Feed Reader Skill for Alexa
7 | This tutorial will walk first-time Alexa skills developers through all the required steps involved in creating a feed reading skill using a template called ‘Feed Reader’. Ask to play your feed and this skill will do so.
8 |
9 | # Let's Get Started
10 | If this is your first time here, you're new to Alexa Skills Development, or you're looking for more detailed instructions, click the **Get Started** button below:
11 |
12 |
13 |
14 |
15 |
16 |
17 | Be sure to take a look at the [Additional Resources](#additional-resources) at the bottom of this page!
18 |
19 |
20 | ## About
21 | **Note:** The rest of this readme assumes you have your developer environment ready to go and that you have some familiarity with CLI (Command Line Interface) Tools, [AWS](https://aws.amazon.com/), and the [ASK Developer Portal](https://developer.amazon.com/alexa-skills-kit). If not, [click here](./instructions/0-intro.md) for a more detailed walkthrough.
22 |
23 |
24 |
25 | ### Usage
26 |
27 | ```text
28 | Alexa, ask feed reader to open first feed.
29 | >> Your first feed is BBC World News.
30 |
31 | Alexa, open feed reader
32 | ```
33 |
34 | ### Repository Contents
35 | * `/.ask` - [ASK CLI (Command Line Interface) Configuration](https://developer.amazon.com/docs/smapi/ask-cli-intro.html)
36 | * `/lambda/custom` - Back-End Logic for the Alexa Skill hosted on [AWS Lambda](https://aws.amazon.com/lambda/)
37 | * `/models` - Voice User Interface and Language Specific Interaction Models
38 | * `/instructions` - Step-by-Step Instructions for Getting Started
39 | * `skill.json` - [Skill Manifest](https://developer.amazon.com/docs/smapi/skill-manifest.html)
40 |
41 | ## Setup w/ ASK CLI
42 |
43 | ### Pre-requisites
44 |
45 | * Node.js (> v4.3)
46 | * Register for an [AWS Account](https://aws.amazon.com/)
47 | * Register for an [Amazon Developer Account](https://developer.amazon.com/)
48 | * Install and Setup [ASK CLI](https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html)
49 |
50 | ### Installation
51 | 1. Clone the repository.
52 |
53 | ```bash
54 | $ git clone https://github.com/alexa/skill-sample-nodejs-feed/
55 | ```
56 |
57 | 2. Initiatialize the [ASK CLI](https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html) by Navigating into the repository and running npm command: `ask init`. Follow the prompts.
58 |
59 | ```bash
60 | $ cd skill-sample-nodejs-feed
61 | $ ask init
62 | ```
63 |
64 | 3. Install npm dependencies by navigating into the `/lambda/custom` directory and running the npm command: `npm install`
65 |
66 | ```bash
67 | $ cd lambda/custom
68 | $ npm install
69 | ```
70 |
71 |
72 | ### Deployment
73 |
74 | ASK CLI will create the skill and the lambda function for you. The Lambda function will be created in ```us-east-1 (Northern Virginia)``` by default.
75 |
76 | 1. Deploy the skill and the lambda function in one step by running the following command:
77 |
78 | ```bash
79 | $ ask deploy
80 | ```
81 |
82 | ### Testing
83 |
84 | 1. To test, you need to login to Alexa Developer Console, and enable the "Test" switch on your skill from the "Test" Tab.
85 |
86 | 2. Simulate verbal interaction with your skill through the command line using the following example:
87 |
88 | ```bash
89 | $ ask simulate -l en-US -t "start feed reader"
90 |
91 | ✓ Simulation created for simulation id: 4a7a9ed8-94b2-40c0-b3bd-fb63d9887fa7
92 | ◡ Waiting for simulation response{
93 | "status": "SUCCESSFUL",
94 | ...
95 | ```
96 |
97 | 3. Once the "Test" switch is enabled, your skill can be tested on devices associated with the developer account as well. Speak to Alexa from any enabled device, from your browser at [echosim.io](https://echosim.io/welcome), or through your Amazon Mobile App and say :
98 |
99 | ```text
100 | Alexa, start feed reader
101 | ```
102 |
103 | ## Customization
104 |
105 | 1. ```./skill.json```
106 |
107 | Change the skill name, example phrase, icons, testing instructions etc ...
108 |
109 | Remember that many information is locale-specific and must be changed for each locale (en-GB and en-US)
110 |
111 | See the Skill [Manifest Documentation](https://developer.amazon.com/docs/smapi/skill-manifest.html) for more information.
112 |
113 | 2. ```./lambda/custom/index.js```
114 |
115 | Modify messages, and facts from the source code to customize the skill.
116 |
117 | 3. ```./models/*.json```
118 |
119 | Change the model definition to replace the invocation name and the sample phrase for each intent. Repeat the operation for each locale you are planning to support.
120 |
121 | ## Additional Resources
122 |
123 | ### Community
124 | * [Amazon Developer Forums](https://forums.developer.amazon.com/spaces/165/index.html) - Join the conversation!
125 | * [Hackster.io](https://www.hackster.io/amazon-alexa) - See what others are building with Alexa.
126 |
127 | ### Tutorials & Guides
128 | * [Voice Design Guide](https://developer.amazon.com/designing-for-voice/) - A great resource for learning conversational and voice user interface design.
129 | * [CodeAcademy: Learn Alexa](https://www.codecademy.com/learn/learn-alexa) - Learn how to build an Alexa Skill from within your browser with this beginner friendly tutorial on CodeAcademy!
130 |
131 | ### Documentation
132 | * [Official Alexa Skills Kit Node.js SDK](https://www.npmjs.com/package/alexa-sdk) - The Official Node.js SDK Documentation
133 | * [Official Alexa Skills Kit Documentation](https://developer.amazon.com/docs/ask-overviews/build-skills-with-the-alexa-skills-kit.html) - Official Alexa Skills Kit Documentation
134 |
135 |
136 |
--------------------------------------------------------------------------------
/hooks/post_new_hook.ps1:
--------------------------------------------------------------------------------
1 | # Powershell script for ask-cli post-new hook for Node.js
2 | # Script Usage: post_new_hook.ps1
3 |
4 | # SKILL_NAME is the preformatted name passed from the CLI, after removing special characters.
5 | # DO_DEBUG is boolean value for debug logging
6 |
7 | # Run this script one level outside of the skill root folder
8 |
9 | # The script does the following:
10 | # - Run "npm install" in each sourceDir in skill.json
11 |
12 | param(
13 | [string] $SKILL_NAME,
14 | [bool] $DO_DEBUG = $False
15 | )
16 |
17 | if ($DO_DEBUG) {
18 | Write-Output "###########################"
19 | Write-Output "###### post-new hook ######"
20 | Write-Output "###########################"
21 | }
22 |
23 | function install_dependencies ($CWD, $SOURCE_DIR) {
24 | $INSTALL_PATH = $SKILL_NAME + "\" +$SOURCE_DIR
25 | Set-Location $INSTALL_PATH
26 | Invoke-Expression "npm install" 2>&1 | Out-Null
27 | $EXEC_RESULT = $?
28 | Set-Location $CWD
29 | return $EXEC_RESULT
30 | }
31 |
32 | $SKILL_FILE_PATH = $SKILL_NAME + "\skill.json"
33 | $ALL_SOURCE_DIRS = Get-Content -Path $SKILL_FILE_PATH | select-string -Pattern "sourceDir" -CaseSensitive
34 | Foreach ($SOURCE_DIR in $ALL_SOURCE_DIRS) {
35 | $FILTER_SOURCE_DIR = $SOURCE_DIR -replace "`"", "" -replace "\s", "" -replace ",","" -replace "sourceDir:", ""
36 | $CWD = (Get-Location).Path
37 | if (install_dependencies $CWD $FILTER_SOURCE_DIR) {
38 | if ($DO_DEBUG) {
39 | Write-Output "Codebase ($FILTER_SOURCE_DIR) built successfully."
40 | }
41 | } else {
42 | if ($DO_DEBUG) {
43 | Write-Output "There was a problem installing dependencies for ($FILTER_SOURCE_DIR)."
44 | }
45 | exit 1
46 | }
47 | }
48 |
49 | if ($DO_DEBUG) {
50 | Write-Output "###########################"
51 | }
52 |
53 | exit 0
54 |
--------------------------------------------------------------------------------
/hooks/post_new_hook.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Shell script for ask-cli post-new hook for Node.js
3 | # Script Usage: post_new_hook.sh
4 |
5 | # SKILL_NAME is the preformatted name passed from the CLI, after removing special characters.
6 | # DO_DEBUG is boolean value for debug logging
7 |
8 | # Run this script one level outside of the skill root folder
9 |
10 | # The script does the following:
11 | # - Run "npm install" in each sourceDir in skill.json
12 |
13 | SKILL_NAME=$1
14 | DO_DEBUG=${2:-false}
15 |
16 | if [ $DO_DEBUG == false ]
17 | then
18 | exec > /dev/null 2>&1
19 | fi
20 |
21 | install_dependencies() {
22 | npm install --prefix "$SKILL_NAME/$1" >/dev/null 2>&1
23 | return $?
24 | }
25 |
26 | echo "###########################"
27 | echo "###### post-new hook ######"
28 | echo "###########################"
29 |
30 | grep "sourceDir" $SKILL_NAME/skill.json | cut -d: -f2 | sed 's/"//g' | sed 's/,//g' | while read -r SOURCE_DIR; do
31 | if install_dependencies $SOURCE_DIR; then
32 | echo "Codebase ($SOURCE_DIR) built successfully."
33 | else
34 | echo "There was a problem installing dependencies for ($SOURCE_DIR)."
35 | exit 1
36 | fi
37 | done
38 | echo "###########################"
39 |
40 | exit 0
41 |
42 |
--------------------------------------------------------------------------------
/hooks/pre_deploy_hook.ps1:
--------------------------------------------------------------------------------
1 | # Powershell script for ask-cli pre-deploy hook for Node.js
2 | # Script Usage: pre_deploy_hook.ps1
3 |
4 | # SKILL_NAME is the preformatted name passed from the CLI, after removing special characters.
5 | # DO_DEBUG is boolean value for debug logging
6 | # TARGET is the deploy TARGET provided to the CLI. (eg: all, skill, lambda etc.)
7 |
8 | # Run this script under the skill root folder
9 |
10 | # The script does the following:
11 | # - Run "npm install" in each sourceDir in skill.json
12 |
13 | param(
14 | [string] $SKILL_NAME,
15 | [bool] $DO_DEBUG = $False,
16 | [string] $TARGET = "all"
17 | )
18 |
19 | function install_dependencies ($CWD, $SOURCE_DIR) {
20 | $INSTALL_PATH = $SKILL_NAME + "\" +$SOURCE_DIR
21 | Set-Location $INSTALL_PATH
22 | Invoke-Expression "npm install" 2>&1 | Out-Null
23 | $EXEC_RESULT = $?
24 | Set-Location $CWD
25 | return $EXEC_RESULT
26 | }
27 |
28 | if ($DO_DEBUG) {
29 | Write-Output "###########################"
30 | Write-Output "##### pre-deploy hook #####"
31 | Write-Output "###########################"
32 | }
33 |
34 | if ($TARGET -eq "all" -Or $TARGET -eq "lambda") {
35 | $ALL_SOURCE_DIRS = Get-Content -Path "skill.json" | select-string -Pattern "sourceDir" -CaseSensitive
36 | Foreach ($SOURCE_DIR in $ALL_SOURCE_DIRS) {
37 | $FILTER_SOURCE_DIR = $SOURCE_DIR -replace "`"", "" -replace "\s", "" -replace ",","" -replace "sourceDir:", ""
38 | $CWD = (Get-Location).Path
39 | if (install_dependencies $CWD $FILTER_SOURCE_DIR) {
40 | if ($DO_DEBUG) {
41 | Write-Output "Codebase ($FILTER_SOURCE_DIR) built successfully."
42 | }
43 | } else {
44 | if ($DO_DEBUG) {
45 | Write-Output "There was a problem installing dependencies for ($FILTER_SOURCE_DIR)."
46 | }
47 | exit 1
48 | }
49 | }
50 | if ($DO_DEBUG) {
51 | Write-Output "###########################"
52 | }
53 | }
54 |
55 | exit 0
--------------------------------------------------------------------------------
/hooks/pre_deploy_hook.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Shell script for ask-cli pre-deploy hook for Node.js
3 | # Script Usage: pre_deploy_hook.sh
4 |
5 | # SKILL_NAME is the preformatted name passed from the CLI, after removing special characters.
6 | # DO_DEBUG is boolean value for debug logging
7 | # TARGET is the deploy TARGET provided to the CLI. (eg: all, skill, lambda etc.)
8 |
9 | # Run this script under skill root folder
10 |
11 | # The script does the following:
12 | # - Run "npm install" in each sourceDir in skill.json
13 |
14 | SKILL_NAME=$1
15 | DO_DEBUG=${2:-false}
16 | TARGET=${3:-"all"}
17 |
18 | if [ $DO_DEBUG == false ]
19 | then
20 | exec > /dev/null 2>&1
21 | fi
22 |
23 | install_dependencies() {
24 | npm install --prefix "$1" >/dev/null 2>&1
25 | return $?
26 | }
27 |
28 | echo "###########################"
29 | echo "##### pre-deploy hook #####"
30 | echo "###########################"
31 |
32 | if [[ $TARGET == "all" || $TARGET == "lambda" ]]; then
33 | grep "sourceDir" ./skill.json | cut -d: -f2 | sed 's/"//g' | sed 's/,//g' | while read -r SOURCE_DIR; do
34 | if install_dependencies $SOURCE_DIR; then
35 | echo "Codebase ($SOURCE_DIR) built successfully."
36 | else
37 | echo "There was a problem installing dependencies for ($SOURCE_DIR)."
38 | exit 1
39 | fi
40 | done
41 | echo "###########################"
42 | fi
43 |
44 | exit 0
45 |
--------------------------------------------------------------------------------
/instructions/0-intro.md:
--------------------------------------------------------------------------------
1 | # Build An Alexa Feed Reader Skill
2 |
3 |
4 | [](1-voice-user-interface.md)[](2-lambda-function.md)[](3-connect-vui-to-code.md)[](4-testing.md)[](5-customization.md)[](6-publication.md)
5 |
6 | # How to Build a Feed Reader Skill for Alexa
7 |
8 | ## What You Will Learn
9 | * [AWS Lambda](http://aws.amazon.com/lambda)
10 | * [Alexa Skills Kit (ASK)](https://developer.amazon.com/alexa-skills-kit)
11 | * Voice User Interface (VUI) Design
12 | * Skill Certification
13 | * State Management
14 | * [Speechcons](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/speechcon-reference)
15 |
16 | ## What You Will Need
17 | * [Amazon Developer Portal Account](http://developer.amazon.com)
18 | * [Amazon Web Services Account](http://aws.amazon.com/)
19 | * The sample code on [GitHub](https://github.com/alexa/skill-sample-nodejs-feed).
20 | * Simple graphical editing tool
21 |
22 | ## What Your Skill Will Do
23 | This is a skill that will read you your favorite RSS feeds with Alexa. You can build favorites, and browse different feeds by categories.
24 |
25 | If you’re in the US, we've also included the new [speechcons](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/speechcon-reference) feature for Alexa skill development. Speechcons are special words and phrases that Alexa pronounces more expressively. We use them in this quiz game to let the user know whether they gave a correct or incorrect answer during the quiz.
26 |
27 |
28 |
29 |
30 |
31 |
32 | **Note:** By creating an Alexa skill based on the Feed skill template, you acknowledge ownership of any RSS/ATOM feed(s) used within the skill, and/or have permission to use the RSS/ATOM feed(s) from the original content owner. Failure to be able to prove ownership or permission to use any feed sources, at any time, will likely cause your skill to be rejected during the certification process, or being removed from the Alexa Skill Store without notice at a later date.
33 |
--------------------------------------------------------------------------------
/instructions/1-voice-user-interface.md:
--------------------------------------------------------------------------------
1 | # Build An Alexa Feed Reader Skill
2 | [](./1-voice-user-interface.md)[](./2-lambda-function.md)[](./3-connect-vui-to-code.md)[](./4-testing.md)[](./5-customization.md)[](./6-publication.md)
3 |
4 | ## Setting up Your Alexa Skill in the Developer Portal
5 |
6 |
7 | 1. Go to the [Alexa Developer Console](https://developer.amazon.com/alexa/console/ask) and _Sign in_ if prompted.
8 |
9 | > If you don't already have an account, you can create a new one for free from the _Sign in_ page.
10 |
11 | 2. Once signed in, select the **Create Skill** button near the top right of the page.
12 |
13 | 3. Give your new skill a **Skill name** that will be the name of the skill in the Alexa Skills Store and the name your users will speak to launch your skill. For example ```Feed Reader```. Once set, click **Create Skill**.
14 |
15 | 4. When prompted to _Choose a template_, make sure _Start from scratch_ is selected and then click **Choose**.
16 |
17 | 5. Build the Interaction Model for your skill.
18 | 1. On the left hand navigation panel. Select the **Invocation** tab. Either use the default invocation name or set your own by entering a different **Skill Inovcation Name**. This is the name that your users will need to say to start your skill.
19 | 2. Next, select the **JSON Editor** tab. In the textfield provided, replace any existing code with the code provided in the [Interaction Model](../models/en-US.json), then click **Build Model**.
20 |
21 | > You should notice that **Intents** and **Slot Types** will auto populate based on the JSON Interaction Model that you have now applied to your skill. Feel free to explore the changes here, to learn about **Intents**, **Slots**, and **Utterances** open our [technical documentation in a new tab](https://developer.amazon.com/docs/custom-skills/define-the-interaction-model-in-json-and-text.html).
22 |
23 | 6. _(Optional)_ Select an intent by expanding the **Intents** from the left side navigation panel. Add some more sample utterances for your newly generated intents. Think of all the different ways that a user could request to make a specific intent happen. A few examples are provided. Be sure to click **Save Model** and **Build Model** after you're done making changes here.
24 |
25 | 7. If your interaction model builds successfully, proceed to the next step.
26 |
27 | If you get an error from your interaction model, check through this list:
28 |
29 | * Did you copy & paste the provided code correctly?
30 | * Did you accidentally add any characters to the Interaction Model or Sample Utterances?
31 |
32 | In our next step of this guide, we will be creating our Lambda function in the AWS developer console, but keep this browser tab open, because we will be returning here in the step [Connect VUI to Code](./3-connect-vui-to-code.md).
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/instructions/2-lambda-function.md:
--------------------------------------------------------------------------------
1 | # Build An Alexa Feed Reader Skill
2 | [](./1-voice-user-interface.md)[](./2-lambda-function.md)[](./3-connect-vui-to-code.md)[](./4-testing.md)[](./5-customization.md)[](./6-publication.md)
3 |
4 | ## Setting Up A Lambda Function Using Amazon Web Services
5 |
6 | In the [Voice User Interface](./1-voice-user-interface.md) step, you built the Voice User Interface (VUI) for our Alexa skill. In this step, we will be creating an AWS Lambda function using [Amazon Web Services](http://aws.amazon.com). You can [read more about what a Lambda function is](http://aws.amazon.com/lambda), but for the purposes of this guide, what you need to know is that AWS Lambda is where our code lives. When a user asks Alexa to use our skill, it is our AWS Lambda function that interprets the appropriate interaction and provides the conversation back to the user.
7 |
8 | 1. Go to the [AWS Console](https://console.aws.amazon.com/console/home) and sign in.
9 |
10 |
11 |
12 | > If you don't already have an account, you will need to create one. [Here is a quick walkthrough for setting up an AWS account](https://github.com/alexa/alexa-cookbook/blob/master/guides/aws-security-and-setup/set-up-aws.md).
13 |
14 | 2. Click _Services_ at the top of the screen, and type ```Lambda``` in the search box. You can also find Lambda in the list of services. It is in the _Compute_ section.
15 |
16 |
17 |
18 | 3. Check that your AWS region is set to US East (N. Virginia): **us-east-1**. AWS Lambda only works with the Alexa Skills Kit in four regions: US East (N. Virginia), EU (Ireland), US West (Oregon) and Asia Pacific (Tokyo). Make sure you choose the region closest to your customers.
19 |
20 |
21 |
22 | 4. Click the **Create function** button near the top right of the page.
23 |
24 |
25 |
26 | 5. When prompted, make sure that **Author from scratch** is selected.
27 | 6. Set the name of the function. ```FeedReader``` is sufficient if you don't have another idea for a name. The name of your function will only be visible to you, but make sure that you name it something meaningful.
28 |
29 |
30 |
31 | 7. Create an AWS Role in IAM with access to DynamoDB, S3 and CloudWatch logs. If you haven't done this before, a [detailed walkthrough for setting up your first role for Lambda](https://alexa.design/create-lambda-role) is available.
32 | 1. Create a new IAM role.
33 | 2. Select the Service type of the role as **Lambda**.
34 |
35 | 3. Add the following policies to the role:
36 | - AmazonDynamoDBFullAccess
37 | - AmazonS3FullAccess
38 | - CloudWatchLogsFullAccess
39 |
40 |
41 | * Once you've set up your role, click the **Create Function** button in the bottom right corner.
42 |
43 | 8. From the _Add triggers_ section on the left add Alexa Skills Kit from the list by clicking on it.
44 |
45 | 9. Once you have selected **Alexa Skills Kit**, scroll down to the bottom of the page. Under _Configure triggers_, select Enable for Skill ID verification. A **Skill ID** Text Box should appear. The value for this input is your Skill ID from the developer portal.
46 |
47 | 10. Now lets secure this lambda function, so that it can only be invoked by your skill. Open up the [developer portal](https://developer.amazon.com/edw/home.html#/skills) and select your skill from the list. You mays till have a browser tab open if you started at the beginning of this tutorial.
48 |
49 | 11. Browse to your list of skills in the [Alexa Developer Console](https://developer.amazon.com/alexa/console/ask) and click the **View Skill ID** link. From the popup Skill ID dialog, select and copy the value. It should look something like the following: `amzn1.ask.skill.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
50 |
51 | 12. Return back to your lambda function in the. You may already have this browser tab open. Otherwise, open the lambda console by clicking here: [AWS Lambda Console](https://console.aws.amazon.com/lambda/home?region=us-east-1#/functions) and selecting the appropriate function. Scroll down to **Configure triggers**, paste the Skill ID in the Skill ID edit box.
52 |
53 | 13. Click the **Add** button. Then click the **Save** button in the top right. You should see a green success message at the top of your screen. Now, click the box that has the Lambda icon followed by the name of your function and scroll down to the field called "Function code".
54 |
55 | 14. Set up your local environment to run the deployment script
56 |
57 | 1. Configure AWS credentials the tool will use to upload code to your Skill. For full details see the instructions on [Configuring the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html).
58 |
59 | 2. The file should have the format, and include keys you retrieve from the AWS console:
60 |
61 | ```
62 | [default]
63 | aws_access_key_id = [KEY FROM AWS]
64 | aws_secret_access_key = [SECRET KEY FROM AWS]
65 | ```
66 |
67 | 3. Setup [NodeJS and NPM](https://nodejs.org/en/download/).
68 |
69 | 4. Get the code and install dependencies:
70 |
71 | ```
72 | git clone https://github.com/alexa/skill-sample-nodejs-feed.git
73 | cd skill-sample-nodejs-feed/lambda/custom
74 | npm install
75 | ```
76 |
77 | 15. Create an S3 Bucket
78 |
79 | In another tab, go to the [Amazon S3 Console](https://s3.console.aws.amazon.com/s3/home?region=us-east-1) and create an AWS S3 Bucket with a unique name like ```feed-skill-bucket-385123```. The S3 bucket name must be unique across all existing bucket names in Amazon S3. In case of a conflict, retry with another name and append with something like your initials or a random value.
80 |
81 |
82 |
83 | 16. **[OPTIONAL]** Create an AWS DynamoDB table
84 | You can manually create a table named MyFeedSkillTable with the case sensitive primary key "userId".
85 |
86 | 
87 |
88 |
89 | 17. Configure the Project to Use Your Feed
90 |
91 | 1. Open ```/lambda/custom/configuration.js``` file.
92 |
93 | 2. Update the following information to configure the skill:
94 |
95 | **appId**: Your Skill's Application ID from the Skill you created at https://developer.amazon.com.
96 | **welcome_message**: A welcome message that will be spoken to the user when they open your skill.
97 | **number\_feeds\_per\_prompt**: The number of items the skill will read each time the user invokes it.
98 | **display\_only\_feed\_title**: A boolean flag that determines whether to speak out the title-only or title and summary of the items in your feed.
99 | **display\_only\_title\_in\_card** : A boolean flag to decide whether to display a card with the title only or title and summary of the items in your feed.
100 | **categories**: The list of RSS feeds you want to include in your Skill. Each feed will be treated as a category.
101 | **speech\_style\_for\_numbering\_feeds**: Naming convention for each item.
102 | **s3BucketName**: Your S3 Bucket Name.
103 | **dynamoDBTableName**: Your DynamoDB Table Name. (If not created, the skill will create it.)
104 |
105 | 3. A sample configuration :
106 |
107 | ```javascript
108 | let config = {
109 | appId : 'amzn1.ask.skill.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
110 | welcome_message : 'Welcome to Feed Skill',
111 | number_feeds_per_prompt : 3,
112 | display_only_feed_title : true,
113 | display_only_title_in_card : true,
114 | categories : {
115 | 'feed name' : 'http://www.example.com/rss-feed.xml'
116 | },
117 | speech_style_for_numbering_feeds : 'Item',
118 | s3BucketName : '{YOUR_S3_BUCKET_NAME}',
119 | dynamoDBTableName : 'MyFeedSkillBucket'
120 | dynamoDBRegion : 'us-east-1'
121 | };
122 | ```
123 |
124 | 18. Deploy Your Skill
125 |
126 | 1. From the commmand-line, go to the `skill-sample-nodejs-feed/lambda/bin/` directory and run `deploy.js` using Node.
127 |
128 | ```
129 | npm install aws-sdk
130 | node deploy.js
131 | ```
132 |
133 |
134 | 2. Go to the the `skill-sample-nodejs-feed/lambda/custom/` directory and zip all of the files. Be sure to only zip the files inside the directory, and not the directory itself. The ```index.js``` file needs to be at the root of the zip file.
135 |
136 | 3. Go to the [AWS Lambda Console](https://console.aws.amazon.com/lambda/home?region=us-east-1#/functions) select your Lambda function, locate the Function code section and upload the file by selecting "Code entry type" as "Upload a .ZIP file".
137 |
138 | 4. Configure the rest of your function
139 | 1. Scroll to the _Basic Settings_ section.
140 | 2. Change the **Timeout** to 30 seconds, since feeds can become an issue.
141 | 3. Leave the defaults for everything else.
142 | 4. Note the ARN of the Lambda you've created, which you'll need later.
143 |
144 | 19. Update the skill interaction model.
145 | 1. During the deploy, a copy of the interaction model was created and named `en-US-updated.json` that contains the categories specified from the configuration file. Locate that file in the _/models_ directory.
146 | 2. Open the file and copy its contents back into the **Intents** JSON Editor of the Alexa Developer Console.
147 | 3. Updating the contents of the interaction model will update the invocation. Doublecheck that you have the invocation name you want to use and save and build the model.
148 |
149 | 20. After you create the function, the ARN value appears in the top right corner. Copy this value for use in the next section of the guide.
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/instructions/3-connect-vui-to-code.md:
--------------------------------------------------------------------------------
1 | # Build An Alexa Feed Reader Skill
2 | [](./1-voice-user-interface.md)[](./2-lambda-function.md)[](./3-connect-vui-to-code.md)[](./4-testing.md)[](./5-customization.md)[](./6-publication.md)
3 |
4 | ## Connecting Your Voice User Interface To Your Lambda Function
5 |
6 | In the [Voice User Interface](./1-voice-user-interface.md) step of this guide, we created a voice user interface for the intents and utterances we expect from our users. In the [Lambda Function](./2-lambda-function.md) step, we created a Lambda function that contains all of our logic for the skill. In this step, we need to connect those two pieces together.
7 |
8 | 1. Go back to the [Amazon Developer Portal](https://developer.amazon.com/edw/home.html#/skills/list) and select your skill from the list. You may still have a browser tab open if you started at the beginning of this tutorial.
9 |
10 | 2. Select the **Endpoint** tab on the left side navigation panel.
11 |
12 | 3. Select the **AWS Lambda ARN** option for your endpoint.
13 |
14 | > You have the ability to host your code anywhere that you would like, but for the purposes of simplicity and frugality, we are using AWS Lambda. ([Read more about Hosting Your Own Custom Skill Web Service](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/developing-an-alexa-skill-as-a-web-service).) With the AWS Free Tier, you get 1,000,000 free requests per month, up to 3.2 million seconds of compute time per month. Learn more at https://aws.amazon.com/free/. In addition, Amazon now offers [AWS Promotional Credits for developers who have live Alexa skills that incur costs on AWS related to those skills](https://developer.amazon.com/alexa-skills-kit/alexa-aws-credits).
15 |
16 | 4. Paste your Lambda's ARN (Amazon Resource Name) into the textbox provided for **Default Region**.
17 |
18 | 5. Click the **Save Endpoints** button at the top of the main panel.
19 |
20 | 6. Click the **Next** button to continue to the next step this guide.
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/instructions/4-testing.md:
--------------------------------------------------------------------------------
1 | # Build An Alexa Feed Reader Skill
2 | [](./1-voice-user-interface.md)[](./2-lambda-function.md)[](./3-connect-vui-to-code.md)[](./4-testing.md)[](./5-customization.md)[](./6-publication.md)
3 |
4 | ## Testing Your Alexa Skill
5 |
6 | So far, we have [created a Voice User Interface](./1-voice-user-interface.md) and [a Lambda function](./2-lambda-function.md), and [connected the two together](./3-connect-vui-to-code.md). Your skill is now ready to test.
7 |
8 | 1. Go back to the [Amazon Developer Portal](https://developer.amazon.com/edw/home.html#/skills/list) and select your skill from the list. You may still have a browser tab open if you started at the beginning of this tutorial.
9 |
10 | 2. Open the **Test** Pane, by selecting the **Test** link from the top navigation menu.
11 |
12 | 3. Enable Testing by activating the **Test is enabled for this skill** slider. It should be underneath the top navigation menu.
13 |
14 | 4. To validate that your skill is working as expected, invoke your skill from the **Alexa Simulator**. You can either use the input box and type in the command to invoke or use your voice by clicking and holding the mic.
15 | - Type ```open``` followed by the invocation name you gave your skill in the [Voice User Interface](./1-voice-user-interface.md) step. For example, ```open feed reader```.
16 |
17 | - Use your voice by clicking and holding the mic on the side panel and saying "Open" followed by the invocation name you gave your skill. If you've forgotten the invocation name for your skill, revisit the **Build** panel on the top navigation menu and select _Invocation_ from the sidebar to review it.
18 |
19 | 5. Ensure your skill works the way that you designed it to.
20 | * After you interact with the Alexa Simulator, you should see the Skill I/O **JSON Input** and **JSON Output** boxes get populated with JSON data. You can also view the **Device Log** to trace your steps.
21 | * If it's not working as expected, you can dig into the JSON to see exactly what Alexa is sending and receiving from the endpoint. If something is broken, AWS Lambda offers an additional testing tool to help you troubleshoot your skill.
22 |
23 | 6. Configure a test event in AWS Lambda.
24 | Now that you are familiar with the **request** and **response** boxes in the Service Simulator, it's important for you to know that you can use your **requests** to directly test your Lambda function every time you update it. To do this:
25 | 1. Enter an utterance in the service simulator, and copy the generated Lambda Request for the next step.
26 |
27 | 2. Open your Lambda function in AWS, open the Actions menu, and select **Configure test events**.
28 |
29 |
30 |
31 | 3. Select **Create New Test Event** and choose any test event in the list, as they are just templated event requests, using "Alexa Start Session" is an easy one to remember.
32 |
33 |
34 |
35 | 4. Type in an Event Name into the Event Name Dialog box. Delete the contents of the code editor, and paste the Lambda request you copied above into the code editor. The Event Name is only visible to you. Name your test event something descriptive and memorable. For our example, we entered an event name as "startSession". Additionally, by copying and pasting your Lambda Request from the service simulator, you can test different utterances and skill events beyond the pre-populated templates in Lambda.
36 |
37 |
38 |
39 | 5. Click the **Create** button. This will save your test event and bring you back to the main configuration for your lambda function.
40 |
41 | 6. Click the **Test** button to execute the "startSession" test event.
42 |
43 |
44 |
45 | This gives you visibility into four things:
46 |
47 | * **Your response, listed in the "Execution Result."**
48 |
49 |
50 |
51 | * **A Summary of the statistics for your request.** This includes things like duration, resources, and memory used.
52 |
53 |
54 |
55 | * **Log output.** By effectively using console.log() statements in your Lambda code, you can track what is happening inside your function, and help to figure out what is happening when something goes wrong. You will find the log to be incredibly valuable as you move into more advanced skills.
56 |
57 |
58 |
59 | * **A link to your [CloudWatch](https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#logs:) logs for this function.** This will show you **all** of the responses and log statements from every user interaction. This is very useful, especially when you are testing your skill from a device with your voice. (It is the "[Click here](https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#logs:)" link in the Log Output description.)
60 |
61 | 7. **Other testing methods to consider:**
62 |
63 | * [Echosim.io](https://echosim.io) - a browser-based Alexa skill testing tool that makes it easy to test your skills without carrying a physical device everywhere you go.
64 | * [Unit Testing with Alexa](https://github.com/alexa/alexa-cookbook/tree/master/testing/postman/README.md) - a modern approach to unit testing your Alexa skills with [Postman](http://getpostman.com) and [Amazon API Gateway](http://aws.amazon.com/apigateway).
65 |
66 | 8. If your sample skill is working properly, you can now customize your skill.
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/instructions/5-customization.md:
--------------------------------------------------------------------------------
1 | # Build An Alexa Feed Reader Skill
2 | [](./1-voice-user-interface.md)[](./2-lambda-function.md)[](./3-connect-vui-to-code.md)[](./4-testing.md)[](./5-customization.md)[](./6-publication.md)
3 |
4 | ## Customize the Skill to be Yours
5 |
6 | At this point, you should have a working copy of our Feed Reader skill. In order to make it your own, you will need to customize it with data and responses that you create. Here are the things you will need to change:
7 |
8 | 1. **Add new utterances and intents to respond to your users.** There are several sentences and responses that you can modify to customize for your skill.
9 |
10 | 2. **New language.** If you are creating this skill for another language other than English, you will need to make sure Alexa's responses are also in that language.
11 |
12 | * For example, if you are creating your skill in German, every single response that Alexa makes has to be in German. You can't use English responses or your skill will fail certification.
13 |
14 | 3. Once you have made the updates listed on this page, you can click **Next** below to move on to Publishing and Certification of your skill.
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/instructions/6-publication.md:
--------------------------------------------------------------------------------
1 | # Build An Alexa Feed Reader Skill
2 | [](./1-voice-user-interface.md)[](./2-lambda-function.md)[](./3-connect-vui-to-code.md)[](./4-testing.md)[](./5-customization.md)[](./6-publication.md)
3 |
4 | ## Get Your Skill Certified and Published
5 |
6 | We are almost done! The last step is to add the metadata that your skill will use in the [Alexa app](http://amazon.com/skills). This page will walk you through the remaining steps to launch your skill!
7 |
8 | 1. Select the **Launch** link from the top navigation menu.
9 |
10 | 2. Fill out the form fields per the guidance on the screen. Hover over the question mark icons for details regarding each respective field. **Fields marked with an asterisk are required!**
11 | * Take the time to get these right so that your skill will pass certification!
12 |
13 | 3. **Write your skill descriptions.**
14 |
15 | * **Spend some time coming up with an enticing, succinct description.** This is one of the few places you have an opportunity to attract new users, so make the most of it! These descriptions show up in the list of skills available in the [Alexa app](http://alexa.amazon.com/spa/index.html#skills).
16 |
17 | 4. **For your example phrases, come up with the three most exciting ways a user can talk to your skill.**
18 |
19 | * **Make sure that each of your example phrases are a perfect match with one of your Sample Utterances.** Incorrect example phrases are one of the most common reasons that skills fail certification, so we have provided a short list of things to consider as you write your example phrases:
20 |
21 | | Common Failure Points for Example Phrases |
22 | | ----------------------------------------- |
23 | | Example phrases **must** adhere to the [supported phrases](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/supported-phrases-to-begin-a-conversation). |
24 | | Example phrases **must** be based on sample utterances specified in your Intent Schema. |
25 | | Your first example phrase **must** include a wake word and your invocation name. |
26 | | Example phrases **must** provide a contextual response. |
27 |
28 | * **Choose three example phrases that are likely to be the most common ways that users will attempt to interact with your skill.** Make sure that each of them works well, and provides an excellent user experience.
29 |
30 | 5. **Create your skill's icons.** You need two sizes of your icon: 108x108px and 512x512px.
31 |
32 |
33 | * **Make sure you have the rights to the icons you create.** Please don't violate any trademarks or copyrights.
34 | * **If you don't have software to make icons, try one of these free options:**
35 |
36 | * [GIMP](https://www.gimp.org/) (Windows/Mac/Linux)
37 | * [Paint.NET](http://www.getpaint.net/index.html) (Windows)
38 | * [Inkscape](http://inkscape.org) (Windows/Mac/Linux)
39 | * [Iconion](http://iconion.com/) (Windows/Mac)
40 |
41 | * To make it easier to get started, we've created blank versions of these icons in both sizes for many formats:
42 |
43 | * [PSD](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/general/icon-templates/psd._TTH_.zip)
44 | * [PNG](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/general/icon-templates/png._TTH_.zip)
45 | * [GIF](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/general/icon-templates/gif._TTH_.zip)
46 | * [PDF](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/general/icon-templates/pdf._TTH_.zip)
47 | * [JPG](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/general/icon-templates/jpg._TTH_.zip)
48 | * [SVG](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/general/icon-templates/svg._TTH_.zip)
49 | * [PDN](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/general/icon-templates/pdn._TTH_.zip) - for [Paint.NET](http://www.getpaint.net/index.html)
50 | * [XCF](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/general/icon-templates/xcf._TTH_.zip) - for [GIMP](https://www.gimp.org/)
51 |
52 | 6. Choose the most appropriate category for your skill.
53 |
54 | 7. **Provide a comprehensive list of keywords for users that are searching for new skills.** This is an optional field, and searching the [Alexa app](http://alexa.amazon.com) will also find the words in your Skill Name and descriptions, so you don't need to overdo it. That being said, if there are words that you want users to find your skill with, you should include them here. Separate the keywords with commas.
55 |
56 | 8. **Privacy Policy URL.** This is an optional field, and should not be required for this Feed Reader skill sample. You can leave it blank.
57 |
58 | 9. **Terms of Use URL.** This is also optional, and you can leave it blank.
59 |
60 |
61 | 10. When you're ready, click **Save and Continue** at the bottom of the screen to move onto **Privacy & Compliance**
62 |
63 | 11. * **Does this skill allow users to make purchases or spend real money?** For this feed reader skill, the answer is no. For future skills, make sure you answer this appropriately.
64 |
65 | * **Does this Alexa skill collect users' personal information?** Again, for this feed reader skill, the answer is no. If you do collect information about a user, such as names, email addresses, phone numbers, and so forth, ensure that you answer Yes to this question.
66 | * Answering "yes" to this question will also require you to provide a link to your Privacy Policy at the bottom of the page.
67 |
68 | * **Is your skill directed to children under the age of 13?** Because you customized this skill with data you provided, it is possible that you created a skill that targets children under the age of 13. For this feed reader skill, the answer is **no** because it doesn't target a specific age group.
69 | * Factors to consider in determining if this skill is directed to children under 13 include:
70 | * Subject matter of the skill
71 | * Presence of child-oriented activities and incentives
72 | * Type of language used in the skill
73 | * Music and other audio content in the skill
74 | * How the skill is described and marketed
75 | * Intended audience for the skill
76 |
77 | If you're not sure, please see the [FTC's COPPA Guidance and FAQ](https://www.ftc.gov/tips-advice/business-center/guidance/complying-coppa-frequently-asked-questions) for more information.
78 |
79 | 12. **Export Compliance.** Be certain that you agree with all of the conditions. If you do, make sure to check this box, as Amazon requires this permission to distribute your skill around the globe.
80 |
81 | 13. **Provide testing instructions.** Testing instructions give you an opportunity to explain your skill, and any special or possibly confusing features, to the certification team. A value is required in this box.
82 |
83 | * Since you are using our Sample, make sure to add a sentence to your Testing Instructions referencing the Sample you used. For example:
84 |
85 | ```
86 | This was built using the Feed Reader Sample.
87 | ```
88 |
89 | This will let the testing team understand what you're providing them, and should decrease the testing time required.
90 |
91 | **Note:** More details on certification are [available here.](https://alexa.design/certification)
92 |
93 | 14. If you feel that your skill is ready for certification, click the **Save and Continue** button at the bottom of the page.
94 |
95 |
96 | 15. **You're done with your submission!** Here are a few things you might need to know:
97 |
98 | * **Certification can take several days to complete.** Please be patient. It takes time because we want to get it right.
99 |
100 | * **Did something go wrong?** Our team of evangelists run [online office hours every Tuesday from 1-2pm Pacific Time](https://attendee.gotowebinar.com/rt/8389200425172113931). They can help answer any questions you might have.
101 |
102 | * **Want the coolest t-shirt you've ever seen?** Every month, we create a brand-new Alexa Developer t-shirt or hoodie, and send them out to developers that published a skill that month. [You can get yours here if you live in the US](https://developer.amazon.com/alexa-skills-kit/alexa-developer-skill-promotion), [here for the UK](https://developer.amazon.com/en-gb/alexa-skills-kit/alexa-developer-skill-promotion), and [here for Germany](https://developer.amazon.com/de-de/alexa-skills-kit/alexa-developer-skill-promotion).
103 |
--------------------------------------------------------------------------------
/lambda/bin/deploy.js:
--------------------------------------------------------------------------------
1 | var config = require('../custom/configuration');
2 | var fs = require('fs');
3 | var AWS = require('aws-sdk');
4 | var s3 = new AWS.S3();
5 |
6 | var modelFile = fs.readFileSync('../../models/en-US.json');
7 | var modelContent = JSON.parse(modelFile);
8 |
9 | function getRandomId() {
10 | return String(Math.floor(100000 + Math.random() * 900000));
11 | }
12 |
13 | function addCategoryValue(category_name) {
14 | value =
15 | {
16 | "id": getRandomId(),
17 | "name": {
18 | "value": category_name,
19 | "synonyms": []
20 | }
21 | };
22 |
23 | return value
24 | }
25 |
26 | var feeds = Object.keys(config.feeds);
27 | if (feeds.length > 1) {
28 | var fileOutput = "";
29 |
30 | var categories = [];
31 |
32 | feeds.forEach(function (feed) {
33 | fileOutput += feed + "\n";
34 | categories.push(addCategoryValue(feed))
35 | });
36 | fileOutput += "Favorite";
37 |
38 | categories.push(addCategoryValue("Favorite"));
39 |
40 | var types = modelContent.interactionModel.languageModel.types;
41 | for (index in types) {
42 | if (types[index].name.toUpperCase() === "CATEGORY") {
43 | types[index].values = categories;
44 | // console.log(categories);
45 | break
46 | }
47 | }
48 |
49 | // Write the model content
50 | console.log("Updating interaction model with Configuration Categories.");
51 | fs.writeFileSync('../../models/en-US-updated.json', JSON.stringify(modelContent));
52 |
53 | fs.writeFile('../../models/CustomSlots-CATEGORY.txt', fileOutput, function (err) {
54 | if (err) {
55 | console.log('Error while creating custom slots value');
56 | console.log(err.message);
57 | } else {
58 | console.log('CustomSlots-CATEGORY.txt generated.');
59 | }
60 | });
61 | }
62 |
63 |
64 | var params = {
65 | Bucket: config.s3BucketName,
66 | VersioningConfiguration: {
67 | Status: 'Enabled'
68 | }
69 | };
70 |
71 | s3.putBucketVersioning(params, function (err, data) {
72 | if (err) {
73 | console.log(err, err.stack);
74 | } else {
75 | console.log('Bucket Versioning Enabled.');
76 | params = {
77 | Bucket: config.s3BucketName,
78 | LifecycleConfiguration: {
79 | Rules: [
80 | {
81 | Prefix: '',
82 | Status: 'Enabled',
83 | Expiration: {
84 | Days: 1
85 | },
86 | NoncurrentVersionExpiration: {
87 | NoncurrentDays: 1
88 | }
89 | }
90 | ]
91 | }
92 | };
93 | s3.putBucketLifecycleConfiguration(params, function (err, data) {
94 | if (err) {
95 | console.log(err, err.stack);
96 | } else {
97 | console.log('Object Expiration rules set.');
98 | }
99 | });
100 | }
101 | });
102 |
--------------------------------------------------------------------------------
/lambda/custom/configuration.js:
--------------------------------------------------------------------------------
1 | let config = {
2 | // TODO Add Application ID
3 | appId : '',
4 | // TODO Add an appropriate welcome message.
5 | welcome_message : '',
6 |
7 | number_feeds_per_prompt : 3,
8 | speak_only_feed_title : true,
9 | display_only_title_in_card : true,
10 |
11 | // TODO Add the category name (to feed name) and the corresponding URL
12 | feeds : {
13 | 'CATEGORY_NAME_1' : '',
14 | 'CATEGORY_NAME_2' : '',
15 | 'CATEGORY_NAME_3' : ''
16 |
17 | },
18 |
19 | speech_style_for_numbering_feeds : 'Item',
20 |
21 | // TODO Add the s3 Bucket Name, dynamoDB Table Name and Region
22 | s3BucketName : '',
23 | dynamoDBTableName : '',
24 | dynamoDBRegion : ''
25 | };
26 |
27 | module.exports = config;
28 |
--------------------------------------------------------------------------------
/lambda/custom/constants.js:
--------------------------------------------------------------------------------
1 | module.exports = Object.freeze({
2 | // States
3 | states : {
4 | START_MODE : '_START_MODE',
5 | FEED_MODE : '_FEED_MODE',
6 | NO_NEW_ITEM : '_NO_NEW_ITEM',
7 | SINGLE_FEED_MODE : '_SINGLE_FEED_MODE'
8 | },
9 |
10 | // Custom constants
11 | terminate : 'TERMINATE',
12 |
13 | // Speech break time
14 | breakTime : {
15 | '50' : '',
16 | '100' : '',
17 | '200' : '',
18 | '250' : '',
19 | '300' : '',
20 | '500' : ''
21 | },
22 |
23 | // Time in minutes after which feeds fetched again.
24 | updateFeedTime : 5
25 | });
26 |
--------------------------------------------------------------------------------
/lambda/custom/eventHandlers.js:
--------------------------------------------------------------------------------
1 | const async = require('async');
2 |
3 | const constants = require('./constants');
4 | const config = require('./configuration');
5 | const feedHelper = require('./feedHelper');
6 | const logHelper = require('./logHelper');
7 | const s3Helper = require('./s3Helper');
8 |
9 | let eventHandlers = {
10 | 'NewSession' : function () {
11 | logHelper.logSessionStarted(this.event.session);
12 |
13 | // Initialize session attributes
14 | this.attributes['start'] = true;
15 | this.attributes['category'] = '';
16 |
17 | if (!this.attributes['favoriteCategories']) {
18 | this.attributes['favoriteCategories'] = [];
19 | }
20 | if (!this.attributes['latestItem']) {
21 | this.attributes['latestItem'] = {};
22 | }
23 |
24 | /*
25 | * If only one category present : STATE = _SINGLE_FEED_MODE
26 | * Else : STATE = _START_MODE
27 | */
28 | if (Object.keys(config.feeds).length === 1) {
29 | this.handler.state = constants.states.SINGLE_FEED_MODE;
30 | } else {
31 | this.handler.state = constants.states.START_MODE;
32 | }
33 | /*
34 | * If request type is LaunchRequest : Give welcome message
35 | * Else If request type is IntentRequest : Call the specific intent directly
36 | * Else : do nothing.
37 | */
38 | if (this.event.request.type === 'LaunchRequest') {
39 | logHelper.logLaunchRequest(this.event.session, this.event.request);
40 |
41 | if (this.handler.state === constants.states.SINGLE_FEED_MODE) {
42 | this.emit('launchSingleMode');
43 | } else {
44 | this.emit('welcome');
45 | }
46 | } else if (this.event.request.type === 'IntentRequest') {
47 | logHelper.logReceiveIntent(this.event.session, this.event.request);
48 |
49 | let intentName = this.event.request.intent.name;
50 | this.emitWithState(intentName);
51 | } else {
52 | console.log('Unexpected request : ' + this.event.request.type);
53 | }
54 | },
55 | 'EndSession' : function (message) {
56 | /*
57 | * If favorite file present : delete it
58 | * If SessionEndedRequest : emit ':saveState'
59 | * Else emit ':tell'
60 | */
61 | if (this.attributes['favoriteFilePresent']) {
62 | logHelper.logAPICall(this.event.session, 'S3');
63 | // Delete favorite file from S3 created in current session
64 | s3Helper.deleteObject(this.attributes['fileNameFavorite'], this.attributes['versionIdFavorite'], (err) => {
65 | if (err) {
66 | logHelper.logAPIError(this.event.session, 'S3', err);
67 | if (message != constants.terminate) {
68 | this.emit('reportError');
69 | }
70 | } else {
71 | logHelper.logAPISuccesses(this.event.session, 'S3');
72 | // Updating session attributes to store only the required attributes within DynamoDB.
73 | deleteAttributes.call(this);
74 |
75 | if (message != constants.terminate) {
76 | message = message || '';
77 | this.response.speak(message);
78 | this.emit(':responseReady');
79 | } else {
80 | this.emit(':saveState', true);
81 | }
82 | }
83 | });
84 | } else {
85 | // Updating session attributes to store only the required attributes within DynamoDB.
86 | deleteAttributes.call(this);
87 |
88 | if (message != constants.terminate) {
89 | message = message || '';
90 |
91 | this.response.speak(message);
92 | this.emit(':responseReady');
93 | } else {
94 | this.emit(':saveState', true);
95 | }
96 | }
97 | }
98 | };
99 |
100 | module.exports = eventHandlers;
101 |
102 | function deleteAttributes() {
103 | let latestItem = this.attributes['latestItem'];
104 | let favoriteCategories = this.attributes['favoriteCategories'];
105 |
106 | Object.keys(this.attributes).forEach((attribute) => {
107 | delete this.attributes[attribute];
108 | });
109 |
110 | this.attributes.latestItem = latestItem;
111 |
112 | if (this.handler.state != constants.states.SINGLE_FEED_MODE) {
113 | this.attributes.favoriteCategories = favoriteCategories;
114 | }
115 |
116 | this.handler.state = '';
117 | }
118 |
--------------------------------------------------------------------------------
/lambda/custom/feedHelper.js:
--------------------------------------------------------------------------------
1 | const FeedParser = require('feedparser');
2 | const fs = require('fs');
3 | const entities = require('html-entities').AllHtmlEntities;
4 | const request = require('request');
5 | const striptags = require('striptags');
6 |
7 | const config = require('./configuration');
8 | const constants = require('./constants');
9 | const logHelper = require('./logHelper');
10 | const s3Helper = require('./s3Helper');
11 |
12 | const feedParser = function () {
13 | return {
14 | getFeed : function (category, fileName, callback) {
15 | let url = config.feeds[category];
16 | let req = request(url);
17 | let feedparser = new FeedParser(null);
18 | let items = [];
19 |
20 | req.on('response', function (res) {
21 | let stream = this;
22 | if (res.statusCode === 200) {
23 | stream.pipe(feedparser);
24 | } else {
25 | return stream.emit('error', new Error('Bad status code'));
26 | }
27 | });
28 |
29 | req.on('error', function (err) {
30 | return callback(err, null);
31 | });
32 |
33 | // Received stream. parse through the stream and create JSON Objects for each item
34 | feedparser.on('readable', function() {
35 | let stream = this;
36 | let item;
37 | while (item = stream.read()) {
38 | let feedItem = {};
39 | // Process feedItem item and push it to items data if it exists
40 | if (item['title'] && item['date']) {
41 | feedItem['title'] = item['title'];
42 | feedItem['title'] = entities.decode(striptags(feedItem['title']));
43 | feedItem['title'] = feedItem['title'].trim();
44 | feedItem['title'] = feedItem['title'].replace(/[&]/g,'and').replace(/[<>]/g,'');
45 |
46 | feedItem['date'] = new Date(item['date']).toUTCString();
47 |
48 | if (item['description']) {
49 | feedItem['description'] = item['description'];
50 | feedItem['description'] = entities.decode(striptags(feedItem['description']));
51 | feedItem['description'] = feedItem['description'].trim();
52 | feedItem['description'] = feedItem['description'].replace(/[&]/g,'and').replace(/[<>]/g,'');
53 | }
54 |
55 | if (item['link']) {
56 | feedItem['link'] = item['link'];
57 | }
58 |
59 | if (item['image'] && item['image'].url) {
60 | feedItem['imageUrl'] = item['image'].url;
61 | }
62 | items.push(feedItem);
63 | }
64 | }
65 | });
66 |
67 | // All items parsed. Store items in S3 and return items
68 | feedparser.on('end', function () {
69 | let count = 0;
70 | items.sort(function (a, b) {
71 | return new Date(b.date) - new Date(a.date);
72 | });
73 | items.forEach(function (feedItem) {
74 | feedItem['count'] = count++;
75 | });
76 | stringifyFeeds(items, (feedData) => {
77 | s3Helper.putObject(fileName, feedData, function (err, data) {
78 | if (err) {
79 | callback(err, data);
80 | } else {
81 | data.items = items;
82 | callback(err, data);
83 | }
84 | });
85 | });
86 | });
87 |
88 | feedparser.on('error', function(err) {
89 | callback(err, null);
90 | });
91 | },
92 | stringifyItems : function (items, callback) {
93 | stringifyFeeds(items, function (feedData) {
94 | callback(feedData);
95 | })
96 | }
97 | };
98 | }();
99 |
100 | function stringifyFeeds(items, callback) {
101 | // Structure items before storing into S3 file.
102 | let feedData = '[';
103 | for (let i = 0; i < items.length; i++) {
104 | feedData += JSON.stringify(items[i]) + ', ';
105 | }
106 | let dataLength = feedData.length;
107 | feedData = feedData.substring(0, dataLength-2) + ']';
108 | callback(feedData);
109 | }
110 |
111 | module.exports = feedParser;
112 |
--------------------------------------------------------------------------------
/lambda/custom/index.js:
--------------------------------------------------------------------------------
1 | const Alexa = require('alexa-sdk');
2 | const config = require('./configuration');
3 | const eventHandlers = require('./eventHandlers');
4 | const stateHandlers = require('./stateHandlers');
5 | const intentHandlers = require('./intentHandlers');
6 | const speechHandlers = require('./speechHandlers');
7 |
8 | exports.handler = function(event, context, callback){
9 | let alexa = Alexa.handler(event, context);
10 | alexa.appId = config.appId;
11 | alexa.dynamoDBTableName = config.dynamoDBTableName;
12 | alexa.registerHandlers(
13 | eventHandlers,
14 | stateHandlers.startModeIntentHandlers,
15 | stateHandlers.feedModeIntentHandlers,
16 | stateHandlers.noNewItemsModeIntentHandlers,
17 | stateHandlers.singleFeedModeIntentHandlers,
18 | intentHandlers,
19 | speechHandlers);
20 | alexa.execute();
21 | };
22 |
--------------------------------------------------------------------------------
/lambda/custom/intentHandlers.js:
--------------------------------------------------------------------------------
1 | const async = require('async');
2 | const config = require('./configuration');
3 | const constants = require('./constants');
4 | const feedHelper = require('./feedHelper');
5 | const logHelper = require('./logHelper');
6 | const s3Helper = require('./s3Helper');
7 |
8 | let items = [];
9 |
10 | let intentHandlers = {
11 | 'selectCategory' : function () {
12 | /*
13 | * If file present for given category :
14 | * * If file never read : initialize parameters and read items
15 | * * Else : continue from last stopped position
16 | * Else : Call feedHelper to fetch feeds.
17 | */
18 | let category = this.attributes['category'];
19 | let fileNameKey = 'fileName' + category;
20 | let versionIdKey = 'versionId' + category;
21 | let indexKey = 'index' + category;
22 | let directionKey = 'direction' + category;
23 | // All files will have following naming convention : '_feeds.json'
24 | this.attributes[fileNameKey] = category + '_feeds.json';
25 |
26 | if (this.attributes[versionIdKey]) {
27 | if (this.attributes[indexKey] === -1) {
28 | /*
29 | * Feed never read.
30 | * Call new item notification to compute number of new items available.
31 | */
32 | newItemNotification.call(this, (newItemCount) => {
33 | /*
34 | * If number of new items = 0 : notify user and ask whether to continue with feed
35 | * Else : notify user the number of new items and begin feed with first page
36 | */
37 | if (newItemCount === 0) {
38 | this.emit('noNewItems');
39 | } else {
40 | this.emit('readItems');
41 | }
42 | });
43 | } else {
44 | if (this.attributes[indexKey] != 0) {
45 | // feed pointer not at start, thus adjust pointer to resume from last read item
46 | this.attributes[indexKey] -= config.number_feeds_per_prompt;
47 | }
48 | this.attributes[directionKey] = 'forward';
49 | this.emit('readItems');
50 | }
51 | } else {
52 | // Category fetched for the first time in the session.
53 | logHelper.logAPICall(this.event.session, 'S3');
54 | s3Helper.getObject(this.attributes[fileNameKey], null, (err, data) => {
55 | if (err) {
56 | logHelper.logAPIError(this.event.session, 'S3');
57 |
58 | if (err.code === 'NoSuchKey') {
59 | fetchFeed.call(this, (newItemCount) => {
60 | if (newItemCount === 0) {
61 | this.emit('noNewItems');
62 | } else {
63 | this.emit('readItems');
64 | }
65 | });
66 | } else {
67 | this.emit('reportError');
68 | }
69 | } else {
70 | let allowedTimePeriod = constants.updateFeedTime*60*1000; // Convert minutes to milliSeconds
71 | let timeSinceLastModified = (new Date()).getTime() - new Date(data.LastModified);
72 | if (timeSinceLastModified < allowedTimePeriod) {
73 | // File is created within allowed time period. Using this feed version.
74 | items = JSON.parse(data.Body);
75 | this.attributes[versionIdKey] = data.VersionId;
76 | // Call new item notification to compute number of new items available
77 | newItemNotification.call(this, (newItemCount) => {
78 | if (newItemCount === 0) {
79 | this.emit('noNewItems');
80 | } else {
81 | this.emit('readItems');
82 | }
83 | });
84 | } else {
85 | // File is much older than allowed time. Calling feedHelper and storing file in S3.
86 | fetchFeed.call(this, (newItemCount) => {
87 | if (newItemCount === 0) {
88 | this.emit('noNewItems');
89 | } else {
90 | this.emit('readItems');
91 | }
92 | });
93 | }
94 | }
95 | });
96 | }
97 | },
98 | 'readItems' : function () {
99 | /*
100 | * Find the items to be read in current pagination using current index position and previous direction
101 | * Store these items in an array and call speechHandler function to process for output
102 | */
103 | loadItems.call(this, () => {
104 | let category = this.attributes['category'];
105 | let indexKey = 'index' + category;
106 | let directionKey = 'direction' + category;
107 | let feedEndedKey = 'feedEnded' + category;
108 | let justStartedKey = 'justStarted' + category;
109 | if (!this.attributes[feedEndedKey]) {
110 | let pagedItems = [];
111 | let feedLength = items.length;
112 | let index;
113 | let currentIndex = this.attributes[indexKey];
114 | if (currentIndex === 0) {
115 | // Mark flag to signify start of feed
116 | this.attributes[justStartedKey] = true;
117 | } else {
118 | this.attributes[justStartedKey] = null;
119 | }
120 | if (this.attributes[directionKey] === 'backward') {
121 | // Adjustment for change in direction
122 | this.attributes[directionKey] = 'forward';
123 | currentIndex += config.number_feeds_per_prompt;
124 | }
125 | let currentPaginationEnd = currentIndex + config.number_feeds_per_prompt;
126 | for (index = currentIndex; index < currentPaginationEnd && index < feedLength; index++) {
127 | pagedItems.push(items[index]);
128 | }
129 | if (index === feedLength) {
130 | // Mark flag to signify end of feed
131 | this.attributes[feedEndedKey] = true;
132 | }
133 | this.attributes[indexKey] = currentPaginationEnd;
134 | if (Object.keys(config.feeds).length === 1) {
135 | this.emit('readPagedItemsSingleMode', pagedItems);
136 | } else {
137 | this.emit('readPagedItems', pagedItems);
138 | }
139 | } else {
140 | this.emit('alreadyEnded');
141 | }
142 | })
143 | },
144 | 'readPreviousItems' : function () {
145 | /*
146 | * Find the items to be read in current pagination using current index position and previous direction
147 | * Store these items in an array and call speechHandler function to process for output
148 | */
149 | loadItems.call(this, () => {
150 | let category = this.attributes['category'];
151 | let indexKey = 'index' + category;
152 | let directionKey = 'direction' + category;
153 | let feedEndedKey = 'feedEnded' + category;
154 | let justStartedKey = 'justStarted' + category;
155 | if (!this.attributes[justStartedKey]) {
156 | let pagedItems = [];
157 | let index;
158 | let currentIndex = this.attributes[indexKey];
159 | if (this.attributes[directionKey] === 'forward') {
160 | // Adjustment for change in direction
161 | currentIndex -= config.number_feeds_per_prompt;
162 | this.attributes[directionKey] = 'backward';
163 | }
164 | let currentPaginationStart = currentIndex - config.number_feeds_per_prompt;
165 | if (this.attributes[feedEndedKey]) {
166 | this.attributes[feedEndedKey] = null;
167 | }
168 | currentIndex--;
169 | for (index = currentIndex; index >= currentPaginationStart && index >= 0; index--) {
170 | pagedItems.unshift(items[index]);
171 | }
172 | if (index === -1) {
173 | // Mark flag to signify start of feed
174 | this.attributes[justStartedKey] = true;
175 | currentPaginationStart = 0;
176 | }
177 | this.attributes[indexKey] = currentPaginationStart;
178 | if (Object.keys(config.feeds).length === 1) {
179 | this.emit('readPagedItemsSingleMode', pagedItems);
180 | } else {
181 | this.emit('readPagedItems', pagedItems);
182 | }
183 | } else {
184 | this.emit('justStarted');
185 | }
186 | })
187 | },
188 | 'startOver' : function () {
189 | /*
190 | * To re-initialize all attributes
191 | * Call load feeds to read first page of items
192 | */
193 | let category = this.attributes['category'];
194 | let indexKey = 'index' + category;
195 | let directionKey = 'direction' + category;
196 | let feedEndedKey = 'feedEnded' + category;
197 | // Reset index and direction
198 | this.attributes[indexKey] = 0;
199 | this.attributes[directionKey] = 'forward';
200 | if (this.attributes[feedEndedKey]) {
201 | this.attributes[feedEndedKey] = null;
202 | }
203 | // Call get feed function to read first page
204 | this.emit('readItems');
205 | },
206 | 'selectFavorite' : function () {
207 | let directionKey = 'directionFavorite';
208 | this.attributes[directionKey] = 'forward';
209 |
210 | if (this.attributes['favoriteFilePresent']) {
211 | // Favorite file present : call get feed to resume favorite feed
212 | let category = 'Favorite';
213 | this.attributes['category'] = category;
214 | let indexKey = 'index' + category;
215 | if (this.attributes[indexKey] != 0) {
216 | // Feed pointer not at start, thus adjust pointer to resume from last read item
217 | this.attributes[indexKey] -= config.number_feeds_per_prompt;
218 | }
219 | // Call readItems since file already present
220 | this.emit('readItems');
221 | } else {
222 | /*
223 | * Fetch the list of favorite categories
224 | * If favorite categories present :
225 | * * For all favorite categories asynchronously - do the following :
226 | * ** If feeds for a category exists in S3, fetch it
227 | * ** Else call Feed Helper
228 | * * Merge all items, remove duplicates, and sort them using time
229 | * Else : give appropriate message to the user
230 | */
231 | let favoriteCategories = this.attributes['favoriteCategories'];
232 | if (favoriteCategories.length > 0) {
233 | let asyncDataCollection = [];
234 | // Add tasks that needs to be performed asynchronously
235 | favoriteCategories.forEach((category) => {
236 | let fileNameKey = 'fileName' + category;
237 | let versionIdKey = 'versionId' + category;
238 |
239 | asyncDataCollection.push((asyncCallback) => {
240 | if (this.attributes[versionIdKey]) {
241 | // File present, thus fetch all items
242 | logHelper.logAPICall(this.event.session, 'S3');
243 | // retrieve feed data stored in S3 storage
244 | s3Helper.getObject(this.attributes[fileNameKey], this.attributes[versionIdKey], (err, data) => {
245 | if (err) {
246 | logHelper.logAPIError(this.event.session, 'S3', err);
247 | asyncCallback(err, data);
248 | } else {
249 | logHelper.logAPISuccesses(this.event.session, 'S3');
250 | if (data && data.Body) {
251 | let feeds = JSON.parse(data.Body);
252 | asyncCallback(null, feeds);
253 | } else {
254 | console.log('Data Retrieved is empty.');
255 | this.emit('feedEmptyError');
256 | }
257 | }
258 | });
259 | } else {
260 | // File not present, thus fetch feeds, store in S3 and return items
261 | let indexKey = 'index' + category;
262 | // Initialize attributes
263 | this.attributes[fileNameKey] = category + '_feeds.json';
264 | this.attributes[indexKey] = -1;
265 |
266 | s3Helper.getObject(this.attributes[fileNameKey], null, (err, data) => {
267 | if (err) {
268 | if (err.code === 'NoSuchKey') {
269 | // File for the category does not exists. Thus parse feed and store in S3.
270 | logHelper.logAPICall(this.event.session, 'Feed Parser');
271 | // Call feedHelper to fetch feeds for given category
272 | feedHelper.getFeed(category, this.attributes[fileNameKey],(error, feed) => {
273 | if (error) {
274 | logHelper.logAPIError(this.event.session, 'Feed Parser', error);
275 | asyncCallback(error, feed)
276 | } else {
277 | logHelper.logAPISuccesses(this.event.session, 'Feed Parser');
278 | if (feed.items) {
279 | this.attributes[versionIdKey] = feed.VersionId;
280 | asyncCallback(null, feed.items);
281 | } else {
282 | console.log('Feed parsed is empty');
283 | this.emit('feedEmptyError');
284 | }
285 | }
286 | });
287 | } else {
288 | asyncCallback(err, data);
289 | }
290 | } else {
291 | let allowedTimePeriod = constants.updateFeedTime*60*1000;
292 | if (new Date() - data.LastModified < allowedTimePeriod) {
293 | // File is created within allowed time period. Using this feed version.
294 | let feedItems = JSON.parse(data.Body);
295 | this.attributes[versionIdKey] = data.VersionId;
296 | // Call new item notification to compute number of new items available
297 | asyncCallback(null, feedItems);
298 | } else {
299 | // File for the category does not exists. Thus parse feed and store in S3.
300 | logHelper.logAPICall(this.event.session, 'Feed Parser');
301 | // Call feedHelper to fetch feeds for given category
302 | feedHelper.getFeed(category, this.attributes[fileNameKey],(error, feed) => {
303 | if (error) {
304 | logHelper.logAPIError(this.event.session, 'Feed Parser', error);
305 | asyncCallback(error, feed)
306 | } else {
307 | logHelper.logAPISuccesses(this.event.session, 'Feed Parser');
308 | if (feed.items) {
309 | this.attributes[versionIdKey] = feed.VersionId;
310 | asyncCallback(null, feed.items);
311 | } else {
312 | console.log('Feed parsed is empty');
313 | this.emit('feedEmptyError');
314 | }
315 | }
316 | });
317 | }
318 | }
319 | });
320 | }
321 | });
322 | });
323 | // Calling async parallel to run above requests simultaneously
324 | async.parallel(asyncDataCollection, (err, results) => {
325 | if (err) {
326 | console.log("Async Error : " + err);
327 | this.emit('reportError');
328 | } else {
329 | let allFeeds = {};
330 | items = [];
331 | // Merge all items and eliminate duplicates
332 | results.forEach(function (feeds) {
333 | feeds.forEach(function (feed) {
334 | allFeeds[feed.title] = feed;
335 | });
336 | });
337 | for (let feed in allFeeds) {
338 | items.push(allFeeds[feed]);
339 | }
340 | // Sort all items based on the date
341 | items.sort(function (a, b) {
342 | return new Date(b.date) - new Date(a.date);
343 | });
344 | // Re-initialize the count letiable since order disrupted during sort
345 | for (let index = 0; index < items.length; index++) {
346 | items[index].count = index;
347 | }
348 | // Save all processed items in a file
349 | let fileNameKey = 'fileNameFavorite';
350 | randomNameGenerator((fileName) => {
351 | this.attributes[fileNameKey] = fileName;
352 | feedHelper.stringifyItems(items, (feedData) => {
353 | logHelper.logAPICall(this.event.session, 'S3');
354 | // Call S3 to store favorite items file
355 | s3Helper.putObject(fileName, feedData, (err, data) => {
356 | if (err) {
357 | logHelper.logAPIError(this.event.session, 'S3', err);
358 | this.emit('reportError');
359 | } else {
360 | logHelper.logAPISuccesses(this.event.session, 'S3');
361 | this.attributes['favoriteFilePresent'] = true;
362 | this.attributes['versionIdFavorite'] = data.VersionId;
363 | this.attributes['category'] = 'Favorite';
364 | // Call new item notification to compute number of new items available
365 | newItemNotification.call(this, (newItemCount) => {
366 | /*
367 | * If number of new items = 0 : notify user and ask whether to continue with feed
368 | * Else : notify user the number of new items and begin feed with first page
369 | */
370 | if (newItemCount === 0) {
371 | this.emit('noNewItems');
372 | } else {
373 | this.emit('readItems');
374 | }
375 | });
376 | }
377 | });
378 | });
379 | });
380 | }
381 | });
382 | } else {
383 | // No categories marked as favorite. Notify user.
384 | this.emit('favoriteListEmpty');
385 | }
386 | }
387 | },
388 | 'addFavorite' : function (category) {
389 | /*
390 | * If requested category does not exists in favorite category list :
391 | * * Add it to favorites
392 | * * Call deleteFavoriteFile
393 | * Else : give appropriate message to the user
394 | */
395 | let categoryIndex = this.attributes['favoriteCategories'].indexOf(category);
396 | if (categoryIndex === -1) {
397 | this.attributes['favoriteCategories'].push(category);
398 | console.log(category + ' added to favorites.');
399 | // Call deleteFavoriteFile since items in favorite category are altered
400 | deleteFavoriteFile.call(this, () => {
401 | this.emit('favoriteAdded', category);
402 | });
403 | } else {
404 | this.emit('favoriteAddExistingError', category);
405 | }
406 | },
407 | 'addCurrentToFavorite' : function () {
408 | /*
409 | * Identify current category
410 | * If category is favorite : give error message
411 | * Else : add category to favorite if not present and give appropriate message
412 | */
413 | let category = this.attributes['category'];
414 | if (category != 'Favorite') {
415 | this.emit('addFavorite', category);
416 | } else {
417 | this.emit('favoriteAddCurrentError');
418 | }
419 | },
420 | 'removeFavorite' : function (category) {
421 | /*
422 | * If requested category exists in favorite category list :
423 | * * remove from favorites
424 | * * call deleteFavoriteFile
425 | * Else : give appropriate message to the user
426 | */
427 | let categoryIndex = this.attributes['favoriteCategories'].indexOf(category);
428 | if (categoryIndex > -1) {
429 | this.attributes['favoriteCategories'].splice(categoryIndex,1);
430 | console.log(category + ' removed from favorites.');
431 | // Call deleteFavoriteFile since items in favorite category are altered
432 | deleteFavoriteFile.call(this, () => {
433 | this.emit('favoriteRemoved', category);
434 | });
435 | } else {
436 | this.emit('favoriteRemoveExistingError', category);
437 | }
438 | },
439 | 'removeCurrentFromFavorite' : function () {
440 | /*
441 | * Identify current category
442 | * If category is favorite : give error message
443 | * Else : remove category from favorite and give appropriate message
444 | */
445 | let category = this.attributes['category'];
446 | if (category != 'Favorite') {
447 | this.emit('removeFavorite', category);
448 | } else {
449 | this.emit('favoriteRemoveCurrentError');
450 | }
451 | },
452 | 'launchSingleMode' : function () {
453 | let category = Object.keys(config.feeds)[0];
454 | let fileNameKey = 'fileName' + category;
455 | let versionIdKey = 'versionId' + category;
456 | // Initialize attributes
457 | this.attributes['category'] = category;
458 | this.attributes[fileNameKey] = 'feeds.json';
459 |
460 | logHelper.logAPICall(this.event.session, 'S3');
461 | s3Helper.getObject(this.attributes[fileNameKey], null, (err, data) => {
462 | if (err) {
463 | logHelper.logAPIError(this.event.session, 'S3', err);
464 | if (err.code === 'NoSuchKey') {
465 | // File for the category does not exists. Thus parse feed and store in S3.
466 | fetchFeed.call(this, (newItemCount) => {
467 | if (newItemCount === 0) {
468 | this.emit('noNewItems');
469 | } else {
470 | this.emit('readItems');
471 | }
472 | });
473 | } else {
474 | logHelper.logAPIError(this.event.session, 's3', err);
475 | this.emit('reportError');
476 | }
477 | } else {
478 | logHelper.logAPISuccesses(this.event.session, 'S3');
479 | if (data) {
480 | let allowedTimePeriod = constants.updateFeedTime*60*1000;
481 | if ((new Date() - data.LastModified < allowedTimePeriod)) {
482 | items = JSON.parse(data.Body);
483 | this.attributes[versionIdKey] = data.VersionID;
484 | this.attributes['feedLength'] = items.length;
485 | // Call new item notification to compute number of new items available
486 | newItemNotification.call(this, (newItemCount) => {
487 | if (newItemCount === 0) {
488 | this.emit('noNewItems');
489 | } else {
490 | this.emit('readItems');
491 | }
492 | });
493 | } else {
494 | // File is much older than allowed time. Calling feedHelper and storing file in S3.
495 | fetchFeed.call(this, (newItemCount) => {
496 | if (newItemCount === 0) {
497 | this.emit('noNewItems');
498 | } else {
499 | this.emit('readItems');
500 | }
501 | });
502 | }
503 | }
504 | }
505 | });
506 | }
507 | };
508 |
509 | module.exports = intentHandlers;
510 |
511 | function fetchFeed(callback) {
512 | let category = this.attributes['category'];
513 | let fileNameKey = 'fileName' + category;
514 | let versionIdKey = 'versionId' + category;
515 |
516 | logHelper.logAPICall(this.event.session, 'Feed Parser');
517 | // Call feedHelper to fetch feeds for given category
518 | feedHelper.getFeed(category, this.attributes[fileNameKey],(err, data) => {
519 | if (err) {
520 | logHelper.logAPIError(this.event.session, 'Feed Parser', err);
521 | this.emit('reportError');
522 | } else {
523 | logHelper.logAPISuccesses(this.event.session, 'Feed Parser');
524 | if (data && data.items) {
525 | items = data.items;
526 | this.attributes[versionIdKey] = data.VersionId;
527 |
528 | if (this.handler.state === constants.states.SINGLE_FEED_MODE) {
529 | this.attributes['feedLength'] = items.length;
530 | }
531 |
532 | // Call new item notification to compute number of new items available
533 | newItemNotification.call(this, (newItemCount) => {
534 | callback(newItemCount);
535 | });
536 | } else {
537 | console.log('Feed parsed is empty');
538 | this.emit('feedEmptyError');
539 | }
540 | }
541 | });
542 | }
543 |
544 | function loadItems(callback) {
545 | // If data already present in global letiable, return back
546 | if (items) {
547 | return callback();
548 | }
549 | /*
550 | * Load items stored in the S3 bucket
551 | * Call specific event based on the status of the feed with the direction passed
552 | */
553 | let category = this.attributes['category'];
554 | let fileNameKey = 'fileName' + category;
555 | let versionIdKey = 'versionId' + category;
556 | logHelper.logAPICall(this.event.session, 'S3');
557 | // Retrieve feed data stored in S3 storage
558 | s3Helper.getObject(this.attributes[fileNameKey], this.attributes[versionIdKey],(err, data) => {
559 | if (err) {
560 | logHelper.logAPIError(this.event.session, 'S3', err);
561 | this.emit('reportError');
562 | } else {
563 | logHelper.logAPISuccesses(this.event.session, 'S3');
564 | if (data && data.Body) {
565 | items = JSON.parse(data.Body);
566 | callback();
567 | } else {
568 | console.log('Data retrieved is empty');
569 | this.emit('feedEmptyError');
570 | }
571 | }
572 | });
573 | }
574 |
575 | function newItemNotification(callback) {
576 | let category = this.attributes['category'];
577 | let indexKey = 'index' + category;
578 | let directionKey = 'direction' + category;
579 | let justStartedKey = 'justStarted' + category;
580 | // Initialize index and direction
581 | this.attributes[indexKey] = 0;
582 | this.attributes[directionKey] = 'forward';
583 | this.attributes[justStartedKey] = true;
584 | // Calculate number of new items available in the feed since the last visit
585 | let newItemCount = -1;
586 | if (this.attributes['latestItem'] && this.attributes['latestItem'][category]) {
587 | let lastItemTitle = this.attributes['latestItem'][category];
588 | for (let index = 0; index < items.length; index++) {
589 | if (items[index].title === lastItemTitle) {
590 | newItemCount = index;
591 | break;
592 | }
593 | }
594 | }
595 | // Update the latest item for the feed for future session
596 | this.attributes['latestItem'][this.attributes['category']] = items[0].title;
597 | this.attributes['newItemCount'] = newItemCount;
598 | // Callback with new item count
599 | callback(newItemCount);
600 | }
601 |
602 | function deleteFavoriteFile(callback) {
603 | /*
604 | * If favorite file present : delete file from S3 and give appropriate message
605 | * Else : give appropriate message
606 | */
607 | if (this.attributes['latestItem'] && this.attributes['latestItem']['Favorite']) {
608 | delete this.attributes['latestItem']['Favorite'];
609 | }
610 |
611 | if (this.attributes['favoriteFilePresent']) {
612 | logHelper.logAPICall(this.event.session, 'S3');
613 | // Delete favorite file from S3 created in current session
614 | s3Helper.deleteObject(this.attributes['fileNameFavorite'], this.attributes['versionIdFavorite'],(err) => {
615 | if (err) {
616 | logHelper.logAPIError(this.event.session, 'S3', err);
617 | this.emit('reportError');
618 | } else {
619 | logHelper.logAPISuccesses(this.event.session, 'S3');
620 | this.attributes['fileNameFavorite'] = null;
621 | this.attributes['favoriteFilePresent'] = false;
622 | this.attributes['versionIdFavorite'] = null;
623 | callback();
624 | }
625 | });
626 | } else {
627 | callback();
628 | }
629 | }
630 |
631 | function randomNameGenerator(callback) {
632 | // Generate file name.
633 | let fileName = '';
634 | let potentialChar = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
635 | for (let i=0; i<10; i++) {
636 | fileName += potentialChar.charAt(Math.floor(Math.random() * potentialChar.length));
637 | }
638 | fileName += '.json';
639 | callback(fileName);
640 | }
641 |
--------------------------------------------------------------------------------
/lambda/custom/logHelper.js:
--------------------------------------------------------------------------------
1 | const logHelper = function () {
2 | return{
3 | logSessionStarted: function(session) {
4 | let sessionStartedJsonEvent =
5 | {
6 | "eventType" : "SessionStarted",
7 | "event" : {
8 | "userId" : session.user.userId,
9 | "sessionId" : session.sessionId,
10 | "datestring" : (new Date()).toISOString()
11 | }
12 | };
13 | logJsonEvent(sessionStartedJsonEvent);
14 | },
15 | logLaunchRequest: function (session, launchRequest) {
16 | let launchRequestJsonEvent =
17 | {
18 | "eventType" : "LaunchRequest",
19 | "event" : {
20 | "datestring" : (new Date()).toISOString(),
21 | "userId" : session.user.userId,
22 | "requestId" : launchRequest.requestId,
23 | "sessionId" : session.sessionId
24 | }
25 | };
26 | logJsonEvent(launchRequestJsonEvent);
27 | },
28 | logReceiveIntent: function(session, intentRequest) {
29 | let receiveIntentJsonEvent =
30 | {
31 | "eventType" : "ReceiveIntent",
32 | "event": {
33 | "intentName": intentRequest.intent.name,
34 | "datestring": (new Date()).toISOString(),
35 | "userId": session.user.userId,
36 | "requestId": intentRequest.requestId,
37 | "intent": intentRequest.intent,
38 | "sessionId": session.sessionId
39 | }
40 | };
41 | logJsonEvent(receiveIntentJsonEvent);
42 | },
43 | logAPICall: function(session, apiName) {
44 | let apiCallJsonEvent =
45 | {
46 | "eventType" : "APICall",
47 | "event": {
48 | "apiName": apiName,
49 | "datestring": (new Date()).toISOString(),
50 | "userId": session.user.userId,
51 | "sessionId": session.sessionId
52 | }
53 | };
54 | logJsonEvent(apiCallJsonEvent);
55 | },
56 | logAPISuccesses: function(session, apiName) {
57 | let apiSuccessJsonEvent =
58 | {
59 | "eventType" : "APISuccess",
60 | "event": {
61 | "apiName": apiName,
62 | "datestring": (new Date()).toISOString(),
63 | "userId": session.user.userId,
64 | "sessionId": session.sessionId
65 | }
66 | };
67 | logJsonEvent(apiSuccessJsonEvent);
68 | },
69 | logAPIError: function(session, apiName, error) {
70 | let apiErrorJsonEvent =
71 | {
72 | "eventType" : "APIError",
73 | "event": {
74 | "apiName": apiName,
75 | "error" : error,
76 | "datestring": (new Date()).toISOString(),
77 | "userId": session.user.userId,
78 | "sessionId": session.sessionId
79 | }
80 | };
81 | logJsonEvent(apiErrorJsonEvent);
82 | }
83 | };
84 | }();
85 |
86 | module.exports = logHelper;
87 |
88 | function logJsonEvent(jsonEvent) {
89 | console.log("%j", jsonEvent);
90 | }
91 |
--------------------------------------------------------------------------------
/lambda/custom/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "skill-sample-nodejs-feed",
3 | "version": "1.0.0",
4 | "description": "An Alexa Skill Template to help create skills that read RSS/Atom feeds. ",
5 | "dependencies": {
6 | "alexa-sdk": "1.0.5",
7 | "async": "2.0.0-rc.6",
8 | "aws-sdk": ">=2.814.0",
9 | "feedparser": "1.1.4",
10 | "html-entities": "1.2.0",
11 | "request": "2.72.0",
12 | "striptags": ">=3.2.0"
13 | },
14 | "author": "Amazon.com"
15 | }
16 |
--------------------------------------------------------------------------------
/lambda/custom/s3Helper.js:
--------------------------------------------------------------------------------
1 | const AWS = require("aws-sdk");
2 | const s3 = new AWS.S3();
3 | const config = require("./configuration");
4 |
5 | const s3Helper = function () {
6 | return {
7 | // Get file from S3
8 | getObject : function (fileName, versionId, callback) {
9 | let params = {
10 | Bucket: config.s3BucketName,
11 | Key: fileName
12 | };
13 | if (versionId) {
14 | params.VersionId = versionId;
15 | }
16 | s3.getObject(params, function (err, data) {
17 | callback(err, data);
18 | });
19 | },
20 | // Put file into S3
21 | putObject : function (fileName, data, callback) {
22 | let expirationDate = new Date();
23 | // Assuming a user would not remain active in the same session for over 1 hr.
24 | expirationDate = new Date(expirationDate.setHours(expirationDate.getHours() + 1));
25 | let params = {
26 | Bucket: config.s3BucketName,
27 | Key: fileName,
28 | Body: data,
29 | Expires: expirationDate
30 | };
31 | s3.putObject(params, function (err, data) {
32 | callback(err, data);
33 | });
34 | },
35 | // Delete file from S3
36 | deleteObject : function (fileName, versionId,callback) {
37 | let params = {
38 | Bucket: config.s3BucketName,
39 | Key : fileName,
40 | VersionId : versionId
41 | };
42 | s3.deleteObject(params, function (err, data) {
43 | callback(err, data);
44 | })
45 | },
46 | // Delete files from S3
47 | deleteObjects : function (objects, callback) {
48 | let params = {
49 | Bucket: config.s3BucketName,
50 | Delete: {
51 | Objects : objects
52 | }
53 | };
54 | s3.deleteObjects(params, function (err, data) {
55 | callback(err, data);
56 | })
57 | }
58 | };
59 | }();
60 |
61 | module.exports = s3Helper;
62 |
--------------------------------------------------------------------------------
/lambda/custom/speechHandlers.js:
--------------------------------------------------------------------------------
1 | const config = require('./configuration');
2 | const constants = require('./constants');
3 |
4 | const speechHandlers = {
5 | 'welcome' : function() {
6 | // Output welcome message with card
7 | let message = config.welcome_message + constants.breakTime['100'] +
8 | ' You can select any of the following feeds : ';
9 | let reprompt = 'You can ask for any of the following feeds : ';
10 | let cardTitle = config.welcome_message;
11 | let cardContent = config.welcome_message + ' You can select any of the following feeds : \n';
12 | // Call category helper to get list of all categories
13 | categoryHelper((categoryList, cardCategoryList) => {
14 | message += categoryList;
15 | reprompt += categoryList;
16 | cardContent += cardCategoryList;
17 |
18 | this.response.cardRenderer(cardTitle, cardContent, null);
19 | this.response.speak(message).listen(reprompt);
20 |
21 | this.emit(':responseReady');
22 |
23 | });
24 | },
25 | 'listCategories' : function () {
26 | // Output the list of all feeds with card
27 | let message;
28 | let reprompt = 'You can ask for any of the following feeds : ';
29 | let cardTitle = 'Feed List';
30 | let cardContent;
31 |
32 | // changing state to START MODE
33 | this.handler.state = constants.states.START_MODE;
34 | // Call category helper to get list of all categories
35 | categoryHelper((categoryList, cardCategoryList) => {
36 | message = categoryList;
37 | reprompt += categoryList;
38 | cardContent = cardCategoryList;
39 |
40 | this.response.cardRenderer(cardTitle, cardContent, null);
41 | this.response.speak(message).listen(reprompt);
42 |
43 | this.emit(':responseReady'); })
44 | },
45 | 'illegalCategory' : function () {
46 | // Output sorry message when category not recognized along with a request to choose the category
47 | let message = 'Sorry, I could not understand. You can select any of the following categories : ';
48 | let reprompt = 'You can ask for any of the following categories : ';
49 | // Call category helper to get list of all categories
50 | categoryHelper((categoryList) => {
51 | message += categoryList;
52 | reprompt += categoryList;
53 |
54 | this.response.speak(message).listen(reprompt);
55 |
56 | this.emit(':responseReady'); })
57 | },
58 | 'listFavorite' : function () {
59 | // Output list of feeds marked as favorite by the user
60 | let message;
61 | let reprompt;
62 | const cardTitle = 'Favorites';
63 | let cardContent;
64 |
65 | let favoriteList = this.attributes['favoriteCategories'];
66 | let favoriteListLength = favoriteList.length;
67 | if (favoriteListLength === 0) {
68 | return this.emit('favoriteListEmpty');
69 | } else if (favoriteListLength === 1) {
70 | message = favoriteList[0] + ' is your favorite feed. ' +
71 | 'You can say open favorite to hear your favorite feeds.';
72 | cardContent = message;
73 | reprompt = 'You have marked ' + favoriteList[0] + ' as your favorite feed.' +
74 | 'You can say open favorite to hear your favorite feeds.';
75 | } else {
76 | message = '';
77 | reprompt = 'Your favorite feeds are : ';
78 | cardContent = 'Your favorite feeds are : \n';
79 | for (let index = 0; index < favoriteListLength; index++) {
80 | message += (index+1) + ' . ' + favoriteList[index] + constants.breakTime['300'];
81 | cardContent += (index+1) + '. ' + favoriteList[index] + '\n';
82 | reprompt += (index+1) + ' . ' + favoriteList[index] + constants.breakTime['300'];
83 | }
84 | message += ' You can say open favorites to hear your favorite feeds.';
85 | cardContent += 'You can say open favorites to hear your favorite feeds.';
86 | reprompt += ' You can say open favorites to hear your favorite feeds.';
87 | }
88 | this.response.cardRenderer(cardTitle, cardContent, null);
89 | this.response.speak(message).listen(reprompt);
90 |
91 | this.emit(':responseReady');
92 | },
93 | 'favoriteAdded' : function (category) {
94 | // Output success message when feed marked as favorite
95 | if (this.attributes['category'] === 'Favorite') {
96 | // Switch to START MODE since favorite feed has been altered
97 | this.handler.state = constants.states.START_MODE;
98 | }
99 | let message = category + ' has been added to your favorites. ' +
100 | 'You can say, open favorites, to hear your favorite feeds.';
101 | const reprompt = 'You can say, open favorites, to hear your favorite feeds.';
102 | this.response.speak(message).listen(reprompt);
103 |
104 | this.emit(':responseReady');
105 | },
106 | 'favoriteRemoved' : function (category) {
107 | // Output success message when feed removed as favorite
108 | if (this.attributes['category'] === 'Favorite') {
109 | // Switch to START MODE since favorite feed has been altered
110 | this.handler.state = constants.states.START_MODE;
111 | }
112 | let message = category + ' has been removed from your favorites. ' +
113 | 'You can say, open favorites, to hear your favorite feeds.';
114 | const reprompt = 'You can say, open favorites, to hear your favorite feeds.';
115 | this.response.speak(message).listen(reprompt);
116 |
117 | this.emit(':responseReady');
118 | },
119 | 'favoriteAddExistingError' : function (category) {
120 | // Output message when feed is already marked as favorite
121 | let message = category + ' is already present in your favorites. ' +
122 | 'You can say, open favorites, to hear your favorite feeds.';
123 | const reprompt = 'You can say, open favorites, to hear your favorite feeds.';
124 | this.emit(':ask', message, reprompt);
125 | },
126 | 'favoriteRemoveExistingError' : function (category) {
127 | // Output message when feed is already absent from favorite
128 | let message = category + ' was not present in your favorites. ' +
129 | 'You can say, open favorites, to hear your favorite feeds.';
130 | const reprompt = 'You can say, open favorites, to hear your favorite feeds.';
131 | this.response.speak(message).listen(reprompt);
132 |
133 | this.emit(':responseReady');
134 | },
135 | 'favoriteAddCurrentError' : function () {
136 | const message = 'Sorry, cannot add favorite to favorites.';
137 | const reprompt = 'You can say next for more items.';
138 | this.response.speak(message).listen(reprompt);
139 |
140 | this.emit(':responseReady');
141 | },
142 | 'favoriteRemoveCurrentError' : function () {
143 | const message = 'Sorry, cannot remove favorite from favorites.';
144 | const reprompt = 'You can say next for more items.';
145 | this.response.speak(message).listen(reprompt);
146 |
147 | this.emit(':responseReady');
148 | },
149 | 'favoriteListEmpty' : function () {
150 | const message = 'You have no feed marked as favorite. ' +
151 | 'You can say mark ' + Object.keys(config.feeds)[0] + ' as favorite.';
152 | const reprompt = 'Your list of favorite feeds is empty. ' +
153 | 'You can say mark ' + Object.keys(config.feeds)[0] + ' as favorite.';
154 | this.response.speak(message).listen(reprompt);
155 |
156 | this.emit(':responseReady');
157 | },
158 | 'noNewItems' : function () {
159 | // Output message when no new items present in the feed
160 | let message = '';
161 | if (Object.keys(config.feeds).length === 1) {
162 | if (this.attributes['start']) {
163 | message = config.welcome_message + " ";
164 | this.attributes['start'] = false;
165 | }
166 | message += 'There are no new items. Would you like to hear older items? ';
167 | this.response.speak(message).listen(message);
168 |
169 | } else {
170 | message = this.attributes['category'] + ' has no new items. Would you like to hear older items? ';
171 | const reprompt = 'There are no new items in the feed. ' +
172 | 'You can say yes to hear older items and no to select other feeds.';
173 | // change state to NO_NEW_ITEM
174 | this.handler.state = constants.states.NO_NEW_ITEM;
175 | this.response.speak(message).listen(reprompt);
176 | }
177 | this.emit(':responseReady');
178 | },
179 | 'readPagedItems' : function (items) {
180 | // Read items to the user
181 | const category = this.attributes['category'];
182 |
183 | let message = '';
184 | let reprompt = '';
185 | const cardTitle = category;
186 | let cardContent = '';
187 | let content = '';
188 |
189 | const feedEndedKey = 'feedEnded' + category;
190 | const justStartedKey = 'justStarted' + category;
191 |
192 | // change state to FEED_MODE
193 | this.handler.state = constants.states.FEED_MODE;
194 | // add message to notify number of new items in the feed
195 | if (this.attributes['newItemCount'] && this.attributes['newItemCount'] > 0) {
196 | let msg;
197 | if (this.attributes['newItemCount'] == 1) {
198 | msg = ' new item. ';
199 | } else {
200 | msg = ' new items. ';
201 | }
202 | cardContent = this.attributes['category'] + ' has ' + this.attributes['newItemCount'] + msg + '\n';
203 | message = this.attributes['category'] + ' has ' + this.attributes['newItemCount'] + msg +
204 | constants.breakTime['200'];
205 | this.attributes['newItemCount'] = null;
206 | }
207 |
208 | items.forEach(function (feed) {
209 | content += config.speech_style_for_numbering_feeds + " " + (feed.count + 1) + ". " + feed.title + ". ";
210 | cardContent += config.speech_style_for_numbering_feeds + " " + (feed.count + 1) + ". " + feed.title;
211 | // If config flag set to display description, append description
212 | if (!config.display_only_title_in_card) {
213 | cardContent += " - ";
214 | cardContent += feed.description;
215 | }
216 | if (!config.speak_only_feed_title) {
217 | content += constants.breakTime['300'];
218 | content += feed.description + " ";
219 | }
220 | cardContent += '\n';
221 | content += constants.breakTime['500'];
222 | });
223 | message += content;
224 | if (this.attributes[feedEndedKey]) {
225 | message += ' You have reached the end of the feed. ' + constants.breakTime['200'] +
226 | ' You can choose any other category, or restart the feed. ';
227 | cardContent += 'You have reached the end of the feed. ' +
228 | 'You can choose any other category, or restart the feed. ';
229 | reprompt = ' You can say, list all feeds, to hear all categories or' +
230 | ' say, restart, to start over the current feed. ';
231 | } else if (this.attributes[justStartedKey]) {
232 | message += 'You can say next for more.';
233 | cardContent += 'You can say next for more.';
234 | reprompt = 'You can say next for more items. You can also say, list all feeds, to hear all categories. ';
235 | } else {
236 | message += 'You can say next for more. ';
237 | cardContent += 'You can say next for more. ';
238 | reprompt = 'You can say next for more items, or say previous for previous items. ' +
239 | 'You can also say, list all feeds, to hear all categories. ';
240 | }
241 | this.response.cardRenderer(cardTitle, cardContent, null);
242 | this.response.speak(message).listen(reprompt);
243 |
244 | this.emit(':responseReady');
245 | },
246 | 'readPagedItemsSingleMode' : function (items) {
247 | // Read items to the user
248 | const category = this.attributes['category'];
249 |
250 | let message = '';
251 | const cardTitle = 'Items';
252 | let cardContent = '';
253 | let content = '';
254 |
255 | const feedEndedKey = 'feedEnded' + category;
256 | // add message to notify number of new items in the feed
257 | if (this.attributes['newItemCount']) {
258 | message += config.welcome_message + constants.breakTime['100'];
259 | if (this.attributes['newItemCount'] > 0) {
260 | let msg;
261 | if (this.attributes['newItemCount'] == 1) {
262 | msg = ' new item. ';
263 | } else {
264 | msg = ' new items. ';
265 | }
266 | cardContent = 'There are ' + this.attributes['newItemCount'] + msg + '\n';
267 | message += this.attributes['category'] + ' has ' + this.attributes['newItemCount'] + msg +
268 | constants.breakTime['200'];
269 | } else {
270 | message += 'There are ' + this.attributes['feedLength'] + ' items in the feed. ';
271 | }
272 | this.attributes['newItemCount'] = null;
273 | // Setting start flag as false
274 | if (this.attributes['start']) {
275 | this.attributes['start'] = false;
276 | }
277 | }
278 |
279 | items.forEach(function (feed) {
280 | content += config.speech_style_for_numbering_feeds + " " + (feed.count + 1) + ". " + feed.title + ". ";
281 | cardContent += config.speech_style_for_numbering_feeds + " " + (feed.count + 1) + ". " + feed.title;
282 | // If config flag set to display description, append description
283 | if (!config.display_only_title_in_card) {
284 | cardContent += " - ";
285 | cardContent += feed.description;
286 | }
287 | if (!config.speak_only_feed_title) {
288 | content += constants.breakTime['300'];
289 | content += feed.description + " ";
290 | }
291 | cardContent += '\n';
292 | content += constants.breakTime['500'];
293 | });
294 | message += content;
295 | if (this.attributes[feedEndedKey]) {
296 | message += ' You have reached the end of the feed. ' + constants.breakTime['200'] +
297 | ' You can say restart to hear the feed from the beginning or say previous to hear newer items. ';
298 | cardContent += 'You have reached the end of the feed. ' +
299 | ' You can say restart to hear the feed from the beginning or say previous to hear newer items. ';
300 | return this.emit(':askWithCard', message, message, cardTitle, cardContent, null);
301 | } else {
302 | message += 'You can say next for more. ';
303 | cardContent += 'You can say next for more. ';
304 | }
305 | this.response.cardRenderer(cardTitle, cardContent, null);
306 | this.response.speak(message).listen(message);
307 |
308 | this.emit(':responseReady');
309 | },
310 | 'feedEmptyError' : function () {
311 | // Output sorry message when requested feed has no items
312 | const message = 'Sorry, the feed is empty. Please select another feed.';
313 | const reprompt = 'Sorry, the feed is empty. Please select another feed.';
314 | this.response.speak(message).listen(reprompt);
315 |
316 | this.emit(':responseReady');
317 | },
318 | 'justStarted' : function () {
319 | // Outputs message when user says previous when already at start of feed
320 | const message = 'Sorry, you are at the start of the feed. ' +
321 | 'You can say next to hear subsequent items or you can say list categories to select other feeds.';
322 | const reprompt = 'You can say next to hear subsequent items or ' +
323 | 'you can say list categories to select other feeds.';
324 | this.response.speak(message).listen(reprompt);
325 |
326 | this.emit(':responseReady');
327 | },
328 | 'alreadyEnded' : function () {
329 | // Outputs message when user says next when already at end of feed
330 | const message = 'Sorry, you are at the end of the feed. ' +
331 | 'You can say list categories to select other feeds or you can say previous to hear previous feeds.';
332 | const reprompt = 'You can say list categories to select other categories or ' +
333 | 'you can say previous to hear previous feeds.';
334 | this.response.speak(message).listen(reprompt);
335 |
336 | this.emit(':responseReady');
337 | },
338 | 'helpStartMode' : function () {
339 | // Outputs helps message when in START MODE
340 | let message = config.welcome_message + constants.breakTime['100'] +
341 | 'To hear any news feed, you can select any category using its name or number. ' +
342 | constants.breakTime['100'] +
343 | 'You can also mark one or more categories as your favorite ' +
344 | ' and ask alexa, open favorites, to hear those feeds. ' +
345 | constants.breakTime['100'] +
346 | 'To mark any category as your favorite, you can say, mark Top Stories as favorite.' +
347 | constants.breakTime['100'] +
348 | 'Here are the available categories : ' +
349 | constants.breakTime['100'];
350 | // Call category helper to get list of all categories
351 | categoryHelper((categoryList) => {
352 | message += categoryList;
353 | this.response.speak(message).listen(message);
354 | this.emit(':responseReady');
355 | });
356 | },
357 | 'helpFeedMode' : function () {
358 | // Outputs helps message when in FEED MODE
359 | const category = this.attributes['category'];
360 | let message = 'You are listening to ' + category +
361 | constants.breakTime['100'] +
362 | 'You can say next or previous to navigate through the feed. ' +
363 | constants.breakTime['100'] +
364 | ' To hear all categories, you can say, get category list.' +
365 | constants.breakTime['100'] +
366 | ' And say restart to start over the current feed. ' +
367 | constants.breakTime['100'] +
368 | 'You can also ask, give details for item 1 to get more information about the item. ' +
369 | constants.breakTime['100'] +
370 | 'What would you like to do?';
371 | this.response.speak(message).listen(message);
372 |
373 | this.emit(':responseReady');
374 | },
375 | 'helpNoNewItemMode' : function () {
376 | // Outputs helps message when in NO NEW ITEM MODE
377 | const message = this.attributes['category'] + ' has no new items. ' +
378 | 'You can say yes to hear older items and no to select other feeds.'
379 | + constants.breakTime['100'] +
380 | 'What would you like to do?';
381 | this.response.speak(message).listen(message);
382 | this.emit(':responseReady');
383 | },
384 | 'helpSingleFeedMode' : function () {
385 | const message = config.welcome_message + 'To navigate through the feed, you can say commands like next and previous.';
386 | this.response.speak(message).listen(message);
387 | this.emit(':responseReady');
388 | },
389 | 'readItemSpeechHelper' : function () {
390 | // Output sorry message to user. Metrics created using cloudwatch logs to see how many users requests are made
391 | const message = 'Sorry, this feature is not available.'
392 | + constants.breakTime['250'] +
393 | 'You can continue navigating through the feed by saying next.';
394 | this.response.speak(message).listen(message);
395 | this.emit(':responseReady');
396 | },
397 | 'sendItemSpeechHelper' : function () {
398 | // Output sorry message to user. Metrics created using cloudwatch logs to see how many users requests are made
399 | const message = 'Sorry, this feature is not available.'
400 | + constants.breakTime['250'] +
401 | 'You can continue navigating through the feed by saying next.';
402 | this.response.speak(message).listen(message);
403 | this.emit(':responseReady');
404 | },
405 | 'itemInfoError' : function () {
406 | // Handle itemInfo request when not in feed mode
407 | const message = 'Sorry, you need to select a feed before requesting item details. ' +
408 | 'What would you like to hear?';
409 | let reprompt = 'You can select any feed using its name or number' +
410 | constants.breakTime['100'] + 'Here are the available feeds : ' +
411 | constants.breakTime['100'];
412 | // Call category helper to get list of all categories
413 | categoryHelper((categoryList) => {
414 | reprompt += categoryList;
415 | this.response.speak(message).listen(reprompt);
416 | this.emit(':responseReady');
417 | });
418 | this.response.speak(message).listen(reprompt);
419 | this.emit(':responseReady');
420 | },
421 | 'sendItemError' : function () {
422 | // Handle sendItem request when not in feed mode
423 | const message = 'Sorry, you need to select a feed before requesting item details. ' +
424 | 'What would you like to hear?';
425 | let reprompt = 'You can select any feed using its name or number' +
426 | constants.breakTime['100'] + 'Here are the available feeds : ' +
427 | constants.breakTime['100'];
428 | // Call category helper to get list of all categories
429 | categoryHelper((categoryList) => {
430 | reprompt += categoryList;
431 | this.response.speak(message).listen(reprompt);
432 | this.emit(':responseReady');
433 | });
434 | this.response.speak(message).listen(reprompt);
435 | this.emit(':responseReady');
436 | },
437 | 'unhandledStartMode' : function () {
438 | // Help user with possible options in _FEED_MODE
439 | let message = 'Sorry, to hear any news feed, you can select any category using its name or number. ' +
440 | constants.breakTime['100'] +
441 | 'Here are the available categories : ' +
442 | constants.breakTime['100'];
443 | // Call category helper to get list of all categories
444 | categoryHelper((categoryList) => {
445 | message += categoryList;
446 | this.response.speak(message).listen(message);
447 | this.emit(':responseReady');
448 | });
449 | },
450 | 'unhandledFeedMode' : function () {
451 | // Help user with possible options in _FEED_MODE
452 | const message = 'Sorry, you can say next or previous to navigate through the feed. ' +
453 | constants.breakTime['100'] +
454 | ' To hear all categories, you can say, get category list.' +
455 | constants.breakTime['100'] +
456 | ' And say restart to start over the current feed. ' +
457 | constants.breakTime['100'] +
458 | 'You can also ask, give details for item one to get more information about the item. ' +
459 | constants.breakTime['100'] +
460 | 'What would you like to do?';
461 | this.response.speak(message).listen(message);
462 | this.emit(':responseReady');
463 | },
464 | 'unhandledNoNewItemMode' : function () {
465 | // Help user with possible options in _NO_NEW_ITEM_MODE
466 | const message = 'Sorry, you can say yes to hear older items and no to select other feeds.';
467 | const reprompt = this.attributes['category'] + ' has no new items. ' +
468 | 'You can say yes to hear older items and no to select other feeds.'
469 | + constants.breakTime['100'] +
470 | 'What would you like to do?';
471 | this.response.speak(message).listen(reprompt);
472 | this.emit(':responseReady');
473 | },
474 | 'unhandledSingleFeedMode' : function () {
475 | const message = 'Sorry, you can say next and previous to navigate through the feed. What would you like to do?';
476 | this.response.speak(message).listen(message);
477 | this.emit(':responseReady');
478 | },
479 | 'reportError' : function () {
480 | // Output error message and close the session
481 | const message = 'Sorry, there are some technical difficulties in fetching the requested information. Please try again later.';
482 | this.emit('EndSession', message);
483 | }
484 | };
485 |
486 | function categoryHelper(callback) {
487 | // Generate a list of categories to serve several functions
488 | let categories = Object.keys(config.feeds);
489 | let categoryList = '';
490 | let cardCategoryList = '';
491 | let index = 0;
492 | categories.forEach(function (category) {
493 | categoryList += (++index) + constants.breakTime['100'] + category + constants.breakTime['200'];
494 | cardCategoryList += (index) + ') ' + category + ' \n';
495 | });
496 | categoryList += '. Which one would you like to hear?';
497 | cardCategoryList += 'Which one would you like to hear?';
498 | callback(categoryList, cardCategoryList);
499 | }
500 |
501 | module.exports = speechHandlers;
502 |
--------------------------------------------------------------------------------
/lambda/custom/stateHandlers.js:
--------------------------------------------------------------------------------
1 | const Alexa = require('alexa-sdk');
2 | const config = require('./configuration');
3 | const constants = require('./constants');
4 |
5 | const stateHandlers = {
6 | startModeIntentHandlers : Alexa.CreateStateHandler(constants.states.START_MODE, {
7 | /*
8 | * All Intent Handlers for state : _START_MODE
9 | */
10 | 'SelectCategory' : function () {
11 | /*
12 | * If requested category is valid : call select category in intentHandler
13 | * Else : report illegal category from speechHandler
14 | */
15 | categoryHelper(this.event.request.intent.slots, (category) =>{
16 | if (category) {
17 | this.attributes['category'] = category;
18 | this.emit('selectCategory');
19 | } else {
20 | this.emit('illegalCategory');
21 | }
22 | });
23 | },
24 | 'GetCategoryList' : function () {
25 | // List all categories to user. Call listCategories in speechHandler
26 | this.emit('listCategories');
27 | },
28 | 'SelectFavorite' : function () {
29 | this.emit('selectFavorite');
30 | },
31 | 'AddFavorite' : function () {
32 | /*
33 | * If requested category is valid : call add favorite in intentHandler
34 | * Else : report illegal category from speechHandler
35 | */
36 | categoryHelper(this.event.request.intent.slots, (category) =>{
37 | if (category) {
38 | this.emit('addFavorite', category);
39 | } else {
40 | this.emit('illegalCategory');
41 | }
42 | });
43 | },
44 | 'RemoveFavorite' : function () {
45 | /*
46 | * If requested category is valid : call remove favorite in intentHandler
47 | * Else : report illegal category from speechHandler
48 | */
49 | categoryHelper(this.event.request.intent.slots, (category) =>{
50 | if (category) {
51 | this.emit('removeFavorite', category);
52 | } else {
53 | this.emit('illegalCategory');
54 | }
55 | });
56 | },
57 | 'ListFavorite' : function () {
58 | // List all favorite categories to user. Call listFavorite in speechHandler
59 | this.emit('listFavorite');
60 | },
61 | 'ItemInfo' : function() {
62 | this.emit('itemInfoError');
63 | },
64 | 'SendItem' : function() {
65 | this.emit('sendItemError');
66 | },
67 | 'AMAZON.HelpIntent' : function () {
68 | this.emit('helpStartMode');
69 | },
70 | 'AMAZON.StopIntent' : function () {
71 | this.emit('EndSession', 'Goodbye');
72 | },
73 | 'AMAZON.CancelIntent' : function () {
74 | this.emit('EndSession', 'Goodbye');
75 | },
76 | 'SessionEndedRequest' : function () {
77 | this.emit('EndSession', constants.terminate);
78 | },
79 | 'Unhandled' : function () {
80 | this.emit('unhandledStartMode');
81 | }
82 | }),
83 | feedModeIntentHandlers: Alexa.CreateStateHandler(constants.states.FEED_MODE, {
84 | /*
85 | * All Intent Handlers for state : _FEED_MODE
86 | */
87 | 'SelectCategory' : function () {
88 | /*
89 | * If requested category is valid : call select category in intentHandler
90 | * Else : report illegal category from speechHandler
91 | */
92 | categoryHelper(this.event.request.intent.slots, (category) =>{
93 | if (category) {
94 | this.attributes['category'] = category;
95 | this.emit('selectCategory');
96 | } else {
97 | this.emit('illegalCategory');
98 | }
99 | });
100 | },
101 | 'GetCategoryList' : function () {
102 | // List all categories to user. Call listCategories in speechHandler
103 | this.emit('listCategories');
104 | },
105 | 'SelectFavorite' : function () {
106 | this.emit('selectFavorite');
107 | },
108 | 'AddFavorite' : function () {
109 | /*
110 | * If requested category is valid : call add favorite in intentHandler
111 | * Else : report illegal category from speechHandler
112 | */
113 | categoryHelper(this.event.request.intent.slots, (category) =>{
114 | if (category) {
115 | this.emit('addFavorite', category);
116 | } else {
117 | this.emit('illegalCategory');
118 | }
119 | });
120 | },
121 | 'AddCurrentToFavorite' : function () {
122 | this.emit('addCurrentToFavorite');
123 | },
124 | 'RemoveFavorite' : function () {
125 | /*
126 | * If requested category is valid : call remove favorite in intentHandler
127 | * Else : report illegal category from speechHandler
128 | */
129 | categoryHelper(this.event.request.intent.slots, (category) =>{
130 | if (category) {
131 | this.emit('removeFavorite', category);
132 | } else {
133 | this.emit('illegalCategory');
134 | }
135 | });
136 | },
137 | 'RemoveCurrentFromFavorite' : function () {
138 | this.emit('removeCurrentFromFavorite');
139 | },
140 | 'ListFavorite' : function () {
141 | // List all categories to user. Call listFavorite in speechHandler
142 | this.emit('listFavorite');
143 | },
144 | 'ItemInfo' : function() {
145 | this.emit('readItemSpeechHelper');
146 | },
147 | 'SendItem' : function() {
148 | this.emit('sendItemSpeechHelper');
149 | },
150 | 'AMAZON.NextIntent' : function () {
151 | this.emit('readItems');
152 | },
153 | 'AMAZON.PreviousIntent' : function () {
154 | this.emit('readPreviousItems');
155 | },
156 | 'AMAZON.StartOverIntent' : function () {
157 | this.emit('startOver');
158 | },
159 | 'AMAZON.HelpIntent' : function () {
160 | this.emit('helpFeedMode');
161 | },
162 | 'AMAZON.StopIntent' : function () {
163 | this.emit('EndSession', 'Good bye .');
164 | },
165 | 'AMAZON.CancelIntent' : function () {
166 | this.emit('EndSession', 'Good bye .');
167 | },
168 | 'SessionEndedRequest' : function () {
169 | this.emit('EndSession', constants.terminate);
170 | },
171 | 'Unhandled' : function () {
172 | // Calling speechHandler
173 | this.emit('unhandledFeedMode');
174 | }
175 | }),
176 | noNewItemsModeIntentHandlers : Alexa.CreateStateHandler(constants.states.NO_NEW_ITEM, {
177 | /*
178 | * All Intent Handlers for state : _NO_NEW_ITEM
179 | */
180 | 'AMAZON.YesIntent' : function () {
181 | this.handler.state = constants.states.FEED_MODE;
182 | this.attributes['newItemCount'] = null;
183 | this.emit('readItems');
184 | },
185 | 'AMAZON.NoIntent' : function () {
186 | this.handler.state = constants.states.START_MODE;
187 | this.attributes['newItemCount'] = null;
188 | this.emit('listCategories');
189 | },
190 | 'AMAZON.HelpIntent' : function () {
191 | this.emit('helpNoNewItemMode');
192 | },
193 | 'AMAZON.StopIntent' : function () {
194 | this.emit('EndSession', 'Good bye .');
195 | },
196 | 'AMAZON.CancelIntent' : function () {
197 | this.emit('EndSession', 'Good bye .');
198 | },
199 | 'SessionEndedRequest' : function () {
200 | this.emit('EndSession', constants.terminate);
201 | },
202 | 'Unhandled' : function () {
203 | this.emit('unhandledNoNewItemMode');
204 | }
205 | }),
206 | singleFeedModeIntentHandlers : Alexa.CreateStateHandler(constants.states.SINGLE_FEED_MODE, {
207 | 'ItemInfo' : function() {
208 | this.emit('readItemSpeechHelper');
209 | },
210 | 'SendItem' : function() {
211 | this.emit('sendItemSpeechHelper')
212 | },
213 | 'AMAZON.NextIntent' : function () {
214 | this.emit('readItems');
215 | },
216 | 'AMAZON.PreviousIntent' : function () {
217 | this.emit('readPreviousItems');
218 | },
219 | 'AMAZON.YesIntent' : function () {
220 | this.attributes['newItemCount'] = null;
221 | this.emit('readItems');
222 | },
223 | 'AMAZON.NoIntent' : function () {
224 | this.attributes['newItemCount'] = null;
225 | this.emit('EndSession', 'Good bye .');
226 | },
227 | 'AMAZON.HelpIntent' : function () {
228 | this.emit('helpSingleFeedMode');
229 | },
230 | 'AMAZON.StopIntent' : function () {
231 | this.emit('EndSession', 'Good bye .');
232 | },
233 | 'AMAZON.CancelIntent' : function () {
234 | this.emit('EndSession', 'Good bye .');
235 | },
236 | 'SessionEndedRequest' : function () {
237 | this.emit('EndSession', constants.terminate);
238 | },
239 | 'Unhandled' : function () {
240 | this.emit('unhandledSingleFeedMode');
241 | }
242 | })
243 | };
244 |
245 | module.exports = stateHandlers;
246 |
247 | function categoryHelper(intentSlot, callback) {
248 | /*
249 | * Extract the category requested by the user
250 | * index stores position of category requested
251 | * category requested in 3 different ways :
252 | * 1) Amazon.NumberIntent - (1,2,3 ...)
253 | * 2) Ordinal - (1st, 2nd, 3rd ...)
254 | * 3) Category Name - (World, Technology, Politics ...)
255 | */
256 | let categoryList = Object.keys(config.feeds);
257 | let index;
258 | if (intentSlot.Index && intentSlot.Index.value) {
259 | index = parseInt(intentSlot.Index.value);
260 | index--;
261 | } else if (intentSlot.Ordinal && intentSlot.Ordinal.value) {
262 | let str = intentSlot.Ordinal.value;
263 | if (str === "second") {
264 | /*
265 | * Alexa Skills Kit passes 'second' instead of '2nd' unlike the case for different numbers.
266 | * Thus, considering this case explicitly.
267 | */
268 | index = 2;
269 | } else {
270 | str = str.substring(0, str.length - 2);
271 | index = parseInt(str);
272 | }
273 | index--;
274 | } else if (intentSlot.Category && intentSlot.Category.value) {
275 | categoryList.forEach(function (item, index, theList) {
276 | theList[index] = item.toLowerCase();
277 | });
278 | index = categoryList.indexOf(intentSlot.Category.value.toLowerCase());
279 | } else {
280 | index = -1;
281 | }
282 | /*
283 | * If index valid : corresponding category is fetched and forwarded
284 | * Else : request user to repeat the category
285 | */
286 | if (index >= 0 && index < categoryList.length) {
287 | let category = Object.keys(config.feeds)[index];
288 | callback(category);
289 | } else {
290 | console.log('Illegal index : ' + index);
291 | callback(null);
292 | }
293 | }
294 |
--------------------------------------------------------------------------------
/models/en-US.json:
--------------------------------------------------------------------------------
1 | {
2 | "interactionModel": {
3 | "languageModel": {
4 | "invocationName": "feed reader",
5 | "intents": [
6 | {
7 | "name": "AddCurrentToFavorite",
8 | "samples": [
9 | "add to favorite",
10 | "add to favorites",
11 | "add as favorite",
12 | "add this to favorite",
13 | "add this to favorites",
14 | "add this to my favorite",
15 | "add this to my favorites",
16 | "add this category to favorite",
17 | "add this category to favorites",
18 | "add this category to my favorite",
19 | "add this category to my favorites",
20 | "add this feed to favorite",
21 | "add this feed to favorites",
22 | "add this feed to my favorite",
23 | "add this feed to my favorites"
24 | ],
25 | "slots": []
26 | },
27 | {
28 | "name": "AddFavorite",
29 | "samples": [
30 | "add {Category} to favorite",
31 | "add {Category} to favorite category",
32 | "add {Category} to favorite categories",
33 | "add {Category} to my favorite",
34 | "add {Category} to my favorite category",
35 | "add {Category} to my favorite categories",
36 | "mark {Category} as favorite",
37 | "mark {Category} as favorite category",
38 | "mark {Category} as favorite categories",
39 | "mark {Category} as my favorite",
40 | "mark {Category} as my favorite category",
41 | "mark {Category} as my favorite categories",
42 | "make {Category} as favorite",
43 | "make {Category} as favorite category",
44 | "make {Category} as favorite categories",
45 | "make {Category} as my favorite",
46 | "make {Category} as my favorite category",
47 | "make {Category} as my favorite categories",
48 | "add category {Index} to favorite",
49 | "add category {Index} to favorite category",
50 | "add category {Index} to favorite categories",
51 | "add category {Index} to my favorite",
52 | "add category {Index} to my favorite category",
53 | "add category {Index} to my favorite categories",
54 | "mark category {Index} as favorite",
55 | "mark category {Index} as favorite category",
56 | "mark category {Index} as favorite categories",
57 | "mark category {Index} as my favorite",
58 | "mark category {Index} as my favorite category",
59 | "mark category {Index} as my favorite categories",
60 | "make category {Index} as favorite",
61 | "make category {Index} as favorite category",
62 | "make category {Index} as favorite categories",
63 | "make category {Index} as my favorite",
64 | "make category {Index} as my favorite category",
65 | "make category {Index} as my favorite categories",
66 | "add {Ordinal} category to favorite",
67 | "add {Ordinal} to favorite category",
68 | "add {Ordinal} to favorite categories",
69 | "add {Ordinal} to my favorite",
70 | "add {Ordinal} to my favorite category",
71 | "add {Ordinal} to my favorite categories",
72 | "mark {Ordinal} as favorite",
73 | "mark {Ordinal} as favorite category",
74 | "mark {Ordinal} as favorite categories",
75 | "mark {Ordinal} as my favorite",
76 | "mark {Ordinal} as my favorite category",
77 | "mark {Ordinal} as my favorite categories",
78 | "make {Ordinal} as favorite",
79 | "make {Ordinal} as favorite category",
80 | "make {Ordinal} as favorite categories",
81 | "make {Ordinal} as my favorite",
82 | "make {Ordinal} as my favorite category",
83 | "make {Ordinal} as my favorite categories",
84 | "add {Ordinal} category to favorite category",
85 | "add {Ordinal} category to favorite categories",
86 | "add {Ordinal} category to my favorite",
87 | "add {Ordinal} category to my favorite category",
88 | "add {Ordinal} category to my favorite categories",
89 | "mark {Ordinal} category as favorite",
90 | "mark {Ordinal} category as favorite category",
91 | "mark {Ordinal} category as favorite categories",
92 | "mark {Ordinal} category as my favorite",
93 | "mark {Ordinal} category as my favorite category",
94 | "mark {Ordinal} category as my favorite categories",
95 | "make {Ordinal} category as favorite",
96 | "make {Ordinal} category as favorite category",
97 | "make {Ordinal} category as favorite categories",
98 | "make {Ordinal} category as my favorite",
99 | "make {Ordinal} category as my favorite category",
100 | "make {Ordinal} category as my favorite categories",
101 | "add {Category} to favorite feed",
102 | "add {Category} to favorite feeds",
103 | "add {Category} to my favorite feed",
104 | "add {Category} to my favorite feeds",
105 | "mark {Category} as favorite feed",
106 | "mark {Category} as favorite feeds",
107 | "mark {Category} as my favorite feed",
108 | "mark {Category} as my favorite feeds",
109 | "make {Category} as favorite feed",
110 | "make {Category} as favorite feeds",
111 | "make {Category} as my favorite feed",
112 | "make {Category} as my favorite feeds",
113 | "add feed {Index} to favorite",
114 | "add feed {Index} to favorite feed",
115 | "add feed {Index} to favorite feeds",
116 | "add feed {Index} to my favorite",
117 | "add feed {Index} to my favorite feed",
118 | "add feed {Index} to my favorite feeds",
119 | "mark feed {Index} as favorite",
120 | "mark feed {Index} as favorite feed",
121 | "mark feed {Index} as favorite feeds",
122 | "mark feed {Index} as my favorite",
123 | "mark feed {Index} as my favorite feed",
124 | "mark feed {Index} as my favorite feeds",
125 | "make feed {Index} as favorite",
126 | "make feed {Index} as favorite feed",
127 | "make feed {Index} as favorite feeds",
128 | "make feed {Index} as my favorite",
129 | "make feed {Index} as my favorite feed",
130 | "make feed {Index} as my favorite feeds",
131 | "add {Ordinal} feed to favorite",
132 | "add {Ordinal} to favorite feed",
133 | "add {Ordinal} to favorite feeds",
134 | "add {Ordinal} to my favorite feed",
135 | "add {Ordinal} to my favorite feeds",
136 | "mark {Ordinal} as favorite feed",
137 | "mark {Ordinal} as favorite feeds",
138 | "mark {Ordinal} as my favorite feed",
139 | "mark {Ordinal} as my favorite feeds",
140 | "make {Ordinal} as favorite feed",
141 | "make {Ordinal} as favorite feeds",
142 | "make {Ordinal} as my favorite feed",
143 | "make {Ordinal} as my favorite feeds",
144 | "add {Ordinal} feed to favorite feed",
145 | "add {Ordinal} feed to favorite feeds",
146 | "add {Ordinal} feed to my favorite",
147 | "add {Ordinal} feed to my favorite feed",
148 | "add {Ordinal} feed to my favorite feeds",
149 | "mark {Ordinal} feed as favorite",
150 | "mark {Ordinal} feed as favorite feed",
151 | "mark {Ordinal} feed as favorite feeds",
152 | "mark {Ordinal} feed as my favorite",
153 | "mark {Ordinal} feed as my favorite feed",
154 | "mark {Ordinal} feed as my favorite feeds",
155 | "make {Ordinal} feed as favorite",
156 | "make {Ordinal} feed as favorite feed",
157 | "make {Ordinal} feed as favorite feeds",
158 | "make {Ordinal} feed as my favorite",
159 | "make {Ordinal} feed as my favorite feed",
160 | "make {Ordinal} feed as my favorite feeds"
161 | ],
162 | "slots": [
163 | {
164 | "name": "Index",
165 | "type": "AMAZON.NUMBER",
166 | "samples": []
167 | },
168 | {
169 | "name": "Ordinal",
170 | "type": "ORDINAL",
171 | "samples": []
172 | },
173 | {
174 | "name": "Category",
175 | "type": "CATEGORY",
176 | "samples": []
177 | }
178 | ]
179 | },
180 | {
181 | "name": "AMAZON.CancelIntent",
182 | "samples": []
183 | },
184 | {
185 | "name": "AMAZON.HelpIntent",
186 | "samples": []
187 | },
188 | {
189 | "name": "AMAZON.NextIntent",
190 | "samples": []
191 | },
192 | {
193 | "name": "AMAZON.NoIntent",
194 | "samples": []
195 | },
196 | {
197 | "name": "AMAZON.PauseIntent",
198 | "samples": []
199 | },
200 | {
201 | "name": "AMAZON.PreviousIntent",
202 | "samples": []
203 | },
204 | {
205 | "name": "AMAZON.ResumeIntent",
206 | "samples": []
207 | },
208 | {
209 | "name": "AMAZON.StartOverIntent",
210 | "samples": []
211 | },
212 | {
213 | "name": "AMAZON.StopIntent",
214 | "samples": []
215 | },
216 | {
217 | "name": "AMAZON.YesIntent",
218 | "samples": []
219 | },
220 | {
221 | "name": "GetCategoryList",
222 | "samples": [
223 | "get categories",
224 | "get all categories",
225 | "get the categories",
226 | "get all the categories",
227 | "get category list",
228 | "get the category list",
229 | "give category list",
230 | "give the category list",
231 | "list categories",
232 | "list the categories",
233 | "list all categories",
234 | "list all the categories",
235 | "back to category",
236 | "back to categories",
237 | "back to all categories",
238 | "back to category list",
239 | "go back to category",
240 | "go back to categories",
241 | "go back to all categories",
242 | "go back to category list",
243 | "get feeds",
244 | "get all feeds",
245 | "get the feeds",
246 | "get all the feeds",
247 | "get feed list",
248 | "get the feed list",
249 | "give feed list",
250 | "give the feed list",
251 | "list feeds",
252 | "list the feeds",
253 | "list all feeds",
254 | "list all the feeds",
255 | "back to feeds",
256 | "back to all feeds",
257 | "back to feed list",
258 | "go back to feeds",
259 | "go back to all feeds",
260 | "go back to feed list"
261 | ],
262 | "slots": []
263 | },
264 | {
265 | "name": "ItemInfo",
266 | "samples": [
267 | "give details of {Ordinal} item",
268 | "give details of item {Index}",
269 | "give details of item number {Index}",
270 | "give more details of {Ordinal} item",
271 | "give more details of item {Index}",
272 | "give more details of item number {Index}",
273 | "details for {Ordinal} item",
274 | "details of item {Index}",
275 | "details of item number {Index}",
276 | "more details of {Ordinal} item",
277 | "more details of item {Index}",
278 | "more details of item number {Index}",
279 | "give details about {Ordinal} item",
280 | "give details about item {Index}",
281 | "give details about item number {Index}",
282 | "give more details about {Ordinal} item",
283 | "give more details about item {Index}",
284 | "give more details about item number {Index}",
285 | "details about {Ordinal} item",
286 | "details about item {Index}",
287 | "details about item number {Index}",
288 | "more details about {Ordinal} item",
289 | "more details about item {Index}",
290 | "more details about item number {Index}",
291 | "give details of {Ordinal} article",
292 | "give details of article {Index}",
293 | "give details of article number {Index}",
294 | "give more details of {Ordinal} article",
295 | "give more details of article {Index}",
296 | "give more details of article number {Index}",
297 | "details for {Ordinal} article",
298 | "details of article {Index}",
299 | "details of article number {Index}",
300 | "more details of {Ordinal} article",
301 | "more details of article {Index}",
302 | "more details of article number {Index}",
303 | "give details about {Ordinal} article",
304 | "give details about article {Index}",
305 | "give details about article number {Index}",
306 | "give more details about {Ordinal} article",
307 | "give more details about article {Index}",
308 | "give more details about article number {Index}",
309 | "details about {Ordinal} article",
310 | "details about article {Index}",
311 | "details about article number {Index}",
312 | "more details about {Ordinal} article",
313 | "more details about article {Index}",
314 | "more details about article number {Index}"
315 | ],
316 | "slots": [
317 | {
318 | "name": "Index",
319 | "type": "AMAZON.NUMBER",
320 | "samples": []
321 | },
322 | {
323 | "name": "Ordinal",
324 | "type": "ORDINAL",
325 | "samples": []
326 | }
327 | ]
328 | },
329 | {
330 | "name": "ListFavorite",
331 | "samples": [
332 | "list favorites",
333 | "list all favorites",
334 | "list my favorites",
335 | "list favorite feeds",
336 | "list my favorite feeds",
337 | "list the favorite feeds",
338 | "which is my favorite feed",
339 | "which are my favorite feeds",
340 | "what is my favorite feed",
341 | "what are my favorite feeds"
342 | ],
343 | "slots": []
344 | },
345 | {
346 | "name": "RemoveCurrentFromFavorite",
347 | "samples": [
348 | "remove from favorite",
349 | "remove from favorites",
350 | "remove as favorite",
351 | "remove this from favorite",
352 | "remove this from favorites",
353 | "remove this from my favorite",
354 | "remove this from my favorites",
355 | "remove this category from favorite",
356 | "remove this category from favorites",
357 | "remove this category from my favorite",
358 | "remove this category from my favorites",
359 | "remove this feed from favorite",
360 | "remove this feed from favorites",
361 | "remove this feed from my favorite",
362 | "remove this feed from my favorites"
363 | ],
364 | "slots": []
365 | },
366 | {
367 | "name": "RemoveFavorite",
368 | "samples": [
369 | "remove {Category}",
370 | "remove {Category} as favorite",
371 | "remove {Category} as my favorite",
372 | "remove {Category} from favorite",
373 | "remove {Category} from favorites",
374 | "remove {Category} from my favorite",
375 | "remove {Category} from my favorites"
376 | ],
377 | "slots": [
378 | {
379 | "name": "Index",
380 | "type": "AMAZON.NUMBER",
381 | "samples": []
382 | },
383 | {
384 | "name": "Ordinal",
385 | "type": "ORDINAL",
386 | "samples": []
387 | },
388 | {
389 | "name": "Category",
390 | "type": "CATEGORY",
391 | "samples": []
392 | }
393 | ]
394 | },
395 | {
396 | "name": "SelectCategory",
397 | "samples": [
398 | "{Index}",
399 | "category {Index}",
400 | "feed {Index}",
401 | "number {Index}",
402 | "open {Index}",
403 | "open category {Index}",
404 | "open feed {Index}",
405 | "open number {Index}",
406 | "open feed number {Index}",
407 | "go to {Index}",
408 | "go to category {Index}",
409 | "go to feed {Index}",
410 | "go to number {Index}",
411 | "to open feed {Index}",
412 | "to open feed number {Index}",
413 | "to go to feed {Index}",
414 | "to go to feed number {Index}",
415 | "to play feed {Index}",
416 | "to play feed number {Index}",
417 | "to begin feed {Index}",
418 | "to begin feed number {Index}",
419 | "to start feed {Index}",
420 | "to start feed number {Index}",
421 | "{Ordinal}",
422 | "{Ordinal} category",
423 | "{Ordinal} feed",
424 | "category {Ordinal}",
425 | "feed {Ordinal}",
426 | "open {Ordinal}",
427 | "open {Ordinal} category",
428 | "open {Ordinal} feed",
429 | "open category {Ordinal}",
430 | "open feed {Ordinal}",
431 | "go to {Ordinal}",
432 | "go to {Ordinal} category",
433 | "go to {Ordinal} feed",
434 | "go to category {Ordinal}",
435 | "go to feed {Ordinal}",
436 | "to open {Ordinal}",
437 | "to open {Ordinal} feed",
438 | "to open feed {Ordinal}",
439 | "to go to {Ordinal}",
440 | "to go to {Ordinal} feed",
441 | "to go to feed {Ordinal}",
442 | "to play {Ordinal}",
443 | "to play {Ordinal} feed",
444 | "to play feed {Ordinal}",
445 | "to begin {Ordinal}",
446 | "to begin {Ordinal} feed",
447 | "to begin feed {Ordinal}",
448 | "to start {Ordinal}",
449 | "to start {Ordinal} feed",
450 | "to start feed {Ordinal}",
451 | "{Category}",
452 | "category {Category}",
453 | "feed {Category}",
454 | "open {Category}",
455 | "open category {Category}",
456 | "open feed {Category}",
457 | "go to {Category}",
458 | "go to Category {Category}",
459 | "go to feed {Category}",
460 | "to open {Category}",
461 | "to open {Category} feed",
462 | "to play {Category}",
463 | "to play {Category} feed",
464 | "to start {Category}",
465 | "to start {Category} feed",
466 | "to begin {Category}",
467 | "to begin {Category} feed"
468 | ],
469 | "slots": [
470 | {
471 | "name": "Index",
472 | "type": "AMAZON.NUMBER",
473 | "samples": []
474 | },
475 | {
476 | "name": "Ordinal",
477 | "type": "ORDINAL",
478 | "samples": []
479 | },
480 | {
481 | "name": "Category",
482 | "type": "CATEGORY",
483 | "samples": []
484 | }
485 | ]
486 | },
487 | {
488 | "name": "SelectFavorite",
489 | "samples": [
490 | "favorite",
491 | "favorites",
492 | "open favorites",
493 | "open favorite feeds",
494 | "open my favorites",
495 | "open my favorite feeds",
496 | "play favorites",
497 | "play favorite feeds",
498 | "play my favorites",
499 | "play my favorite feeds",
500 | "start favorites",
501 | "start favorite feeds",
502 | "start my favorites",
503 | "start my favorite feeds",
504 | "to open favorites",
505 | "to open favorite feeds",
506 | "to open my favorites",
507 | "to open my favorite feeds",
508 | "to play favorites",
509 | "to play favorite feeds",
510 | "to play my favorites",
511 | "to play my favorite feeds",
512 | "to begin favorites",
513 | "to begin favorite feeds",
514 | "to begin my favorites",
515 | "to begin my favorite feeds",
516 | "to start favorites",
517 | "to start favorite feeds",
518 | "to start my favorites",
519 | "to start my favorite feeds"
520 | ],
521 | "slots": []
522 | },
523 | {
524 | "name": "SendItem",
525 | "samples": [
526 | "send details of {Ordinal}",
527 | "send details of {Ordinal} item",
528 | "send details of item {Index}",
529 | "send details of item number {Index}",
530 | "send the details of {Ordinal} item",
531 | "send the details of item {Index}",
532 | "send the details of item number {Index}",
533 | "send the link of {Ordinal}",
534 | "send the link of item {Index}",
535 | "send the link of item number {Index}",
536 | "send details of {Ordinal} article",
537 | "send details of article {Index}",
538 | "send details of article number {Index}",
539 | "send the details of {Ordinal} article",
540 | "send the details of article {Index}",
541 | "send the details of article number {Index}",
542 | "send the link of article {Index}",
543 | "send the link of article number {Index}"
544 | ],
545 | "slots": [
546 | {
547 | "name": "Index",
548 | "type": "AMAZON.NUMBER",
549 | "samples": []
550 | },
551 | {
552 | "name": "Ordinal",
553 | "type": "ORDINAL",
554 | "samples": []
555 | }
556 | ]
557 | }
558 | ],
559 | "types": [
560 | {
561 | "name": "CATEGORY",
562 | "values": [
563 | {
564 | "id": null,
565 | "name": {
566 | "value": "CATEGORY_NAME_1",
567 | "synonyms": []
568 | }
569 | },
570 | {
571 | "id": null,
572 | "name": {
573 | "value": "CATEGORY_NAME_2",
574 | "synonyms": []
575 | }
576 | },
577 | {
578 | "id": null,
579 | "name": {
580 | "value": "CATEGORY_NAME_3",
581 | "synonyms": []
582 | }
583 | },
584 | {
585 | "id": null,
586 | "name": {
587 | "value": "Favorite",
588 | "synonyms": []
589 | }
590 | }
591 | ]
592 | },
593 | {
594 | "name": "ORDINAL",
595 | "values": [
596 | {
597 | "id": null,
598 | "name": {
599 | "value": "1st",
600 | "synonyms": []
601 | }
602 | },
603 | {
604 | "id": null,
605 | "name": {
606 | "value": "2nd",
607 | "synonyms": []
608 | }
609 | },
610 | {
611 | "id": null,
612 | "name": {
613 | "value": "3rd",
614 | "synonyms": []
615 | }
616 | },
617 | {
618 | "id": null,
619 | "name": {
620 | "value": "4th",
621 | "synonyms": []
622 | }
623 | },
624 | {
625 | "id": null,
626 | "name": {
627 | "value": "5th",
628 | "synonyms": []
629 | }
630 | },
631 | {
632 | "id": null,
633 | "name": {
634 | "value": "6th",
635 | "synonyms": []
636 | }
637 | },
638 | {
639 | "id": null,
640 | "name": {
641 | "value": "7th",
642 | "synonyms": []
643 | }
644 | },
645 | {
646 | "id": null,
647 | "name": {
648 | "value": "8th",
649 | "synonyms": []
650 | }
651 | },
652 | {
653 | "id": null,
654 | "name": {
655 | "value": "9th",
656 | "synonyms": []
657 | }
658 | },
659 | {
660 | "id": null,
661 | "name": {
662 | "value": "10th",
663 | "synonyms": []
664 | }
665 | },
666 | {
667 | "id": null,
668 | "name": {
669 | "value": "11th",
670 | "synonyms": []
671 | }
672 | },
673 | {
674 | "id": null,
675 | "name": {
676 | "value": "12th",
677 | "synonyms": []
678 | }
679 | },
680 | {
681 | "id": null,
682 | "name": {
683 | "value": "13th",
684 | "synonyms": []
685 | }
686 | },
687 | {
688 | "id": null,
689 | "name": {
690 | "value": "14th",
691 | "synonyms": []
692 | }
693 | },
694 | {
695 | "id": null,
696 | "name": {
697 | "value": "15th",
698 | "synonyms": []
699 | }
700 | },
701 | {
702 | "id": null,
703 | "name": {
704 | "value": "16th",
705 | "synonyms": []
706 | }
707 | },
708 | {
709 | "id": null,
710 | "name": {
711 | "value": "17th",
712 | "synonyms": []
713 | }
714 | },
715 | {
716 | "id": null,
717 | "name": {
718 | "value": "18th",
719 | "synonyms": []
720 | }
721 | },
722 | {
723 | "id": null,
724 | "name": {
725 | "value": "19th",
726 | "synonyms": []
727 | }
728 | },
729 | {
730 | "id": null,
731 | "name": {
732 | "value": "20th",
733 | "synonyms": []
734 | }
735 | },
736 | {
737 | "id": null,
738 | "name": {
739 | "value": "21st",
740 | "synonyms": []
741 | }
742 | },
743 | {
744 | "id": null,
745 | "name": {
746 | "value": "22nd",
747 | "synonyms": []
748 | }
749 | },
750 | {
751 | "id": null,
752 | "name": {
753 | "value": "23rd",
754 | "synonyms": []
755 | }
756 | },
757 | {
758 | "id": null,
759 | "name": {
760 | "value": "24th",
761 | "synonyms": []
762 | }
763 | },
764 | {
765 | "id": null,
766 | "name": {
767 | "value": "25th",
768 | "synonyms": []
769 | }
770 | },
771 | {
772 | "id": null,
773 | "name": {
774 | "value": "26th",
775 | "synonyms": []
776 | }
777 | },
778 | {
779 | "id": null,
780 | "name": {
781 | "value": "27th",
782 | "synonyms": []
783 | }
784 | },
785 | {
786 | "id": null,
787 | "name": {
788 | "value": "28th",
789 | "synonyms": []
790 | }
791 | },
792 | {
793 | "id": null,
794 | "name": {
795 | "value": "29th",
796 | "synonyms": []
797 | }
798 | },
799 | {
800 | "id": null,
801 | "name": {
802 | "value": "30th",
803 | "synonyms": []
804 | }
805 | },
806 | {
807 | "id": null,
808 | "name": {
809 | "value": "31st",
810 | "synonyms": []
811 | }
812 | },
813 | {
814 | "id": null,
815 | "name": {
816 | "value": "32nd",
817 | "synonyms": []
818 | }
819 | },
820 | {
821 | "id": null,
822 | "name": {
823 | "value": "33rd",
824 | "synonyms": []
825 | }
826 | },
827 | {
828 | "id": null,
829 | "name": {
830 | "value": "34th",
831 | "synonyms": []
832 | }
833 | },
834 | {
835 | "id": null,
836 | "name": {
837 | "value": "35th",
838 | "synonyms": []
839 | }
840 | },
841 | {
842 | "id": null,
843 | "name": {
844 | "value": "36th",
845 | "synonyms": []
846 | }
847 | },
848 | {
849 | "id": null,
850 | "name": {
851 | "value": "37th",
852 | "synonyms": []
853 | }
854 | },
855 | {
856 | "id": null,
857 | "name": {
858 | "value": "38th",
859 | "synonyms": []
860 | }
861 | },
862 | {
863 | "id": null,
864 | "name": {
865 | "value": "39th",
866 | "synonyms": []
867 | }
868 | },
869 | {
870 | "id": null,
871 | "name": {
872 | "value": "40th",
873 | "synonyms": []
874 | }
875 | },
876 | {
877 | "id": null,
878 | "name": {
879 | "value": "41st",
880 | "synonyms": []
881 | }
882 | },
883 | {
884 | "id": null,
885 | "name": {
886 | "value": "42nd",
887 | "synonyms": []
888 | }
889 | },
890 | {
891 | "id": null,
892 | "name": {
893 | "value": "43rd",
894 | "synonyms": []
895 | }
896 | },
897 | {
898 | "id": null,
899 | "name": {
900 | "value": "44th",
901 | "synonyms": []
902 | }
903 | },
904 | {
905 | "id": null,
906 | "name": {
907 | "value": "45th",
908 | "synonyms": []
909 | }
910 | },
911 | {
912 | "id": null,
913 | "name": {
914 | "value": "46th",
915 | "synonyms": []
916 | }
917 | },
918 | {
919 | "id": null,
920 | "name": {
921 | "value": "47th",
922 | "synonyms": []
923 | }
924 | },
925 | {
926 | "id": null,
927 | "name": {
928 | "value": "48th",
929 | "synonyms": []
930 | }
931 | },
932 | {
933 | "id": null,
934 | "name": {
935 | "value": "49th",
936 | "synonyms": []
937 | }
938 | },
939 | {
940 | "id": null,
941 | "name": {
942 | "value": "50th",
943 | "synonyms": []
944 | }
945 | },
946 | {
947 | "id": null,
948 | "name": {
949 | "value": "51st",
950 | "synonyms": []
951 | }
952 | },
953 | {
954 | "id": null,
955 | "name": {
956 | "value": "52nd",
957 | "synonyms": []
958 | }
959 | },
960 | {
961 | "id": null,
962 | "name": {
963 | "value": "53rd",
964 | "synonyms": []
965 | }
966 | },
967 | {
968 | "id": null,
969 | "name": {
970 | "value": "54th",
971 | "synonyms": []
972 | }
973 | },
974 | {
975 | "id": null,
976 | "name": {
977 | "value": "55th",
978 | "synonyms": []
979 | }
980 | },
981 | {
982 | "id": null,
983 | "name": {
984 | "value": "56th",
985 | "synonyms": []
986 | }
987 | },
988 | {
989 | "id": null,
990 | "name": {
991 | "value": "57th",
992 | "synonyms": []
993 | }
994 | },
995 | {
996 | "id": null,
997 | "name": {
998 | "value": "58th",
999 | "synonyms": []
1000 | }
1001 | },
1002 | {
1003 | "id": null,
1004 | "name": {
1005 | "value": "59th",
1006 | "synonyms": []
1007 | }
1008 | },
1009 | {
1010 | "id": null,
1011 | "name": {
1012 | "value": "60th",
1013 | "synonyms": []
1014 | }
1015 | },
1016 | {
1017 | "id": null,
1018 | "name": {
1019 | "value": "61st",
1020 | "synonyms": []
1021 | }
1022 | },
1023 | {
1024 | "id": null,
1025 | "name": {
1026 | "value": "62nd",
1027 | "synonyms": []
1028 | }
1029 | },
1030 | {
1031 | "id": null,
1032 | "name": {
1033 | "value": "63rd",
1034 | "synonyms": []
1035 | }
1036 | },
1037 | {
1038 | "id": null,
1039 | "name": {
1040 | "value": "64th",
1041 | "synonyms": []
1042 | }
1043 | },
1044 | {
1045 | "id": null,
1046 | "name": {
1047 | "value": "65th",
1048 | "synonyms": []
1049 | }
1050 | },
1051 | {
1052 | "id": null,
1053 | "name": {
1054 | "value": "66th",
1055 | "synonyms": []
1056 | }
1057 | },
1058 | {
1059 | "id": null,
1060 | "name": {
1061 | "value": "67th",
1062 | "synonyms": []
1063 | }
1064 | },
1065 | {
1066 | "id": null,
1067 | "name": {
1068 | "value": "68th",
1069 | "synonyms": []
1070 | }
1071 | },
1072 | {
1073 | "id": null,
1074 | "name": {
1075 | "value": "69th",
1076 | "synonyms": []
1077 | }
1078 | },
1079 | {
1080 | "id": null,
1081 | "name": {
1082 | "value": "70th",
1083 | "synonyms": []
1084 | }
1085 | }
1086 | ]
1087 | }
1088 | ]
1089 | }
1090 | }
1091 | }
1092 |
--------------------------------------------------------------------------------
/skill.json:
--------------------------------------------------------------------------------
1 | {
2 | "skillManifest": {
3 | "publishingInformation": {
4 | "locales": {
5 | "en-US": {
6 | "summary": "Have Alexa read feeds to you from an RSS/Atom feed",
7 | "examplePhrases": [
8 | "Alexa open feed reader",
9 | "Alexa tell feed reader to play favorites",
10 | "Alexa ask feed reader to get categories"
11 | ],
12 | "name": "Feed Reader",
13 | "description": "Have Alexa read feeds to you from an RSS/Atom feed. Create a list of favorites and manage your feeds by category."
14 | }
15 | },
16 | "isAvailableWorldwide": true,
17 | "testingInstructions": "Sample Testing Instructions.",
18 | "distributionCountries": []
19 | },
20 | "apis": {
21 | "custom": {
22 | "endpoint": {
23 | "sourceDir": "lambda/custom",
24 | "uri":"ask-custom-feed"
25 | }
26 | }
27 | },
28 | "manifestVersion": "1.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------