b64e2537 Andy McCall 1 month ago ↶
1
2
3
use super :: { now_ms, Pool} ;
4
use sqlx:: FromRow;
5
6
# [ derive ( Clone, Debug, FromRow ) ]
7
pub struct Hook {
8
pub id : i64 ,
9
pub repo_id : Option< i64 > ,
10
pub org_id : Option< i64 > ,
11
pub url : String,
12
pub secret : String,
13
pub events : String,
14
pub active : bool ,
15
pub created_ms : i64 ,
9756c67d Andy McCall 1 month ago ↶
16
17
pub format : String,
b64e2537 Andy McCall 1 month ago ↶
18
}
9756c67d Andy McCall 1 month ago ↶
19
20
21
pub const FORMATS : [ & str ; 3 ] = [ " oxygenhub" , " discord" , " slack" ] ;
b64e2537 Andy McCall 1 month ago ↶
22
9756c67d Andy McCall 1 month ago ↶
23
24
25
pub fn format_for_url ( url : & str ) -> & 'static str {
26
let host = url.split ( " ://" ) .nth ( 1 ) .unwrap_or ( " " ) .split ( [ ' /' , ' :' ] ) .next ( ) .unwrap_or ( " " ) .to_ascii_lowercase ( ) ;
27
if matches! ( host.as_str ( ) , " discord.com" | " discordapp.com" | " canary.discord.com" | " ptb.discord.com" )
28
& & url.contains ( " /api/webhooks/" )
29
{
30
" discord"
31
} else if host = = " hooks.slack.com" {
32
" slack"
33
} else {
34
" oxygenhub"
35
}
36
}
37
38
39
40
pub fn masked_url ( url : & str ) -> String {
41
let ( scheme, rest) = url.split_once ( " ://" ) .unwrap_or ( ( " " , url) ) ;
42
let ( path, query) = rest.split_once ( ' ?' ) .map ( | ( p , q ) | ( p, Some ( q) ) ) .unwrap_or ( ( rest, None ) ) ;
43
let parts: Vec< String> =
44
path.split ( ' /' )
45
.enumerate ( )
46
.map ( | ( i , seg ) | {
47
if i > 0 & & seg.chars ( ) .count ( ) > 14 {
48
format! ( " {} …" , seg.chars ( ) .take ( 4 ) .collect:: < String> ( ) )
49
} else {
50
seg.to_string ( )
51
}
52
} )
53
.collect ( ) ;
54
let mut out = if scheme.is_empty ( ) { parts.join ( " /" ) } else { format! ( " {scheme} ://{} " , parts.join ( " /" ) ) } ;
55
if query.is_some ( ) {
56
out.push_str ( " ?…" ) ;
57
}
58
out
59
}
60
b64e2537 Andy McCall 1 month ago ↶
61
impl Hook {
62
63
pub fn wants ( & self , event : & str ) -> bool {
64
self .active & & self .events.split ( ' ,' ) .map ( str :: trim) .any ( | e | e = = " *" | | event = = e | | event.starts_with ( & format! ( " {e} ." ) ) )
65
}
66
}
67
68
# [ derive ( Clone, Debug, FromRow ) ]
69
pub struct Delivery {
70
pub id : i64 ,
71
pub hook_id : i64 ,
72
pub delivery : String,
73
pub event : String,
74
pub payload : String,
75
pub created_ms : i64 ,
76
pub attempts : i64 ,
77
pub next_ms : i64 ,
78
pub status : Option< i64 > ,
79
pub response : String,
80
pub delivered_ms : Option< i64 > ,
81
}
82
83
pub async fn hooks_for_repo ( pool : & Pool, repo_id : i64 ) -> sqlx:: Result< Vec< Hook> > {
5d90e0e0 Andy McCall 20 days ago
84
sqlx:: query_as( " SELECT * FROM hooks WHERE repo_id = $1 ORDER BY id" ) .bind ( repo_id) .fetch_all ( pool) .await
b64e2537 Andy McCall 1 month ago ↶
85
}
86
87
pub async fn hooks_for_org ( pool : & Pool, org_id : i64 ) -> sqlx:: Result< Vec< Hook> > {
5d90e0e0 Andy McCall 20 days ago
88
sqlx:: query_as( " SELECT * FROM hooks WHERE org_id = $1 ORDER BY id" ) .bind ( org_id) .fetch_all ( pool) .await
b64e2537 Andy McCall 1 month ago ↶
89
}
90
91
92
93
pub async fn hooks_hearing ( pool : & Pool, repo_id : i64 , owner : & str ) -> sqlx:: Result< Vec< Hook> > {
94
let mut out = hooks_for_repo ( pool, repo_id) .await? ;
95
if let Some ( org) = super :: accounts:: org_by_name( pool, owner) .await? {
96
out.extend ( hooks_for_org ( pool, org.id) .await? ) ;
97
}
98
Ok ( out)
99
}
100
101
pub async fn hook ( pool : & Pool, id : i64 ) -> sqlx:: Result< Option< Hook> > {
5d90e0e0 Andy McCall 20 days ago
102
sqlx:: query_as( " SELECT * FROM hooks WHERE id = $1" ) .bind ( id) .fetch_optional ( pool) .await
b64e2537 Andy McCall 1 month ago ↶
103
}
104
105
pub async fn create_hook (
106
pool : & Pool,
107
repo_id : Option< i64 > ,
108
org_id : Option< i64 > ,
109
url : & str ,
110
secret : & str ,
111
events : & str ,
112
) -> sqlx:: Result< Hook> {
5d90e0e0 Andy McCall 20 days ago
113
sqlx:: query_as(
114
" INSERT INTO hooks (repo_id, org_id, url, secret, events, created_ms, format) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *" ,
115
)
116
.bind ( repo_id)
117
.bind ( org_id)
118
.bind ( url)
119
.bind ( secret)
120
.bind ( events)
121
.bind ( now_ms ( ) )
122
.bind ( format_for_url ( url) )
123
.fetch_one ( pool)
124
.await
9756c67d Andy McCall 1 month ago ↶
125
}
126
127
128
pub async fn update_hook (
129
pool : & Pool,
130
id : i64 ,
131
url : Option< & str > ,
132
secret : Option< & str > ,
133
events : & str ,
134
format : & str ,
135
active : bool ,
136
) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
137
sqlx:: query(
138
" UPDATE hooks SET url = COALESCE($1, url), secret = COALESCE($2, secret), events = $3, format = $4, active = $5 WHERE id = $6" ,
139
)
140
.bind ( url)
141
.bind ( secret)
142
.bind ( events)
143
.bind ( format)
144
.bind ( active)
145
.bind ( id)
146
.execute ( pool)
147
.await? ;
9756c67d Andy McCall 1 month ago ↶
148
Ok ( ( ) )
149
}
150
151
pub async fn set_hook_format ( pool : & Pool, id : i64 , format : & str ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
152
sqlx:: query( " UPDATE hooks SET format = $1 WHERE id = $2" ) .bind ( format) .bind ( id) .execute ( pool) .await? ;
9756c67d Andy McCall 1 month ago ↶
153
Ok ( ( ) )
154
}
155
156
157
pub async fn last_attempt ( pool : & Pool, hook_id : i64 ) -> sqlx:: Result< Option< Delivery> > {
5d90e0e0 Andy McCall 20 days ago
158
sqlx:: query_as( " SELECT * FROM deliveries WHERE hook_id = $1 AND attempts > 0 ORDER BY id DESC LIMIT 1" )
9756c67d Andy McCall 1 month ago ↶
159
.bind ( hook_id)
160
.fetch_optional ( pool)
b64e2537 Andy McCall 1 month ago ↶
161
.await
162
}
163
164
pub async fn set_hook_active ( pool : & Pool, id : i64 , active : bool ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
165
sqlx:: query( " UPDATE hooks SET active = $1 WHERE id = $2" ) .bind ( active) .bind ( id) .execute ( pool) .await? ;
b64e2537 Andy McCall 1 month ago ↶
166
Ok ( ( ) )
167
}
168
169
pub async fn delete_hook ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
170
sqlx:: query( " DELETE FROM hooks WHERE id = $1" ) .bind ( id) .execute ( pool) .await? ;
b64e2537 Andy McCall 1 month ago ↶
171
Ok ( ( ) )
172
}
173
c9893553 Andy McCall 20 days ago ↶
174
175
176
pub const MAX_WAITING : i64 = 1000 ;
177
178
179
180
pub async fn queue_delivery ( pool : & Pool, hook_id : i64 , delivery : & str , event : & str , payload : & str ) -> sqlx:: Result< Option< i64 > > {
5d90e0e0 Andy McCall 20 days ago
181
let waiting: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM deliveries WHERE hook_id = $1 AND delivered_ms IS NULL AND next_ms < $2" )
c9893553 Andy McCall 20 days ago ↶
182
.bind ( hook_id)
183
.bind ( i64 :: MAX / 4 )
184
.fetch_one ( pool)
185
.await? ;
186
if waiting > = MAX_WAITING {
187
return Ok ( None ) ;
188
}
b64e2537 Andy McCall 1 month ago ↶
189
let now = now_ms ( ) ;
190
sqlx:: query_scalar(
5d90e0e0 Andy McCall 20 days ago
191
" INSERT INTO deliveries (hook_id, delivery, event, payload, created_ms, next_ms) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id" ,
b64e2537 Andy McCall 1 month ago ↶
192
)
193
.bind ( hook_id)
194
.bind ( delivery)
195
.bind ( event)
196
.bind ( payload)
197
.bind ( now)
198
.bind ( now)
199
.fetch_one ( pool)
200
.await
c9893553 Andy McCall 20 days ago ↶
201
.map ( Some )
b64e2537 Andy McCall 1 month ago ↶
202
}
203
204
pub async fn due_deliveries ( pool : & Pool, limit : i64 ) -> sqlx:: Result< Vec< Delivery> > {
5d90e0e0 Andy McCall 20 days ago
205
sqlx:: query_as( " SELECT * FROM deliveries WHERE delivered_ms IS NULL AND next_ms <= $1 ORDER BY next_ms LIMIT $2" )
b64e2537 Andy McCall 1 month ago ↶
206
.bind ( now_ms ( ) )
207
.bind ( limit)
208
.fetch_all ( pool)
209
.await
210
}
211
212
pub async fn deliveries_for_hook ( pool : & Pool, hook_id : i64 , limit : i64 ) -> sqlx:: Result< Vec< Delivery> > {
5d90e0e0 Andy McCall 20 days ago
213
sqlx:: query_as( " SELECT * FROM deliveries WHERE hook_id = $1 ORDER BY created_ms DESC, id DESC LIMIT $2" )
b64e2537 Andy McCall 1 month ago ↶
214
.bind ( hook_id)
215
.bind ( limit)
216
.fetch_all ( pool)
217
.await
218
}
219
220
pub async fn delivery ( pool : & Pool, id : i64 ) -> sqlx:: Result< Option< Delivery> > {
5d90e0e0 Andy McCall 20 days ago
221
sqlx:: query_as( " SELECT * FROM deliveries WHERE id = $1" ) .bind ( id) .fetch_optional ( pool) .await
b64e2537 Andy McCall 1 month ago ↶
222
}
223
224
225
pub async fn record_attempt ( pool : & Pool, id : i64 , attempts : i64 , status : Option< i64 > , response : & str ) -> sqlx:: Result< ( ) > {
226
let ok = status.is_some_and ( | s | ( 200 .. 300 ) .contains ( & s) ) ;
227
let now = now_ms ( ) ;
228
let delivered: Option< i64 > = if ok { Some ( now) } else { None } ;
229
230
let backoff_ms = [ 60_000 i64 , 300_000 , 1_800_000 , 7_200_000 , 21_600_000 ] ;
231
let next = if ok | | attempts as usize > = backoff_ms.len ( ) { i64 :: MAX / 4 } else { now + backoff_ms[ attempts as usize ] } ;
5d90e0e0 Andy McCall 20 days ago
232
sqlx:: query( " UPDATE deliveries SET attempts = $1, status = $2, response = $3, delivered_ms = $4, next_ms = $5 WHERE id = $6" )
b64e2537 Andy McCall 1 month ago ↶
233
.bind ( attempts + 1 )
234
.bind ( status)
235
.bind ( & response.chars ( ) .take ( 2000 ) .collect:: < String> ( ) )
236
.bind ( delivered)
237
.bind ( next)
238
.bind ( id)
239
.execute ( pool)
240
.await? ;
241
Ok ( ( ) )
242
}
243
244
245
pub async fn redeliver ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
246
sqlx:: query( " UPDATE deliveries SET delivered_ms = NULL, next_ms = $1 WHERE id = $2" ) .bind ( now_ms ( ) ) .bind ( id) .execute ( pool) .await? ;
b64e2537 Andy McCall 1 month ago ↶
247
Ok ( ( ) )
248
}
249
250
pub async fn prune_deliveries ( pool : & Pool, older_than_ms : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
251
sqlx:: query( " DELETE FROM deliveries WHERE created_ms < $1" ) .bind ( older_than_ms) .execute ( pool) .await? ;
b64e2537 Andy McCall 1 month ago ↶
252
Ok ( ( ) )
253
}
254
255
256
257
# [ derive ( Clone, Debug, FromRow ) ]
258
pub struct Check {
259
pub id : i64 ,
260
pub repo_id : i64 ,
261
pub state : String,
262
pub name : String,
263
pub status : String,
264
pub summary : String,
265
pub url : String,
266
pub reporter_id : Option< i64 > ,
267
pub reporter : String,
268
pub seal : String,
269
pub started_ms : i64 ,
270
pub finished_ms : Option< i64 > ,
271
}
272
273
impl Check {
274
pub fn done ( & self ) -> bool {
275
matches! ( self .status.as_str ( ) , " passed" | " failed" | " cancelled" | " skipped" )
276
}
277
}
278
279
pub async fn checks_for_state ( pool : & Pool, repo_id : i64 , state : & str ) -> sqlx:: Result< Vec< Check> > {
5d90e0e0 Andy McCall 20 days ago
280
sqlx:: query_as( " SELECT * FROM checks WHERE repo_id = $1 AND state = $2 ORDER BY name" ) .bind ( repo_id) .bind ( state) .fetch_all ( pool) .await
b64e2537 Andy McCall 1 month ago ↶
281
}
282
283
284
pub async fn upsert_check (
285
pool : & Pool,
286
repo_id : i64 ,
287
state : & str ,
288
name : & str ,
289
status : & str ,
290
summary : & str ,
291
url : & str ,
292
reporter : Option< ( i64 , & str ) > ,
293
seal : & str ,
294
) -> sqlx:: Result< Check> {
295
let now = now_ms ( ) ;
296
let finished: Option< i64 > = if matches! ( status, " passed" | " failed" | " cancelled" | " skipped" ) { Some ( now) } else { None } ;
297
sqlx:: query_as(
5d90e0e0 Andy McCall 20 days ago
298
" INSERT INTO checks (repo_id, state, name, status, summary, url, reporter_id, reporter, seal, started_ms, finished_ms) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) \
b64e2537 Andy McCall 1 month ago ↶
299
ON CONFLICT (repo_id, state, name) DO UPDATE SET status = excluded.status, summary = excluded.summary, url = excluded.url, reporter_id = excluded.reporter_id, reporter = excluded.reporter, seal = CASE WHEN excluded.seal = '' THEN checks.seal ELSE excluded.seal END, finished_ms = excluded.finished_ms RETURNING *" ,
300
)
301
.bind ( repo_id)
302
.bind ( state)
303
.bind ( name)
304
.bind ( status)
305
.bind ( summary)
306
.bind ( url)
307
.bind ( reporter.map ( | r | r.0 ) )
308
.bind ( reporter.map ( | r | r.1 ) .unwrap_or ( " " ) )
309
.bind ( seal)
310
.bind ( now)
311
.bind ( finished)
312
.fetch_one ( pool)
313
.await
314
}
315
316
pub async fn required_checks ( pool : & Pool, repo_id : i64 ) -> sqlx:: Result< Vec< String> > {
5d90e0e0 Andy McCall 20 days ago
317
let s: String = sqlx:: query_scalar( " SELECT required_checks FROM repositories WHERE id = $1" ) .bind ( repo_id) .fetch_one ( pool) .await? ;
b64e2537 Andy McCall 1 month ago ↶
318
Ok ( s.split ( ' ,' ) .map ( str :: trim) .filter ( | x | ! x.is_empty ( ) ) .map ( String :: from) .collect ( ) )
319
}
320
321
pub async fn set_required_checks ( pool : & Pool, repo_id : i64 , names : & [String]) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
322
sqlx:: query( " UPDATE repositories SET required_checks = $1 WHERE id = $2" ) .bind ( names.join ( " ," ) ) .bind ( repo_id) .execute ( pool) .await? ;
b64e2537 Andy McCall 1 month ago ↶
323
Ok ( ( ) )
324
}