Monorepo for Tangled
1package db
2
3import (
4 "database/sql"
5 "fmt"
6 "strings"
7 "time"
8
9 "github.com/bluesky-social/indigo/atproto/syntax"
10 "tangled.org/core/appview/models"
11 "tangled.org/core/orm"
12)
13
14// GetWebhooks returns all webhooks for a repository
15func GetWebhooks(e Execer, filters ...orm.Filter) ([]models.Webhook, error) {
16 var conditions []string
17 var args []any
18 for _, filter := range filters {
19 conditions = append(conditions, filter.Condition())
20 args = append(args, filter.Arg()...)
21 }
22
23 whereClause := ""
24 if conditions != nil {
25 whereClause = " where " + strings.Join(conditions, " and ")
26 }
27
28 query := fmt.Sprintf(`
29 select
30 id,
31 repo_at,
32 url,
33 secret,
34 active,
35 events,
36 created_at,
37 updated_at
38 from webhooks
39 %s
40 order by created_at desc
41 `, whereClause)
42
43 rows, err := e.Query(query, args...)
44 if err != nil {
45 return nil, fmt.Errorf("failed to query webhooks: %w", err)
46 }
47 defer rows.Close()
48
49 var webhooks []models.Webhook
50 for rows.Next() {
51 var wh models.Webhook
52 var createdAt, updatedAt, eventsStr string
53 var secret sql.NullString
54 var active int
55
56 err := rows.Scan(
57 &wh.Id,
58 &wh.RepoAt,
59 &wh.Url,
60 &secret,
61 &active,
62 &eventsStr,
63 &createdAt,
64 &updatedAt,
65 )
66 if err != nil {
67 return nil, fmt.Errorf("failed to scan webhook: %w", err)
68 }
69
70 if secret.Valid {
71 wh.Secret = secret.String
72 }
73 wh.Active = active == 1
74 if eventsStr != "" {
75 wh.Events = strings.Split(eventsStr, ",")
76 }
77
78 if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
79 wh.CreatedAt = t
80 }
81 if t, err := time.Parse(time.RFC3339, updatedAt); err == nil {
82 wh.UpdatedAt = t
83 }
84
85 webhooks = append(webhooks, wh)
86 }
87
88 if err = rows.Err(); err != nil {
89 return nil, fmt.Errorf("failed to iterate webhooks: %w", err)
90 }
91
92 return webhooks, nil
93}
94
95// GetWebhook returns a single webhook by ID
96func GetWebhook(e Execer, id int64) (*models.Webhook, error) {
97 webhooks, err := GetWebhooks(e, orm.FilterEq("id", id))
98 if err != nil {
99 return nil, err
100 }
101
102 if len(webhooks) == 0 {
103 return nil, sql.ErrNoRows
104 }
105
106 if len(webhooks) != 1 {
107 return nil, fmt.Errorf("expected 1 webhook, got %d", len(webhooks))
108 }
109
110 return &webhooks[0], nil
111}
112
113// AddWebhook creates a new webhook
114func AddWebhook(e Execer, webhook *models.Webhook) error {
115 eventsStr := strings.Join(webhook.Events, ",")
116 active := 0
117 if webhook.Active {
118 active = 1
119 }
120
121 result, err := e.Exec(`
122 insert into webhooks (repo_at, url, secret, active, events)
123 values (?, ?, ?, ?, ?)
124 `, webhook.RepoAt.String(), webhook.Url, webhook.Secret, active, eventsStr)
125
126 if err != nil {
127 return fmt.Errorf("failed to insert webhook: %w", err)
128 }
129
130 id, err := result.LastInsertId()
131 if err != nil {
132 return fmt.Errorf("failed to get webhook id: %w", err)
133 }
134
135 webhook.Id = id
136 return nil
137}
138
139// UpdateWebhook updates an existing webhook
140func UpdateWebhook(e Execer, webhook *models.Webhook) error {
141 eventsStr := strings.Join(webhook.Events, ",")
142 active := 0
143 if webhook.Active {
144 active = 1
145 }
146
147 _, err := e.Exec(`
148 update webhooks
149 set url = ?, secret = ?, active = ?, events = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
150 where id = ?
151 `, webhook.Url, webhook.Secret, active, eventsStr, webhook.Id)
152
153 if err != nil {
154 return fmt.Errorf("failed to update webhook: %w", err)
155 }
156
157 return nil
158}
159
160// DeleteWebhook deletes a webhook
161func DeleteWebhook(e Execer, id int64) error {
162 _, err := e.Exec(`delete from webhooks where id = ?`, id)
163 if err != nil {
164 return fmt.Errorf("failed to delete webhook: %w", err)
165 }
166 return nil
167}
168
169// AddWebhookDelivery records a webhook delivery attempt
170func AddWebhookDelivery(e Execer, delivery *models.WebhookDelivery) error {
171 success := 0
172 if delivery.Success {
173 success = 1
174 }
175
176 result, err := e.Exec(`
177 insert into webhook_deliveries (
178 webhook_id,
179 event,
180 delivery_id,
181 url,
182 request_body,
183 response_code,
184 response_body,
185 success
186 ) values (?, ?, ?, ?, ?, ?, ?, ?)
187 `,
188 delivery.WebhookId,
189 delivery.Event,
190 delivery.DeliveryId,
191 delivery.Url,
192 delivery.RequestBody,
193 delivery.ResponseCode,
194 delivery.ResponseBody,
195 success,
196 )
197
198 if err != nil {
199 return fmt.Errorf("failed to insert webhook delivery: %w", err)
200 }
201
202 id, err := result.LastInsertId()
203 if err != nil {
204 return fmt.Errorf("failed to get delivery id: %w", err)
205 }
206
207 delivery.Id = id
208 return nil
209}
210
211// GetWebhookDeliveries returns recent deliveries for a webhook
212func GetWebhookDeliveries(e Execer, webhookId int64, limit int) ([]models.WebhookDelivery, error) {
213 if limit <= 0 {
214 limit = 20
215 }
216
217 query := `
218 select
219 id,
220 webhook_id,
221 event,
222 delivery_id,
223 url,
224 request_body,
225 response_code,
226 response_body,
227 success,
228 created_at
229 from webhook_deliveries
230 where webhook_id = ?
231 order by created_at desc
232 limit ?
233 `
234
235 rows, err := e.Query(query, webhookId, limit)
236 if err != nil {
237 return nil, fmt.Errorf("failed to query webhook deliveries: %w", err)
238 }
239 defer rows.Close()
240
241 var deliveries []models.WebhookDelivery
242 for rows.Next() {
243 var d models.WebhookDelivery
244 var createdAt string
245 var success int
246 var responseCode sql.NullInt64
247 var responseBody sql.NullString
248
249 err := rows.Scan(
250 &d.Id,
251 &d.WebhookId,
252 &d.Event,
253 &d.DeliveryId,
254 &d.Url,
255 &d.RequestBody,
256 &responseCode,
257 &responseBody,
258 &success,
259 &createdAt,
260 )
261 if err != nil {
262 return nil, fmt.Errorf("failed to scan delivery: %w", err)
263 }
264
265 d.Success = success == 1
266 if responseCode.Valid {
267 d.ResponseCode = int(responseCode.Int64)
268 }
269 if responseBody.Valid {
270 d.ResponseBody = responseBody.String
271 }
272
273 if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
274 d.CreatedAt = t
275 }
276
277 deliveries = append(deliveries, d)
278 }
279
280 if err = rows.Err(); err != nil {
281 return nil, fmt.Errorf("failed to iterate deliveries: %w", err)
282 }
283
284 return deliveries, nil
285}
286
287// GetWebhooksForRepo is a convenience function to get all webhooks for a repository
288func GetWebhooksForRepo(e Execer, repoAt syntax.ATURI) ([]models.Webhook, error) {
289 return GetWebhooks(e, orm.FilterEq("repo_at", repoAt.String()))
290}
291
292// GetActiveWebhooksForRepo returns only active webhooks for a repository
293func GetActiveWebhooksForRepo(e Execer, repoAt syntax.ATURI) ([]models.Webhook, error) {
294 return GetWebhooks(e,
295 orm.FilterEq("repo_at", repoAt.String()),
296 orm.FilterEq("active", 1),
297 )
298}