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}