-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.go
170 lines (136 loc) · 4.02 KB
/
search.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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package mexinfo
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
customsearch "google.golang.org/api/customsearch/v1"
"google.golang.org/api/option"
)
const (
version = "v0"
slackRequestTimestampHeader = "X-Slack-Request-Timestamp"
slackSignatureHeader = "X-Slack-Signature"
)
type oldTimeStampError struct {
s string
}
func (e *oldTimeStampError) Error() string {
return e.s
}
// Message is slack message
type Message struct {
ResponseType string `json:"response_type"`
Text string `json:"text"`
UnfurlLinks bool `json:"unfurl_links"`
}
// Result is customsearch result
type Result struct {
Position int64
Result *customsearch.Result
}
// MexSearch brings Mexican information to the Sumo world
func MexSearch(w http.ResponseWriter, r *http.Request) {
setup(r.Context())
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Fatalf("Couldn't read request body: %v", err)
}
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
if r.Method != "POST" {
http.Error(w, "Only POST requests are accepted", 405)
}
if err := r.ParseForm(); err != nil {
http.Error(w, "Couldn't parse form", 400)
log.Fatalf("ParseForm: %v", err)
}
// Reset r.Body as ParseForm depletes it by reading the io.ReadCloser.
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
result, err := verifyWebHook(r)
if err != nil {
log.Fatalf("verifyWebhook: %v", err)
}
if !result {
log.Fatalf("signatures did not match.")
}
if len(r.Form["text"]) == 0 {
log.Fatalf("emtpy text in form")
}
// todo: search
query := strings.Join(r.Form["text"], " ")
log.Printf("query %s", query)
msg, err := makeSearchRequest(r.Context(), query)
if err != nil {
log.Fatalf("makeSearchRequest: %v", err)
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(msg); err != nil {
log.Fatalf("json.Marshal: %v", err)
}
log.Printf("send message %v", msg)
}
func verifyWebHook(r *http.Request) (bool, error) {
slackSigningSecret := os.Getenv("SLACK_SECRET")
timeStamp := r.Header.Get(slackRequestTimestampHeader)
slackSignature := r.Header.Get(slackSignatureHeader)
t, err := strconv.ParseInt(timeStamp, 10, 64)
if err != nil {
return false, fmt.Errorf("strconv.ParseInt(%s): %v", timeStamp, err)
}
if ageOk, age := checkTimestamp(t); !ageOk {
return false, &oldTimeStampError{fmt.Sprintf("checkTimestamp(%v): %v %v", t, ageOk, age)}
}
if timeStamp == "" || slackSignature == "" {
return false, fmt.Errorf("either timeStamp or signature headers were blank")
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return false, fmt.Errorf("ioutil.ReadAll(%v): %v", r.Body, err)
}
// Reset the body so other calls won't fail.
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
baseString := fmt.Sprintf("%s:%s:%s", version, timeStamp, body)
signature := getSignature([]byte(baseString), []byte(slackSigningSecret))
trimmed := strings.TrimPrefix(slackSignature, fmt.Sprintf("%s=", version))
signatureInHeader, err := hex.DecodeString(trimmed)
if err != nil {
return false, fmt.Errorf("hex.DecodeString(%v): %v", trimmed, err)
}
return hmac.Equal(signature, signatureInHeader), nil
}
func makeSearchRequest(ctx context.Context, query string) (*Message, error) {
apiKey := os.Getenv("SEARCH_API_KEY")
id := os.Getenv("SEARCH_ID")
q := "メキシコ " + query
customsearchService, err := customsearch.NewService(ctx, option.WithAPIKey(apiKey))
search := customsearchService.Cse.List(q)
search.Cx(id)
search.Start(1)
call, err := search.Do()
if err != nil {
return nil, err
}
if len(call.Items) == 0 {
return nil, fmt.Errorf("not found")
}
return formatSlackMessage(call.Items[0].Link)
}
func getSignature(base []byte, secret []byte) []byte {
h := hmac.New(sha256.New, secret)
h.Write(base)
return h.Sum(nil)
}
func checkTimestamp(timeStamp int64) (bool, time.Duration) {
t := time.Since(time.Unix(timeStamp, 0))
return t.Minutes() <= 5, t
}