-
Notifications
You must be signed in to change notification settings - Fork 89
/
webhooks.go
181 lines (154 loc) · 5.59 KB
/
webhooks.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
package helix
import (
"net/http"
"regexp"
)
type WebhookSubscription struct {
Topic string `json:"topic"`
Callback string `json:"callback"`
ExpiresAt Time `json:"expires_at"`
}
type ManyWebhookSubscriptions struct {
Total int `json:"total"`
WebhookSubscriptions []WebhookSubscription `json:"data"`
Pagination Pagination `json:"pagination"`
}
type WebhookSubscriptionsResponse struct {
ResponseCommon
Data ManyWebhookSubscriptions
}
type WebhookSubscriptionsParams struct {
After string `query:"after"`
First int `query:"first,20"` // Limit 100
}
// GetWebhookSubscriptions gets webhook subscriptions, in order of expiration.
// Requires an app access token.
func (c *Client) GetWebhookSubscriptions(params *WebhookSubscriptionsParams) (*WebhookSubscriptionsResponse, error) {
resp, err := c.get("/webhooks/subscriptions", &ManyWebhookSubscriptions{}, params)
if err != nil {
return nil, err
}
webhooks := &WebhookSubscriptionsResponse{}
resp.HydrateResponseCommon(&webhooks.ResponseCommon)
webhooks.Data.Total = resp.Data.(*ManyWebhookSubscriptions).Total
webhooks.Data.WebhookSubscriptions = resp.Data.(*ManyWebhookSubscriptions).WebhookSubscriptions
webhooks.Data.Pagination = resp.Data.(*ManyWebhookSubscriptions).Pagination
return webhooks, nil
}
type WebhookSubscriptionResponse struct {
ResponseCommon
}
type WebhookSubscriptionPayload struct {
Mode string `json:"hub.mode"`
Topic string `json:"hub.topic"`
Callback string `json:"hub.callback"`
LeaseSeconds int `json:"hub.lease_seconds,omitempty"`
Secret string `json:"hub.secret,omitempty"`
}
func (c *Client) PostWebhookSubscription(payload *WebhookSubscriptionPayload) (*WebhookSubscriptionResponse, error) {
resp, err := c.postAsJSON("/webhooks/hub", nil, payload)
if err != nil {
return nil, err
}
webhook := &WebhookSubscriptionResponse{}
resp.HydrateResponseCommon(&webhook.ResponseCommon)
return webhook, nil
}
// Regular expressions used for parsing webhook link headers
var (
UserFollowsRegexp = regexp.MustCompile("helix/users/follows\\?first=1(&from_id=(?P<from_id>\\d+))?(&to_id=(?P<to_id>\\d+))?>")
StreamChangedRegexp = regexp.MustCompile("helix/streams\\?user_id=(?P<user_id>\\d+)>")
UserChangedRegexp = regexp.MustCompile("helix/users\\?id=(?P<id>\\d+)>")
GameAnalyticsRegexp = regexp.MustCompile("helix/analytics\\?game_id=(?P<game_id>\\w+)>")
ExtensionAnalyticsRegexp = regexp.MustCompile("helix/analytics\\?extension_id=(?P<extension_id>\\w+)>")
)
// WebhookTopic is a topic that relates to a specific webhook event.
type WebhookTopic int
// Enumerated webhook topics
const (
UserFollowsTopic WebhookTopic = iota
StreamChangedTopic
UserChangedTopic
GameAnalyticsTopic
ExtensionAnalyticsTopic
)
// GetWebhookTopicFromRequest inspects the "Link" request header to
// determine if it matches against any recognised webhooks topics.
// The matched topic is returned. Otherwise -1 is returned.
func GetWebhookTopicFromRequest(req *http.Request) WebhookTopic {
header := getLinkHeaderFromWebhookRequest(req)
if UserFollowsRegexp.MatchString(header) {
return UserFollowsTopic
}
if StreamChangedRegexp.MatchString(header) {
return StreamChangedTopic
}
if UserChangedRegexp.MatchString(header) {
return UserChangedTopic
}
if GameAnalyticsRegexp.MatchString(header) {
return GameAnalyticsTopic
}
if ExtensionAnalyticsRegexp.MatchString(header) {
return ExtensionAnalyticsTopic
}
return -1
}
// GetWebhookTopicValuesFromRequest inspects the "Link" request header to
// determine if it matches against any recognised webhooks topics and
// returns the unique values specified in the header.
//
// For example, say we receive a "User Follows" webhook event from Twitch.
// Its "Link" header value look likes the following:
//
// <https://api.twitch.tv/helix/webhooks/hub>; rel="hub", <https://api.twitch.tv/helix/users/follows?first=1&from_id=111116&to_id=22222>; rel="self"
//
// From which GetWebhookTopicValuesFromRequest will return a map with the
// values of from_id and to_id:
//
// map[from_id:111116 to_id:22222]
//
// This is particularly useful for webhooks events that do not have a distinguishable
// JSON payload, such as the "Stream Changed" down event.
//
// Additionally, if topic is not known you can pass -1 as its value and
func GetWebhookTopicValuesFromRequest(req *http.Request, topic WebhookTopic) map[string]string {
values := make(map[string]string)
webhookTopic := topic
header := getLinkHeaderFromWebhookRequest(req)
// Webhook topic may not be known, so let's attempt to
// determine its value based on the request.
if webhookTopic < 0 && header != "" {
webhookTopic = GetWebhookTopicFromRequest(req)
}
switch webhookTopic {
case UserFollowsTopic:
values = findStringSubmatchMap(UserFollowsRegexp, header)
case StreamChangedTopic:
values = findStringSubmatchMap(StreamChangedRegexp, header)
case UserChangedTopic:
values = findStringSubmatchMap(UserChangedRegexp, header)
case GameAnalyticsTopic:
values = findStringSubmatchMap(GameAnalyticsRegexp, header)
case ExtensionAnalyticsTopic:
values = findStringSubmatchMap(ExtensionAnalyticsRegexp, header)
}
return values
}
func getLinkHeaderFromWebhookRequest(req *http.Request) string {
return req.Header.Get("link")
}
func findStringSubmatchMap(r *regexp.Regexp, s string) map[string]string {
captures := make(map[string]string)
match := r.FindStringSubmatch(s)
if match == nil {
return captures
}
for i, name := range r.SubexpNames() {
if i == 0 || name == "" {
continue
}
captures[name] = match[i]
}
return captures
}