├── 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 | [](https://opensource.org/licenses/Apache-2.0)
7 | [](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 |
--------------------------------------------------------------------------------