1package backend23import (4 "context"5 "encoding/json"67 "charm.land/log/v2"8 "github.com/charmbracelet/soft-serve/pkg/db"9 "github.com/charmbracelet/soft-serve/pkg/db/models"10 "github.com/charmbracelet/soft-serve/pkg/proto"11 "github.com/charmbracelet/soft-serve/pkg/store"12 "github.com/charmbracelet/soft-serve/pkg/utils"13 "github.com/charmbracelet/soft-serve/pkg/webhook"14 "github.com/google/uuid"15)1617// CreateWebhook creates a webhook for a repository.18func (b *Backend) CreateWebhook(ctx context.Context, repo proto.Repository, url string, contentType webhook.ContentType, secret string, events []webhook.Event, active bool) error {19 dbx := db.FromContext(ctx)20 datastore := store.FromContext(ctx)21 url = utils.Sanitize(url)2223 // Validate webhook URL to prevent SSRF attacks24 if err := webhook.ValidateWebhookURL(url); err != nil {25 return err //nolint:wrapcheck26 }2728 return dbx.TransactionContext(ctx, func(tx *db.Tx) error {29 lastID, err := datastore.CreateWebhook(ctx, tx, repo.ID(), url, secret, int(contentType), active)30 if err != nil {31 return db.WrapError(err)32 }3334 evs := make([]int, len(events))35 for i, e := range events {36 evs[i] = int(e)37 }38 if err := datastore.CreateWebhookEvents(ctx, tx, lastID, evs); err != nil {39 return db.WrapError(err)40 }4142 return nil43 })44}4546// Webhook returns a webhook for a repository.47func (b *Backend) Webhook(ctx context.Context, repo proto.Repository, id int64) (webhook.Hook, error) {48 dbx := db.FromContext(ctx)49 datastore := store.FromContext(ctx)5051 var wh webhook.Hook52 if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {53 h, err := datastore.GetWebhookByID(ctx, tx, repo.ID(), id)54 if err != nil {55 return db.WrapError(err)56 }57 events, err := datastore.GetWebhookEventsByWebhookID(ctx, tx, id)58 if err != nil {59 return db.WrapError(err)60 }6162 wh = webhook.Hook{63 Webhook: h,64 ContentType: webhook.ContentType(h.ContentType), //nolint:gosec65 Events: make([]webhook.Event, len(events)),66 }67 for i, e := range events {68 wh.Events[i] = webhook.Event(e.Event)69 }7071 return nil72 }); err != nil {73 return webhook.Hook{}, db.WrapError(err)74 }7576 return wh, nil77}7879// ListWebhooks lists webhooks for a repository.80func (b *Backend) ListWebhooks(ctx context.Context, repo proto.Repository) ([]webhook.Hook, error) {81 dbx := db.FromContext(ctx)82 datastore := store.FromContext(ctx)8384 var webhooks []models.Webhook85 webhookEvents := map[int64][]models.WebhookEvent{}86 if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {87 var err error88 webhooks, err = datastore.GetWebhooksByRepoID(ctx, tx, repo.ID())89 if err != nil {90 return err91 }9293 for _, h := range webhooks {94 events, err := datastore.GetWebhookEventsByWebhookID(ctx, tx, h.ID)95 if err != nil {96 return err97 }98 webhookEvents[h.ID] = events99 }100101 return nil102 }); err != nil {103 return nil, db.WrapError(err)104 }105106 hooks := make([]webhook.Hook, len(webhooks))107 for i, h := range webhooks {108 events := make([]webhook.Event, len(webhookEvents[h.ID]))109 for i, e := range webhookEvents[h.ID] {110 events[i] = webhook.Event(e.Event)111 }112113 hooks[i] = webhook.Hook{114 Webhook: h,115 ContentType: webhook.ContentType(h.ContentType), //nolint:gosec116 Events: events,117 }118 }119120 return hooks, nil121}122123// UpdateWebhook updates a webhook.124func (b *Backend) UpdateWebhook(ctx context.Context, repo proto.Repository, id int64, url string, contentType webhook.ContentType, secret string, updatedEvents []webhook.Event, active bool) error {125 dbx := db.FromContext(ctx)126 datastore := store.FromContext(ctx)127128 // Validate webhook URL to prevent SSRF attacks129 if err := webhook.ValidateWebhookURL(url); err != nil {130 return err131 }132133 return dbx.TransactionContext(ctx, func(tx *db.Tx) error {134 if err := datastore.UpdateWebhookByID(ctx, tx, repo.ID(), id, url, secret, int(contentType), active); err != nil {135 return db.WrapError(err)136 }137138 currentEvents, err := datastore.GetWebhookEventsByWebhookID(ctx, tx, id)139 if err != nil {140 return db.WrapError(err)141 }142143 // Delete events that are no longer in the list.144 toBeDeleted := make([]int64, 0)145 for _, e := range currentEvents {146 found := false147 for _, ne := range updatedEvents {148 if int(ne) == e.Event {149 found = true150 break151 }152 }153 if !found {154 toBeDeleted = append(toBeDeleted, e.ID)155 }156 }157158 if err := datastore.DeleteWebhookEventsByID(ctx, tx, toBeDeleted); err != nil {159 return db.WrapError(err)160 }161162 // Prune events that are already in the list.163 newEvents := make([]int, 0)164 for _, e := range updatedEvents {165 found := false166 for _, ne := range currentEvents {167 if int(e) == ne.Event {168 found = true169 break170 }171 }172 if !found {173 newEvents = append(newEvents, int(e))174 }175 }176177 if err := datastore.CreateWebhookEvents(ctx, tx, id, newEvents); err != nil {178 return db.WrapError(err)179 }180181 return nil182 })183}184185// DeleteWebhook deletes a webhook for a repository.186func (b *Backend) DeleteWebhook(ctx context.Context, repo proto.Repository, id int64) error {187 dbx := db.FromContext(ctx)188 datastore := store.FromContext(ctx)189190 return dbx.TransactionContext(ctx, func(tx *db.Tx) error {191 _, err := datastore.GetWebhookByID(ctx, tx, repo.ID(), id)192 if err != nil {193 return db.WrapError(err)194 }195 if err := datastore.DeleteWebhookForRepoByID(ctx, tx, repo.ID(), id); err != nil {196 return db.WrapError(err)197 }198199 return nil200 })201}202203// ListWebhookDeliveries lists webhook deliveries for a webhook.204func (b *Backend) ListWebhookDeliveries(ctx context.Context, id int64) ([]webhook.Delivery, error) {205 dbx := db.FromContext(ctx)206 datastore := store.FromContext(ctx)207208 var deliveries []models.WebhookDelivery209 if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {210 var err error211 deliveries, err = datastore.ListWebhookDeliveriesByWebhookID(ctx, tx, id)212 if err != nil {213 return db.WrapError(err)214 }215216 return nil217 }); err != nil {218 return nil, db.WrapError(err)219 }220221 ds := make([]webhook.Delivery, len(deliveries))222 for i, d := range deliveries {223 ds[i] = webhook.Delivery{224 WebhookDelivery: d,225 Event: webhook.Event(d.Event),226 }227 }228229 return ds, nil230}231232// RedeliverWebhookDelivery redelivers a webhook delivery.233func (b *Backend) RedeliverWebhookDelivery(ctx context.Context, repo proto.Repository, id int64, delID uuid.UUID) error {234 dbx := db.FromContext(ctx)235 datastore := store.FromContext(ctx)236237 var delivery models.WebhookDelivery238 var wh models.Webhook239 if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {240 var err error241 wh, err = datastore.GetWebhookByID(ctx, tx, repo.ID(), id)242 if err != nil {243 log.Errorf("error getting webhook: %v", err)244 return db.WrapError(err)245 }246247 delivery, err = datastore.GetWebhookDeliveryByID(ctx, tx, id, delID)248 if err != nil {249 return db.WrapError(err)250 }251252 return nil253 }); err != nil {254 return db.WrapError(err)255 }256257 log.Infof("redelivering webhook delivery %s for webhook %d\n\n%s\n\n", delID, id, delivery.RequestBody)258259 var payload json.RawMessage260 if err := json.Unmarshal([]byte(delivery.RequestBody), &payload); err != nil {261 log.Errorf("error unmarshaling webhook payload: %v", err)262 return err263 }264265 return webhook.SendWebhook(ctx, wh, webhook.Event(delivery.Event), payload)266}267268// WebhookDelivery returns a webhook delivery.269func (b *Backend) WebhookDelivery(ctx context.Context, webhookID int64, id uuid.UUID) (webhook.Delivery, error) {270 dbx := db.FromContext(ctx)271 datastore := store.FromContext(ctx)272273 var delivery webhook.Delivery274 if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {275 d, err := datastore.GetWebhookDeliveryByID(ctx, tx, webhookID, id)276 if err != nil {277 return db.WrapError(err)278 }279280 delivery = webhook.Delivery{281 WebhookDelivery: d,282 Event: webhook.Event(d.Event),283 }284285 return nil286 }); err != nil {287 return webhook.Delivery{}, db.WrapError(err)288 }289290 return delivery, nil291}