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/utils"
 13	"github.com/charmbracelet/soft-serve/pkg/webhook"
 14	"github.com/google/uuid"
 15)
 16
 17// 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)
 22
 23	return dbx.TransactionContext(ctx, func(tx *db.Tx) error {
 24		lastID, err := datastore.CreateWebhook(ctx, tx, repo.ID(), url, secret, int(contentType), active)
 25		if err != nil {
 26			return db.WrapError(err)
 27		}
 28
 29		evs := make([]int, len(events))
 30		for i, e := range events {
 31			evs[i] = int(e)
 32		}
 33		if err := datastore.CreateWebhookEvents(ctx, tx, lastID, evs); err != nil {
 34			return db.WrapError(err)
 35		}
 36
 37		return nil
 38	})
 39}
 40
 41// Webhook returns a webhook for a repository.
 42func (b *Backend) Webhook(ctx context.Context, repo proto.Repository, id int64) (webhook.Hook, error) {
 43	dbx := db.FromContext(ctx)
 44	datastore := store.FromContext(ctx)
 45
 46	var wh webhook.Hook
 47	if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {
 48		h, err := datastore.GetWebhookByID(ctx, tx, repo.ID(), id)
 49		if err != nil {
 50			return db.WrapError(err)
 51		}
 52		events, err := datastore.GetWebhookEventsByWebhookID(ctx, tx, id)
 53		if err != nil {
 54			return db.WrapError(err)
 55		}
 56
 57		wh = webhook.Hook{
 58			Webhook:     h,
 59			ContentType: webhook.ContentType(h.ContentType), //nolint:gosec
 60			Events:      make([]webhook.Event, len(events)),
 61		}
 62		for i, e := range events {
 63			wh.Events[i] = webhook.Event(e.Event)
 64		}
 65
 66		return nil
 67	}); err != nil {
 68		return webhook.Hook{}, db.WrapError(err)
 69	}
 70
 71	return wh, nil
 72}
 73
 74// ListWebhooks lists webhooks for a repository.
 75func (b *Backend) ListWebhooks(ctx context.Context, repo proto.Repository) ([]webhook.Hook, error) {
 76	dbx := db.FromContext(ctx)
 77	datastore := store.FromContext(ctx)
 78
 79	var webhooks []models.Webhook
 80	webhookEvents := map[int64][]models.WebhookEvent{}
 81	if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {
 82		var err error
 83		webhooks, err = datastore.GetWebhooksByRepoID(ctx, tx, repo.ID())
 84		if err != nil {
 85			return err
 86		}
 87
 88		for _, h := range webhooks {
 89			events, err := datastore.GetWebhookEventsByWebhookID(ctx, tx, h.ID)
 90			if err != nil {
 91				return err
 92			}
 93			webhookEvents[h.ID] = events
 94		}
 95
 96		return nil
 97	}); err != nil {
 98		return nil, db.WrapError(err)
 99	}
