This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / appview / xrpc / webhooks.go
10 kB 382 lines
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}