1package backend23import (4 "context"5 "encoding/json"67 "github.com/charmbracelet/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/webhook"13 "github.com/google/uuid"14)1516// CreateWebhook creates a webhook for a repository.17func (b *Backend) CreateWebhook(ctx context.Context, repo proto.Repository, url string, contentType webhook.ContentType, secret string, events []webhook.Event, active bool) error {18 dbx := db.FromContext(ctx)19 datastore := store.FromContext(ctx)2021 return dbx.TransactionContext(ctx, func(tx *db.Tx) error {22 lastID, err := datastore.CreateWebhook(ctx, tx, repo.ID(), url, secret, int(contentType), active)23 if err != nil {24 return db.WrapError(err)25 }2627 evs := make([]int, len(events))28 for i, e := range events {29 evs[i] = int(e)30 }31 if err := datastore.CreateWebhookEvents(ctx, tx, lastID, evs); err != nil {32 return db.WrapError(err)33 }3435 return nil36 })37}3839// Webhook returns a webhook for a repository.40func (b *Backend) Webhook(ctx context.Context, repo proto.Repository, id int64) (webhook.Hook, error) {41 dbx := db.FromContext(ctx)42 datastore := store.FromContext(ctx)4344 var wh webhook.Hook45 if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {46 h, err := datastore.GetWebhookByID(ctx, tx, repo.ID(), id)47 if err != nil {48 return db.WrapError(err)49 }50 events, err := datastore.GetWebhookEventsByWebhookID(ctx, tx, id)51 if err != nil {52 return db.WrapError(err)53 }5455 wh = webhook.Hook{56 Webhook: h,57 ContentType: webhook.ContentType(h.ContentType), //nolint:gosec58 Events: make([]webhook.Event, len(events)),59 }60 for i, e := range events {61 wh.Events[i] = webhook.Event(e.Event)62 }6364 return nil65 }); err != nil {66 return webhook.Hook{}, db.WrapError(err)67 }6869 return wh, nil70}7172// ListWebhooks lists webhooks for a repository.73func (b *Backend) ListWebhooks(ctx context.Context, repo proto.Repository) ([]webhook.Hook, error) {74 dbx := db.FromContext(ctx)75 datastore := store.FromContext(ctx)7677 var webhooks []models.Webhook78 webhookEvents := map[int64][]models.WebhookEvent{}79 if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {80 var err error81 webhooks, err = datastore.GetWebhooksByRepoID(ctx, tx, repo.ID())82 if err != nil {83 return err84 }8586 for _, h := range webhooks {87 events, err := datastore.GetWebhookEventsByWebhookID(ctx, tx, h.ID)88 if err != nil {89 return err90 }91 webhookEvents[h.ID] = events92 }9394 return nil95 }); err != nil {96 return nil, db.WrapError(err)97 }9899 hooks := make([]webhook.Hook, len(webhooks))100 for i, h := range webhooks {101 events := make([]webhook.Event, len(webhookEvents[h.ID]))102 for i, e := range webhookEvents[h.ID] {103 events[i] = webhook.Event(e.Event)104 }105106 hooks[i] = webhook.Hook{107 Webhook: h,108 ContentType: webhook.ContentType(h.ContentType), //nolint:gosec109 Events: events,110 }111 }112113 return hooks, nil114}115116// UpdateWebhook updates a webhook.117func (b *Backend) UpdateWebhook(ctx context.Context, repo proto.Repository, id int64, url string, contentType webhook.ContentType, secret string, updatedEvents []webhook.Event, active bool) error {118 dbx := db.FromContext(ctx)119 datastore := store.FromContext(ctx)120121 return dbx.TransactionContext(ctx, func(tx *db.Tx) error {122 if err := datastore.UpdateWebhookByID(ctx, tx, repo.ID(), id, url, secret, int(contentType), active); err != nil {123 return db.WrapError(err)124 }125126 currentEvents, err := datastore.GetWebhookEventsByWebhookID(ctx, tx, id)127 if err != nil {128 return db.WrapError(err)129 }130131 // Delete events that are no longer in the list.132 toBeDeleted := make([]int64, 0)133 for _, e := range currentEvents {134 found := false135 for _, ne := range updatedEvents {136 if int(ne) == e.Event {137 found = true138 break139 }140 }141 if !found {142 toBeDeleted = append(toBeDeleted, e.ID)143 }144 }145146 if err := datastore.DeleteWebhookEventsByID(ctx, tx, toBeDeleted); err != nil {147 return db.WrapError(err)148 }149150 // Prune events that are already in the list.151 newEvents := make([]int, 0)152 for _, e := range updatedEvents {153 found := false154 for _, ne := range currentEvents {155 if int(e) == ne.Event {156 found = true157 break158 }159 }160 if !found {161 newEvents = append(newEvents, int(e))162 }163 }164165 if err := datastore.CreateWebhookEvents(ctx, tx, id, newEvents); err != nil {166 return db.WrapError(err)167 }168169 return nil170 })171}172173// DeleteWebhook deletes a webhook for a repository.174func (b *Backend) DeleteWebhook(ctx context.Context, repo proto.Repository, id int64) error {175 dbx := db.FromContext(ctx)176 datastore := store.FromContext(ctx)177178 return dbx.TransactionContext(ctx, func(tx *db.Tx) error {179 _, err := datastore.GetWebhookByID(ctx, tx, repo.ID(), id)180 if err != nil {181 return db.WrapError(err)182 }183 if err := datastore.DeleteWebhookForRepoByID(ctx, tx, repo.ID(), id); err != nil {184 return db.WrapError(err)185 }186187 return nil188 })189}190191// ListWebhookDeliveries lists webhook deliveries for a webhook.192func (b *Backend) ListWebhookDeliveries(ctx context.Context, id int64) ([]webhook.Delivery, error) {193 dbx := db.FromContext(ctx)194 datastore := store.FromContext(ctx)195196 var deliveries []models.WebhookDelivery197 if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {198 var err error199 deliveries, err = datastore.ListWebhookDeliveriesByWebhookID(ctx, tx, id)200 if err != nil {201 return db.WrapError(err)202 }203204 return nil205 }); err != nil {206 return nil, db.WrapError(err)207 }208209 ds := make([]webhook.Delivery, len(deliveries))210 for i, d := range deliveries {211 ds[i] = webhook.Delivery{212 WebhookDelivery: d,213 Event: webhook.Event(d.Event),214 }215 }216217 return ds, nil218}219220// RedeliverWebhookDelivery redelivers a webhook delivery.221func (b *Backend) RedeliverWebhookDelivery(ctx context.Context, repo proto.Repository, id int64, delID uuid.UUID) error {222 dbx := db.FromContext(ctx)223 datastore := store.FromContext(ctx)224225 var delivery models.WebhookDelivery226 var wh models.Webhook227 if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {228 var err error229 wh, err = datastore.GetWebhookByID(ctx, tx, repo.ID(), id)230 if err != nil {231 log.Errorf("error getting webhook: %v", err)232 return db.WrapError(err)233 }234235 delivery, err = datastore.GetWebhookDeliveryByID(ctx, tx, id, delID)236 if err != nil {237 return db.WrapError(err)238 }239240 return nil241 }); err != nil {242 return db.WrapError(err)243 }244245 log.Infof("redelivering webhook delivery %s for webhook %d\n\n%s\n\n", delID, id, delivery.RequestBody)246247 var payload json.RawMessage248 if err := json.Unmarshal([]byte(delivery.RequestBody), &payload); err != nil {249 log.Errorf("error unmarshaling webhook payload: %v", err)250 return err251 }252253 return webhook.SendWebhook(ctx, wh, webhook.Event(delivery.Event), payload)254}255256// WebhookDelivery returns a webhook delivery.257func (b *Backend) WebhookDelivery(ctx context.Context, webhookID int64, id uuid.UUID) (webhook.Delivery, error) {258 dbx := db.FromContext(ctx)259 datastore := store.FromContext(ctx)260261 var delivery webhook.Delivery262 if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {263 d, err := datastore.GetWebhookDeliveryByID(ctx, tx, webhookID, id)264 if err != nil {265 return db.WrapError(err)266 }267268 delivery = webhook.Delivery{269 WebhookDelivery: d,270 Event: webhook.Event(d.Event),271 }272273 return nil274 }); err != nil {275 return webhook.Delivery{}, db.WrapError(err)276 }277278 return delivery, nil279}