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}