This repository has been archived by the owner on Mar 21, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwebhook.go
266 lines (248 loc) · 7.32 KB
/
webhook.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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
package fbot
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
// EventType helps to distinguish the different type of events.
type EventType int
const (
// EventError is triggered when the webhook is called with invalid JSON content.
EventError EventType = 1 + iota
// EventMessage is triggered when a user sends Text, stickers or other content.
// Only text is available at the moment.
EventMessage
// EventPayload is triggered when a quickReply or postback Payload is sent.
EventPayload
// EventRead is triggered when a user read a message.
EventRead
// EventAttachment is triggered when attachemnts are send.
EventAttachment
// EventReferral is triggered when referring through a link or other source.
EventReferral
)
// Event contains information about a user action.
type Event struct {
// Type helps to decide how to react to an event.
Type EventType
// ChatID identifies the user. It's a Facebook user ID.
ChatID int64
// Time describes when the event occured.
Time time.Time
// Text is a message a user send for EventMessage and error description for EventError.
Text string
// Payload is a predefined payload for a quick reply or postback sent with EventPayload.
Payload string
// MessageID is a unique ID for each message.
MessageID string
// Attachments are multiple attachment types.
Attachments []Attachment
// Ref contains the ref data from the URL for EventReferral.
// Ref is also set for EventPayload if the Event was triggered through the Get Started button
// and the user used a refferal link to get there.
Ref string
}
// Attachment describes an attachment.
// Type is one of "image", "video", audio, "location", "file" or "feedback".
// Currently only the URL field is loaded because we only use "file".
// If a sticker is sent the type is "image" and Sticker != 0.
// For more see: https://developers.facebook.com/docs/messenger-platform/webhook-reference/message
type Attachment struct {
Type string
URL string
Sticker int64
}
// Webhook returns a handler for HTTP requests that can be registered with Facebook.
// The passed event handler will be called with all received events.
func (c Client) Webhook(handler func(Event), secret, verifyToken string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
if r.FormValue("hub.verify_token") == verifyToken {
fmt.Fprintln(w, r.FormValue("hub.challenge"))
return
}
fmt.Fprintln(w, "Incorrect verify token.")
return
}
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
handler(Event{Type: EventError, Text: fmt.Sprintf("method not allowed: %s", r.Method)})
return
}
// Read body
data, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "unable to read body", http.StatusInternalServerError)
handler(Event{Type: EventError, Text: fmt.Sprintf("unable to read body: %v", err)})
return
}
// Authenticate using header
signature := r.Header.Get("X-Hub-Signature")
if !validSignature(signature, secret, data) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
handler(Event{Type: EventError, Text: fmt.Sprintf("invalid signature header: %#v", signature)})
return
}
// Parse JSON
var rec receive
if err := json.Unmarshal(data, &rec); err != nil {
http.Error(w, "JSON invalid", http.StatusBadRequest)
handler(Event{Type: EventError, Text: fmt.Sprintf("invalid JSON \"%s\": %v", data, err)})
return
}
_ = r.Body.Close()
// Return response as soon as possible.
// Facebook doesn't care about the event handling.
// Responses are sent separatly.
fmt.Fprintln(w, `{"status":"ok"}`)
for _, e := range rec.Entry {
for _, m := range e.Messaging {
if event := getEvent(m); event.Type != 0 {
handler(event)
}
}
}
})
}
// Expects a signature of the form "sha1=xxx".
// Generates the sha1 sum for the given secret and data.
// Checks equality with constant timing to prevent timing attacks.
func validSignature(signature, secret string, data []byte) bool {
// Remove " sha1=" from header, compute sha1 of secret+body, compare them
if len(signature) <= 5 {
return false
}
sum, err := hex.DecodeString(signature[5:])
if err != nil {
return false
}
mac := hmac.New(sha1.New, []byte(secret))
mac.Write(data)
return hmac.Equal(sum, mac.Sum(nil))
}
func getEvent(m messageInfo) Event {
if m.Postback != nil {
ref := ""
if m.Postback.Referral != nil {
ref = m.Postback.Referral.Ref
}
return Event{
Type: EventPayload,
ChatID: m.Sender.ID,
Time: msToTime(m.Timestamp),
Payload: m.Postback.Payload,
Ref: ref,
}
}
if m.Read != nil {
return Event{
Type: EventRead,
ChatID: m.Sender.ID,
Time: msToTime(m.Read.Watermark),
}
}
if m.Referral != nil {
return Event{
Type: EventReferral,
ChatID: m.Sender.ID,
Time: msToTime(m.Timestamp),
Ref: m.Referral.Ref,
}
}
if m.Message != nil {
if m.Message.IsEcho {
return Event{}
}
if m.Message.QuickReply != nil {
return Event{
Type: EventPayload,
ChatID: m.Sender.ID,
Time: msToTime(m.Timestamp),
Payload: m.Message.QuickReply.Payload,
}
}
if m.Message.Attachments != nil {
var as []Attachment
for _, a := range m.Message.Attachments {
if a.Type == "fallback" {
as = append(as, Attachment{
Type: a.Type,
URL: a.URL,
Sticker: a.Payload.Sticker,
})
} else {
as = append(as, Attachment{
Type: a.Type,
URL: a.Payload.URL,
Sticker: a.Payload.Sticker,
})
}
}
return Event{
Type: EventAttachment,
ChatID: m.Sender.ID,
Time: msToTime(m.Timestamp),
MessageID: m.Message.MID,
Attachments: as,
}
}
return Event{
Type: EventMessage,
ChatID: m.Sender.ID,
Time: msToTime(m.Timestamp),
Text: m.Message.Text,
MessageID: m.Message.MID,
}
}
return Event{}
}
func msToTime(ms int64) time.Time {
return time.Unix(ms/int64(time.Microsecond), 0)
}
type receive struct {
Entry []struct {
Messaging []messageInfo `json:"messaging"`
} `json:"entry"`
}
type messageInfo struct {
Sender struct {
ID int64 `json:"id,string"`
} `json:"sender"`
Timestamp int64 `json:"timestamp"`
Message *struct {
IsEcho bool `json:"is_echo,omitempty"`
Text string `json:"text"`
QuickReply *quickReply `json:"quick_reply,omitempty"`
MID string `json:"mid,omitempty"`
Attachments []struct {
Type string `json:"type,omitempty"`
Payload struct {
Sticker int64 `json:"sticker_id,omitempty"`
URL string `json:"url,omitempty"`
} `json:"payload,omitempty"`
// used by fallback
URL string `json:"url,omitempty"`
} `json:"attachments,omitempty"`
} `json:"message"`
Postback *struct {
Payload string `json:"payload"`
Referral *referral `json:"referral"`
} `json:"postback"`
Read *struct {
Watermark int64 `json:"watermark"`
} `json:"read"`
Referral *referral `json:"referral"`
}
// For now Source and Type are ignored.
// The can be used later to handle ADS and other sources.
// https://developers.facebook.com/docs/messenger-platform/referral-params
type referral struct {
Ref string `json:"ref"`
Source string `json:"source"`
Type string `json:"type"`
}