├── License.txt ├── README.md ├── alexa.go ├── alexa_test.go └── samples ├── helloworld-lambda-go ├── .gitignore ├── Makefile ├── README.md └── handler.go └── helloworld ├── .gitignore ├── README.md ├── helloworld.go ├── interaction.json └── skill.json /License.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ericdaugherty/alexa-skills-kit-golang 2 | 3 | alexa-skills-kit-golang is a lightweight port of the Amazon [alexa-skills-kit-java](https://github.com/amzn/alexa-skills-kit-java) 4 | SDK and Samples. 5 | 6 | [![License](https://img.shields.io/badge/License-Apache%202.0-lightgrey.svg)](https://opensource.org/licenses/Apache-2.0) 7 | [![godoc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/ericdaugherty/alexa-skills-kit-golang) 8 | 9 | ## Usage 10 | 11 | This explanation assumes familiarity with with AWS Documentation. Please 12 | review [Developing an Alexa Skill as a Lambda Function](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/developing-an-alexa-skill-as-a-lambda-function) before proceeding. This SDK addresses some of the steps documented here for you, but you should be familiar with the entire process. 13 | 14 | The samples directory provides example usage. 15 | 16 | The Alexa struct is the initial interface point with the SDK. Alexa must be 17 | initialized first. The struct is defined as: 18 | 19 | ```Go 20 | type Alexa struct { 21 | ApplicationID string 22 | RequestHandler RequestHandler 23 | IgnoreApplicationID bool 24 | IgnoreTimestamp bool 25 | } 26 | ``` 27 | 28 | The ApplicationID must match the ApplicationID defined in the Alexa Skills 29 | 30 | The RequestHandler is an interface that must be implemented, and is called to handle requests. 31 | 32 | IgnoreApplicationID and IgnoreTimestamp should be used during debugging to test with hard-coded requests. 33 | 34 | Requests from Alexa should be passed into the Alexa.ProcessRequest method. 35 | 36 | ```Go 37 | func (alexa *Alexa) ProcessRequest(context context.Context, requestEnv *RequestEnvelope) (*ResponseEnvelope, error) 38 | ``` 39 | 40 | This method takes the incoming request and validates it, and the calls the 41 | appropriate callback methods on the RequestHandler interface implementation. 42 | 43 | The ResponseEnvelope is returned and can be converted to JSON to be passed 44 | back to the Alexa skill. 45 | 46 | RequestHandler interface is defined as: 47 | ```Go 48 | type RequestHandler interface { 49 | OnSessionStarted(context.Context, *Request, *Session, *Context, *Response) error 50 | OnLaunch(context.Context, *Request, *Session, *Context, *Response) error 51 | OnIntent(context.Context, *Request, *Session, *Context, *Response) error 52 | OnSessionEnded(context.Context, *Request, *Session, *Context, *Response) error 53 | } 54 | ``` 55 | 56 | For a summary of these methods, please see the [Handling Requests Sent By Alexa](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/handling-requests-sent-by-alexa) documentation. 57 | 58 | You can directly manipulate the Response struct, but it is not initialized by default and use of the connivence methods is recommended. 59 | 60 | These methods include: 61 | ```Go 62 | func (r *Response) SetSimpleCard(title string, content string) 63 | func (r *Response) SetStandardCard(title string, text string, smallImageURL string, largeImageURL string) 64 | func (r *Response) SetLinkAccountCard() 65 | func (r *Response) SetOutputText(text string) 66 | func (r *Response) SetOutputSSML(ssml string) 67 | func (r *Response) SetRepromptText(text string) 68 | func (r *Response) SetRepromptSSML(ssml string) 69 | ``` 70 | 71 | And more. These methods handle initializing any required struts within the Response struct as well as setting all required fields. 72 | 73 | ## samples 74 | 75 | [HelloWorld](https://github.com/ericdaugherty/alexa-skills-kit-golang/tree/master/samples/helloworld) 76 | 77 | ## Limitations 78 | 79 | This version does not support use as a standalone web server as it does not implement 80 | any of the HTTPS validation. It was developed to be used as an AWS Lambda function 81 | using AWS Labda Go support. 82 | -------------------------------------------------------------------------------- /alexa.go: -------------------------------------------------------------------------------- 1 | package alexa 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "math" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | const sdkVersion = "1.0" 14 | const launchRequestName = "LaunchRequest" 15 | const intentRequestName = "IntentRequest" 16 | const sessionEndedRequestName = "SessionEndedRequest" 17 | 18 | var timestampTolerance = 150 19 | 20 | // ErrRequestEnvelopeNil reports that the request envelope was nil 21 | // there might be edge case which causes panic if for whatever reason this object is empty 22 | var ErrRequestEnvelopeNil = errors.New("request envelope was nil") 23 | 24 | // Alexa defines the primary interface to use to create an Alexa request handler. 25 | type Alexa struct { 26 | ApplicationID string 27 | RequestHandler RequestHandler 28 | IgnoreApplicationID bool 29 | IgnoreTimestamp bool 30 | } 31 | 32 | // RequestHandler defines the interface that must be implemented to handle 33 | // Alexa Requests 34 | type RequestHandler interface { 35 | OnSessionStarted(context.Context, *Request, *Session, *Context, *Response) error 36 | OnLaunch(context.Context, *Request, *Session, *Context, *Response) error 37 | OnIntent(context.Context, *Request, *Session, *Context, *Response) error 38 | OnSessionEnded(context.Context, *Request, *Session, *Context, *Response) error 39 | } 40 | 41 | // RequestEnvelope contains the data passed from Alexa to the request handler. 42 | type RequestEnvelope struct { 43 | Version string `json:"version"` 44 | Session *Session `json:"session"` 45 | Request *Request `json:"request"` 46 | Context *Context `json:"context"` 47 | } 48 | 49 | // Session contains the session data from the Alexa request. 50 | type Session struct { 51 | New bool `json:"new"` 52 | SessionID string `json:"sessionId"` 53 | Attributes struct { 54 | String map[string]interface{} `json:"string"` 55 | } `json:"attributes"` 56 | User struct { 57 | UserID string `json:"userId"` 58 | AccessToken string `json:"accessToken"` 59 | } `json:"user"` 60 | Application struct { 61 | ApplicationID string `json:"applicationId"` 62 | } `json:"application"` 63 | } 64 | 65 | // Context contains the context data from the Alexa Request. 66 | type Context struct { 67 | System struct { 68 | Device struct { 69 | DeviceID string `json:"deviceId"` 70 | SupportedInterfaces struct { 71 | AudioPlayer struct { 72 | } `json:"AudioPlayer"` 73 | } `json:"supportedInterfaces"` 74 | } `json:"device"` 75 | Application struct { 76 | ApplicationID string `json:"applicationId"` 77 | } `json:"application"` 78 | User struct { 79 | UserID string `json:"userId"` 80 | AccessToken string `json:"accessToken"` 81 | Permissions struct { 82 | ConsentToken string `json:"consentToken"` 83 | } `json:"permissions"` 84 | } `json:"user"` 85 | APIEndpoint string `json:"apiEndpoint"` 86 | APIAccessToken string `json:"apiAccessToken"` 87 | } `json:"System"` 88 | AudioPlayer struct { 89 | PlayerActivity string `json:"playerActivity"` 90 | Token string `json:"token"` 91 | OffsetInMilliseconds int `json:"offsetInMilliseconds"` 92 | } `json:"AudioPlayer"` 93 | } 94 | 95 | // Request contains the data in the request within the main request. 96 | type Request struct { 97 | Locale string `json:"locale"` 98 | Timestamp string `json:"timestamp"` 99 | Type string `json:"type"` 100 | RequestID string `json:"requestId"` 101 | DialogState string `json:"dialogState"` 102 | Intent Intent `json:"intent"` 103 | Name string `json:"name"` 104 | } 105 | 106 | // Intent contains the data about the Alexa Intent requested. 107 | type Intent struct { 108 | Name string `json:"name"` 109 | ConfirmationStatus string `json:"confirmationStatus,omitempty"` 110 | Slots map[string]IntentSlot `json:"slots"` 111 | } 112 | 113 | // IntentSlot contains the data for one Slot 114 | type IntentSlot struct { 115 | Name string `json:"name"` 116 | ConfirmationStatus string `json:"confirmationStatus,omitempty"` 117 | Value string `json:"value"` 118 | Resolutions *Resolutions `json:"resolutions,omitempty"` 119 | 120 | // SlotValue is a BETA field and may be removed by Amazon without warning. 121 | // See https://developer.amazon.com/en-US/docs/alexa/custom-skills/collect-multiple-values-in-a-slot.html. 122 | SlotValue *IntentSlotValue `json:"slotValue,omitempty"` 123 | } 124 | 125 | // IntentSlotValue contains the value or values of a slot. 126 | // When Type == "Simple", Value and Resolutions are populated. 127 | // When Type == "List", Values is populated. 128 | type IntentSlotValue struct { 129 | Type string `json:"type"` 130 | Values []*IntentSlotValue `json:"values"` 131 | Value string `json:"value"` 132 | Resolutions *Resolutions `json:"resolutions,omitempty"` 133 | } 134 | 135 | // Resolutions contain the (optional) ID of a slot 136 | type Resolutions struct { 137 | ResolutionsPerAuthority []struct { 138 | Authority string `json:"authority"` 139 | Status struct { 140 | Code string `json:"code"` 141 | } `json:"status"` 142 | Values []struct { 143 | Value struct { 144 | Name string `json:"name"` 145 | ID string `json:"id"` 146 | } `json:"value"` 147 | } `json:"values"` 148 | } `json:"resolutionsPerAuthority"` 149 | } 150 | 151 | // ResponseEnvelope contains the Response and additional attributes. 152 | type ResponseEnvelope struct { 153 | Version string `json:"version"` 154 | SessionAttributes map[string]interface{} `json:"sessionAttributes,omitempty"` 155 | Response *Response `json:"response"` 156 | } 157 | 158 | // Response contains the body of the response. 159 | type Response struct { 160 | OutputSpeech *OutputSpeech `json:"outputSpeech,omitempty"` 161 | Card *Card `json:"card,omitempty"` 162 | Reprompt *Reprompt `json:"reprompt,omitempty"` 163 | Directives []interface{} `json:"directives,omitempty"` 164 | ShouldSessionEnd bool `json:"shouldEndSession"` 165 | } 166 | 167 | // OutputSpeech contains the data the defines what Alexa should say to the user. 168 | type OutputSpeech struct { 169 | Type string `json:"type"` 170 | Text string `json:"text,omitempty"` 171 | SSML string `json:"ssml,omitempty"` 172 | } 173 | 174 | // Card contains the data displayed to the user by the Alexa app. 175 | type Card struct { 176 | Type string `json:"type"` 177 | Title string `json:"title,omitempty"` 178 | Content string `json:"content,omitempty"` 179 | Text string `json:"text,omitempty"` 180 | Image *Image `json:"image,omitempty"` 181 | } 182 | 183 | // Image provides URL(s) to the image to display in resposne to the request. 184 | type Image struct { 185 | SmallImageURL string `json:"smallImageUrl,omitempty"` 186 | LargeImageURL string `json:"largeImageUrl,omitempty"` 187 | } 188 | 189 | // Reprompt contains data about whether Alexa should prompt the user for more data. 190 | type Reprompt struct { 191 | OutputSpeech *OutputSpeech `json:"outputSpeech,omitempty"` 192 | } 193 | 194 | // AudioPlayerDirective contains device level instructions on how to handle the response. 195 | type AudioPlayerDirective struct { 196 | Type string `json:"type"` 197 | PlayBehavior string `json:"playBehavior,omitempty"` 198 | AudioItem *AudioItem `json:"audioItem,omitempty"` 199 | } 200 | 201 | // AudioItem contains an audio Stream definition for playback. 202 | type AudioItem struct { 203 | Stream Stream `json:"stream,omitempty"` 204 | } 205 | 206 | // Stream contains instructions on playing an audio stream. 207 | type Stream struct { 208 | Token string `json:"token"` 209 | URL string `json:"url"` 210 | OffsetInMilliseconds int `json:"offsetInMilliseconds"` 211 | } 212 | 213 | // DialogDirective contains directives for use in Dialog prompts. 214 | type DialogDirective struct { 215 | Type string `json:"type"` 216 | SlotToElicit string `json:"slotToElicit,omitempty"` 217 | SlotToConfirm string `json:"slotToConfirm,omitempty"` 218 | UpdatedIntent *Intent `json:"updatedIntent,omitempty"` 219 | } 220 | 221 | // ProcessRequest handles a request passed from Alexa 222 | func (alexa *Alexa) ProcessRequest(ctx context.Context, requestEnv *RequestEnvelope) (*ResponseEnvelope, error) { 223 | if requestEnv == nil { 224 | return nil, ErrRequestEnvelopeNil 225 | } 226 | 227 | if !alexa.IgnoreApplicationID { 228 | err := alexa.verifyApplicationID(requestEnv) 229 | if err != nil { 230 | return nil, err 231 | } 232 | } 233 | if !alexa.IgnoreTimestamp { 234 | err := alexa.verifyTimestamp(requestEnv) 235 | if err != nil { 236 | return nil, err 237 | } 238 | } else { 239 | log.Println("Ignoring timestamp verification.") 240 | } 241 | 242 | request := requestEnv.Request 243 | session := requestEnv.Session 244 | if session.Attributes.String == nil { 245 | session.Attributes.String = make(map[string]interface{}) 246 | } 247 | context := requestEnv.Context 248 | 249 | responseEnv := &ResponseEnvelope{} 250 | responseEnv.Version = sdkVersion 251 | responseEnv.Response = &Response{} 252 | responseEnv.Response.ShouldSessionEnd = true // Set default value. 253 | 254 | response := responseEnv.Response 255 | 256 | // If it is a new session, invoke onSessionStarted 257 | if session.New { 258 | err := alexa.RequestHandler.OnSessionStarted(ctx, request, session, context, response) 259 | if err != nil { 260 | log.Println("Error handling OnSessionStarted.", err.Error()) 261 | return nil, err 262 | } 263 | } 264 | 265 | switch requestEnv.Request.Type { 266 | case launchRequestName: 267 | err := alexa.RequestHandler.OnLaunch(ctx, request, session, context, response) 268 | if err != nil { 269 | log.Println("Error handling OnLaunch.", err.Error()) 270 | return nil, err 271 | } 272 | case intentRequestName: 273 | err := alexa.RequestHandler.OnIntent(ctx, request, session, context, response) 274 | if err != nil { 275 | log.Println("Error handling OnIntent.", err.Error()) 276 | return nil, err 277 | } 278 | case sessionEndedRequestName: 279 | err := alexa.RequestHandler.OnSessionEnded(ctx, request, session, context, response) 280 | if err != nil { 281 | log.Println("Error handling OnSessionEnded.", err.Error()) 282 | return nil, err 283 | } 284 | } 285 | 286 | // Copy Session Attributes into ResponseEnvelope 287 | responseEnv.SessionAttributes = make(map[string]interface{}) 288 | for n, v := range session.Attributes.String { 289 | fmt.Println("Setting ", n, "to", v) 290 | responseEnv.SessionAttributes[n] = v 291 | } 292 | 293 | return responseEnv, nil 294 | } 295 | 296 | // SetTimestampTolerance sets the maximum number of seconds to allow between 297 | // the current time and the request Timestamp. Default value is 150 seconds. 298 | func (alexa *Alexa) SetTimestampTolerance(seconds int) { 299 | timestampTolerance = seconds 300 | } 301 | 302 | // SetSimpleCard creates a new simple card with the specified content. 303 | func (r *Response) SetSimpleCard(title string, content string) { 304 | r.Card = &Card{Type: "Simple", Title: title, Content: content} 305 | } 306 | 307 | // SetStandardCard creates a new standard card with the specified content. 308 | func (r *Response) SetStandardCard(title string, text string, smallImageURL string, largeImageURL string) { 309 | r.Card = &Card{Type: "Standard", Title: title, Text: text} 310 | r.Card.Image = &Image{SmallImageURL: smallImageURL, LargeImageURL: largeImageURL} 311 | } 312 | 313 | // SetLinkAccountCard creates a new LinkAccount card. 314 | func (r *Response) SetLinkAccountCard() { 315 | r.Card = &Card{Type: "LinkAccount"} 316 | } 317 | 318 | // SetOutputText sets the OutputSpeech type to text and sets the value specified. 319 | func (r *Response) SetOutputText(text string) { 320 | r.OutputSpeech = &OutputSpeech{Type: "PlainText", Text: text} 321 | } 322 | 323 | // SetOutputSSML sets the OutputSpeech type to ssml and sets the value specified. 324 | func (r *Response) SetOutputSSML(ssml string) { 325 | r.OutputSpeech = &OutputSpeech{Type: "SSML", SSML: ssml} 326 | } 327 | 328 | // SetRepromptText created a Reprompt if needed and sets the OutputSpeech type to text and sets the value specified. 329 | func (r *Response) SetRepromptText(text string) { 330 | if r.Reprompt == nil { 331 | r.Reprompt = &Reprompt{} 332 | } 333 | r.Reprompt.OutputSpeech = &OutputSpeech{Type: "PlainText", Text: text} 334 | } 335 | 336 | // SetRepromptSSML created a Reprompt if needed and sets the OutputSpeech type to ssml and sets the value specified. 337 | func (r *Response) SetRepromptSSML(ssml string) { 338 | if r.Reprompt == nil { 339 | r.Reprompt = &Reprompt{} 340 | } 341 | r.Reprompt.OutputSpeech = &OutputSpeech{Type: "SSML", SSML: ssml} 342 | } 343 | 344 | // AddAudioPlayer adds an AudioPlayer directive to the Response. 345 | func (r *Response) AddAudioPlayer(playerType, playBehavior, streamToken, url string, offsetInMilliseconds int) { 346 | d := AudioPlayerDirective{ 347 | Type: playerType, 348 | PlayBehavior: playBehavior, 349 | AudioItem: &AudioItem{ 350 | Stream: Stream{ 351 | Token: streamToken, 352 | URL: url, 353 | OffsetInMilliseconds: offsetInMilliseconds, 354 | }, 355 | }, 356 | } 357 | r.Directives = append(r.Directives, d) 358 | } 359 | 360 | // AddDialogDirective adds a Dialog directive to the Response. 361 | func (r *Response) AddDialogDirective(dialogType, slotToElicit, slotToConfirm string, intent *Intent) { 362 | d := DialogDirective{ 363 | Type: dialogType, 364 | SlotToElicit: slotToElicit, 365 | SlotToConfirm: slotToConfirm, 366 | UpdatedIntent: intent, 367 | } 368 | r.Directives = append(r.Directives, d) 369 | } 370 | 371 | // verifyApplicationId verifies that the ApplicationID sent in the request 372 | // matches the one configured for this skill. 373 | func (alexa *Alexa) verifyApplicationID(request *RequestEnvelope) error { 374 | if request == nil { 375 | return ErrRequestEnvelopeNil 376 | } 377 | 378 | appID := alexa.ApplicationID 379 | requestAppID := request.Session.Application.ApplicationID 380 | if appID == "" { 381 | return errors.New("application ID was set to an empty string") 382 | } 383 | if requestAppID == "" { 384 | return errors.New("request Application ID was set to an empty string") 385 | } 386 | if appID != requestAppID { 387 | return errors.New("request Application ID does not match expected ApplicationId") 388 | } 389 | 390 | return nil 391 | } 392 | 393 | // verifyTimestamp compares the request timestamp to the current timestamp 394 | // and returns an error if they are too far apart. 395 | func (alexa *Alexa) verifyTimestamp(request *RequestEnvelope) error { 396 | if request == nil { 397 | return ErrRequestEnvelopeNil 398 | } 399 | 400 | timestamp, err := time.Parse(time.RFC3339, request.Request.Timestamp) 401 | if err != nil { 402 | return errors.New("unable to parse request timestamp. Err: " + err.Error()) 403 | } 404 | now := time.Now() 405 | delta := now.Sub(timestamp) 406 | deltaSecsAbs := math.Abs(delta.Seconds()) 407 | if deltaSecsAbs > float64(timestampTolerance) { 408 | return errors.New("invalid Timestamp. The request timestamp " + timestamp.String() + " was off the current time " + now.String() + " by more than " + strconv.FormatInt(int64(timestampTolerance), 10) + " seconds.") 409 | } 410 | 411 | return nil 412 | } 413 | -------------------------------------------------------------------------------- /alexa_test.go: -------------------------------------------------------------------------------- 1 | package alexa 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | const applicationID = "amzn1.ask.skill.ABC123" 12 | 13 | const recipeIntentString = `{ 14 | "session": { 15 | "new": false, 16 | "sessionId": "amzn1.echo-api.session.[unique-value-here]", 17 | "attributes": {}, 18 | "user": { 19 | "userId": "amzn1.ask.account.[unique-value-here]" 20 | }, 21 | "application": { 22 | "applicationId": "amzn1.ask.skill.ABC123" 23 | } 24 | }, 25 | "version": "1.0", 26 | "request": { 27 | "locale": "en-US", 28 | "timestamp": "2016-10-27T21:06:28Z", 29 | "type": "IntentRequest", 30 | "requestId": "amzn1.echo-api.request.xyz789", 31 | "intent": { 32 | "name": "RecipeIntent", 33 | "slots": { 34 | "Item": { 35 | "name": "Item", 36 | "value": "snowball", 37 | "confirmationStatus": "NONE", 38 | "resolutions": { 39 | "resolutionsPerAuthority": [{ 40 | "authority": "amzn1.er-authority.echo-sdk.amzn1.ask.skill.4711.Topic", 41 | "status": { 42 | "code": "ER_SUCCESS_MATCH" 43 | }, 44 | "values": [{ 45 | "value": { 46 | "name": "snowball", 47 | "id": "5ad4bf3d7dd9e2567968d8a239dce2d3" 48 | } 49 | }] 50 | }] 51 | } 52 | } 53 | } 54 | } 55 | }, 56 | "context": { 57 | "AudioPlayer": { 58 | "playerActivity": "IDLE" 59 | }, 60 | "System": { 61 | "device": { 62 | "supportedInterfaces": { 63 | "AudioPlayer": {} 64 | } 65 | }, 66 | "application": { 67 | "applicationId": "amzn1.ask.skill.[unique-value-here]" 68 | }, 69 | "user": { 70 | "userId": "amzn1.ask.account.[unique-value-here]" 71 | } 72 | } 73 | } 74 | }` 75 | 76 | // TestAlexaJSON Verifies that the Alexa Struct parses an Alexa JSON String correctly. 77 | func TestAlexaJSON(t *testing.T) { 78 | request := createRecipeRequest() 79 | 80 | if request.Version != "1.0" { 81 | t.Error("Expected Request Version to be 1.0 but was", request.Version) 82 | } 83 | if request.Session.New { 84 | t.Error("Expected Session.new to be false but was true.") 85 | } 86 | if request.Session.User.UserID != "amzn1.ask.account.[unique-value-here]" { 87 | t.Error("Expected Session.User.UserId to be amzn1.ask.account.[unique-value-here] but was", request.Session.User.UserID) 88 | } 89 | if request.Request.RequestID != "amzn1.echo-api.request.xyz789" { 90 | t.Error("Expected request.Request.RequestID to be amzn1.echo-api.request.xyz789 but was", request.Request.RequestID) 91 | } 92 | if request.Request.Intent.Slots["Item"].Resolutions.ResolutionsPerAuthority[0].Values[0].Value.Name != "snowball" { 93 | t.Error("Expected request.Request.Intent.Slots['Item'].Resolutions.ResolutionsPerAuthority[0].Values[0].Value.Name to be snowball but was", 94 | request.Request.Intent.Slots["Item"].Resolutions.ResolutionsPerAuthority[0].Values[0].Value.Name) 95 | } 96 | } 97 | 98 | func TestAlexaAppIDValidation(t *testing.T) { 99 | request := createRecipeRequest() 100 | 101 | alexa := getAlexa() 102 | ctx := context.Background() 103 | _, err := alexa.ProcessRequest(ctx, request) 104 | if err != nil { 105 | t.Error("Expected ProcessRequest to succeed but got error", err) 106 | } 107 | 108 | alexa = &Alexa{ApplicationID: "amzn1.ask.skill.ABC123456", RequestHandler: &emptyRequestHandler{}} 109 | _, err = alexa.ProcessRequest(ctx, request) 110 | if err == nil { 111 | t.Error("Expected ProcessRequest to fail due to invalid Application ID but no err was returned.") 112 | } 113 | 114 | alexa = &Alexa{ApplicationID: "", RequestHandler: &emptyRequestHandler{}} 115 | _, err = alexa.ProcessRequest(ctx, request) 116 | if err == nil { 117 | t.Error("Expected ProcessRequest to fail due to an empty Application ID but no err was returned.") 118 | } 119 | 120 | alexa = &Alexa{ApplicationID: applicationID, RequestHandler: &emptyRequestHandler{}} 121 | request.Session.Application.ApplicationID = "" 122 | _, err = alexa.ProcessRequest(ctx, request) 123 | if err == nil { 124 | t.Error("Expected ProcessRequest to fail due to an empty Request Application ID but no err was returned.") 125 | } 126 | } 127 | 128 | func TestAlexaTimestampValidation(t *testing.T) { 129 | request := createRecipeRequest() 130 | 131 | alexa := getAlexa() 132 | duration, _ := time.ParseDuration("-145s") 133 | request.Request.Timestamp = time.Now().Add(duration).Format(time.RFC3339) 134 | ctx := context.Background() 135 | _, err := alexa.ProcessRequest(ctx, request) 136 | if err != nil { 137 | t.Error("Expected ProcessRequest to succeed but got error", err) 138 | } 139 | 140 | duration, _ = time.ParseDuration("-151s") 141 | request.Request.Timestamp = time.Now().Add(duration).Format(time.RFC3339) 142 | _, err = alexa.ProcessRequest(ctx, request) 143 | if err == nil { 144 | t.Error("Expected ProcessRequest to fail to due to an invalid timetamp but no err was returned.") 145 | } 146 | 147 | duration, _ = time.ParseDuration("151s") 148 | request.Request.Timestamp = time.Now().Add(duration).Format(time.RFC3339) 149 | _, err = alexa.ProcessRequest(ctx, request) 150 | if err == nil { 151 | t.Error("Expected ProcessRequest to fail to due to an invalid timetamp but no err was returned.") 152 | } 153 | 154 | request.Request.Timestamp = "UNPARSEABLE" 155 | _, err = alexa.ProcessRequest(ctx, request) 156 | if err == nil { 157 | t.Error("Expected ProcessRequest to fail because the timestamp could not be parsed but no err was returned") 158 | } 159 | 160 | alexa.SetTimestampTolerance(0) 161 | duration, _ = time.ParseDuration("-1s") 162 | request.Request.Timestamp = time.Now().Add(duration).Format(time.RFC3339) 163 | _, err = alexa.ProcessRequest(ctx, request) 164 | alexa.SetTimestampTolerance(150) 165 | if err == nil { 166 | t.Error("Expected ProcessRequest to fail to due to an invalid timetamp but no err was returned.") 167 | } 168 | 169 | // Test Disabled Timestamp 170 | duration, _ = time.ParseDuration("151s") 171 | request.Request.Timestamp = time.Now().Add(duration).Format(time.RFC3339) 172 | alexa.IgnoreTimestamp = true 173 | _, err = alexa.ProcessRequest(ctx, request) 174 | if err != nil { 175 | t.Error("Expected ProcessRequest to pass even with an invalid timestamp because validation is disabled.") 176 | } 177 | 178 | } 179 | 180 | func TestAlexaOnSessionStartedCalled(t *testing.T) { 181 | request := createRecipeRequest() 182 | 183 | handler := &emptyRequestHandler{} 184 | alexa := getAlexaWithHandler(handler) 185 | ctx := context.Background() 186 | _, err := alexa.ProcessRequest(ctx, request) 187 | if err != nil { 188 | t.Error("Error processing request. " + err.Error()) 189 | } 190 | if handler.OnSessionStartedCalled { 191 | t.Error("On SessionStarted was called when session was not new.") 192 | } 193 | 194 | handler = &emptyRequestHandler{} 195 | alexa = getAlexaWithHandler(handler) 196 | request.Session.New = true 197 | _, err = alexa.ProcessRequest(ctx, request) 198 | if err != nil { 199 | t.Error("Error processing request. " + err.Error()) 200 | } 201 | if !handler.OnSessionStartedCalled { 202 | t.Error("On SessionStarted was not called for a new session.") 203 | } 204 | 205 | handler = &emptyRequestHandler{} 206 | alexa = getAlexaWithHandler(handler) 207 | request.Session.New = true 208 | handler.OnSessionStartThrowsErr = true 209 | _, err = alexa.ProcessRequest(ctx, request) 210 | if !handler.OnSessionStartedCalled { 211 | t.Error("On SessionStarted was not called for a new session.") 212 | } 213 | if err == nil { 214 | t.Error("OnSessionStart should have returned an error.") 215 | } 216 | } 217 | 218 | func TestAlexaOnLaunchCalled(t *testing.T) { 219 | request := createRecipeRequest() 220 | request.Request.Type = launchRequestName 221 | 222 | handler := &emptyRequestHandler{} 223 | alexa := getAlexaWithHandler(handler) 224 | ctx := context.Background() 225 | _, err := alexa.ProcessRequest(ctx, request) 226 | if err != nil { 227 | t.Error("Error processing request. " + err.Error()) 228 | } 229 | if !handler.OnLaunchCalled { 230 | t.Error("On Launch was not called.") 231 | } 232 | 233 | handler = &emptyRequestHandler{} 234 | alexa = getAlexaWithHandler(handler) 235 | handler.OnLaunchThrowsErr = true 236 | _, err = alexa.ProcessRequest(ctx, request) 237 | if !handler.OnLaunchCalled { 238 | t.Error("OnLaunch was not called.") 239 | } 240 | if err == nil { 241 | t.Error("OnLaunch should have returned an error.") 242 | } 243 | } 244 | 245 | func TestAlexaOnIntentCalled(t *testing.T) { 246 | request := createRecipeRequest() 247 | request.Request.Type = intentRequestName 248 | 249 | handler := &emptyRequestHandler{} 250 | alexa := getAlexaWithHandler(handler) 251 | ctx := context.Background() 252 | _, err := alexa.ProcessRequest(ctx, request) 253 | if err != nil { 254 | t.Error("Error processing request. " + err.Error()) 255 | } 256 | if !handler.OnIntentCalled { 257 | t.Error("OnIntent was not called.") 258 | } 259 | 260 | handler = &emptyRequestHandler{} 261 | alexa = getAlexaWithHandler(handler) 262 | handler.OnIntentThrowsErr = true 263 | _, err = alexa.ProcessRequest(ctx, request) 264 | if !handler.OnIntentCalled { 265 | t.Error("OnIntent was not called.") 266 | } 267 | if err == nil { 268 | t.Error("OnIntent should have returned an error.") 269 | } 270 | } 271 | 272 | func TestAlexaOnSessionEndedCalled(t *testing.T) { 273 | request := createRecipeRequest() 274 | request.Request.Type = sessionEndedRequestName 275 | 276 | handler := &emptyRequestHandler{} 277 | alexa := getAlexaWithHandler(handler) 278 | 279 | ctx := context.Background() 280 | _, err := alexa.ProcessRequest(ctx, request) 281 | if err != nil { 282 | t.Error("Error processing request. " + err.Error()) 283 | } 284 | if !handler.OnSessionEndedCalled { 285 | t.Error("OnSessionEnded was not called.") 286 | } 287 | 288 | handler = &emptyRequestHandler{} 289 | alexa = getAlexaWithHandler(handler) 290 | handler.OnSessionEndedThrowsErr = true 291 | _, err = alexa.ProcessRequest(ctx, request) 292 | if !handler.OnSessionEndedCalled { 293 | t.Error("OnSessionEnded was not called.") 294 | } 295 | if err == nil { 296 | t.Error("OnSessionEnded should have returned an error.") 297 | } 298 | } 299 | 300 | func TestAlexaSessionAttributesSet(t *testing.T) { 301 | request := createRecipeRequest() 302 | request.Request.Type = intentRequestName 303 | 304 | handler := &emptyRequestHandler{} 305 | handler.OnIntentSetsSessionAttr = true 306 | alexa := getAlexaWithHandler(handler) 307 | ctx := context.Background() 308 | resp, err := alexa.ProcessRequest(ctx, request) 309 | if err != nil { 310 | t.Error("Error processing request. " + err.Error()) 311 | } 312 | if !handler.OnIntentCalled { 313 | t.Error("OnIntent was not called.") 314 | } 315 | if resp.SessionAttributes["myNewAttr"] != "Set123" { 316 | t.Error("Session Attribute myNewAttr should be Set123 in ResponseEnvelope but was", resp.SessionAttributes["myNewAttr"]) 317 | } 318 | 319 | } 320 | 321 | func TestAlexaSimpleTextResponse(t *testing.T) { 322 | request := createRecipeRequest() 323 | 324 | alexa := getAlexaWithHandler(&simpleResponseHandler{}) 325 | ctx := context.Background() 326 | responseEnv, err := alexa.ProcessRequest(ctx, request) 327 | if err != nil { 328 | t.Error("Error processing request. " + err.Error()) 329 | } 330 | 331 | if responseEnv.Response.OutputSpeech.Text != "Response Text" { 332 | t.Errorf("Response Text should have been %s but was %s", "Response Text", responseEnv.Response.OutputSpeech.Text) 333 | } 334 | if responseEnv.Response.OutputSpeech.Type != "PlainText" { 335 | t.Errorf("Response Type should have been %s but was %s", "PlainText", responseEnv.Response.OutputSpeech.Type) 336 | } 337 | 338 | if responseEnv.Response.Reprompt.OutputSpeech.Text != "Reprompt Text" { 339 | t.Errorf("Response Text should have been %s but was %s", "Reprompt Text", responseEnv.Response.OutputSpeech.Text) 340 | } 341 | if responseEnv.Response.Reprompt.OutputSpeech.Type != "PlainText" { 342 | t.Errorf("Response Type should have been %s but was %s", "PlainText", responseEnv.Response.OutputSpeech.Type) 343 | } 344 | } 345 | 346 | func TestSimpleSSMLResponse(t *testing.T) { 347 | request := createRecipeRequest() 348 | 349 | alexa := getAlexaWithHandler(&simpleSSMLResponseHandler{}) 350 | ctx := context.Background() 351 | responseEnv, err := alexa.ProcessRequest(ctx, request) 352 | if err != nil { 353 | t.Error("Error processing request. " + err.Error()) 354 | } 355 | 356 | if responseEnv.Response.OutputSpeech.SSML != "This output speech uses SSML." { 357 | t.Errorf("Response Text should have been %s but was %s", "This output speech uses SSML.", responseEnv.Response.OutputSpeech.SSML) 358 | } 359 | if responseEnv.Response.OutputSpeech.Type != "SSML" { 360 | t.Errorf("Response Type should have been %s but was %s", "SSML", responseEnv.Response.OutputSpeech.Type) 361 | } 362 | 363 | if responseEnv.Response.Reprompt.OutputSpeech.SSML != "This Reprompt speech uses SSML." { 364 | t.Errorf("Response Text should have been %s but was %s", "This Reprompt speech uses SSML.", responseEnv.Response.OutputSpeech.SSML) 365 | } 366 | if responseEnv.Response.Reprompt.OutputSpeech.Type != "SSML" { 367 | t.Errorf("Response Type should have been %s but was %s", "SSML", responseEnv.Response.OutputSpeech.Type) 368 | } 369 | } 370 | 371 | func TestCards(t *testing.T) { 372 | request := createRecipeRequest() 373 | 374 | cardHandler := &simpleCardResponseHandler{Type: "Simple"} 375 | alexa := getAlexaWithHandler(cardHandler) 376 | ctx := context.Background() 377 | responseEnv, err := alexa.ProcessRequest(ctx, request) 378 | if err != nil { 379 | t.Error("Error processing request. " + err.Error()) 380 | } 381 | if responseEnv.Response.Card.Type != "Simple" { 382 | t.Errorf("Card Type should be Simple but was %s", responseEnv.Response.Card.Type) 383 | } 384 | if responseEnv.Response.Card.Content != "Simple Content" { 385 | t.Errorf("Card Content should be 'Simple Content' but was %s", responseEnv.Response.Card.Content) 386 | } 387 | 388 | cardHandler.Type = "Standard" 389 | responseEnv, err = alexa.ProcessRequest(ctx, request) 390 | if err != nil { 391 | t.Error("Error processing request. " + err.Error()) 392 | } 393 | if responseEnv.Response.Card.Type != "Standard" { 394 | t.Errorf("Card Type should be Standard but was %s", responseEnv.Response.Card.Type) 395 | } 396 | if responseEnv.Response.Card.Text != "Standard Body Text" { 397 | t.Errorf("Card Content should be 'Standard Body Text' but was %s", responseEnv.Response.Card.Text) 398 | } 399 | 400 | cardHandler.Type = "LinkAccount" 401 | responseEnv, err = alexa.ProcessRequest(ctx, request) 402 | if err != nil { 403 | t.Error("Error processing request. " + err.Error()) 404 | } 405 | if responseEnv.Response.Card.Type != "LinkAccount" { 406 | t.Errorf("Card Type should be LinkAccount but was %s", responseEnv.Response.Card.Type) 407 | } 408 | 409 | } 410 | 411 | func TestAudioPlayer(t *testing.T) { 412 | request := createRecipeRequest() 413 | 414 | audioPlayerHandler := &simpleAudioPlayerResponseHandler{Type: "Simple"} 415 | alexa := getAlexaWithHandler(audioPlayerHandler) 416 | ctx := context.Background() 417 | responseEnv, err := alexa.ProcessRequest(ctx, request) 418 | if err != nil { 419 | t.Error("Error processing request. " + err.Error()) 420 | } 421 | if len(responseEnv.Response.Directives) != 1 { 422 | t.Fatalf("Response should contain 1 directive but contains %d", len(responseEnv.Response.Directives)) 423 | } 424 | 425 | exp := `{"type":"AudioPlayer.Play","playBehavior":"REPLACE_ALL","audioItem":{"stream":{"token":"track2-long-audio","url":"https://my-audio-hosting-site.com/audio/sample-song-2.mp3","offsetInMilliseconds":100}}}` 426 | 427 | b, err := json.Marshal(responseEnv.Response.Directives[0]) 428 | if err != nil { 429 | t.Fatalf("Error marshaling response. %s", err.Error()) 430 | } 431 | if string(b) != exp { 432 | t.Errorf("Expected JSON of "+exp+" but was %s", string(b)) 433 | } 434 | } 435 | 436 | func TestSimpleDialogDirective(t *testing.T) { 437 | request := createRecipeRequest() 438 | 439 | simpleDialogDirectiveResponseHandler := &simpleDialogDirectiveResponseHandler{Type: "Simple"} 440 | alexa := getAlexaWithHandler(simpleDialogDirectiveResponseHandler) 441 | ctx := context.Background() 442 | responseEnv, err := alexa.ProcessRequest(ctx, request) 443 | if err != nil { 444 | t.Error("Error processing request. " + err.Error()) 445 | } 446 | if len(responseEnv.Response.Directives) != 1 { 447 | t.Fatalf("Response should contain 1 directive but contains %d", len(responseEnv.Response.Directives)) 448 | } 449 | 450 | exp := `{"type":"Dialog.Delegate","updatedIntent":{"name":"PlanMyTrip","confirmationStatus":"NONE","slots":{"travelDate":{"name":"travelDate","confirmationStatus":"NONE","value":"2017-04-21"}}}}` 451 | 452 | b, err := json.Marshal(responseEnv.Response.Directives[0]) 453 | if err != nil { 454 | t.Fatalf("Error marshaling response. %s", err.Error()) 455 | } 456 | if string(b) != exp { 457 | t.Errorf("Expected JSON of "+exp+" but was %s", string(b)) 458 | } 459 | } 460 | 461 | func TestNoIntentDialogDirective(t *testing.T) { 462 | request := createRecipeRequest() 463 | 464 | simpleDialogDirectiveResponseHandler := &simpleDialogDirectiveResponseHandler{Type: "NoIntent"} 465 | alexa := getAlexaWithHandler(simpleDialogDirectiveResponseHandler) 466 | ctx := context.Background() 467 | responseEnv, err := alexa.ProcessRequest(ctx, request) 468 | if err != nil { 469 | t.Error("Error processing request. " + err.Error()) 470 | } 471 | if len(responseEnv.Response.Directives) != 1 { 472 | t.Fatalf("Response should contain 1 directive but contains %d", len(responseEnv.Response.Directives)) 473 | } 474 | 475 | exp := `{"type":"Dialog.Delegate"}` 476 | 477 | b, err := json.Marshal(responseEnv.Response.Directives[0]) 478 | if err != nil { 479 | t.Fatalf("Error marshaling response. %s", err.Error()) 480 | } 481 | if string(b) != exp { 482 | t.Errorf("Expected JSON of "+exp+" but was %s", string(b)) 483 | } 484 | } 485 | 486 | func getAlexa() *Alexa { 487 | return &Alexa{ApplicationID: applicationID, RequestHandler: &emptyRequestHandler{}} 488 | } 489 | 490 | func getAlexaWithHandler(handler RequestHandler) *Alexa { 491 | return &Alexa{ApplicationID: applicationID, RequestHandler: handler} 492 | } 493 | 494 | func createRecipeRequest() *RequestEnvelope { 495 | var request RequestEnvelope 496 | var jsonBlob = []byte(recipeIntentString) 497 | json.Unmarshal(jsonBlob, &request) 498 | request.Request.Timestamp = time.Now().Format(time.RFC3339) 499 | return &request 500 | } 501 | 502 | type emptyRequestHandler struct { 503 | OnSessionStartedCalled bool 504 | OnSessionStartThrowsErr bool 505 | OnLaunchCalled bool 506 | OnLaunchThrowsErr bool 507 | OnIntentCalled bool 508 | OnIntentThrowsErr bool 509 | OnIntentSetsSessionAttr bool 510 | OnSessionEndedCalled bool 511 | OnSessionEndedThrowsErr bool 512 | } 513 | 514 | func (h *emptyRequestHandler) OnSessionStarted(context.Context, *Request, *Session, *Context, *Response) error { 515 | h.OnSessionStartedCalled = true 516 | if h.OnSessionStartThrowsErr { 517 | return errors.New("error in OnSessionStarted") 518 | } 519 | return nil 520 | } 521 | 522 | func (h *emptyRequestHandler) OnLaunch(context.Context, *Request, *Session, *Context, *Response) error { 523 | h.OnLaunchCalled = true 524 | if h.OnLaunchThrowsErr { 525 | return errors.New("error in OnLaunch") 526 | } 527 | return nil 528 | } 529 | 530 | func (h *emptyRequestHandler) OnIntent(c context.Context, req *Request, s *Session, aContext *Context, res *Response) error { 531 | h.OnIntentCalled = true 532 | if h.OnIntentSetsSessionAttr { 533 | s.Attributes.String["myNewAttr"] = "Set123" 534 | } 535 | if h.OnIntentThrowsErr { 536 | return errors.New("error in OnIntent") 537 | } 538 | return nil 539 | } 540 | 541 | func (h *emptyRequestHandler) OnSessionEnded(context.Context, *Request, *Session, *Context, *Response) error { 542 | h.OnSessionEndedCalled = true 543 | if h.OnSessionEndedThrowsErr { 544 | return errors.New("error in OnSessionEnded") 545 | } 546 | return nil 547 | } 548 | 549 | type simpleResponseHandler struct { 550 | } 551 | 552 | func (h *simpleResponseHandler) OnSessionStarted(context.Context, *Request, *Session, *Context, *Response) error { 553 | return nil 554 | } 555 | 556 | func (h *simpleResponseHandler) OnLaunch(context.Context, *Request, *Session, *Context, *Response) error { 557 | return nil 558 | } 559 | 560 | func (h *simpleResponseHandler) OnIntent(context context.Context, request *Request, session *Session, aContext *Context, response *Response) error { 561 | 562 | response.SetOutputText("Response Text") 563 | response.SetRepromptText("Reprompt Text") 564 | 565 | return nil 566 | } 567 | 568 | func (h *simpleResponseHandler) OnSessionEnded(context.Context, *Request, *Session, *Context, *Response) error { 569 | return nil 570 | } 571 | 572 | type simpleSSMLResponseHandler struct { 573 | } 574 | 575 | func (h *simpleSSMLResponseHandler) OnSessionStarted(context.Context, *Request, *Session, *Context, *Response) error { 576 | return nil 577 | } 578 | 579 | func (h *simpleSSMLResponseHandler) OnLaunch(context.Context, *Request, *Session, *Context, *Response) error { 580 | return nil 581 | } 582 | 583 | func (h *simpleSSMLResponseHandler) OnIntent(context context.Context, request *Request, session *Session, aContext *Context, response *Response) error { 584 | 585 | response.SetOutputSSML("This output speech uses SSML.") 586 | response.SetRepromptSSML("This Reprompt speech uses SSML.") 587 | 588 | return nil 589 | } 590 | 591 | func (h *simpleSSMLResponseHandler) OnSessionEnded(context.Context, *Request, *Session, *Context, *Response) error { 592 | return nil 593 | } 594 | 595 | type simpleCardResponseHandler struct { 596 | Type string 597 | } 598 | 599 | func (h *simpleCardResponseHandler) OnSessionStarted(context.Context, *Request, *Session, *Context, *Response) error { 600 | return nil 601 | } 602 | 603 | func (h *simpleCardResponseHandler) OnLaunch(context.Context, *Request, *Session, *Context, *Response) error { 604 | return nil 605 | } 606 | 607 | func (h *simpleCardResponseHandler) OnIntent(context context.Context, request *Request, session *Session, aContext *Context, response *Response) error { 608 | 609 | switch h.Type { 610 | case "Simple": 611 | response.SetSimpleCard("Simple Title", "Simple Content") 612 | case "Standard": 613 | response.SetStandardCard("Standard Title", "Standard Body Text", "http://small.url", "http://large.url") 614 | case "LinkAccount": 615 | response.SetLinkAccountCard() 616 | } 617 | response.SetOutputSSML("This output speech uses SSML.") 618 | response.SetRepromptSSML("This Reprompt speech uses SSML.") 619 | 620 | return nil 621 | } 622 | 623 | func (h *simpleCardResponseHandler) OnSessionEnded(context.Context, *Request, *Session, *Context, *Response) error { 624 | return nil 625 | } 626 | 627 | type simpleAudioPlayerResponseHandler struct { 628 | Type string 629 | } 630 | 631 | func (h *simpleAudioPlayerResponseHandler) OnSessionStarted(context.Context, *Request, *Session, *Context, *Response) error { 632 | return nil 633 | } 634 | 635 | func (h *simpleAudioPlayerResponseHandler) OnLaunch(context.Context, *Request, *Session, *Context, *Response) error { 636 | return nil 637 | } 638 | 639 | func (h *simpleAudioPlayerResponseHandler) OnIntent(context context.Context, request *Request, session *Session, aContext *Context, response *Response) error { 640 | 641 | response.AddAudioPlayer("AudioPlayer.Play", "REPLACE_ALL", "track2-long-audio", "https://my-audio-hosting-site.com/audio/sample-song-2.mp3", 100) 642 | 643 | return nil 644 | } 645 | 646 | func (h *simpleAudioPlayerResponseHandler) OnSessionEnded(context.Context, *Request, *Session, *Context, *Response) error { 647 | return nil 648 | } 649 | 650 | type simpleDialogDirectiveResponseHandler struct { 651 | Type string 652 | } 653 | 654 | func (h *simpleDialogDirectiveResponseHandler) OnSessionStarted(context.Context, *Request, *Session, *Context, *Response) error { 655 | return nil 656 | } 657 | 658 | func (h *simpleDialogDirectiveResponseHandler) OnLaunch(context.Context, *Request, *Session, *Context, *Response) error { 659 | return nil 660 | } 661 | 662 | func (h *simpleDialogDirectiveResponseHandler) OnIntent(context context.Context, request *Request, session *Session, aContext *Context, response *Response) error { 663 | 664 | switch h.Type { 665 | case "Simple": 666 | i := &Intent{ 667 | Name: "PlanMyTrip", 668 | ConfirmationStatus: "NONE", 669 | Slots: map[string]IntentSlot{ 670 | "travelDate": IntentSlot{ 671 | Name: "travelDate", 672 | ConfirmationStatus: "NONE", 673 | Value: "2017-04-21", 674 | }, 675 | }, 676 | } 677 | response.AddDialogDirective("Dialog.Delegate", "", "", i) 678 | case "NoIntent": 679 | response.AddDialogDirective("Dialog.Delegate", "", "", nil) 680 | } 681 | 682 | return nil 683 | } 684 | 685 | func (h *simpleDialogDirectiveResponseHandler) OnSessionEnded(context.Context, *Request, *Session, *Context, *Response) error { 686 | return nil 687 | } 688 | -------------------------------------------------------------------------------- /samples/helloworld-lambda-go/.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | handler 3 | *.out 4 | -------------------------------------------------------------------------------- /samples/helloworld-lambda-go/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # This is free and unencumbered software released into the public domain. 3 | # 4 | # Anyone is free to copy, modify, publish, use, compile, sell, or 5 | # distribute this software, either in source code form or as a compiled 6 | # binary, for any purpose, commercial or non-commercial, and by any 7 | # means. 8 | # 9 | # In jurisdictions that recognize copyright laws, the author or authors 10 | # of this software dedicate any and all copyright interest in the 11 | # software to the public domain. We make this dedication for the benefit 12 | # of the public at large and to the detriment of our heirs and 13 | # successors. We intend this dedication to be an overt act of 14 | # relinquishment in perpetuity of all present and future rights to this 15 | # software under copyright law. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | # IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | # 25 | # For more information, please refer to 26 | # 27 | 28 | HANDLER ?= handler 29 | PACKAGE ?= $(HANDLER) 30 | GOPATH ?= $(HOME)/go 31 | 32 | WORKDIR = $(CURDIR:$(GOPATH)%=/go%) 33 | ifeq ($(WORKDIR),$(CURDIR)) 34 | WORKDIR = /tmp 35 | endif 36 | 37 | ROLE_ARN=`aws iam get-role --role-name lambda_basic_execution --query 'Role.Arn' --output text` 38 | 39 | all: build pack 40 | 41 | build: 42 | @GOARCH=amd64 GOOS=linux go build -o $(HANDLER) 43 | 44 | pack: 45 | @zip $(PACKAGE).zip $(HANDLER) 46 | 47 | clean: 48 | @rm -rf $(HANDLER) $(PACKAGE).zip 49 | 50 | create: 51 | @aws lambda create-function \ 52 | --function-name HelloAlexa \ 53 | --zip-file fileb://handler.zip \ 54 | --role $(ROLE_ARN) \ 55 | --runtime go1.x \ 56 | --handler handler 57 | 58 | update: 59 | @aws lambda update-function-code \ 60 | --function-name HelloAlexa \ 61 | --zip-file fileb://handler.zip 62 | 63 | invoke: 64 | @aws lambda invoke \ 65 | --function-name HelloAlexa invoke.out 66 | 67 | .PHONY: all build pack clean create update invoke 68 | -------------------------------------------------------------------------------- /samples/helloworld-lambda-go/README.md: -------------------------------------------------------------------------------- 1 | # HelloWorld Sample using aws-lambda-go 2 | 3 | This sample is made for aws-lambda-go, lambda go runtime from sample helloworld that via shim. 4 | 5 | Support aws-lambda-go style kick handlers. 6 | This is able to work with ask-cli. 7 | 8 | This also assumes you have the [Amazon AWS CLI](https://aws.amazon.com/cli/) installed and configured. You should also have the "lambda_basic_execution" 9 | role. 10 | 11 | First, create an Alexa Skill following the instructions described in the [Java HelloWorld Sample](https://github.com/amzn/alexa-skills-kit-java/tree/master/samples/src/main/java/helloworld) 12 | 13 | Second, retrieve dependencies 14 | 15 | ``` 16 | go get -u github.com/aws/aws-lambda-go/lambda 17 | go get -u github.com/ericdaugherty/alexa-skills-kit-golang 18 | ``` 19 | 20 | Third, compile the sample using the included Makefile 21 | 22 | ``` 23 | make 24 | ``` 25 | 26 | Then, create a new Lambda function using the included Makefile: 27 | 28 | ``` 29 | make create 30 | ``` 31 | 32 | You can directry invoke lambda function(just invoke): 33 | 34 | ``` 35 | make invoke 36 | ``` 37 | 38 | You can now test the HelloAlexa skill via an Echo attached to your Amazon account or using the Amazon Alexa Console. 39 | 40 | Once the lambda function is created, you can use the Makefile to build and 41 | update your function. 42 | 43 | ``` 44 | make 45 | make update 46 | ``` 47 | -------------------------------------------------------------------------------- /samples/helloworld-lambda-go/handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | 8 | "github.com/aws/aws-lambda-go/lambda" 9 | alexa "github.com/ericdaugherty/alexa-skills-kit-golang" 10 | ) 11 | 12 | var a = &alexa.Alexa{ApplicationID: "amzn1.ask.skill.08857461-c080-49d6-8646-2f0ca2c14914", RequestHandler: &HelloWorld{}, IgnoreTimestamp: true} 13 | 14 | const cardTitle = "HelloWorld" 15 | 16 | // HelloWorld handles reqeusts from the HelloWorld skill. 17 | type HelloWorld struct{} 18 | 19 | // Handle processes calls from Lambda 20 | func Handle(ctx context.Context, requestEnv *alexa.RequestEnvelope) (interface{}, error) { 21 | return a.ProcessRequest(ctx, requestEnv) 22 | } 23 | 24 | // OnSessionStarted called when a new session is created. 25 | func (h *HelloWorld) OnSessionStarted(context context.Context, request *alexa.Request, session *alexa.Session, aContext *alexa.Context, response *alexa.Response) error { 26 | 27 | log.Printf("OnSessionStarted requestId=%s, sessionId=%s", request.RequestID, session.SessionID) 28 | 29 | return nil 30 | } 31 | 32 | // OnLaunch called with a reqeust is received of type LaunchRequest 33 | func (h *HelloWorld) OnLaunch(context context.Context, request *alexa.Request, session *alexa.Session, aContext *alexa.Context, response *alexa.Response) error { 34 | speechText := "Welcome to the Alexa Skills Kit, you can say hello" 35 | 36 | log.Printf("OnLaunch requestId=%s, sessionId=%s", request.RequestID, session.SessionID) 37 | 38 | response.SetSimpleCard(cardTitle, speechText) 39 | response.SetOutputText(speechText) 40 | response.SetRepromptText(speechText) 41 | 42 | response.ShouldSessionEnd = true 43 | 44 | return nil 45 | } 46 | 47 | // OnIntent called with a reqeust is received of type IntentRequest 48 | func (h *HelloWorld) OnIntent(context context.Context, request *alexa.Request, session *alexa.Session, aContext *alexa.Context, response *alexa.Response) error { 49 | 50 | log.Printf("OnIntent requestId=%s, sessionId=%s, intent=%s", request.RequestID, session.SessionID, request.Intent.Name) 51 | 52 | switch request.Intent.Name { 53 | case "HelloWorldIntent": 54 | log.Println("HelloWorldIntent triggered") 55 | speechText := "Hello World" 56 | 57 | response.SetSimpleCard(cardTitle, speechText) 58 | response.SetOutputText(speechText) 59 | 60 | log.Printf("Set Output speech, value now: %s", response.OutputSpeech.Text) 61 | case "AMAZON.HelpIntent": 62 | log.Println("AMAZON.HelpIntent triggered") 63 | speechText := "You can say hello to me!" 64 | 65 | response.SetSimpleCard("HelloWorld", speechText) 66 | response.SetOutputText(speechText) 67 | response.SetRepromptText(speechText) 68 | default: 69 | return errors.New("Invalid Intent") 70 | } 71 | 72 | return nil 73 | } 74 | 75 | // OnSessionEnded called with a reqeust is received of type SessionEndedRequest 76 | func (h *HelloWorld) OnSessionEnded(context context.Context, request *alexa.Request, session *alexa.Session, aContext *alexa.Context, response *alexa.Response) error { 77 | 78 | log.Printf("OnSessionEnded requestId=%s, sessionId=%s", request.RequestID, session.SessionID) 79 | 80 | return nil 81 | } 82 | 83 | func main() { 84 | lambda.Start(Handle) 85 | } 86 | -------------------------------------------------------------------------------- /samples/helloworld/.gitignore: -------------------------------------------------------------------------------- 1 | helloworld 2 | *.zip 3 | -------------------------------------------------------------------------------- /samples/helloworld/README.md: -------------------------------------------------------------------------------- 1 | #HelloWorld 2 | 3 | This sample provides an outline on how to develop and deploy an Alexa application in Go using the [AWS CLI](https://aws.amazon.com/cli/) and [ASK CLI](https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html) (Command Line Interface). Please install these tools before proceeding. 4 | 5 | This Sample assumes a valid Go install and basic working knowledge in Go. 6 | 7 | First, we will compile and deploy the Go HelloWorld code as an AWS Lambda Function. 8 | 9 | Compile the HelloWorld Go code and upload it as an AWS Lambda Function. 10 | 11 | Compile and package: 12 | ``` 13 | GOARCH=amd64 GOOS=linux go build -o helloworld 14 | zip helloworld.zip helloworld 15 | ``` 16 | 17 | Create/upload AWS Lambda Function: 18 | 19 | First, we need to determine the Role ARN to use. Ideally, we can use the lambda_basic_execution role. Execute the following command to get the ARN for the lambda_basic_execution role. 20 | 21 | ``` 22 | aws iam get-role --role-name lambda_basic_execution --query 'Role.Arn' --output text 23 | ``` 24 | 25 | Now, create the function, using the lambda_basic_execution ARN you found above in place of 26 | 27 | ``` 28 | aws lambda create-function \ 29 | --function-name HelloAlexa \ 30 | --zip-file fileb://helloworld.zip \ 31 | --role \ 32 | --runtime go1.x \ 33 | --handler helloworld 34 | ``` 35 | 36 | This should return a JSON snippet. Make note of the FunctionArn. 37 | 38 | We need to make this Lamdba callable from an Alexa Skill by adding a permission: 39 | ``` 40 | aws lambda add-permission \ 41 | --function-name HelloAlexa \ 42 | --statement-id "1234" \ 43 | --action "lambda:InvokeFunction" \ 44 | --principal "alexa-appkit.amazon.com" 45 | ``` 46 | 47 | Now we can create a new Alexa Skill using the provides skills.json definition. You will need to edit the skill.json file to reflect the correct Lamda ARN. 48 | 49 | Edit the line below and replace 'TBD' with the correct value returned when you crated the Lambda function. 50 | ``` 51 | "uri": "arn:aws:lambda:TBD" 52 | ``` 53 | 54 | Now, create the skill: 55 | ``` 56 | ask api create-skill -f skill.json 57 | ``` 58 | 59 | Confirm the success of the creation by calling the get-skill-status as specified in the response: 60 | ``` 61 | ask api get-skill-status -s amzn1.ask.skill. 62 | ``` 63 | 64 | Now we need to define the interaction model used by this skill. A sample interaction model is provided and does not need to be modified for this example. You simply need to update the model as follows (note, replace the below skill id with the one returned in the previous command): 65 | 66 | ``` 67 | ask api update-model -s amzn1.ask.skill. -f interaction.json -l en-US 68 | ``` 69 | 70 | You can check the status again with ask api get-skill-status. It may take a moment for the interaction model to process. 71 | 72 | Finally, enable the skill so you can test it: 73 | ``` 74 | ask api enable-skill -s amzn1.ask.skill. 75 | ``` 76 | 77 | ## Test 78 | 79 | You can now test the function by talking to your Alexa or using the [Alexa Console](https://developer.amazon.com/edw/home.html#/skills). 80 | 81 | ## Update 82 | 83 | You can update the Go Lambda function by recompiling and zipping as above and calling: 84 | 85 | ``` 86 | aws lambda update-function-code \ 87 | --function-name HelloAlexa \ 88 | --zip-file fileb://helloworld.zip 89 | ``` 90 | -------------------------------------------------------------------------------- /samples/helloworld/helloworld.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | 8 | "github.com/aws/aws-lambda-go/lambda" 9 | alexa "github.com/ericdaugherty/alexa-skills-kit-golang" 10 | ) 11 | 12 | var a = &alexa.Alexa{ApplicationID: "amzn1.ask.skill.", RequestHandler: &HelloWorld{}, IgnoreApplicationID: true, IgnoreTimestamp: true} 13 | 14 | const cardTitle = "HelloWorld" 15 | 16 | // HelloWorld handles reqeusts from the HelloWorld skill. 17 | type HelloWorld struct{} 18 | 19 | // Handle processes calls from Lambda 20 | func Handle(ctx context.Context, requestEnv *alexa.RequestEnvelope) (interface{}, error) { 21 | return a.ProcessRequest(ctx, requestEnv) 22 | } 23 | 24 | // OnSessionStarted called when a new session is created. 25 | func (h *HelloWorld) OnSessionStarted(ctx context.Context, request *alexa.Request, session *alexa.Session, ctxPtr *alexa.Context, response *alexa.Response) error { 26 | 27 | log.Printf("OnSessionStarted requestId=%s, sessionId=%s", request.RequestID, session.SessionID) 28 | 29 | return nil 30 | } 31 | 32 | // OnLaunch called with a reqeust is received of type LaunchRequest 33 | func (h *HelloWorld) OnLaunch(ctx context.Context, request *alexa.Request, session *alexa.Session, ctxPtr *alexa.Context, response *alexa.Response) error { 34 | speechText := "Welcome to the Alexa Skills Kit, you can say hello" 35 | 36 | log.Printf("OnLaunch requestId=%s, sessionId=%s", request.RequestID, session.SessionID) 37 | 38 | response.SetSimpleCard(cardTitle, speechText) 39 | response.SetOutputText(speechText) 40 | response.SetRepromptText(speechText) 41 | 42 | response.ShouldSessionEnd = true 43 | 44 | return nil 45 | } 46 | 47 | // OnIntent called with a reqeust is received of type IntentRequest 48 | func (h *HelloWorld) OnIntent(ctx context.Context, request *alexa.Request, session *alexa.Session, ctxPtr *alexa.Context, response *alexa.Response) error { 49 | 50 | log.Printf("OnIntent requestId=%s, sessionId=%s, intent=%s", request.RequestID, session.SessionID, request.Intent.Name) 51 | 52 | switch request.Intent.Name { 53 | case "HelloWorldIntent": 54 | log.Println("HelloWorldIntent triggered") 55 | speechText := "Hello World" 56 | 57 | response.SetSimpleCard(cardTitle, speechText) 58 | response.SetOutputText(speechText) 59 | 60 | log.Printf("Set Output speech, value now: %s", response.OutputSpeech.Text) 61 | case "AMAZON.HelpIntent": 62 | log.Println("AMAZON.HelpIntent triggered") 63 | speechText := "You can say hello to me!" 64 | 65 | response.SetSimpleCard("HelloWorld", speechText) 66 | response.SetOutputText(speechText) 67 | response.SetRepromptText(speechText) 68 | default: 69 | return errors.New("Invalid Intent") 70 | } 71 | 72 | return nil 73 | } 74 | 75 | // OnSessionEnded called with a reqeust is received of type SessionEndedRequest 76 | func (h *HelloWorld) OnSessionEnded(ctx context.Context, request *alexa.Request, session *alexa.Session, ctxPtr *alexa.Context, response *alexa.Response) error { 77 | 78 | log.Printf("OnSessionEnded requestId=%s, sessionId=%s", request.RequestID, session.SessionID) 79 | 80 | return nil 81 | } 82 | 83 | func main() { 84 | lambda.Start(Handle) 85 | } 86 | -------------------------------------------------------------------------------- /samples/helloworld/interaction.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "interactionModel":{ 4 | "languageModel":{ 5 | "invocationName":"hello world", 6 | "types":[], 7 | "intents":[ 8 | { 9 | "name": "AMAZON.CancelIntent", 10 | "samples": [] 11 | }, 12 | { 13 | "name": "AMAZON.HelpIntent", 14 | "samples": [] 15 | }, 16 | { 17 | "name": "AMAZON.StopIntent", 18 | "samples": [] 19 | }, 20 | { 21 | "name":"HelloWorldIntent", 22 | "slots":[ 23 | 24 | ], 25 | "samples":[ 26 | "hello", 27 | "say hello", 28 | "say hello world" 29 | ] 30 | } 31 | ] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /samples/helloworld/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest": { 3 | "publishingInformation": { 4 | "locales": { 5 | "en-US": { 6 | "summary": "Hello World Sample Skill", 7 | "examplePhrases": [ 8 | "Alexa open hello world", 9 | "Alexa ask hello world to say hi" 10 | ], 11 | "name": "HelloWorld", 12 | "description": "Hello World Sample Skill" 13 | } 14 | }, 15 | "isAvailableWorldwide": true, 16 | "testingInstructions": "Sample Testing Instructions.", 17 | "category": "EDUCATION_AND_REFERENCE", 18 | "distributionCountries": [] 19 | }, 20 | "apis": { 21 | "custom": { 22 | "endpoint": { 23 | "uri": "arn:aws:lambda:TBD" 24 | } 25 | } 26 | }, 27 | "manifestVersion": "1.0" 28 | } 29 | } 30 | --------------------------------------------------------------------------------