Monorepo for Tangled tangled.org
2

Configure Feed

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

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