Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420package repo
import ( "net/http" "strconv" "strings" "time"
"github.com/go-chi/chi/v5" "tangled.org/core/api/org_tangled" "tangled.org/core/appview/models" "tangled.org/core/appview/pages")
// webhookEventFields maps event checkbox form fields to webhook eventsvar webhookEventFields = []struct { field string event models.WebhookEvent}{ {"event_push", models.WebhookEventPush}, {"event_repo_renamed", models.WebhookEventRepoRenamed}, {"event_pull_request_created", models.WebhookEventPullRequestCreated}, {"event_pull_request_resubmitted", models.WebhookEventPullRequestResubmitted}, {"event_pull_request_merged", models.WebhookEventPullRequestMerged}, {"event_pull_request_closed", models.WebhookEventPullRequestClosed}, {"event_pull_request_reopened", models.WebhookEventPullRequestReopened},}
func webhookEventsFromForm(r *http.Request) []string { events := []string{} for _, ef := range webhookEventFields { if r.FormValue(ef.field) == "on" { events = append(events, string(ef.event)) } } return events}
// webhookFromXrpc adapts a spindle listWebhooks item into the view model the// settings templates render.func webhookFromXrpc(wh *org_tangled.TempWebhookListWebhooks_Webhook) models.Webhook { m := models.Webhook{ Id: wh.Id, Url: wh.Url, Active: wh.Active, Events: wh.Events, } if t, err := time.Parse(time.RFC3339, wh.CreatedAt); err == nil { m.CreatedAt = t } if wh.UpdatedAt != nil { if t, err := time.Parse(time.RFC3339, *wh.UpdatedAt); err == nil { m.UpdatedAt = t } } return m}
func deliveryFromXrpc(d *org_tangled.TempWebhookGetDeliveriesForWebhooks_Delivery) models.WebhookDelivery { m := models.WebhookDelivery{ Id: d.Id, DeliveryId: d.DeliveryId, Event: d.Event, Url: d.Url, Success: d.Success, } if d.RequestBody != nil { m.RequestBody = *d.RequestBody } if d.ResponseBody != nil { m.ResponseBody = *d.ResponseBody } if d.ResponseCode != nil { m.ResponseCode = int(*d.ResponseCode) } if t, err := time.Parse(time.RFC3339, d.CreatedAt); err == nil { m.CreatedAt = t } return m}
// Webhooks displays the webhooks settings page. Webhooks now live on the repo's// spindle; this handler proxies to the spindle's XRPC API.func (rp *Repo) Webhooks(w http.ResponseWriter, r *http.Request) { l := rp.logger.With("handler", "Webhooks")
f, err := rp.repoResolver.Resolve(r) if err != nil { l.Error("failed to get repo and knot", "err", err) w.WriteHeader(http.StatusBadRequest) return }
user := rp.oauth.GetMultiAccountUser(r)
if f.Spindle == "" { rp.pages.RepoWebhooksSettings(w, pages.RepoWebhooksSettingsParams{ BaseParams: pages.BaseParamsFromContext(r.Context()), RepoInfo: rp.repoResolver.GetRepoInfo(r, user), }) rp.pages.Notice(w, "webhooks-error", "This repository has no spindle configured; webhooks are unavailable.") return }
client, err := rp.oauth.SpindleServiceClient(r, f.Spindle, org_tangled.TempWebhookListWebhooksNSID) if err != nil { l.Error("failed to build spindle client", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to authorize with spindle") return }
out, err := org_tangled.TempWebhookListWebhooks(r.Context(), client, f.RepoDid) if err != nil { l.Error("failed to list webhooks", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to load webhooks") return }
webhooks := make([]models.Webhook, 0, len(out.Webhooks)) for _, wh := range out.Webhooks { webhooks = append(webhooks, webhookFromXrpc(wh)) }
const deliveriesPerWebhook = 4 deliveriesMap := make(map[int64][]models.WebhookDelivery, len(webhooks)) ids := make([]int64, 0, len(webhooks)) for _, webhook := range webhooks { ids = append(ids, webhook.Id) } if len(ids) > 0 { if dclient, err := rp.oauth.SpindleServiceClient(r, f.Spindle, org_tangled.TempWebhookGetDeliveriesForWebhooksNSID); err == nil { dout, err := org_tangled.TempWebhookGetDeliveriesForWebhooks(r.Context(), dclient, ids, deliveriesPerWebhook, f.RepoDid) if err != nil { l.Error("failed to get webhook deliveries", "err", err) } else { for _, d := range dout.Deliveries { deliveriesMap[d.WebhookId] = append(deliveriesMap[d.WebhookId], deliveryFromXrpc(d)) } } } }
rp.pages.RepoWebhooksSettings(w, pages.RepoWebhooksSettingsParams{ BaseParams: pages.BaseParamsFromContext(r.Context()), RepoInfo: rp.repoResolver.GetRepoInfo(r, user), Webhooks: webhooks, WebhookDeliveries: deliveriesMap, })}
// AddWebhook creates a new webhook on the repo's spindle.func (rp *Repo) AddWebhook(w http.ResponseWriter, r *http.Request) { l := rp.logger.With("handler", "AddWebhook")
f, err := rp.repoResolver.Resolve(r) if err != nil { l.Error("failed to get repo and knot", "err", err) w.WriteHeader(http.StatusBadRequest) return } if f.Spindle == "" { rp.pages.Notice(w, "webhooks-error", "This repository has no spindle configured; webhooks are unavailable.") return }
url := strings.TrimSpace(r.FormValue("url")) if url == "" { rp.pages.Notice(w, "webhooks-error", "Webhook URL is required") return } if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { rp.pages.Notice(w, "webhooks-error", "Webhook URL must start with http:// or https://") return }
events := webhookEventsFromForm(r) if len(events) == 0 { rp.pages.Notice(w, "webhooks-error", "At least one event must be enabled") return }
active := r.FormValue("active") == "on" input := &org_tangled.TempWebhookCreateWebhook_Input{ RepoDid: f.RepoDid, Url: url, Events: events, Active: &active, } if secret := strings.TrimSpace(r.FormValue("secret")); secret != "" { input.Secret = &secret }
client, err := rp.oauth.SpindleServiceClient(r, f.Spindle, org_tangled.TempWebhookCreateWebhookNSID) if err != nil { l.Error("failed to build spindle client", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to authorize with spindle") return } if _, err := org_tangled.TempWebhookCreateWebhook(r.Context(), client, input); err != nil { l.Error("failed to add webhook", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to create webhook") return }
rp.pages.HxRefresh(w)}
// UpdateWebhook updates an existing webhook on the repo's spindle.func (rp *Repo) UpdateWebhook(w http.ResponseWriter, r *http.Request) { l := rp.logger.With("handler", "UpdateWebhook")
f, err := rp.repoResolver.Resolve(r) if err != nil { l.Error("failed to get repo and knot", "err", err) w.WriteHeader(http.StatusBadRequest) return } if f.Spindle == "" { rp.pages.Notice(w, "webhooks-error", "This repository has no spindle configured; webhooks are unavailable.") return }
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { l.Error("invalid webhook id", "err", err) w.WriteHeader(http.StatusBadRequest) return }
active := r.FormValue("active") == "on" input := &org_tangled.TempWebhookUpdateWebhook_Input{ Id: id, RepoDid: f.RepoDid, Active: &active, } url := strings.TrimSpace(r.FormValue("url")) if url != "" { if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { rp.pages.Notice(w, "webhooks-error", "Webhook URL must start with http:// or https://") return } input.Url = &url } if secret := strings.TrimSpace(r.FormValue("secret")); secret != "" { input.Secret = &secret } if events := webhookEventsFromForm(r); len(events) > 0 { input.Events = events }
client, err := rp.oauth.SpindleServiceClient(r, f.Spindle, org_tangled.TempWebhookUpdateWebhookNSID) if err != nil { l.Error("failed to build spindle client", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to authorize with spindle") return } if err := org_tangled.TempWebhookUpdateWebhook(r.Context(), client, input); err != nil { l.Error("failed to update webhook", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to update webhook") return }
rp.pages.HxRefresh(w)}
// DeleteWebhook deletes a webhook on the repo's spindle.func (rp *Repo) DeleteWebhook(w http.ResponseWriter, r *http.Request) { l := rp.logger.With("handler", "DeleteWebhook")
f, err := rp.repoResolver.Resolve(r) if err != nil { l.Error("failed to get repo and knot", "err", err) w.WriteHeader(http.StatusBadRequest) return } if f.Spindle == "" { w.WriteHeader(http.StatusNotFound) return }
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { l.Error("invalid webhook id", "err", err) w.WriteHeader(http.StatusBadRequest) return }
client, err := rp.oauth.SpindleServiceClient(r, f.Spindle, org_tangled.TempWebhookDeleteWebhookNSID) if err != nil { l.Error("failed to build spindle client", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to authorize with spindle") return } if err := org_tangled.TempWebhookDeleteWebhook(r.Context(), client, &org_tangled.TempWebhookDeleteWebhook_Input{ Id: id, RepoDid: f.RepoDid, }); err != nil { l.Error("failed to delete webhook", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to delete webhook") return }
rp.pages.HxRefresh(w)}
// ToggleWebhook toggles the active state of a webhook on the repo's spindle.func (rp *Repo) ToggleWebhook(w http.ResponseWriter, r *http.Request) { l := rp.logger.With("handler", "ToggleWebhook")
f, err := rp.repoResolver.Resolve(r) if err != nil { l.Error("failed to get repo and knot", "err", err) w.WriteHeader(http.StatusBadRequest) return } if f.Spindle == "" { w.WriteHeader(http.StatusNotFound) return }
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { l.Error("invalid webhook id", "err", err) w.WriteHeader(http.StatusBadRequest) return }
client, err := rp.oauth.SpindleServiceClient(r, f.Spindle, org_tangled.TempWebhookToggleWebhookNSID) if err != nil { l.Error("failed to build spindle client", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to authorize with spindle") return } if _, err := org_tangled.TempWebhookToggleWebhook(r.Context(), client, &org_tangled.TempWebhookToggleWebhook_Input{ Id: id, RepoDid: f.RepoDid, }); err != nil { l.Error("failed to toggle webhook", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to toggle webhook") return }
rp.pages.HxRefresh(w)}
// WebhookDeliveries returns all deliveries for a webhook (for modal display).func (rp *Repo) WebhookDeliveries(w http.ResponseWriter, r *http.Request) { l := rp.logger.With("handler", "WebhookDeliveries")
f, err := rp.repoResolver.Resolve(r) if err != nil { l.Error("failed to get repo and knot", "err", err) w.WriteHeader(http.StatusBadRequest) return } if f.Spindle == "" { w.WriteHeader(http.StatusNotFound) return }
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { l.Error("invalid webhook id", "err", err) w.WriteHeader(http.StatusBadRequest) return }
user := rp.oauth.GetMultiAccountUser(r)
// look up the webhook (for its URL header) and its deliveries listClient, err := rp.oauth.SpindleServiceClient(r, f.Spindle, org_tangled.TempWebhookListWebhooksNSID) if err != nil { l.Error("failed to build spindle client", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to authorize with spindle") return } listOut, err := org_tangled.TempWebhookListWebhooks(r.Context(), listClient, f.RepoDid) if err != nil { l.Error("failed to list webhooks", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to load deliveries") return } var webhook *models.Webhook for _, wh := range listOut.Webhooks { if wh.Id == id { m := webhookFromXrpc(wh) webhook = &m break } } if webhook == nil { w.WriteHeader(http.StatusNotFound) return }
client, err := rp.oauth.SpindleServiceClient(r, f.Spindle, org_tangled.TempWebhookGetDeliveriesForWebhooksNSID) if err != nil { l.Error("failed to build spindle client", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to authorize with spindle") return } dout, err := org_tangled.TempWebhookGetDeliveriesForWebhooks(r.Context(), client, []int64{id}, 100, f.RepoDid) if err != nil { l.Error("failed to get webhook deliveries", "err", err) rp.pages.Notice(w, "webhooks-error", "Failed to load deliveries") return } deliveries := make([]models.WebhookDelivery, 0, len(dout.Deliveries)) for _, d := range dout.Deliveries { deliveries = append(deliveries, deliveryFromXrpc(d)) }
rp.pages.WebhookDeliveriesList(w, pages.WebhookDeliveriesListParams{ BaseParams: pages.BaseParamsFromContext(r.Context()), RepoInfo: rp.repoResolver.GetRepoInfo(r, user), Webhook: webhook, Deliveries: deliveries, })}