18e381ea Andy McCall 1 month ago ↶
1
2
3
4
use super :: { now_ms, Pool} ;
5
use sqlx:: FromRow;
6
7
# [ derive ( Clone, Debug, FromRow ) ]
8
pub struct Notification {
9
pub id : i64 ,
10
pub account_id : i64 ,
11
pub at_ms : i64 ,
12
pub kind : String,
13
pub title : String,
14
pub url : String,
15
pub actor : String,
16
pub read : bool ,
eb0c44a9 Andy McCall 1 month ago ↶
17
pub repo_id : Option< i64 > ,
18
pub thread : String,
19
pub reason : String,
20
pub done : bool ,
21
}
22
23
24
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
25
pub enum Reason {
26
Participating,
27
Watching,
28
}
29
30
impl Reason {
31
pub fn as_str ( self ) -> & 'static str {
32
match self {
33
Reason:: Participating => " participating" ,
34
Reason:: Watching => " watching" ,
35
}
36
}
18e381ea Andy McCall 1 month ago ↶
37
}
38
eb0c44a9 Andy McCall 1 month ago ↶
39
40
pub fn thread_of ( url : & str ) -> & str {
41
url.split ( ' #' ) .next ( ) .unwrap_or ( url)
42
}
43
44
45
pub struct New < 'a > {
46
pub account_id : i64 ,
47
pub repo_id : Option< i64 > ,
48
pub reason : Reason,
49
pub kind : & 'a str ,
50
pub title : & 'a str ,
51
pub url : & 'a str ,
52
pub actor : & 'a str ,
53
}
54
55
pub async fn add ( pool : & Pool, n : New< '_ > ) -> sqlx:: Result< ( ) > {
56
let New { account_id, repo_id, reason, kind, title, url, actor } = n;
57
ea14c4ec Andy McCall 19 days ago ↶
58
sqlx:: query( " UPDATE notifications SET done = FALSE WHERE account_id = $1 AND thread = $2 AND done = TRUE" )
eb0c44a9 Andy McCall 1 month ago ↶
59
.bind ( account_id)
60
.bind ( thread_of ( url) )
61
.execute ( pool)
62
.await? ;
5d90e0e0 Andy McCall 19 days ago
63
sqlx:: query( " INSERT INTO notifications (account_id, at_ms, kind, title, url, actor, repo_id, thread, reason) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)" )
18e381ea Andy McCall 1 month ago ↶
64
.bind ( account_id)
65
.bind ( now_ms ( ) )
66
.bind ( kind)
67
.bind ( title)
68
.bind ( url)
69
.bind ( actor)
eb0c44a9 Andy McCall 1 month ago ↶
70
.bind ( repo_id)
71
.bind ( thread_of ( url) )
72
.bind ( reason.as_str ( ) )
18e381ea Andy McCall 1 month ago ↶
73
.execute ( pool)
74
.await? ;
75
Ok ( ( ) )
76
}
77
eb0c44a9 Andy McCall 1 month ago ↶
78
79
# [ derive ( Clone, Debug, FromRow ) ]
80
pub struct Thread {
81
82
pub id : i64 ,
83
pub at_ms : i64 ,
84
pub kind : String,
85
pub title : String,
86
pub url : String,
87
pub actor : String,
88
pub thread : String,
89
pub repo_id : Option< i64 > ,
90
pub repo : Option< String> ,
91
pub count : i64 ,
92
pub unread : i64 ,
93
pub participating : bool ,
94
pub done : bool ,
95
}
96
97
98
# [ derive ( Clone, Debug, Default ) ]
99
pub struct Filter {
100
pub done : bool ,
101
pub unread_only : bool ,
102
103
pub reason : Option< String> ,
104
105
pub kind : Option< String> ,
106
pub repo_id : Option< i64 > ,
107
}
108
109
pub async fn threads ( pool : & Pool, account_id : i64 , f : & Filter, limit : i64 ) -> sqlx:: Result< Vec< Thread> > {
97b18869 Andy McCall 19 days ago ↶
110
111
112
eb0c44a9 Andy McCall 1 month ago ↶
113
let mut sql = String :: from(
97b18869 Andy McCall 19 days ago ↶
114
" SELECT id, at_ms, kind, title, url, actor, thread, repo_id, \
115
(SELECT r.owner || '/' || r.name FROM repositories r WHERE r.id = t.repo_id) AS repo, \
116
count, unread, participating > 0 AS participating, done FROM (\
117
SELECT n.id, n.at_ms, n.kind, n.title, n.url, n.actor, n.thread, n.repo_id, n.done, \
118
ROW_NUMBER() OVER (PARTITION BY n.thread ORDER BY n.at_ms DESC, n.id DESC) AS newest, \
119
COUNT(*) OVER (PARTITION BY n.thread) AS count, \
120
SUM(CASE WHEN n.read THEN 0 ELSE 1 END) OVER (PARTITION BY n.thread) AS unread, \
121
SUM(CASE WHEN n.reason = 'participating' THEN 1 ELSE 0 END) OVER (PARTITION BY n.thread) AS participating \
5d90e0e0 Andy McCall 19 days ago
122
FROM notifications n WHERE n.account_id = $1 AND n.done = $2" ,
eb0c44a9 Andy McCall 1 month ago ↶
123
) ;
5d90e0e0 Andy McCall 19 days ago
124
125
let mut next = 3 ;
eb0c44a9 Andy McCall 1 month ago ↶
126
if f.kind.is_some ( ) {
5d90e0e0 Andy McCall 19 days ago
127
sql.push_str ( & format! ( " AND (n.kind = ${} OR n.kind LIKE ${} || '.%')" , next, next + 1 ) ) ;
128
next + = 2 ;
eb0c44a9 Andy McCall 1 month ago ↶
129
}
130
if f.repo_id.is_some ( ) {
5d90e0e0 Andy McCall 19 days ago
131
sql.push_str ( & format! ( " AND n.repo_id = ${next} " ) ) ;
132
next + = 1 ;
eb0c44a9 Andy McCall 1 month ago ↶
133
}
97b18869 Andy McCall 19 days ago ↶
134
sql.push_str ( " ) t WHERE newest = 1" ) ;
eb0c44a9 Andy McCall 1 month ago ↶
135
if f.unread_only {
97b18869 Andy McCall 19 days ago ↶
136
sql.push_str ( " AND unread > 0" ) ;
eb0c44a9 Andy McCall 1 month ago ↶
137
}
138
match f.reason.as_deref ( ) {
97b18869 Andy McCall 19 days ago ↶
139
Some ( " participating" ) => sql.push_str ( " AND participating > 0" ) ,
140
Some ( " watching" ) => sql.push_str ( " AND participating = 0" ) ,
eb0c44a9 Andy McCall 1 month ago ↶
141
_ => { }
142
}
5d90e0e0 Andy McCall 19 days ago
143
sql.push_str ( & format! ( " ORDER BY at_ms DESC LIMIT ${next} " ) ) ;
eb0c44a9 Andy McCall 1 month ago ↶
144
let mut q = sqlx:: query_as:: < _ , Thread> ( & sql) .bind ( account_id) .bind ( f.done) ;
145
if let Some ( k) = & f.kind {
146
q = q.bind ( k) .bind ( k) ;
147
}
148
if let Some ( r) = f.repo_id {
149
q = q.bind ( r) ;
150
}
151
q.bind ( limit) .fetch_all ( pool) .await
152
}
153
154
155
pub async fn set_thread ( pool : & Pool, account_id : i64 , thread : & str , read : Option< bool > , done : Option< bool > ) -> sqlx:: Result< ( ) > {
156
if let Some ( r) = read {
157
if r {
ea14c4ec Andy McCall 19 days ago ↶
158
sqlx:: query( " UPDATE notifications SET read = TRUE WHERE account_id = $1 AND thread = $2" )
eb0c44a9 Andy McCall 1 month ago ↶
159
.bind ( account_id)
160
.bind ( thread)
161
.execute ( pool)
162
.await? ;
163
} else {
164
ea14c4ec Andy McCall 19 days ago ↶
165
sqlx:: query( " UPDATE notifications SET read = FALSE WHERE id = (SELECT id FROM notifications WHERE account_id = $1 AND thread = $2 ORDER BY at_ms DESC, id DESC LIMIT 1)" )
eb0c44a9 Andy McCall 1 month ago ↶
166
.bind ( account_id)
167
.bind ( thread)
168
.execute ( pool)
169
.await? ;
170
}
171
}
172
if let Some ( d) = done {
ea14c4ec Andy McCall 19 days ago ↶
173
let read_too = if d { " , read = TRUE" } else { " " } ;
5d90e0e0 Andy McCall 19 days ago
174
sqlx:: query( & format! ( " UPDATE notifications SET done = $1{read_too} WHERE account_id = $2 AND thread = $3" ) )
eb0c44a9 Andy McCall 1 month ago ↶
175
.bind ( d)
176
.bind ( account_id)
177
.bind ( thread)
178
.execute ( pool)
179
.await? ;
180
}
181
Ok ( ( ) )
182
}
183
184
185
pub async fn get ( pool : & Pool, account_id : i64 , id : i64 ) -> sqlx:: Result< Option< Notification> > {
5d90e0e0 Andy McCall 19 days ago
186
sqlx:: query_as( " SELECT * FROM notifications WHERE account_id = $1 AND id = $2" ) .bind ( account_id) .bind ( id) .fetch_optional ( pool) .await
eb0c44a9 Andy McCall 1 month ago ↶
187
}
188
18e381ea Andy McCall 1 month ago ↶
189
pub async fn inbox ( pool : & Pool, account_id : i64 , limit : i64 ) -> sqlx:: Result< Vec< Notification> > {
5d90e0e0 Andy McCall 19 days ago
190
sqlx:: query_as( " SELECT * FROM notifications WHERE account_id = $1 ORDER BY read, at_ms DESC, id DESC LIMIT $2" )
18e381ea Andy McCall 1 month ago ↶
191
.bind ( account_id)
192
.bind ( limit)
193
.fetch_all ( pool)
194
.await
195
}
196
197
pub async fn unread ( pool : & Pool, account_id : i64 ) -> sqlx:: Result< i64 > {
ea14c4ec Andy McCall 19 days ago ↶
198
sqlx:: query_scalar( " SELECT COUNT(*) FROM notifications WHERE account_id = $1 AND read = FALSE AND done = FALSE" )
eb0c44a9 Andy McCall 1 month ago ↶
199
.bind ( account_id)
200
.fetch_one ( pool)
201
.await
18e381ea Andy McCall 1 month ago ↶
202
}
203
204
pub async fn mark_read ( pool : & Pool, account_id : i64 , id : Option< i64 > ) -> sqlx:: Result< ( ) > {
205
match id {
206
Some ( id) => {
ea14c4ec Andy McCall 19 days ago ↶
207
sqlx:: query( " UPDATE notifications SET read = TRUE WHERE account_id = $1 AND id = $2" )
5d90e0e0 Andy McCall 19 days ago
208
.bind ( account_id)
209
.bind ( id)
210
.execute ( pool)
211
.await?
18e381ea Andy McCall 1 month ago ↶
212
}
ea14c4ec Andy McCall 19 days ago ↶
213
None => sqlx:: query( " UPDATE notifications SET read = TRUE WHERE account_id = $1" ) .bind ( account_id) .execute ( pool) .await? ,
18e381ea Andy McCall 1 month ago ↶
214
} ;
215
Ok ( ( ) )
216
}
217
218
219
bff9106e Andy McCall 21 days ago ↶
220
221
222
223
pub async fn owners_of ( pool : & Pool, repo_id : i64 ) -> sqlx:: Result< Vec< i64 > > {
224
let owner: Option< String> =
5d90e0e0 Andy McCall 19 days ago
225
sqlx:: query_scalar( " SELECT owner FROM repositories WHERE id = $1" ) .bind ( repo_id) .fetch_optional ( pool) .await? ;
bff9106e Andy McCall 21 days ago ↶
226
let Some ( owner) = owner else { return Ok ( Vec :: new( ) ) } ;
227
if let Some ( a) = super :: accounts:: account_by_name( pool, & owner) .await? {
228
return Ok ( vec! [ a.id] ) ;
229
}
230
match super :: accounts:: org_by_name( pool, & owner) .await? {
231
Some ( o) => {
232
Ok ( super :: accounts:: org_members( pool, o.id) .await? .into_iter ( ) .filter ( | ( _, r ) | r = = " owner" ) .map ( | ( a , _) | a.id ) .collect ( ) )
233
}
234
None => Ok ( Vec :: new( ) ) ,
235
}
236
}
237
238
239
18e381ea Andy McCall 1 month ago ↶
240
pub async fn watching ( pool : & Pool, account_id : i64 , repo_id : i64 ) -> sqlx:: Result< bool > {
5d90e0e0 Andy McCall 19 days ago
241
let muted: Option< bool > = sqlx:: query_scalar( " SELECT muted FROM watches WHERE account_id = $1 AND repo_id = $2" )
18e381ea Andy McCall 1 month ago ↶
242
.bind ( account_id)
243
.bind ( repo_id)
bff9106e Andy McCall 21 days ago ↶
244
.fetch_optional ( pool)
18e381ea Andy McCall 1 month ago ↶
245
.await? ;
bff9106e Andy McCall 21 days ago ↶
246
match muted {
247
Some ( m) => Ok ( ! m) ,
248
None => Ok ( owners_of ( pool, repo_id) .await? .contains ( & account_id) ) ,
249
}
18e381ea Andy McCall 1 month ago ↶
250
}
251
bff9106e Andy McCall 21 days ago ↶
252
18e381ea Andy McCall 1 month ago ↶
253
pub async fn set_watch ( pool : & Pool, account_id : i64 , repo_id : i64 , on : bool ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 19 days ago
254
sqlx:: query( " INSERT INTO watches (account_id, repo_id, muted) VALUES ($1, $2, $3) ON CONFLICT (account_id, repo_id) DO UPDATE SET muted = excluded.muted" )
bff9106e Andy McCall 21 days ago ↶
255
.bind ( account_id)
256
.bind ( repo_id)
257
.bind ( ! on)
258
.execute ( pool)
259
.await? ;
18e381ea Andy McCall 1 month ago ↶
260
Ok ( ( ) )
261
}
262
bff9106e Andy McCall 21 days ago ↶
263
264
18e381ea Andy McCall 1 month ago ↶
265
pub async fn watchers ( pool : & Pool, repo_id : i64 ) -> sqlx:: Result< Vec< i64 > > {
bff9106e Andy McCall 21 days ago ↶
266
let rows: Vec< ( i64 , bool ) > =
5d90e0e0 Andy McCall 19 days ago
267
sqlx:: query_as( " SELECT account_id, muted FROM watches WHERE repo_id = $1" ) .bind ( repo_id) .fetch_all ( pool) .await? ;
bff9106e Andy McCall 21 days ago ↶
268
let mut out: Vec< i64 > = rows.iter ( ) .filter ( | ( _, m ) | ! m) .map ( | ( a , _) | * a) .collect ( ) ;
269
for o in owners_of ( pool, repo_id) .await? {
270
if ! rows.iter ( ) .any ( | ( a , _) | * a = = o) & & ! out.contains ( & o) {
271
out.push ( o) ;
272
}
273
}
274
Ok ( out)
18e381ea Andy McCall 1 month ago ↶
275
}
276
277
pub async fn subscribe ( pool : & Pool, account_id : i64 , kind : & str , target_id : i64 ) -> sqlx:: Result< ( ) > {
d1bf2d69 Andy McCall 19 days ago ↶
278
sqlx:: query( " INSERT INTO subscriptions (account_id, kind, target_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING" )
18e381ea Andy McCall 1 month ago ↶
279
.bind ( account_id)
280
.bind ( kind)
281
.bind ( target_id)
282
.execute ( pool)
283
.await? ;
284
Ok ( ( ) )
285
}
286
287
pub async fn unsubscribe ( pool : & Pool, account_id : i64 , kind : & str , target_id : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 19 days ago
288
sqlx:: query( " DELETE FROM subscriptions WHERE account_id = $1 AND kind = $2 AND target_id = $3" )
18e381ea Andy McCall 1 month ago ↶
289
.bind ( account_id)
290
.bind ( kind)
291
.bind ( target_id)
292
.execute ( pool)
293
.await? ;
294
Ok ( ( ) )
295
}
296
297
pub async fn subscribed ( pool : & Pool, account_id : i64 , kind : & str , target_id : i64 ) -> sqlx:: Result< bool > {
5d90e0e0 Andy McCall 19 days ago
298
let n: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM subscriptions WHERE account_id = $1 AND kind = $2 AND target_id = $3" )
18e381ea Andy McCall 1 month ago ↶
299
.bind ( account_id)
300
.bind ( kind)
301
.bind ( target_id)
302
.fetch_one ( pool)
303
.await? ;
304
Ok ( n > 0 )
305
}
306
307
pub async fn subscribers ( pool : & Pool, kind : & str , target_id : i64 ) -> sqlx:: Result< Vec< i64 > > {
5d90e0e0 Andy McCall 19 days ago
308
sqlx:: query_scalar( " SELECT account_id FROM subscriptions WHERE kind = $1 AND target_id = $2" )
18e381ea Andy McCall 1 month ago ↶
309
.bind ( kind)
310
.bind ( target_id)
311
.fetch_all ( pool)
312
.await
313
}
314
315
316
317
# [ derive ( Clone, Debug, FromRow ) ]
318
pub struct Hit {
319
pub kind : String,
320
pub repo_id : i64 ,
321
pub target_id : i64 ,
322
pub number : i64 ,
323
pub title : String,
324
pub snippet : String,
325
}
326
327
328
pub async fn index ( pool : & Pool, kind : & str , repo_id : i64 , target_id : i64 , number : i64 , title : & str , body : & str ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 19 days ago
329
sqlx:: query( " DELETE FROM search WHERE kind = $1 AND target_id = $2" ) .bind ( kind) .bind ( target_id) .execute ( pool) .await? ;
330
sqlx:: query( " INSERT INTO search (kind, repo_id, target_id, number, title, body) VALUES ($1, $2, $3, $4, $5, $6)" )
18e381ea Andy McCall 1 month ago ↶
331
.bind ( kind)
332
.bind ( repo_id)
333
.bind ( target_id)
334
.bind ( number)
335
.bind ( title)
336
.bind ( body)
337
.execute ( pool)
338
.await? ;
339
Ok ( ( ) )
340
}
341
342
343
3b1bc27f Andy McCall 19 days ago
344
# [ cfg ( not ( feature = " postgres" ) ) ]
18e381ea Andy McCall 1 month ago ↶
345
pub async fn search ( pool : & Pool, query : & str , repo_id : Option< i64 > , limit : i64 ) -> sqlx:: Result< Vec< Hit> > {
346
let terms: Vec< String> = query.split_whitespace ( ) .map ( | t | format! ( " \" {} \" " , t.replace ( ' "' , " " ) ) ) .collect ( ) ;
347
if terms.is_empty ( ) {
348
return Ok ( vec! [ ] ) ;
349
}
350
let q = terms.join ( " " ) ;
5d90e0e0 Andy McCall 19 days ago
351
let sql = " SELECT kind, repo_id, target_id, number, title, snippet(search, 5, '<mark>', '</mark>', '…', 12) AS snippet FROM search WHERE search MATCH $1 AND ($2 IS NULL OR repo_id = $3) ORDER BY rank LIMIT $4" ;
18e381ea Andy McCall 1 month ago ↶
352
sqlx:: query_as( sql) .bind ( q) .bind ( repo_id) .bind ( repo_id) .bind ( limit) .fetch_all ( pool) .await
353
}
3b1bc27f Andy McCall 19 days ago
354
355
356
357
358
# [ cfg ( feature = " postgres" ) ]
359
pub async fn search ( pool : & Pool, query : & str , repo_id : Option< i64 > , limit : i64 ) -> sqlx:: Result< Vec< Hit> > {
360
if query.split_whitespace ( ) .next ( ) .is_none ( ) {
361
return Ok ( vec! [ ] ) ;
362
}
363
let sql = " SELECT kind, repo_id, target_id, number, title, \
364
ts_headline('english', body, q, 'StartSel=\" <mark>\" , StopSel=\" </mark>\" , MaxWords=12, MinWords=4') AS snippet \
365
FROM search, plainto_tsquery('english', $1) q WHERE doc @@ q AND ($2 IS NULL OR repo_id = $3) \
366
ORDER BY ts_rank(doc, q) DESC LIMIT $4" ;
367
sqlx:: query_as( sql) .bind ( query) .bind ( repo_id) .bind ( repo_id) .bind ( limit) .fetch_all ( pool) .await
368
}