-
Notifications
You must be signed in to change notification settings - Fork 0
/
alexa.go
107 lines (100 loc) · 3.18 KB
/
alexa.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package main
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"os"
"github.com/pkg/errors"
"github.com/saphoooo/AlexaSkills/alexa"
"github.com/saphoooo/AlexaSkills/spoonacular"
)
type alexaCookingResponse struct{}
// sendCookingResponse returns the search results to Alexa Skills API
func (a alexaCookingResponse) sendCookingResponse(cr *cookingResponse) error {
text, err := resultsToText(getCookingInstructionIntent(cr.Params))
if err != nil {
return err
}
resp, err := skillsNewPlainTextResponse(text)
if err != nil {
return err
}
err = keepCookingParams(cr.Pool, cr.Key, cr.Params)
if err != nil {
return err
}
jsonReply(cr.ResponseWriter, resp)
return nil
}
// skillsNewPlainTextResponse marshals a text in Alexa SkillsResponse format
func skillsNewPlainTextResponse(text string) ([]byte, error) {
r := &alexa.SkillsResponse{
Version: "1.0",
Response: &alexa.Response{
ShouldEndSession: true,
OutputSpeech: &alexa.OutputSpeech{
Type: "PlainText",
Text: text,
},
},
}
m, err := json.Marshal(r)
if err != nil {
return nil, err
}
return m, nil
}
// skillsSlotParser iterates over Alexa response slots to capture their content
// see https://developer.amazon.com/fr-FR/docs/alexa/custom-skills/request-types-reference.html#intentrequest
func skillsSlotParser(slot map[string]interface{}, params *spoonacular.GetCookingParams) error {
for key := range slot {
var newSlot alexa.Slot
s, err := json.Marshal(slot[key])
if err != nil {
return errors.WithMessage(err, "unable to marshal "+key)
}
err = json.Unmarshal(s, &newSlot)
if err != nil {
return errors.WithMessage(err, "unable to unmarshal "+key)
}
if newSlot.Resolutions != nil {
switch key {
case "Foods":
params.FoodName = newSlot.Resolutions.ResolutionsPerAuthority[0].Values[0].Value.Name
case "DietTypes":
params.DietTypes = newSlot.Resolutions.ResolutionsPerAuthority[0].Values[0].Value.Name
default:
return errors.WithMessage(err, "unknow slot: "+key)
}
}
}
return nil
}
// NewSkillsVerifier makes the necessary checks to ensure that the request comes from Alexa Skills
// see https://developer.amazon.com/fr-FR/docs/alexa/custom-skills/security-testing-for-an-alexa-skill.html#22-skills-hosted-as-web-services-on-your-own-endpoint
func NewSkillsVerifier(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buffer, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// robotClone duplicates the request to perform validation on is content
// instead of draining the original request body
robotClone := ioutil.NopCloser(bytes.NewBuffer(buffer))
original := ioutil.NopCloser(bytes.NewBuffer(buffer))
var v alexa.Verifier
err = json.NewDecoder(robotClone).Decode(&v)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if v.Context.System.Application.ApplicationID != os.Getenv("ALEXA_SKILLID") {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
r.Body = original
h.ServeHTTP(w, r)
})
}