/
webhooks.go
272 lines (234 loc) · 9.26 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
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
267
268
269
270
271
272
// Copyright (c) 2022, John Moore
// All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree.
package clickup
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
)
type WebhookEventMessage struct {
Event WebhookEvent `json:"event"`
HistoryItems []struct {
ID string `json:"id"`
Type int `json:"type"`
Date string `json:"date"`
Field string `json:"field"`
ParentID string `json:"parent_id"`
Data struct {
StatusType string `json:"status_type"`
} `json:"data"`
User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Color string `json:"color"`
Initials string `json:"initials"`
ProfilePicture string `json:"profilePicture"`
} `json:"user"`
Before struct {
Status string `json:"status"`
Color string `json:"color"`
Orderindex int `json:"-"`
Type string `json:"type"`
} `json:"before"`
After struct {
Status string `json:"status"`
Color string `json:"color"`
Orderindex int `json:"-"`
Type string `json:"type"`
} `json:"after"`
} `json:"history_items"`
TaskID string `json:"task_id"`
WebhookID string `json:"webhook_id"`
}
type WebhookEvent string
const (
EventAll WebhookEvent = "*"
EventTaskCreated WebhookEvent = "taskCreated"
EventTaskUpdated WebhookEvent = "taskUpdated"
EventTaskDeleted WebhookEvent = "taskDeleted"
EventTaskPriorityUpdated WebhookEvent = "taskPriorityUpdated"
EventTaskStatusUpdated WebhookEvent = "taskStatusUpdated"
EventTaskAssigneeUpdated WebhookEvent = "taskAssigneeUpdated"
EventTaskDueDateUpdated WebhookEvent = "taskDueDateUpdated"
EventTaskTagUpdated WebhookEvent = "taskTagUpdated"
EventTaskMoved WebhookEvent = "taskMoved"
EventTaskCommentPosted WebhookEvent = "taskCommentPosted"
EventTaskCommentUpdated WebhookEvent = "taskCommentUpdated"
EventTaskTimeEstimateUpdated WebhookEvent = "taskTimeEstimateUpdated"
EventTaskTimeTrackedUpdated WebhookEvent = "taskTimeTrackedUpdated"
EventListCreated WebhookEvent = "listCreated"
EventListUpdated WebhookEvent = "listUpdated"
EventListDeleted WebhookEvent = "listDeleted"
EventFolderCreated WebhookEvent = "folderCreated"
EventFolderUpdated WebhookEvent = "folderUpdated"
EventFolderDeleted WebhookEvent = "folderDeleted"
EventSpaceCreated WebhookEvent = "spaceCreated"
EventSpaceUpdated WebhookEvent = "spaceUpdated"
EventSpaceDeleted WebhookEvent = "spaceDeleted"
EventGoalCreated WebhookEvent = "goalCreated"
EventGoalUpdated WebhookEvent = "goalUpdated"
EventGoalDeleted WebhookEvent = "goalDeleted"
EventKeyResultCreated WebhookEvent = "keyResultCreated"
EventKeyResultUpdated WebhookEvent = "keyResultUpdated"
EventKeyResultDeleted WebhookEvent = "keyResultDeleted"
)
type WebhookHealth struct {
Status string `json:"status"`
FailCount int `json:"fail_count"`
}
type Webhook struct {
ID string `json:"id"`
UserID int `json:"userid"`
TeamID int `json:"team_id"`
Endpoint string `json:"endpoint"`
ClientID string `json:"client_id"`
Events []WebhookEvent `json:"events"`
TaskID int `json:"task_id"`
ListID int `json:"list_id"`
FolderID int `json:"folder_id"`
SpaceID int `json:"space_id"`
Health *WebhookHealth `json:"health"`
Secret string `json:"secret"`
}
type WebhooksQueryResponse struct {
Webhooks []Webhook `json:"webhooks"`
}
type CreateWebhookResponse struct {
ID string `json:"id"`
Webhook struct {
ID string `json:"id"`
UserID int `json:"userid"`
TeamID int `json:"team_id"`
Endpoint string `json:"endpoint"`
ClientID string `json:"client_id"`
Events []WebhookEvent `json:"events"`
TaskID int `json:"task_id"`
ListID int `json:"list_id"`
FolderID int `json:"folder_id"`
SpaceID int `json:"space_id"`
Health *WebhookHealth `json:"health"`
Secret string `json:"secret"`
} `json:"webhook"`
}
type UpdateWebhookResponse struct {
CreateWebhookResponse
}
type CreateWebhookRequest struct {
Endpoint string `json:"endpoint,omitempty"`
Events []WebhookEvent `json:"events,omitempty"`
TaskID string `json:"task_id,omitempty"`
ListID string `json:"list_id,omitempty"`
FolderID string `json:"folder_id,omitempty"`
}
type webhookVerifyResult struct {
validSignature bool
signatureFromClickup string
signatureGenerated string
}
// VerifyWebhookSignature compares a generated signature using secret that
// is returned with the webhook CRUD operations with the x-signature http header
// that is sent with the http request to the webhook endpoint.
// It should be noted that err will be nil even if the signature is not valid,
// thus the WebhookVerifyResult.ValidSignature() should be called.
func VerifyWebhookSignature(webhookRequest *http.Request, secret string) (*webhookVerifyResult, error) {
h := hmac.New(sha256.New, []byte(secret))
var buf bytes.Buffer
body := io.TeeReader(webhookRequest.Body, &buf)
webhookRequest.Body = io.NopCloser(&buf)
if _, err := io.Copy(h, body); err != nil {
return nil, err
}
sha := hex.EncodeToString(h.Sum(nil))
sigHeader := webhookRequest.Header.Get("X-Signature")
return &webhookVerifyResult{
validSignature: sigHeader == sha,
signatureFromClickup: sigHeader,
signatureGenerated: sha,
}, nil
}
// Valid returns true if the wwebhookVerifyResult's signature matches the
// signature generated for the webhook upon creation.
func (w *webhookVerifyResult) Valid() bool {
return w.validSignature
}
func (w *webhookVerifyResult) SignatureFromClickup() string {
return w.signatureFromClickup
}
func (w *webhookVerifyResult) SignatureGenerated() string {
return w.signatureGenerated
}
// CreateWebhook activates a new webhook for workspaceID using the parameters of webhook.
// You can scope the webhook to a list, folder, or even specific task by setting the appropriate ID fields
// on webhook (CreateWebhookRequest). See WebhookEvent for a listing of optional event types.
// The caller should keep track of the Secret provided with the CreateWebhookResponse to compare against the
// signature sent in a webhook's message body.
func (c *Client) CreateWebhook(ctx context.Context, workspaceID string, webhook *CreateWebhookRequest) (*CreateWebhookResponse, error) {
if workspaceID == "" {
return nil, fmt.Errorf("must provide workspace id to create webhook: %w", ErrValidation)
}
b, err := json.Marshal(webhook)
if err != nil {
return nil, fmt.Errorf("unable to serialize new webhook: %w", err)
}
buf := bytes.NewBuffer(b)
endpoint := fmt.Sprintf("/team/%s/webhook", workspaceID)
var newWebhook CreateWebhookResponse
if err := c.call(ctx, http.MethodPost, endpoint, buf, &newWebhook); err != nil {
return nil, fmt.Errorf("failed to make clickup request: %w", err)
}
return &newWebhook, nil
}
type UpdateWebhookRequest struct {
ID string `json:"id"`
Endpoint string `json:"endpoint,omitempty"`
Events []WebhookEvent `json:"events,omitempty"`
TaskID string `json:"task_id,omitempty"`
ListID string `json:"list_id,omitempty"`
FolderID string `json:"folder_id,omitempty"`
Status string `json:"status,omitempty"`
}
// UpdateWebhook changes an existing webhook.
func (c *Client) UpdateWebhook(ctx context.Context, webhook *UpdateWebhookRequest) (*UpdateWebhookResponse, error) {
if webhook.ID == "" {
return nil, fmt.Errorf("must provide a webhook id: %w", ErrValidation)
}
b, err := json.Marshal(webhook)
if err != nil {
return nil, fmt.Errorf("unable to serialize webhook: %w", err)
}
buf := bytes.NewBuffer(b)
endpoint := fmt.Sprintf("/webhook/%s", webhook.ID)
var updatedWebhook UpdateWebhookResponse
if err := c.call(ctx, http.MethodPut, endpoint, buf, &updatedWebhook); err != nil {
return nil, fmt.Errorf("failed to make clickup request: %w", err)
}
return &updatedWebhook, nil
}
// DeleteWebhook removes an existing webhook.
func (c *Client) DeleteWebhook(ctx context.Context, webhookID string) error {
if webhookID == "" {
return fmt.Errorf("must provide a webhook id to delete: %w", ErrValidation)
}
return c.call(ctx, http.MethodDelete, fmt.Sprintf("/webhook/%s", webhookID), nil, &struct{}{})
}
// WebhooksFor returns a listing of all webhooks for a workspace.
func (c *Client) WebhooksFor(ctx context.Context, workspaceID string) (*WebhooksQueryResponse, error) {
if workspaceID == "" {
return nil, fmt.Errorf("must provide a workspace id to get webhooks: %w", ErrValidation)
}
endpoint := fmt.Sprintf("/team/%s/webhook", workspaceID)
var webhooks WebhooksQueryResponse
if err := c.call(ctx, http.MethodGet, endpoint, nil, &webhooks); err != nil {
return nil, fmt.Errorf("failed to make clickup request: %w", err)
}
return &webhooks, nil
}