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> > {
84
sqlx:: query_as( " SELECT * FROM hooks WHERE repo_id = ? ORDER BY id" ) .bind ( repo_id) .fetch_all ( pool) .await
85
}
86
87
pub async fn hooks_for_org ( pool : & Pool, org_id : i64 ) -> sqlx:: Result< Vec< Hook> > {
88
sqlx:: query_as( " SELECT * FROM hooks WHERE org_id = ? ORDER BY id" ) .bind ( org_id) .fetch_all ( pool) .await
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> > {
102
sqlx:: query_as( " SELECT * FROM hooks WHERE id = ?" ) .bind ( id) .fetch_optional ( pool) .await
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> {
9756c67d Andy McCall 1 month ago ↶
113
sqlx:: query_as( " INSERT INTO hooks (repo_id, org_id, url, secret, events, created_ms, format) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *" )
b64e2537 Andy McCall 1 month ago ↶
114
.bind ( repo_id)
115
.bind ( org_id)
116
.bind ( url)
117
.bind ( secret)
118
.bind ( events)
119
.bind ( now_ms ( ) )
9756c67d Andy McCall 1 month ago ↶
120
.bind ( format_for_url ( url) )
b64e2537 Andy McCall 1 month ago ↶
121
.fetch_one ( pool)
9756c67d Andy McCall 1 month ago ↶
122
.await
123
}
124
125
126
pub async fn update_hook (
127
pool : & Pool,
128
id : i64 ,
129
url : Option< & str > ,
130
secret : Option< & str > ,
131
events : & str ,
132
format : & str ,
133
active : bool ,
134
) -> sqlx:: Result< ( ) > {
135
sqlx:: query( " UPDATE hooks SET url = COALESCE(?, url), secret = COALESCE(?, secret), events = ?, format = ?, active = ? WHERE id = ?" )
136
.bind ( url)
137
.bind ( secret)
138
.bind ( events)
139
.bind ( format)
140
.bind ( active)
141
.bind ( id)
142
.execute ( pool)
143
.await? ;
144
Ok ( ( ) )
145
}
146
147
pub async fn set_hook_format ( pool : & Pool, id : i64 , format : & str ) -> sqlx:: Result< ( ) > {
148
sqlx:: query( " UPDATE hooks SET format = ? WHERE id = ?" ) .bind ( format) .bind ( id) .execute ( pool) .await? ;
149
Ok ( ( ) )
150
}
151
152
153
pub async fn last_attempt ( pool : & Pool, hook_id : i64 ) -> sqlx:: Result< Option< Delivery> > {
154
sqlx:: query_as( " SELECT * FROM deliveries WHERE hook_id = ? AND attempts > 0 ORDER BY id DESC LIMIT 1" )
155
.bind ( hook_id)
156
.fetch_optional ( pool)
b64e2537 Andy McCall 1 month ago ↶
157
.await
158
}
159
160
pub async fn set_hook_active ( pool : & Pool, id : i64 , active : bool ) -> sqlx:: Result< ( ) > {
161
sqlx:: query( " UPDATE hooks SET active = ? WHERE id = ?" ) .bind ( active) .bind ( id) .execute ( pool) .await? ;
162
Ok ( ( ) )
163
}
164
165
pub async fn delete_hook ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
166
sqlx:: query( " DELETE FROM hooks WHERE id = ?" ) .bind ( id) .execute ( pool) .await? ;
167
Ok ( ( ) )
168
}
169
c9893553 Andy McCall 22 days ago ↶
170
171
172
pub const MAX_WAITING : i64 = 1000 ;
173
174
175
176
pub async fn queue_delivery ( pool : & Pool, hook_id : i64 , delivery : & str , event : & str , payload : & str ) -> sqlx:: Result< Option< i64 > > {
177
let waiting: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM deliveries WHERE hook_id = ? AND delivered_ms IS NULL AND next_ms < ?" )
178
.bind ( hook_id)
179
.bind ( i64 :: MAX / 4 )
180
.fetch_one ( pool)
181
.await? ;
182
if waiting > = MAX_WAITING {
183
return Ok ( None ) ;
184
}
b64e2537 Andy McCall 1 month ago ↶
185
let now = now_ms ( ) ;
186
sqlx:: query_scalar(
187
" INSERT INTO deliveries (hook_id, delivery, event, payload, created_ms, next_ms) VALUES (?, ?, ?, ?, ?, ?) RETURNING id" ,
188
)
189
.bind ( hook_id)
190
.bind ( delivery)
191
.bind ( event)
192
.bind ( payload)
193
.bind ( now)
194
.bind ( now)
195
.fetch_one ( pool)
196
.await
c9893553 Andy McCall 22 days ago ↶
197
.map ( Some )
b64e2537 Andy McCall 1 month ago ↶
198
}
199
200
pub async fn due_deliveries ( pool : & Pool, limit : i64 ) -> sqlx:: Result< Vec< Delivery> > {
201
sqlx:: query_as( " SELECT * FROM deliveries WHERE delivered_ms IS NULL AND next_ms <= ? ORDER BY next_ms LIMIT ?" )
202
.bind ( now_ms ( ) )
203
.bind ( limit)
204
.fetch_all ( pool)
205
.await
206
}
207
208
pub async fn deliveries_for_hook ( pool : & Pool, hook_id : i64 , limit : i64 ) -> sqlx:: Result< Vec< Delivery> > {
209
sqlx:: query_as( " SELECT * FROM deliveries WHERE hook_id = ? ORDER BY created_ms DESC, id DESC LIMIT ?" )
210
.bind ( hook_id)
211
.bind ( limit)
212
.fetch_all ( pool)
213
.await
214
}
215
216
pub async fn delivery ( pool : & Pool, id : i64 ) -> sqlx:: Result< Option< Delivery> > {
217
sqlx:: query_as( " SELECT * FROM deliveries WHERE id = ?" ) .bind ( id) .fetch_optional ( pool) .await
218
}
219
220
221
pub async fn record_attempt ( pool : & Pool, id : i64 , attempts : i64 , status : Option< i64 > , response : & str ) -> sqlx:: Result< ( ) > {
222
let ok = status.is_some_and ( | s | ( 200 .. 300 ) .contains ( & s) ) ;
223
let now = now_ms ( ) ;
224
let delivered: Option< i64 > = if ok { Some ( now) } else { None } ;
225
226
let backoff_ms = [ 60_000 i64 , 300_000 , 1_800_000 , 7_200_000 , 21_600_000 ] ;
227
let next = if ok | | attempts as usize > = backoff_ms.len ( ) { i64 :: MAX / 4 } else { now + backoff_ms[ attempts as usize ] } ;
228
sqlx:: query( " UPDATE deliveries SET attempts = ?, status = ?, response = ?, delivered_ms = ?, next_ms = ? WHERE id = ?" )
229
.bind ( attempts + 1 )
230
.bind ( status)
231
.bind ( & response.chars ( ) .take ( 2000 ) .collect:: < String> ( ) )
232
.bind ( delivered)
233
.bind ( next)
234
.bind ( id)
235
.execute ( pool)
236
.await? ;
237
Ok ( ( ) )
238
}
239
240
241
pub async fn redeliver ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
242
sqlx:: query( " UPDATE deliveries SET delivered_ms = NULL, next_ms = ? WHERE id = ?" ) .bind ( now_ms ( ) ) .bind ( id) .execute ( pool) .await? ;
243
Ok ( ( ) )
244
}
245
246
pub async fn prune_deliveries ( pool : & Pool, older_than_ms : i64 ) -> sqlx:: Result< ( ) > {
247
sqlx:: query( " DELETE FROM deliveries WHERE created_ms < ?" ) .bind ( older_than_ms) .execute ( pool) .await? ;
248
Ok ( ( ) )
249
}
250
251
252
253
# [ derive ( Clone, Debug, FromRow ) ]
254
pub struct Check {
255
pub id : i64 ,
256
pub repo_id : i64 ,
257
pub state : String,
258
pub name : String,
259
pub status : String,
260
pub summary : String,
261
pub url : String,
262
pub reporter_id : Option< i64 > ,
263
pub reporter : String,
264
pub seal : String,
265
pub started_ms : i64 ,
266
pub finished_ms : Option< i64 > ,
267
}
268
269
impl Check {
270
pub fn done ( & self ) -> bool {
271
matches! ( self .status.as_str ( ) , " passed" | " failed" | " cancelled" | " skipped" )
272
}
273
}
274
275
pub async fn checks_for_state ( pool : & Pool, repo_id : i64 , state : & str ) -> sqlx:: Result< Vec< Check> > {
276
sqlx:: query_as( " SELECT * FROM checks WHERE repo_id = ? AND state = ? ORDER BY name" ) .bind ( repo_id) .bind ( state) .fetch_all ( pool) .await
277
}
278
279
280
pub async fn upsert_check (
281
pool : & Pool,
282
repo_id : i64 ,
283
state : & str ,
284
name : & str ,
285
status : & str ,
286
summary : & str ,
287
url : & str ,
288
reporter : Option< ( i64 , & str ) > ,
289
seal : & str ,
290
) -> sqlx:: Result< Check> {
291
let now = now_ms ( ) ;
292
let finished: Option< i64 > = if matches! ( status, " passed" | " failed" | " cancelled" | " skipped" ) { Some ( now) } else { None } ;
293
sqlx:: query_as(
294
" INSERT INTO checks (repo_id, state, name, status, summary, url, reporter_id, reporter, seal, started_ms, finished_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) \
295
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 *" ,
296
)
297
.bind ( repo_id)
298
.bind ( state)
299
.bind ( name)
300
.bind ( status)
301
.bind ( summary)
302
.bind ( url)
303
.bind ( reporter.map ( | r | r.0 ) )
304
.bind ( reporter.map ( | r | r.1 ) .unwrap_or ( " " ) )
305
.bind ( seal)
306
.bind ( now)
307
.bind ( finished)
308
.fetch_one ( pool)
309
.await
310
}
311
312
pub async fn required_checks ( pool : & Pool, repo_id : i64 ) -> sqlx:: Result< Vec< String> > {
313
let s: String = sqlx:: query_scalar( " SELECT required_checks FROM repositories WHERE id = ?" ) .bind ( repo_id) .fetch_one ( pool) .await? ;
314
Ok ( s.split ( ' ,' ) .map ( str :: trim) .filter ( | x | ! x.is_empty ( ) ) .map ( String :: from) .collect ( ) )
315
}
316
317
pub async fn set_required_checks ( pool : & Pool, repo_id : i64 , names : & [String]) -> sqlx:: Result< ( ) > {
318
sqlx:: query( " UPDATE repositories SET required_checks = ? WHERE id = ?" ) .bind ( names.join ( " ," ) ) .bind ( repo_id) .execute ( pool) .await? ;
319
Ok ( ( ) )
320
}