webhooks.go

  1package backend
  2
  3import (
  4	"context"
  5	"encoding/json"
  6
  7	"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)
 15
 16// 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)
 20
 21	return dbx.TransactionContext(ctx, func(tx *db.Tx) error { //nolint:wrapcheck
 22		lastID, err := datastore.CreateWebhook(ctx, tx, repo.ID(), url, secret, int(contentType), active)
 23		if err != nil {
 24			return db.WrapError(err)
 25		}
 26
 27		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		}
 34
 35		return nil
 36	})
 37}
 38
 39// 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)
 43
 44	var wh webhook.Hook
 45	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		}
 54
 55		wh = webhook.Hook{
 56			Webhook:     h,
 57			ContentType: webhook.ContentType(h.ContentType), //nolint:gosec
 58			Events:      make([]webhook.Event, len(events)),
 59		}
 60		for i, e := range events {
 61			wh.Events[i] = webhook.Event(e.Event)
 62		}
 63
 64		return nil
 65	}); err != nil {
 66		return webhook.Hook{}, db.WrapError(err) //nolint:wrapcheck
 67	}
 68
 69	return wh, nil
 70}
 71
 72// 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)
 76
 77	var webhooks []models.Webhook
 78	webhookEvents := map[int64][]models.WebhookEvent{}
 79	if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {
 80		var err error
 81		webhooks, err = datastore.GetWebhooksByRepoID(ctx, tx, repo.ID())
 82		if err != nil {
 83			return err //nolint:wrapcheck
 84		}
 85
 86		for _, h := range webhooks {
 87			events, err := datastore.GetWebhookEventsByWebhookID(ctx, tx, h.ID)
 88			if err != nil {
 89				return err //nolint:wrapcheck
 90			}
 91			webhookEvents[h.ID] = events
 92		}
 93
 94		return nil
 95	}); err != nil {
 96		return nil, db.WrapError(err) //nolint:wrapcheck
 97	}
 98
 99	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		}
105
106		hooks[i] = webhook.Hook{
107			Webhook:     h,
108			ContentType: webhook.ContentType(h.ContentType), //nolint:gosec
109			Events:      events,
110		}
111	}
112
113	return hooks, nil
114}
115
116// 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)
120
121	return dbx.TransactionContext(ctx, func(tx *db.Tx) error { //nolint:wrapcheck
122		if err := datastore.UpdateWebhookByID(ctx, tx, repo.ID(), id, url, secret, int(contentType), active); err != nil {
123			return db.WrapError(err)
124		}
125
126		currentEvents, err := datastore.GetWebhookEventsByWebhookID(ctx, tx, id)
127		if err != nil {
128			return db.WrapError(err)
129		}
130
131		// Delete events that are no longer in the list.
132		toBeDeleted := make([]int64, 0)
133		for _, e := range currentEvents {
134			found := false
135			for _, ne := range updatedEvents {
136				if int(ne) == e.Event {
137					found = true
138					break
139				}
140			}
141			if !found {
142				toBeDeleted = append(toBeDeleted, e.ID)
143			}
144		}
145
146		if err := datastore.DeleteWebhookEventsByID(ctx, tx, toBeDeleted); err != nil {
147			return db.WrapError(err)
148		}
149
150		// Prune events that are already in the list.
151		newEvents := make([]int, 0)
152		for _, e := range updatedEvents {
153			found := false
154			for _, ne := range currentEvents {
155				if int(e) == ne.Event {
156					found = true
157					break
158				}
159			}
160			if !found {
161				newEvents = append(newEvents, int(e))
162			}
163		}
164
165		if err := datastore.CreateWebhookEvents(ctx, tx, id, newEvents); err != nil {
166			return db.WrapError(err)
167		}
168
169		return nil
170	})
171}
172
173// 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)
177
178	return dbx.TransactionContext(ctx, func(tx *db.Tx) error { //nolint:wrapcheck
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		}
186
187		return nil
188	})
189}
190
191// 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)
195
196	var deliveries []models.WebhookDelivery
197	if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {
198		var err error
199		deliveries, err = datastore.ListWebhookDeliveriesByWebhookID(ctx, tx, id)
200		if err != nil {
201			return db.WrapError(err)
202		}
203
204		return nil
205	}); err != nil {
206		return nil, db.WrapError(err) //nolint:wrapcheck
207	}
208
209	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	}
216
217	return ds, nil
218}
219
220// 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)
224
225	var delivery models.WebhookDelivery
226	var wh models.Webhook
227	if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {
228		var err error
229		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		}
234
235		delivery, err = datastore.GetWebhookDeliveryByID(ctx, tx, id, delID)
236		if err != nil {
237			return db.WrapError(err)
238		}
239
240		return nil
241	}); err != nil {
242		return db.WrapError(err) //nolint:wrapcheck
243	}
244
245	log.Infof("redelivering webhook delivery %s for webhook %d\n\n%s\n\n", delID, id, delivery.RequestBody)
246
247	var payload json.RawMessage
248	if err := json.Unmarshal([]byte(delivery.RequestBody), &payload); err != nil {
249		log.Errorf("error unmarshaling webhook payload: %v", err)
250		return err //nolint:wrapcheck
251	}
252
253	return webhook.SendWebhook(ctx, wh, webhook.Event(delivery.Event), payload) //nolint:wrapcheck
254}
255
256// 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)
260
261	var delivery webhook.Delivery
262	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		}
267
268		delivery = webhook.Delivery{
269			WebhookDelivery: d,
270			Event:           webhook.Event(d.Event),
271		}
272
273		return nil
274	}); err != nil {
275		return webhook.Delivery{}, db.WrapError(err) //nolint:wrapcheck
276	}
277
278	return delivery, nil
279}