Signed-off-by: Anirudh Oppiliappan anirudh@tangled.org
+31
appview/db/db.go
+31
appview/db/db.go
···
568
568
unique (from_at, to_at)
569
569
);
570
570
571
+
create table if not exists webhooks (
572
+
id integer primary key autoincrement,
573
+
repo_at text not null,
574
+
url text not null,
575
+
secret text not null,
576
+
active integer not null default 1,
577
+
events text not null, -- comma-separated list of events
578
+
created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
579
+
updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
580
+
581
+
foreign key (repo_at) references repos(at_uri) on delete cascade
582
+
);
583
+
584
+
create table if not exists webhook_deliveries (
585
+
id integer primary key autoincrement,
586
+
webhook_id integer not null,
587
+
event text not null,
588
+
delivery_id text not null,
589
+
url text not null,
590
+
request_body text not null,
591
+
response_code integer,
592
+
response_body text,
593
+
success integer not null default 0,
594
+
created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
595
+
596
+
foreign key (webhook_id) references webhooks(id) on delete cascade
597
+
);
598
+
571
599
create table if not exists migrations (
572
600
id integer primary key autoincrement,
573
601
name text unique
···
578
606
create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read);
579
607
create index if not exists idx_references_from_at on reference_links(from_at);
580
608
create index if not exists idx_references_to_at on reference_links(to_at);
609
+
create index if not exists idx_webhooks_repo_at on webhooks(repo_at);
610
+
create index if not exists idx_webhook_deliveries_webhook_id on webhook_deliveries(webhook_id);
611
+
create index if not exists idx_webhook_deliveries_created_at on webhook_deliveries(created_at desc);
581
612
`)
582
613
if err != nil {
583
614
return nil, err
+294
appview/db/webhooks.go
+294
appview/db/webhooks.go
···
1
+
package db
2
+
3
+
import (
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
15
+
func 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 active int
54
+
55
+
err := rows.Scan(
56
+
&wh.Id,
57
+
&wh.RepoAt,
58
+
&wh.Url,
59
+
&wh.Secret,
60
+
&active,
61
+
&eventsStr,
62
+
&createdAt,
63
+
&updatedAt,
64
+
)
65
+
if err != nil {
66
+
return nil, fmt.Errorf("failed to scan webhook: %w", err)
67
+
}
68
+
69
+
wh.Active = active == 1
70
+
if eventsStr != "" {
71
+
wh.Events = strings.Split(eventsStr, ",")
72
+
}
73
+
74
+
if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
75
+
wh.CreatedAt = t
76
+
}
77
+
if t, err := time.Parse(time.RFC3339, updatedAt); err == nil {
78
+
wh.UpdatedAt = t
79
+
}
80
+
81
+
webhooks = append(webhooks, wh)
82
+
}
83
+
84
+
if err = rows.Err(); err != nil {
85
+
return nil, fmt.Errorf("failed to iterate webhooks: %w", err)
86
+
}
87
+
88
+
return webhooks, nil
89
+
}
90
+
91
+
// GetWebhook returns a single webhook by ID
92
+
func GetWebhook(e Execer, id int64) (*models.Webhook, error) {
93
+
webhooks, err := GetWebhooks(e, orm.FilterEq("id", id))
94
+
if err != nil {
95
+
return nil, err
96
+
}
97
+
98
+
if len(webhooks) == 0 {
99
+
return nil, sql.ErrNoRows
100
+
}
101
+
102
+
if len(webhooks) != 1 {
103
+
return nil, fmt.Errorf("expected 1 webhook, got %d", len(webhooks))
104
+
}
105
+
106
+
return &webhooks[0], nil
107
+
}
108
+
109
+
// AddWebhook creates a new webhook
110
+
func AddWebhook(e Execer, webhook *models.Webhook) error {
111
+
eventsStr := strings.Join(webhook.Events, ",")
112
+
active := 0
113
+
if webhook.Active {
114
+
active = 1
115
+
}
116
+
117
+
result, err := e.Exec(`
118
+
insert into webhooks (repo_at, url, secret, active, events)
119
+
values (?, ?, ?, ?, ?)
120
+
`, webhook.RepoAt.String(), webhook.Url, webhook.Secret, active, eventsStr)
121
+
122
+
if err != nil {
123
+
return fmt.Errorf("failed to insert webhook: %w", err)
124
+
}
125
+
126
+
id, err := result.LastInsertId()
127
+
if err != nil {
128
+
return fmt.Errorf("failed to get webhook id: %w", err)
129
+
}
130
+
131
+
webhook.Id = id
132
+
return nil
133
+
}
134
+
135
+
// UpdateWebhook updates an existing webhook
136
+
func UpdateWebhook(e Execer, webhook *models.Webhook) error {
137
+
eventsStr := strings.Join(webhook.Events, ",")
138
+
active := 0
139
+
if webhook.Active {
140
+
active = 1
141
+
}
142
+
143
+
_, err := e.Exec(`
144
+
update webhooks
145
+
set url = ?, secret = ?, active = ?, events = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
146
+
where id = ?
147
+
`, webhook.Url, webhook.Secret, active, eventsStr, webhook.Id)
148
+
149
+
if err != nil {
150
+
return fmt.Errorf("failed to update webhook: %w", err)
151
+
}
152
+
153
+
return nil
154
+
}
155
+
156
+
// DeleteWebhook deletes a webhook
157
+
func DeleteWebhook(e Execer, id int64) error {
158
+
_, err := e.Exec(`delete from webhooks where id = ?`, id)
159
+
if err != nil {
160
+
return fmt.Errorf("failed to delete webhook: %w", err)
161
+
}
162
+
return nil
163
+
}
164
+
165
+
// AddWebhookDelivery records a webhook delivery attempt
166
+
func AddWebhookDelivery(e Execer, delivery *models.WebhookDelivery) error {
167
+
success := 0
168
+
if delivery.Success {
169
+
success = 1
170
+
}
171
+
172
+
result, err := e.Exec(`
173
+
insert into webhook_deliveries (
174
+
webhook_id,
175
+
event,
176
+
delivery_id,
177
+
url,
178
+
request_body,
179
+
response_code,
180
+
response_body,
181
+
success
182
+
) values (?, ?, ?, ?, ?, ?, ?, ?)
183
+
`,
184
+
delivery.WebhookId,
185
+
delivery.Event,
186
+
delivery.DeliveryId,
187
+
delivery.Url,
188
+
delivery.RequestBody,
189
+
delivery.ResponseCode,
190
+
delivery.ResponseBody,
191
+
success,
192
+
)
193
+
194
+
if err != nil {
195
+
return fmt.Errorf("failed to insert webhook delivery: %w", err)
196
+
}
197
+
198
+
id, err := result.LastInsertId()
199
+
if err != nil {
200
+
return fmt.Errorf("failed to get delivery id: %w", err)
201
+
}
202
+
203
+
delivery.Id = id
204
+
return nil
205
+
}
206
+
207
+
// GetWebhookDeliveries returns recent deliveries for a webhook
208
+
func GetWebhookDeliveries(e Execer, webhookId int64, limit int) ([]models.WebhookDelivery, error) {
209
+
if limit <= 0 {
210
+
limit = 20
211
+
}
212
+
213
+
query := `
214
+
select
215
+
id,
216
+
webhook_id,
217
+
event,
218
+
delivery_id,
219
+
url,
220
+
request_body,
221
+
response_code,
222
+
response_body,
223
+
success,
224
+
created_at
225
+
from webhook_deliveries
226
+
where webhook_id = ?
227
+
order by created_at desc
228
+
limit ?
229
+
`
230
+
231
+
rows, err := e.Query(query, webhookId, limit)
232
+
if err != nil {
233
+
return nil, fmt.Errorf("failed to query webhook deliveries: %w", err)
234
+
}
235
+
defer rows.Close()
236
+
237
+
var deliveries []models.WebhookDelivery
238
+
for rows.Next() {
239
+
var d models.WebhookDelivery
240
+
var createdAt string
241
+
var success int
242
+
var responseCode sql.NullInt64
243
+
var responseBody sql.NullString
244
+
245
+
err := rows.Scan(
246
+
&d.Id,
247
+
&d.WebhookId,
248
+
&d.Event,
249
+
&d.DeliveryId,
250
+
&d.Url,
251
+
&d.RequestBody,
252
+
&responseCode,
253
+
&responseBody,
254
+
&success,
255
+
&createdAt,
256
+
)
257
+
if err != nil {
258
+
return nil, fmt.Errorf("failed to scan delivery: %w", err)
259
+
}
260
+
261
+
d.Success = success == 1
262
+
if responseCode.Valid {
263
+
d.ResponseCode = int(responseCode.Int64)
264
+
}
265
+
if responseBody.Valid {
266
+
d.ResponseBody = responseBody.String
267
+
}
268
+
269
+
if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
270
+
d.CreatedAt = t
271
+
}
272
+
273
+
deliveries = append(deliveries, d)
274
+
}
275
+
276
+
if err = rows.Err(); err != nil {
277
+
return nil, fmt.Errorf("failed to iterate deliveries: %w", err)
278
+
}
279
+
280
+
return deliveries, nil
281
+
}
282
+
283
+
// GetWebhooksForRepo is a convenience function to get all webhooks for a repository
284
+
func GetWebhooksForRepo(e Execer, repoAt syntax.ATURI) ([]models.Webhook, error) {
285
+
return GetWebhooks(e, orm.FilterEq("repo_at", repoAt.String()))
286
+
}
287
+
288
+
// GetActiveWebhooksForRepo returns only active webhooks for a repository
289
+
func GetActiveWebhooksForRepo(e Execer, repoAt syntax.ATURI) ([]models.Webhook, error) {
290
+
return GetWebhooks(e,
291
+
orm.FilterEq("repo_at", repoAt.String()),
292
+
orm.FilterEq("active", 1),
293
+
)
294
+
}
+31
appview/models/webhook.go
+31
appview/models/webhook.go
···
1
+
package models
2
+
3
+
import (
4
+
"time"
5
+
6
+
"github.com/bluesky-social/indigo/atproto/syntax"
7
+
)
8
+
9
+
type Webhook struct {
10
+
Id int64
11
+
RepoAt syntax.ATURI
12
+
Url string
13
+
Secret string
14
+
Active bool
15
+
Events []string // e.g., ["push", "pull_request", "issues"]
16
+
CreatedAt time.Time
17
+
UpdatedAt time.Time
18
+
}
19
+
20
+
type WebhookDelivery struct {
21
+
Id int64
22
+
WebhookId int64
23
+
Event string
24
+
DeliveryId string // UUID for tracking
25
+
Url string
26
+
RequestBody string
27
+
ResponseCode int
28
+
ResponseBody string
29
+
Success bool
30
+
CreatedAt time.Time
31
+
}
History
6 rounds
5 comments
anirudh.fi
submitted
#5
1 commit
expand
collapse
appview/{db,models}: webhook tables and crud ops
Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>
3/3 success
expand
collapse
expand 0 comments
pull request successfully merged
anirudh.fi
submitted
#4
1 commit
expand
collapse
appview/{db,models}: webhook tables and crud ops
Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>
3/3 success
expand
collapse
expand 0 comments
anirudh.fi
submitted
#3
1 commit
expand
collapse
appview/{db,models}: webhook tables and crud ops
Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>
3/3 success
expand
collapse
expand 3 comments
the db code needs to be updated accordingly to handle null strings, by reading into a sql.Null[string] and checking for s.Valid.
Oh, right...
anirudh.fi
submitted
#2
1 commit
expand
collapse
appview/{db,models}: webhook tables and crud ops
Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>
3/3 success
expand
collapse
anirudh.fi
submitted
#1
1 commit
expand
collapse
appview:{db,models}: webhook tables and crud ops
Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>
3/3 success
expand
collapse
expand 1 comment
- we dont need this index
- would be nice to make [this] more strongly typed, we could have an enum for this, like
type WebhookEvent stringwith more concrete variants
anirudh.fi
submitted
#0
1 commit
expand
collapse
appview:{db,models}: webhook tables and crud ops
Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>
@oppi.li, done.