100
101	hooks := make([]webhook.Hook, len(webhooks))
102	for i, h := range webhooks {
103		events := make([]webhook.Event, len(webhookEvents[h.ID]))
104		for i, e := range webhookEvents[h.ID] {
105			events[i] = webhook.Event(e.Event)
106		}
107
108		hooks[i] = webhook.Hook{
109			Webhook:     h,
110			ContentType: webhook.ContentType(h.ContentType), //nolint:gosec
111			Events:      events,
112		}
113	}
114
115	return hooks, nil
116}
117
118// UpdateWebhook updates a webhook.
119func (b *Backend) UpdateWebhook(ctx context.Context, repo proto.Repository, id int64, url string, contentType webhook.ContentType, secret string, updatedEvents []webhook.Event, active bool) error {
120	dbx := db.FromContext(ctx)
121	datastore := store.FromContext(ctx)
122
123	return dbx.TransactionContext(ctx, func(tx *db.Tx) error {
124		if err := datastore.UpdateWebhookByID(ctx, tx, repo.ID(), id, url, secret, int(contentType), active); err != nil {
125			return db.WrapError(err)
126		}
127
128		currentEvents, err := datastore.GetWebhookEventsByWebhookID(ctx, tx, id)
129		if err != nil {
130			return db.WrapError(err)
131		}
132
133		// Delete events that are no longer in the list.
134		toBeDeleted := make([]int64, 0)
135		for _, e := range currentEvents {
136			found := false
137			for _, ne := range updatedEvents {
138				if int(ne) == e.Event {
139					found = true
140					break
141				}
142			}
143			if !found {
144				toBeDeleted = append(toBeDeleted, e.ID)
145			}
146		}
147
148		if err := datastore.DeleteWebhookEventsByID(ctx, tx, toBeDeleted); err != nil {
149			return db.WrapError(err)
150		}
151
152		// Prune events that are already in the list.
153		newEvents := make([]int, 0)
154		for _, e := range updatedEvents {
155			found := false
156			for _, ne := range currentEvents {
157				if int(e) == ne.Event {
158					found = true
159					break
160				}
161			}
162			if !found {
163				newEvents = append(newEvents, int(e))
164			}
165		}
166
167		if err := datastore.CreateWebhookEvents(ctx, tx, id, newEvents); err != nil {
168			return db.WrapError(err)
169		}
170
171		return nil
172	})
173}
174
175// DeleteWebhook deletes a webhook for a repository.
176func (b *Backend) DeleteWebhook(ctx context.Context, repo proto.Repository, id int64) error {
177	dbx := db.FromContext(ctx)
178	datastore := store.FromContext(ctx)
179
180	return dbx.TransactionContext(ctx, func(tx *db.Tx) error {
181		_, err := datastore.GetWebhookByID(ctx, tx, repo.ID(), id)
182		if err != nil {
183			return db.WrapError(err)
184		}
185		if err := datastore.DeleteWebhookForRepoByID(ctx, tx, repo.ID(), id); err != nil {
186			return db.WrapError(err)
187		}
188
189		return nil
190	})
191}
192
193// ListWebhookDeliveries lists webhook deliveries for a webhook.
194func (b *Backend) ListWebhookDeliveries(ctx context.Context, id int64) ([]webhook.Delivery, error) {
195	dbx := db.FromContext(ctx)
196	datastore := store.FromContext(ctx)
197
198	var deliveries []models.WebhookDelivery
199	if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {
200		var err error
201		deliveries, err = datastore.ListWebhookDeliveriesByWebhookID(ctx, tx, id)
202		if err != nil {
203			return db.WrapError(err)
204		}
205
206		return nil
207	}); err != nil {
208		return nil, db.WrapError(err)
209	}
210
211	ds := make([]webhook.Delivery, len(deliveries))
212	for i, d := range deliveries {
213		ds[i] = webhook.Delivery{
214			WebhookDelivery: d,
215			Event:           webhook.Event(d.Event),
216		}
217	}
218
219	return ds, nil
220}
221
222// RedeliverWebhookDelivery redelivers a webhook delivery.
223func (b *Backend) RedeliverWebhookDelivery(ctx context.Context, repo proto.Repository, id int64, delID uuid.UUID) error {
224	dbx := db.FromContext(ctx)
225	datastore := store.FromContext(ctx)
226
227	var delivery models.WebhookDelivery
228	var wh models.Webhook
229	if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {
230		var err error
231		wh, err = datastore.GetWebhookByID(ctx, tx, repo.ID(), id)
232		if err != nil {
233			log.Errorf("error getting webhook: %v", err)
234			return db.WrapError(err)
235		}
236
237		delivery, err = datastore.GetWebhookDeliveryByID(ctx, tx, id, delID)
238		if err != nil {
239			return db.WrapError(err)
240		}
241
242		return nil
243	}); err != nil {
244		return db.WrapError(err)
245	}
246
247	log.Infof("redelivering webhook delivery %s for webhook %d\n\n%s\n\n", delID, id, delivery.RequestBody)
248
249	var payload json.RawMessage
250	if err := json.Unmarshal([]byte(delivery.RequestBody), &payload); err != nil {
251		log.Errorf("error unmarshaling webhook payload: %v", err)
252		return err
253	}
254
255	return webhook.SendWebhook(ctx, wh, webhook.Event(delivery.Event), payload)
256}
257
258// WebhookDelivery returns a webhook delivery.
259func (b *Backend) WebhookDelivery(ctx context.Context, webhookID int64, id uuid.UUID) (webhook.Delivery, error) {
260	dbx := db.FromContext(ctx)
261	datastore := store.FromContext(ctx)
262
263	var delivery webhook.Delivery
264	if err := dbx.TransactionContext(ctx, func(tx *db.Tx) error {
265		d, err := datastore.GetWebhookDeliveryByID(ctx, tx, webhookID, id)
266		if err != nil {
267			return db.WrapError(err)
268		}
269
270		delivery = webhook.Delivery{
271			WebhookDelivery: d,
272			Event:           webhook.Event(d.Event),
273		}
274
275		return nil
276	}); err != nil {
277		return webhook.Delivery{}, db.WrapError(err)
278	}
279
280	return delivery, nil
281}