This repository has no description
0

Configure Feed

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

core / appview / repo / webhooks.go
10 kB 383 lines
1package repo 2 3import ( 4 "net/http" 5 "strconv" 6 "strings" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 "github.com/go-chi/chi/v5" 10 "tangled.org/core/appview/db" 11 "tangled.org/core/appview/models" 12 "tangled.org/core/appview/pages" 13) 14 15// webhookEventFields maps event checkbox form fields to webhook events 16var webhookEventFields = []struct { 17 field string 18 event models.WebhookEvent 19}{ 20 {"event_push", models.WebhookEventPush}, 21 {"event_repo_renamed", models.WebhookEventRepoRenamed}, 22 {"event_pull_request_created", models.WebhookEventPullRequestCreated}, 23 {"event_pull_request_resubmitted", models.WebhookEventPullRequestResubmitted}, 24 {"event_pull_request_merged", models.WebhookEventPullRequestMerged}, 25 {"event_pull_request_closed", models.WebhookEventPullRequestClosed}, 26 {"event_pull_request_reopened", models.WebhookEventPullRequestReopened}, 27} 28 29func webhookEventsFromForm(r *http.Request) []string { 30 events := []string{} 31 for _, ef := range webhookEventFields { 32 if r.FormValue(ef.field) == "on" { 33 events = append(events, string(ef.event)) 34 } 35 } 36 return events 37} 38 39// Webhooks displays the webhooks settings page 40func (rp *Repo) Webhooks(w http.ResponseWriter, r *http.Request) { 41 l := rp.logger.With("handler", "Webhooks") 42 43 f, err := rp.repoResolver.Resolve(r) 44 if err != nil { 45 l.Error("failed to get repo and knot", "err", err) 46 w.WriteHeader(http.StatusBadRequest) 47 return 48 } 49 50 user := rp.oauth.GetMultiAccountUser(r) 51 52 webhooks, err := db.GetWebhooksForRepo(rp.db, f.RepoDid) 53 if err != nil { 54 l.Error("failed to get webhooks", "err", err) 55 rp.pages.Notice(w, "webhooks-error", "Failed to load webhooks") 56 return 57 } 58 59 // fetch recent deliveries for each webhook 60 deliveriesMap := make(map[int64][]models.WebhookDelivery) 61 for _, webhook := range webhooks { 62 deliveries, err := db.GetWebhookDeliveries(rp.db, webhook.Id, 4) 63 if err != nil { 64 l.Error("failed to get webhook deliveries", "webhook_id", webhook.Id, "err", err) 65 // continue even if we can't get deliveries for one webhook 66 continue 67 } 68 deliveriesMap[webhook.Id] = deliveries 69 } 70 71 rp.pages.RepoWebhooksSettings(w, pages.RepoWebhooksSettingsParams{ 72 BaseParams: pages.BaseParamsFromContext(r.Context()), 73 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 74 Webhooks: webhooks, 75 WebhookDeliveries: deliveriesMap, 76 }) 77} 78 79// AddWebhook creates a new webhook 80func (rp *Repo) AddWebhook(w http.ResponseWriter, r *http.Request) { 81 l := rp.logger.With("handler", "AddWebhook") 82 83 f, err := rp.repoResolver.Resolve(r) 84 if err != nil { 85 l.Error("failed to get repo and knot", "err", err) 86 w.WriteHeader(http.StatusBadRequest) 87 return 88 } 89 90 url := strings.TrimSpace(r.FormValue("url")) 91 if url == "" { 92 rp.pages.Notice(w, "webhooks-error", "Webhook URL is required") 93 return 94 } 95 96 if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { 97 rp.pages.Notice(w, "webhooks-error", "Webhook URL must start with http:// or https://") 98 return 99 } 100 101 secret := strings.TrimSpace(r.FormValue("secret")) 102 // if secret is empty, we don't sign 103 104 active := r.FormValue("active") == "on" 105 106 events := webhookEventsFromForm(r) 107 if len(events) == 0 { 108 rp.pages.Notice(w, "webhooks-error", "At least one event must be enabled") 109 return 110 } 111 112 webhook := &models.Webhook{ 113 RepoDid: syntax.DID(f.RepoDid), 114 Url: url, 115 Secret: secret, 116 Active: active, 117 Events: events, 118 } 119 120 tx, err := rp.db.Begin() 121 if err != nil { 122 l.Error("failed to start transaction", "err", err) 123 rp.pages.Notice(w, "webhooks-error", "Failed to create webhook") 124 return 125 } 126 defer tx.Rollback() 127 128 if err := db.AddWebhook(tx, webhook); err != nil { 129 l.Error("failed to add webhook", "err", err) 130 rp.pages.Notice(w, "webhooks-error", "Failed to create webhook") 131 return 132 } 133 134 if err := tx.Commit(); err != nil { 135 l.Error("failed to commit transaction", "err", err) 136 rp.pages.Notice(w, "webhooks-error", "Failed to create webhook") 137 return 138 } 139 140 rp.pages.HxRefresh(w) 141} 142 143// UpdateWebhook updates an existing webhook 144func (rp *Repo) UpdateWebhook(w http.ResponseWriter, r *http.Request) { 145 l := rp.logger.With("handler", "UpdateWebhook") 146 147 f, err := rp.repoResolver.Resolve(r) 148 if err != nil { 149 l.Error("failed to get repo and knot", "err", err) 150 w.WriteHeader(http.StatusBadRequest) 151 return 152 } 153 154 idStr := chi.URLParam(r, "id") 155 id, err := strconv.ParseInt(idStr, 10, 64) 156 if err != nil { 157 l.Error("invalid webhook id", "err", err) 158 w.WriteHeader(http.StatusBadRequest) 159 return 160 } 161 162 webhook, err := db.GetWebhook(rp.db, id) 163 if err != nil { 164 l.Error("failed to get webhook", "err", err) 165 rp.pages.Notice(w, "webhooks-error", "Webhook not found") 166 return 167 } 168 169 // Verify webhook belongs to this repo 170 if string(webhook.RepoDid) != f.RepoDid { 171 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoDid, "current_repo", f.RepoDid) 172 w.WriteHeader(http.StatusForbidden) 173 return 174 } 175 176 url := strings.TrimSpace(r.FormValue("url")) 177 if url != "" { 178 if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { 179 rp.pages.Notice(w, "webhooks-error", "Webhook URL must start with http:// or https://") 180 return 181 } 182 webhook.Url = url 183 } 184 185 secret := strings.TrimSpace(r.FormValue("secret")) 186 if secret != "" { 187 webhook.Secret = secret 188 } 189 190 webhook.Active = r.FormValue("active") == "on" 191 192 events := webhookEventsFromForm(r) 193 if len(events) > 0 { 194 webhook.Events = events 195 } 196 197 tx, err := rp.db.Begin() 198 if err != nil { 199 l.Error("failed to start transaction", "err", err) 200 rp.pages.Notice(w, "webhooks-error", "Failed to update webhook") 201 return 202 } 203 defer tx.Rollback() 204 205 if err := db.UpdateWebhook(tx, webhook); err != nil { 206 l.Error("failed to update webhook", "err", err) 207 rp.pages.Notice(w, "webhooks-error", "Failed to update webhook") 208 return 209 } 210 211 if err := tx.Commit(); err != nil { 212 l.Error("failed to commit transaction", "err", err) 213 rp.pages.Notice(w, "webhooks-error", "Failed to update webhook") 214 return 215 } 216 217 rp.pages.HxRefresh(w) 218} 219 220// DeleteWebhook deletes a webhook 221func (rp *Repo) DeleteWebhook(w http.ResponseWriter, r *http.Request) { 222 l := rp.logger.With("handler", "DeleteWebhook") 223 224 f, err := rp.repoResolver.Resolve(r) 225 if err != nil { 226 l.Error("failed to get repo and knot", "err", err) 227 w.WriteHeader(http.StatusBadRequest) 228 return 229 } 230 231 idStr := chi.URLParam(r, "id") 232 id, err := strconv.ParseInt(idStr, 10, 64) 233 if err != nil { 234 l.Error("invalid webhook id", "err", err) 235 w.WriteHeader(http.StatusBadRequest) 236 return 237 } 238 239 webhook, err := db.GetWebhook(rp.db, id) 240 if err != nil { 241 l.Error("failed to get webhook", "err", err) 242 w.WriteHeader(http.StatusNotFound) 243 return 244 } 245 246 // Verify webhook belongs to this repo 247 if string(webhook.RepoDid) != f.RepoDid { 248 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoDid, "current_repo", f.RepoDid) 249 w.WriteHeader(http.StatusForbidden) 250 return 251 } 252 253 tx, err := rp.db.Begin() 254 if err != nil { 255 l.Error("failed to start transaction", "err", err) 256 rp.pages.Notice(w, "webhooks-error", "Failed to delete webhook") 257 return 258 } 259 defer tx.Rollback() 260 261 if err := db.DeleteWebhook(tx, id); err != nil { 262 l.Error("failed to delete webhook", "err", err) 263 rp.pages.Notice(w, "webhooks-error", "Failed to delete webhook") 264 return 265 } 266 267 if err := tx.Commit(); err != nil { 268 l.Error("failed to commit transaction", "err", err) 269 rp.pages.Notice(w, "webhooks-error", "Failed to delete webhook") 270 return 271 } 272 273 rp.pages.HxRefresh(w) 274} 275 276// ToggleWebhook toggles the active state of a webhook 277func (rp *Repo) ToggleWebhook(w http.ResponseWriter, r *http.Request) { 278 l := rp.logger.With("handler", "ToggleWebhook") 279 280 f, err := rp.repoResolver.Resolve(r) 281 if err != nil { 282 l.Error("failed to get repo and knot", "err", err) 283 w.WriteHeader(http.StatusBadRequest) 284 return 285 } 286 287 idStr := chi.URLParam(r, "id") 288 id, err := strconv.ParseInt(idStr, 10, 64) 289 if err != nil { 290 l.Error("invalid webhook id", "err", err) 291 w.WriteHeader(http.StatusBadRequest) 292 return 293 } 294 295 webhook, err := db.GetWebhook(rp.db, id) 296 if err != nil { 297 l.Error("failed to get webhook", "err", err) 298 w.WriteHeader(http.StatusNotFound) 299 return 300 } 301 302 // Verify webhook belongs to this repo 303 if string(webhook.RepoDid) != f.RepoDid { 304 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoDid, "current_repo", f.RepoDid) 305 w.WriteHeader(http.StatusForbidden) 306 return 307 } 308 309 // Toggle the active state 310 webhook.Active = !webhook.Active 311 312 tx, err := rp.db.Begin() 313 if err != nil { 314 l.Error("failed to start transaction", "err", err) 315 rp.pages.Notice(w, "webhooks-error", "Failed to toggle webhook") 316 return 317 } 318 defer tx.Rollback() 319 320 if err := db.UpdateWebhook(tx, webhook); err != nil { 321 l.Error("failed to update webhook", "err", err) 322 rp.pages.Notice(w, "webhooks-error", "Failed to toggle webhook") 323 return 324 } 325 326 if err := tx.Commit(); err != nil { 327 l.Error("failed to commit transaction", "err", err) 328 rp.pages.Notice(w, "webhooks-error", "Failed to toggle webhook") 329 return 330 } 331 332 rp.pages.HxRefresh(w) 333} 334 335// WebhookDeliveries returns all deliveries for a webhook (for modal display) 336func (rp *Repo) WebhookDeliveries(w http.ResponseWriter, r *http.Request) { 337 l := rp.logger.With("handler", "WebhookDeliveries") 338 339 f, err := rp.repoResolver.Resolve(r) 340 if err != nil { 341 l.Error("failed to get repo and knot", "err", err) 342 w.WriteHeader(http.StatusBadRequest) 343 return 344 } 345 346 idStr := chi.URLParam(r, "id") 347 id, err := strconv.ParseInt(idStr, 10, 64) 348 if err != nil { 349 l.Error("invalid webhook id", "err", err) 350 w.WriteHeader(http.StatusBadRequest) 351 return 352 } 353 354 webhook, err := db.GetWebhook(rp.db, id) 355 if err != nil { 356 l.Error("failed to get webhook", "err", err) 357 w.WriteHeader(http.StatusNotFound) 358 return 359 } 360 361 // Verify webhook belongs to this repo 362 if string(webhook.RepoDid) != f.RepoDid { 363 l.Error("webhook does not belong to repo", "webhook_repo", webhook.RepoDid, "current_repo", f.RepoDid) 364 w.WriteHeader(http.StatusForbidden) 365 return 366 } 367 368 deliveries, err := db.GetWebhookDeliveries(rp.db, webhook.Id, 100) 369 if err != nil { 370 l.Error("failed to get webhook deliveries", "err", err) 371 rp.pages.Notice(w, "webhooks-error", "Failed to load deliveries") 372 return 373 } 374 375 user := rp.oauth.GetMultiAccountUser(r) 376 377 rp.pages.WebhookDeliveriesList(w, pages.WebhookDeliveriesListParams{ 378 BaseParams: pages.BaseParamsFromContext(r.Context()), 379 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 380 Webhook: webhook, 381 Deliveries: deliveries, 382 }) 383}