This repository has no description
1package xrpc
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "strconv"
8 "strings"
9
10 "github.com/bluesky-social/indigo/atproto/syntax"
11 "tangled.org/core/api/tangled"
12 "tangled.org/core/appview/db"
13 "tangled.org/core/appview/models"
14 "tangled.org/core/hostutil"
15 xrpcerr "tangled.org/core/xrpc/errors"
16)
17
18// resolveOwnedRepo loads the repo by its DID and checks the actor owns it
19func (x *Xrpc) resolveOwnedRepo(r *http.Request, repoDid string) (*models.Repo, *xrpcerr.XrpcError, int) {
20 did, ok := actorDid(r)
21 if !ok {
22 e := xrpcerr.MissingActorDidError
23 return nil, &e, http.StatusForbidden
24 }
25
26 repo, err := db.GetRepoByDid(x.DB, repoDid)
27 if err != nil {
28 e := notFoundError("repo not found")
29 return nil, &e, http.StatusNotFound
30 }
31
32 if repo.Did != did {
33 e := xrpcerr.AccessControlError(did)
34 return nil, &e, http.StatusForbidden
35 }
36
37 return repo, nil, http.StatusOK
38}
39
40func (x *Xrpc) WebhookList(w http.ResponseWriter, r *http.Request) {
41 l := x.Logger.With("handler", "WebhookList")
42
43 repo, xerr, status := x.resolveOwnedRepo(r, r.URL.Query().Get("repoDid"))
44 if xerr != nil {
45 writeError(w, *xerr, status)
46 return
47 }
48
49 webhooks, err := db.GetWebhooksForRepo(x.DB, string(repo.RepoDid))
50 if err != nil {
51 l.Error("failed to get webhooks", "err", err)
52 writeError(w, errInternal, http.StatusInternalServerError)
53 return
54 }
55
56 items := make([]*tangled.TempRepoListWebhooks_Webhook, 0, len(webhooks))
57 for i := range webhooks {
58 wh := &webhooks[i]
59 updated := wh.UpdatedAt.UTC().Format(timeFormat)
60 items = append(items, &tangled.TempRepoListWebhooks_Webhook{
61 Id: wh.Id,
62 Url: wh.Url,
63 Active: wh.Active,
64 Events: wh.Events,
65 CreatedAt: wh.CreatedAt.UTC().Format(timeFormat),
66 UpdatedAt: &updated,
67 })
68 }
69
70 x.writeJSON(w, &tangled.TempRepoListWebhooks_Output{Webhooks: items})
71}
72
73func (x *Xrpc) WebhookCreate(w http.ResponseWriter, r *http.Request) {
74 l := x.Logger.With("handler", "WebhookCreate")
75
76 var input tangled.TempRepoCreateWebhook_Input
77 if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
78 writeError(w, errBadRequestBody, http.StatusBadRequest)
79 return
80 }
81
82 repo, xerr, status := x.resolveOwnedRepo(r, input.RepoDid)
83 if xerr != nil {
84 writeError(w, *xerr, status)
85 return
86 }
87
88 url := strings.TrimSpace(input.Url)
89 if err := hostutil.ValidateExternalURL(url, x.Config.Core.Dev); err != nil {
90 writeError(w, badRequestError(err.Error()), http.StatusBadRequest)
91 return
92 }
93 if len(input.Events) == 0 {
94 writeError(w, xrpcErrorTag("NoEventsSelected", "at least one event must be specified"), http.StatusBadRequest)
95 return
96 }
97
98 active := true
99 if input.Active != nil {
100 active = *input.Active
101 }
102 secret := ""
103 if input.Secret != nil {
104 secret = strings.TrimSpace(*input.Secret)
105 }
106
107 webhook := &models.Webhook{
108 RepoDid: syntax.DID(repo.RepoDid),
109 Url: url,
110 Secret: secret,
111 Active: active,
112 Events: input.Events,
113 }
114
115 tx, err := x.DB.Begin()
116 if err != nil {
117 l.Error("failed to start transaction", "err", err)
118 writeError(w, errInternal, http.StatusInternalServerError)
119 return
120 }
121 defer tx.Rollback()
122
123 if err := db.AddWebhook(tx, webhook); err != nil {
124 l.Error("failed to add webhook", "err", err)
125 writeError(w, errInternal, http.StatusInternalServerError)
126 return
127 }
128 if err := tx.Commit(); err != nil {
129 l.Error("failed to commit transaction", "err", err)
130 writeError(w, errInternal, http.StatusInternalServerError)
131 return
132 }
133
134 x.writeJSON(w, &tangled.TempRepoCreateWebhook_Output{Id: webhook.Id})
135}
136
137func (x *Xrpc) WebhookUpdate(w http.ResponseWriter, r *http.Request) {
138 l := x.Logger.With("handler", "WebhookUpdate")
139
140 var input tangled.TempRepoUpdateWebhook_Input
141 if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
142 writeError(w, errBadRequestBody, http.StatusBadRequest)
143 return
144 }
145
146 repo, xerr, status := x.resolveOwnedRepo(r, input.RepoDid)
147 if xerr != nil {
148 writeError(w, *xerr, status)
149 return
150 }
151
152 webhook, err := db.GetWebhook(x.DB, input.Id)
153 if err != nil || string(webhook.RepoDid) != repo.RepoDid {
154 writeError(w, xrpcErrorTag("WebhookNotFound", "webhook not found"), http.StatusNotFound)
155 return
156 }
157
158 if input.Url != nil {
159 url := strings.TrimSpace(*input.Url)
160 if url != "" {
161 if err := hostutil.ValidateExternalURL(url, x.Config.Core.Dev); err != nil {
162 writeError(w, badRequestError(err.Error()), http.StatusBadRequest)
163 return
164 }
165 webhook.Url = url
166 }
167 }
168 if input.Secret != nil {
169 webhook.Secret = strings.TrimSpace(*input.Secret)
170 }
171 if input.Active != nil {
172 webhook.Active = *input.Active
173 }
174 if len(input.Events) > 0 {
175 webhook.Events = input.Events
176 }
177
178 tx, err := x.DB.Begin()
179 if err != nil {
180 l.Error("failed to start transaction", "err", err)
181 writeError(w, errInternal, http.StatusInternalServerError)
182 return
183 }
184 defer tx.Rollback()
185
186 if err := db.UpdateWebhook(tx, webhook); err != nil {
187 l.Error("failed to update webhook", "err", err)
188 writeError(w, errInternal, http.StatusInternalServerError)
189 return
190 }
191 if err := tx.Commit(); err != nil {
192 l.Error("failed to commit transaction", "err", err)
193 writeError(w, errInternal, http.StatusInternalServerError)
194 return
195 }
196
197 w.WriteHeader(http.StatusOK)
198}
199
200func (x *Xrpc) WebhookDelete(w http.ResponseWriter, r *http.Request) {
201 l := x.Logger.With("handler", "WebhookDelete")
202
203 var input tangled.TempRepoDeleteWebhook_Input
204 if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
205 writeError(w, errBadRequestBody, http.StatusBadRequest)
206 return
207 }
208
209 repo, xerr, status := x.resolveOwnedRepo(r, input.RepoDid)
210 if xerr != nil {
211 writeError(w, *xerr, status)
212 return
213 }
214
215 webhook, err := db.GetWebhook(x.DB, input.Id)
216 if err != nil || string(webhook.RepoDid) != repo.RepoDid {
217 writeError(w, xrpcErrorTag("WebhookNotFound", "webhook not found"), http.StatusNotFound)
218 return
219 }
220
221 tx, err := x.DB.Begin()
222 if err != nil {
223 l.Error("failed to start transaction", "err", err)
224 writeError(w, errInternal, http.StatusInternalServerError)
225 return
226 }
227 defer tx.Rollback()
228
229 if err := db.DeleteWebhook(tx, input.Id); err != nil {
230 l.Error("failed to delete webhook", "err", err)
231 writeError(w, errInternal, http.StatusInternalServerError)
232 return
233 }
234 if err := tx.Commit(); err != nil {
235 l.Error("failed to commit transaction", "err", err)
236 writeError(w, errInternal, http.StatusInternalServerError)
237 return
238 }
239
240 w.WriteHeader(http.StatusOK)
241}
242
243func (x *Xrpc) WebhookToggle(w http.ResponseWriter, r *http.Request) {
244 l := x.Logger.With("handler", "WebhookToggle")
245
246 var input tangled.TempRepoToggleWebhook_Input
247 if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
248 writeError(w, errBadRequestBody, http.StatusBadRequest)
249 return
250 }
251
252 repo, xerr, status := x.resolveOwnedRepo(r, input.RepoDid)
253 if xerr != nil {
254 writeError(w, *xerr, status)
255 return
256 }
257
258 webhook, err := db.GetWebhook(x.DB, input.Id)
259 if err != nil || string(webhook.RepoDid) != repo.RepoDid {
260 writeError(w, xrpcErrorTag("WebhookNotFound", "webhook not found"), http.StatusNotFound)
261 return
262 }
263
264 webhook.Active = !webhook.Active
265
266 tx, err := x.DB.Begin()
267 if err != nil {
268 l.Error("failed to start transaction", "err", err)
269 writeError(w, errInternal, http.StatusInternalServerError)
270 return
271 }
272 defer tx.Rollback()
273
274 if err := db.UpdateWebhook(tx, webhook); err != nil {
275 l.Error("failed to toggle webhook", "err", err)
276 writeError(w, errInternal, http.StatusInternalServerError)
277 return
278 }
279 if err := tx.Commit(); err != nil {
280 l.Error("failed to commit transaction", "err", err)
281 writeError(w, errInternal, http.StatusInternalServerError)
282 return
283 }
284
285 x.writeJSON(w, &tangled.TempRepoToggleWebhook_Output{Active: webhook.Active})
286}
287
288func (x *Xrpc) WebhookListDeliveries(w http.ResponseWriter, r *http.Request) {
289 l := x.Logger.With("handler", "WebhookListDeliveries")
290
291 q := r.URL.Query()
292 repo, xerr, status := x.resolveOwnedRepo(r, q.Get("repoDid"))
293 if xerr != nil {
294 writeError(w, *xerr, status)
295 return
296 }
297
298 id, err := strconv.ParseInt(q.Get("id"), 10, 64)
299 if err != nil {
300 writeError(w, badRequestError("invalid webhook id"), http.StatusBadRequest)
301 return
302 }
303
304 webhook, err := db.GetWebhook(x.DB, id)
305 if err != nil || string(webhook.RepoDid) != repo.RepoDid {
306 writeError(w, xrpcErrorTag("WebhookNotFound", "webhook not found"), http.StatusNotFound)
307 return
308 }
309
310 limit := 100
311 if s := q.Get("limit"); s != "" {
312 if n, err := strconv.Atoi(s); err == nil && n > 0 && n <= 100 {
313 limit = n
314 }
315 }
316
317 deliveries, err := db.GetWebhookDeliveries(x.DB, webhook.Id, limit)
318 if err != nil {
319 l.Error("failed to get webhook deliveries", "err", err)
320 writeError(w, errInternal, http.StatusInternalServerError)
321 return
322 }
323
324 items := make([]*tangled.TempRepoListWebhookDeliveries_Delivery, 0, len(deliveries))
325 for i := range deliveries {
326 d := &deliveries[i]
327 item := &tangled.TempRepoListWebhookDeliveries_Delivery{
328 Id: d.Id,
329 DeliveryId: d.DeliveryId,
330 Event: d.Event,
331 Url: d.Url,
332 Success: d.Success,
333 CreatedAt: d.CreatedAt.UTC().Format(timeFormat),
334 }
335 if d.RequestBody != "" {
336 rb := d.RequestBody
337 item.RequestBody = &rb
338 }
339 if d.ResponseBody != "" {
340 rb := d.ResponseBody
341 item.ResponseBody = &rb
342 }
343 if d.ResponseCode != 0 {
344 rc := int64(d.ResponseCode)
345 item.ResponseCode = &rc
346 }
347 items = append(items, item)
348 }
349
350 x.writeJSON(w, &tangled.TempRepoListWebhookDeliveries_Output{Deliveries: items})
351}
352
353func (x *Xrpc) WebhookRetryDelivery(w http.ResponseWriter, r *http.Request) {
354 var input tangled.TempRepoRetryWebhookDelivery_Input
355 if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
356 writeError(w, errBadRequestBody, http.StatusBadRequest)
357 return
358 }
359
360 repo, xerr, status := x.resolveOwnedRepo(r, input.RepoDid)
361 if xerr != nil {
362 writeError(w, *xerr, status)
363 return
364 }
365
366 webhook, err := db.GetWebhook(x.DB, input.WebhookId)
367 if err != nil || string(webhook.RepoDid) != repo.RepoDid {
368 writeError(w, xrpcErrorTag("WebhookNotFound", "webhook not found"), http.StatusNotFound)
369 return
370 }
371
372 delivery, err := db.GetWebhookDelivery(x.DB, input.DeliveryId)
373 if err != nil || delivery.WebhookId != webhook.Id {
374 writeError(w, xrpcErrorTag("DeliveryNotFound", "delivery not found"), http.StatusNotFound)
375 return
376 }
377
378 // re-dispatch async; the new attempt is recorded as its own delivery
379 go x.Webhooks.Redeliver(context.Background(), *webhook, *delivery)
380
381 w.WriteHeader(http.StatusOK)
382}