├── .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 | [![Voice User Interface](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/1-off._TTH_.png)](./instructions/1-voice-user-interface.md)[![Lambda Function](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/2-off._TTH_.png)](./instructions/2-lambda-function.md)[![Connect VUI to Code](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/3-off._TTH_.png)](./instructions/3-connect-vui-to-code.md)[![Testing](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/4-off._TTH_.png)](./instructions/4-testing.md)[![Customization](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/5-off._TTH_.png)](./instructions/5-customization.md)[![Publication](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/6-off._TTH_.png)](./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 | [![Voice User Interface](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/1-off._TTH_.png)](1-voice-user-interface.md)[![Lambda Function](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/2-off._TTH_.png)](2-lambda-function.md)[![Connect VUI to Code](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/3-off._TTH_.png)](3-connect-vui-to-code.md)[![Testing](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/4-off._TTH_.png)](4-testing.md)[![Customization](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/5-off._TTH_.png)](5-customization.md)[![Publication](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/6-off._TTH_.png)](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 | [![Voice User Interface](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/1-on._TTH_.png)](./1-voice-user-interface.md)[![Lambda Function](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/2-off._TTH_.png)](./2-lambda-function.md)[![Connect VUI to Code](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/3-off._TTH_.png)](./3-connect-vui-to-code.md)[![Testing](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/4-off._TTH_.png)](./4-testing.md)[![Customization](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/5-off._TTH_.png)](./5-customization.md)[![Publication](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/6-off._TTH_.png)](./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 | [![Voice User Interface](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/1-locked._TTH_.png)](./1-voice-user-interface.md)[![Lambda Function](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/2-on._TTH_.png)](./2-lambda-function.md)[![Connect VUI to Code](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/3-off._TTH_.png)](./3-connect-vui-to-code.md)[![Testing](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/4-off._TTH_.png)](./4-testing.md)[![Customization](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/5-off._TTH_.png)](./5-customization.md)[![Publication](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/6-off._TTH_.png)](./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 | ![alt text](https://cloud.githubusercontent.com/assets/7671574/17307587/b80787f2-57ea-11e6-9be2-3df26e8e5947.png "AWS DynamoDB Screenshot") 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 | [![Voice User Interface](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/1-locked._TTH_.png)](./1-voice-user-interface.md)[![Lambda Function](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/2-locked._TTH_.png)](./2-lambda-function.md)[![Connect VUI to Code](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/3-on._TTH_.png)](./3-connect-vui-to-code.md)[![Testing](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/4-off._TTH_.png)](./4-testing.md)[![Customization](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/5-off._TTH_.png)](./5-customization.md)[![Publication](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/6-off._TTH_.png)](./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 | [![Voice User Interface](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/1-locked._TTH_.png)](./1-voice-user-interface.md)[![Lambda Function](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/2-locked._TTH_.png)](./2-lambda-function.md)[![Connect VUI to Code](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/3-locked._TTH_.png)](./3-connect-vui-to-code.md)[![Testing](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/4-on._TTH_.png)](./4-testing.md)[![Customization](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/5-off._TTH_.png)](./5-customization.md)[![Publication](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/6-off._TTH_.png)](./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 | [![Voice User Interface](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/1-locked._TTH_.png)](./1-voice-user-interface.md)[![Lambda Function](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/2-locked._TTH_.png)](./2-lambda-function.md)[![Connect VUI to Code](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/3-locked._TTH_.png)](./3-connect-vui-to-code.md)[![Testing](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/4-locked._TTH_.png)](./4-testing.md)[![Customization](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/5-on._TTH_.png)](./5-customization.md)[![Publication](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/6-off._TTH_.png)](./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 | [![Voice User Interface](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/1-locked._TTH_.png)](./1-voice-user-interface.md)[![Lambda Function](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/2-locked._TTH_.png)](./2-lambda-function.md)[![Connect VUI to Code](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/3-locked._TTH_.png)](./3-connect-vui-to-code.md)[![Testing](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/4-locked._TTH_.png)](./4-testing.md)[![Customization](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/5-locked._TTH_.png)](./5-customization.md)[![Publication](https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexa-skills-kit/tutorials/navigation/6-on._TTH_.png)](./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 | --------------------------------------------------------------------------------