+296 -45 +/-3 browse
1 | diff --git a/core/src/db/subscriptions.rs b/core/src/db/subscriptions.rs |
2 | index e2770e0..1591dde 100644 |
3 | --- a/core/src/db/subscriptions.rs |
4 | +++ b/core/src/db/subscriptions.rs |
5 | @@ -134,7 +134,7 @@ impl Connection { |
6 | receive_confirmation) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *;", |
7 | ) |
8 | .unwrap(); |
9 | - let ret = stmt.query_row( |
10 | + let val = stmt.query_row( |
11 | rusqlite::params![ |
12 | &new_val.list, |
13 | &new_val.address, |
14 | @@ -169,14 +169,14 @@ impl Connection { |
15 | )) |
16 | }, |
17 | )?; |
18 | - |
19 | - trace!("add_subscription {:?}.", &ret); |
20 | - Ok(ret) |
21 | + trace!("add_subscription {:?}.", &val); |
22 | + // table entry might be modified by triggers, so don't rely on RETURNING value. |
23 | + self.list_subscription(list_pk, val.pk()) |
24 | } |
25 | |
26 | /// Create subscription candidate. |
27 | pub fn add_candidate_subscription( |
28 | - &mut self, |
29 | + &self, |
30 | list_pk: i64, |
31 | mut new_val: ListSubscription, |
32 | ) -> Result<DbVal<ListCandidateSubscription>> { |
33 | @@ -185,7 +185,7 @@ impl Connection { |
34 | "INSERT INTO candidate_subscription(list, address, name, accepted) VALUES(?, ?, ?, ?) \ |
35 | RETURNING *;", |
36 | )?; |
37 | - let ret = stmt.query_row( |
38 | + let val = stmt.query_row( |
39 | rusqlite::params![&new_val.list, &new_val.address, &new_val.name, None::<i64>,], |
40 | |row| { |
41 | let pk = row.get("pk")?; |
42 | @@ -203,47 +203,79 @@ impl Connection { |
43 | )?; |
44 | drop(stmt); |
45 | |
46 | - trace!("add_candidate_subscription {:?}.", &ret); |
47 | - Ok(ret) |
48 | + trace!("add_candidate_subscription {:?}.", &val); |
49 | + // table entry might be modified by triggers, so don't rely on RETURNING value. |
50 | + self.candidate_subscription(val.pk()) |
51 | + } |
52 | + |
53 | + /// Fetch subscription candidate by primary key. |
54 | + pub fn candidate_subscription(&self, pk: i64) -> Result<DbVal<ListCandidateSubscription>> { |
55 | + let mut stmt = self |
56 | + .connection |
57 | + .prepare("SELECT * FROM candidate_subscription WHERE pk = ?;")?; |
58 | + let val = stmt |
59 | + .query_row(rusqlite::params![&pk], |row| { |
60 | + let _pk: i64 = row.get("pk")?; |
61 | + debug_assert_eq!(pk, _pk); |
62 | + Ok(DbVal( |
63 | + ListCandidateSubscription { |
64 | + pk, |
65 | + list: row.get("list")?, |
66 | + address: row.get("address")?, |
67 | + name: row.get("name")?, |
68 | + accepted: row.get("accepted")?, |
69 | + }, |
70 | + pk, |
71 | + )) |
72 | + }) |
73 | + .map_err(|err| { |
74 | + if matches!(err, rusqlite::Error::QueryReturnedNoRows) { |
75 | + Error::from(err) |
76 | + .chain_err(|| NotFound("Candidate subscription with this pk not found!")) |
77 | + } else { |
78 | + err.into() |
79 | + } |
80 | + })?; |
81 | + |
82 | + Ok(val) |
83 | } |
84 | |
85 | /// Accept subscription candidate. |
86 | pub fn accept_candidate_subscription(&mut self, pk: i64) -> Result<DbVal<ListSubscription>> { |
87 | - let tx = self.connection.transaction()?; |
88 | - let mut stmt = tx.prepare( |
89 | + let val = self.connection.query_row( |
90 | "INSERT INTO subscription(list, address, name, enabled, digest, verified, \ |
91 | hide_address, receive_duplicates, receive_own_posts, receive_confirmation) SELECT \ |
92 | list, address, name, 1, 0, 0, 0, 1, 1, 0 FROM candidate_subscription WHERE pk = ? \ |
93 | RETURNING *;", |
94 | - )?; |
95 | - let ret = stmt.query_row(rusqlite::params![&pk], |row| { |
96 | - let pk = row.get("pk")?; |
97 | - Ok(DbVal( |
98 | - ListSubscription { |
99 | + rusqlite::params![&pk], |
100 | + |row| { |
101 | + let pk = row.get("pk")?; |
102 | + Ok(DbVal( |
103 | + ListSubscription { |
104 | + pk, |
105 | + list: row.get("list")?, |
106 | + address: row.get("address")?, |
107 | + account: row.get("account")?, |
108 | + name: row.get("name")?, |
109 | + digest: row.get("digest")?, |
110 | + enabled: row.get("enabled")?, |
111 | + verified: row.get("verified")?, |
112 | + hide_address: row.get("hide_address")?, |
113 | + receive_duplicates: row.get("receive_duplicates")?, |
114 | + receive_own_posts: row.get("receive_own_posts")?, |
115 | + receive_confirmation: row.get("receive_confirmation")?, |
116 | + }, |
117 | pk, |
118 | - list: row.get("list")?, |
119 | - address: row.get("address")?, |
120 | - account: row.get("account")?, |
121 | - name: row.get("name")?, |
122 | - digest: row.get("digest")?, |
123 | - enabled: row.get("enabled")?, |
124 | - verified: row.get("verified")?, |
125 | - hide_address: row.get("hide_address")?, |
126 | - receive_duplicates: row.get("receive_duplicates")?, |
127 | - receive_own_posts: row.get("receive_own_posts")?, |
128 | - receive_confirmation: row.get("receive_confirmation")?, |
129 | - }, |
130 | - pk, |
131 | - )) |
132 | - })?; |
133 | - drop(stmt); |
134 | - tx.execute( |
135 | - "UPDATE candidate_subscription SET accepted = ? WHERE pk = ?;", |
136 | - [&ret.pk, &pk], |
137 | + )) |
138 | + }, |
139 | )?; |
140 | - tx.commit()?; |
141 | |
142 | - trace!("accept_candidate_subscription {:?}.", &ret); |
143 | + trace!("accept_candidate_subscription {:?}.", &val); |
144 | + // table entry might be modified by triggers, so don't rely on RETURNING value. |
145 | + let ret = self.list_subscription(val.list, val.pk())?; |
146 | + |
147 | + // assert that [ref:accept_candidate] trigger works. |
148 | + debug_assert_eq!(Some(ret.pk), self.candidate_subscription(pk)?.accepted); |
149 | Ok(ret) |
150 | } |
151 | |
152 | @@ -553,3 +585,186 @@ impl Connection { |
153 | Ok(()) |
154 | } |
155 | } |
156 | + |
157 | + #[cfg(test)] |
158 | + mod tests { |
159 | + use super::*; |
160 | + |
161 | + #[test] |
162 | + fn test_subscription_ops() { |
163 | + use tempfile::TempDir; |
164 | + |
165 | + let tmp_dir = TempDir::new().unwrap(); |
166 | + let db_path = tmp_dir.path().join("mpot.db"); |
167 | + let config = Configuration { |
168 | + send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
169 | + db_path, |
170 | + data_path: tmp_dir.path().to_path_buf(), |
171 | + administrators: vec![], |
172 | + }; |
173 | + |
174 | + let mut db = Connection::open_or_create_db(config).unwrap().trusted(); |
175 | + let list = db |
176 | + .create_list(MailingList { |
177 | + pk: -1, |
178 | + name: "foobar chat".into(), |
179 | + id: "foo-chat".into(), |
180 | + address: "foo-chat@example.com".into(), |
181 | + description: None, |
182 | + archive_url: None, |
183 | + }) |
184 | + .unwrap(); |
185 | + let secondary_list = db |
186 | + .create_list(MailingList { |
187 | + pk: -1, |
188 | + name: "foobar chat2".into(), |
189 | + id: "foo-chat2".into(), |
190 | + address: "foo-chat2@example.com".into(), |
191 | + description: None, |
192 | + archive_url: None, |
193 | + }) |
194 | + .unwrap(); |
195 | + for i in 0..4 { |
196 | + let sub = db |
197 | + .add_subscription( |
198 | + list.pk(), |
199 | + ListSubscription { |
200 | + pk: -1, |
201 | + list: list.pk(), |
202 | + address: format!("{i}@example.com"), |
203 | + account: None, |
204 | + name: Some(format!("User{i}")), |
205 | + digest: false, |
206 | + hide_address: false, |
207 | + receive_duplicates: false, |
208 | + receive_own_posts: false, |
209 | + receive_confirmation: false, |
210 | + enabled: true, |
211 | + verified: false, |
212 | + }, |
213 | + ) |
214 | + .unwrap(); |
215 | + assert_eq!(db.list_subscription(list.pk(), sub.pk()).unwrap(), sub); |
216 | + assert_eq!( |
217 | + db.list_subscription_by_address(list.pk(), &sub.address) |
218 | + .unwrap(), |
219 | + sub |
220 | + ); |
221 | + } |
222 | + |
223 | + assert_eq!(db.accounts().unwrap(), vec![]); |
224 | + assert_eq!( |
225 | + db.remove_subscription(list.pk(), "nonexistent@example.com") |
226 | + .map_err(|err| err.to_string()) |
227 | + .unwrap_err(), |
228 | + NotFound("list or list owner not found!").to_string() |
229 | + ); |
230 | + |
231 | + let cand = db |
232 | + .add_candidate_subscription( |
233 | + list.pk(), |
234 | + ListSubscription { |
235 | + pk: -1, |
236 | + list: list.pk(), |
237 | + address: "4@example.com".into(), |
238 | + account: None, |
239 | + name: Some("User4".into()), |
240 | + digest: false, |
241 | + hide_address: false, |
242 | + receive_duplicates: false, |
243 | + receive_own_posts: false, |
244 | + receive_confirmation: false, |
245 | + enabled: true, |
246 | + verified: false, |
247 | + }, |
248 | + ) |
249 | + .unwrap(); |
250 | + let accepted = db.accept_candidate_subscription(cand.pk()).unwrap(); |
251 | + |
252 | + assert_eq!(db.account(5).unwrap(), None); |
253 | + assert_eq!( |
254 | + db.remove_account("4@example.com") |
255 | + .map_err(|err| err.to_string()) |
256 | + .unwrap_err(), |
257 | + NotFound("account not found!").to_string() |
258 | + ); |
259 | + |
260 | + let acc = db |
261 | + .add_account(Account { |
262 | + pk: -1, |
263 | + name: accepted.name.clone(), |
264 | + address: accepted.address.clone(), |
265 | + public_key: None, |
266 | + password: String::new(), |
267 | + enabled: true, |
268 | + }) |
269 | + .unwrap(); |
270 | + |
271 | + // Test [ref:add_account] SQL trigger (see schema.sql) |
272 | + assert_eq!( |
273 | + db.list_subscription(list.pk(), accepted.pk()) |
274 | + .unwrap() |
275 | + .account, |
276 | + Some(acc.pk()) |
277 | + ); |
278 | + // Test [ref:add_account_to_subscription] SQL trigger (see schema.sql) |
279 | + let sub = db |
280 | + .add_subscription( |
281 | + secondary_list.pk(), |
282 | + ListSubscription { |
283 | + pk: -1, |
284 | + list: secondary_list.pk(), |
285 | + address: "4@example.com".into(), |
286 | + account: None, |
287 | + name: Some("User4".into()), |
288 | + digest: false, |
289 | + hide_address: false, |
290 | + receive_duplicates: false, |
291 | + receive_own_posts: false, |
292 | + receive_confirmation: false, |
293 | + enabled: true, |
294 | + verified: true, |
295 | + }, |
296 | + ) |
297 | + .unwrap(); |
298 | + assert_eq!(sub.account, Some(acc.pk())); |
299 | + // Test [ref:verify_subscription_email] SQL trigger (see schema.sql) |
300 | + assert!(!sub.verified); |
301 | + |
302 | + assert_eq!(db.accounts().unwrap(), vec![acc.clone()]); |
303 | + |
304 | + assert_eq!( |
305 | + db.update_account(AccountChangeset { |
306 | + address: "nonexistent@example.com".into(), |
307 | + ..AccountChangeset::default() |
308 | + }) |
309 | + .map_err(|err| err.to_string()) |
310 | + .unwrap_err(), |
311 | + NotFound("account with this address not found!").to_string() |
312 | + ); |
313 | + assert_eq!( |
314 | + db.update_account(AccountChangeset { |
315 | + address: acc.address.clone(), |
316 | + ..AccountChangeset::default() |
317 | + }) |
318 | + .map_err(|err| err.to_string()), |
319 | + Ok(()) |
320 | + ); |
321 | + assert_eq!( |
322 | + db.update_account(AccountChangeset { |
323 | + address: acc.address.clone(), |
324 | + enabled: Some(Some(false)), |
325 | + ..AccountChangeset::default() |
326 | + }) |
327 | + .map_err(|err| err.to_string()), |
328 | + Ok(()) |
329 | + ); |
330 | + assert!(!db.account(acc.pk()).unwrap().unwrap().enabled); |
331 | + assert_eq!( |
332 | + db.remove_account("4@example.com") |
333 | + .map_err(|err| err.to_string()), |
334 | + Ok(()) |
335 | + ); |
336 | + assert_eq!(db.accounts().unwrap(), vec![]); |
337 | + } |
338 | + } |
339 | diff --git a/core/src/schema.sql b/core/src/schema.sql |
340 | index c5928c7..d556c54 100644 |
341 | --- a/core/src/schema.sql |
342 | +++ b/core/src/schema.sql |
343 | @@ -179,6 +179,7 @@ CREATE INDEX IF NOT EXISTS post_msgid_idx ON post(message_id); |
344 | CREATE INDEX IF NOT EXISTS list_idx ON list(id); |
345 | CREATE INDEX IF NOT EXISTS subscription_idx ON subscription(address); |
346 | |
347 | + -- [tag:accept_candidate]: Update candidacy with 'subscription' foreign key on 'subscription' insert. |
348 | CREATE TRIGGER IF NOT EXISTS accept_candidate AFTER INSERT ON subscription |
349 | FOR EACH ROW |
350 | BEGIN |
351 | @@ -186,13 +187,17 @@ BEGIN |
352 | WHERE candidate_subscription.list = NEW.list AND candidate_subscription.address = NEW.address; |
353 | END; |
354 | |
355 | - CREATE TRIGGER IF NOT EXISTS verify_candidate AFTER INSERT ON subscription |
356 | + -- [tag:verify_subscription_email]: If list settings require e-mail to be verified, |
357 | + -- update new subscription's 'verify' column value. |
358 | + CREATE TRIGGER IF NOT EXISTS verify_subscription_email AFTER INSERT ON subscription |
359 | FOR EACH ROW |
360 | BEGIN |
361 | UPDATE subscription SET verified = 0, last_modified = unixepoch() |
362 | WHERE subscription.pk = NEW.pk AND EXISTS (SELECT 1 FROM list WHERE pk = NEW.list AND verify = 1); |
363 | END; |
364 | |
365 | + -- [tag:add_account]: Update list subscription entries with 'account' foreign |
366 | + -- key, if addresses match. |
367 | CREATE TRIGGER IF NOT EXISTS add_account AFTER INSERT ON account |
368 | FOR EACH ROW |
369 | BEGIN |
370 | @@ -200,67 +205,86 @@ BEGIN |
371 | WHERE subscription.address = NEW.address; |
372 | END; |
373 | |
374 | + -- [tag:add_account_to_subscription]: When adding a new 'subscription', auto |
375 | + -- set 'account' value if there already exists an 'account' entry with the same |
376 | + -- address. |
377 | CREATE TRIGGER IF NOT EXISTS add_account_to_subscription AFTER INSERT ON subscription |
378 | FOR EACH ROW |
379 | + WHEN NEW.account IS NULL AND EXISTS (SELECT 1 FROM account WHERE address = NEW.address) |
380 | BEGIN |
381 | UPDATE subscription |
382 | - SET account = acc.pk, |
383 | + SET account = (SELECT pk FROM account WHERE address = NEW.address), |
384 | last_modified = unixepoch() |
385 | - FROM (SELECT * FROM account) AS acc |
386 | - WHERE subscription.account = acc.address; |
387 | + WHERE subscription.pk = NEW.pk; |
388 | END; |
389 | |
390 | + -- [tag:last_modified_list] update last_modified on every change. |
391 | CREATE TRIGGER IF NOT EXISTS last_modified_list AFTER UPDATE ON list |
392 | FOR EACH ROW |
393 | + WHEN NEW.last_modified != OLD.last_modified |
394 | BEGIN |
395 | UPDATE list SET last_modified = unixepoch() |
396 | WHERE pk = NEW.pk; |
397 | END; |
398 | |
399 | + -- [tag:last_modified_owner] update last_modified on every change. |
400 | CREATE TRIGGER IF NOT EXISTS last_modified_owner AFTER UPDATE ON owner |
401 | FOR EACH ROW |
402 | + WHEN NEW.last_modified != OLD.last_modified |
403 | BEGIN |
404 | UPDATE owner SET last_modified = unixepoch() |
405 | WHERE pk = NEW.pk; |
406 | END; |
407 | |
408 | + -- [tag:last_modified_post_policy] update last_modified on every change. |
409 | CREATE TRIGGER IF NOT EXISTS last_modified_post_policy AFTER UPDATE ON post_policy |
410 | FOR EACH ROW |
411 | + WHEN NEW.last_modified != OLD.last_modified |
412 | BEGIN |
413 | UPDATE post_policy SET last_modified = unixepoch() |
414 | WHERE pk = NEW.pk; |
415 | END; |
416 | |
417 | + -- [tag:last_modified_subscription_policy] update last_modified on every change. |
418 | CREATE TRIGGER IF NOT EXISTS last_modified_subscription_policy AFTER UPDATE ON subscription_policy |
419 | FOR EACH ROW |
420 | + WHEN NEW.last_modified != OLD.last_modified |
421 | BEGIN |
422 | UPDATE subscription_policy SET last_modified = unixepoch() |
423 | WHERE pk = NEW.pk; |
424 | END; |
425 | |
426 | + -- [tag:last_modified_subscription] update last_modified on every change. |
427 | CREATE TRIGGER IF NOT EXISTS last_modified_subscription AFTER UPDATE ON subscription |
428 | FOR EACH ROW |
429 | + WHEN NEW.last_modified != OLD.last_modified |
430 | BEGIN |
431 | UPDATE subscription SET last_modified = unixepoch() |
432 | WHERE pk = NEW.pk; |
433 | END; |
434 | |
435 | + -- [tag:last_modified_account] update last_modified on every change. |
436 | CREATE TRIGGER IF NOT EXISTS last_modified_account AFTER UPDATE ON account |
437 | FOR EACH ROW |
438 | + WHEN NEW.last_modified != OLD.last_modified |
439 | BEGIN |
440 | UPDATE account SET last_modified = unixepoch() |
441 | WHERE pk = NEW.pk; |
442 | END; |
443 | |
444 | + -- [tag:last_modified_candidate_subscription] update last_modified on every change. |
445 | CREATE TRIGGER IF NOT EXISTS last_modified_candidate_subscription AFTER UPDATE ON candidate_subscription |
446 | FOR EACH ROW |
447 | + WHEN NEW.last_modified != OLD.last_modified |
448 | BEGIN |
449 | UPDATE candidate_subscription SET last_modified = unixepoch() |
450 | WHERE pk = NEW.pk; |
451 | END; |
452 | |
453 | + -- [tag:last_modified_templates] update last_modified on every change. |
454 | CREATE TRIGGER IF NOT EXISTS last_modified_templates AFTER UPDATE ON templates |
455 | FOR EACH ROW |
456 | + WHEN NEW.last_modified != OLD.last_modified |
457 | BEGIN |
458 | UPDATE templates SET last_modified = unixepoch() |
459 | WHERE pk = NEW.pk; |
460 | diff --git a/core/src/schema.sql.m4 b/core/src/schema.sql.m4 |
461 | index f1df735..32df925 100644 |
462 | --- a/core/src/schema.sql.m4 |
463 | +++ b/core/src/schema.sql.m4 |
464 | @@ -2,8 +2,12 @@ define(xor, `(($1) OR ($2)) AND NOT (($1) AND ($2))')dnl |
465 | define(BOOLEAN_TYPE, `$1 BOOLEAN CHECK ($1 in (0, 1)) NOT NULL')dnl |
466 | define(BOOLEAN_FALSE, `0')dnl |
467 | define(BOOLEAN_TRUE, `1')dnl |
468 | - define(update_last_modified, `CREATE TRIGGER IF NOT EXISTS last_modified_$1 AFTER UPDATE ON $1 |
469 | + define(__TAG, `tag')dnl # Write the string '['+'tag'+':'+... with a macro so that tagref check doesn't pick up on it as a duplicate. |
470 | + define(TAG, `['__TAG()`:$1]')dnl |
471 | + define(update_last_modified, `-- 'TAG(last_modified_$1)` update last_modified on every change. |
472 | + CREATE TRIGGER IF NOT EXISTS last_modified_$1 AFTER UPDATE ON $1 |
473 | FOR EACH ROW |
474 | + WHEN NEW.last_modified != OLD.last_modified |
475 | BEGIN |
476 | UPDATE $1 SET last_modified = unixepoch() |
477 | WHERE pk = NEW.pk; |
478 | @@ -189,6 +193,7 @@ CREATE INDEX IF NOT EXISTS post_msgid_idx ON post(message_id); |
479 | CREATE INDEX IF NOT EXISTS list_idx ON list(id); |
480 | CREATE INDEX IF NOT EXISTS subscription_idx ON subscription(address); |
481 | |
482 | + -- TAG(accept_candidate): Update candidacy with 'subscription' foreign key on 'subscription' insert. |
483 | CREATE TRIGGER IF NOT EXISTS accept_candidate AFTER INSERT ON subscription |
484 | FOR EACH ROW |
485 | BEGIN |
486 | @@ -196,13 +201,17 @@ BEGIN |
487 | WHERE candidate_subscription.list = NEW.list AND candidate_subscription.address = NEW.address; |
488 | END; |
489 | |
490 | - CREATE TRIGGER IF NOT EXISTS verify_candidate AFTER INSERT ON subscription |
491 | + -- TAG(verify_subscription_email): If list settings require e-mail to be verified, |
492 | + -- update new subscription's 'verify' column value. |
493 | + CREATE TRIGGER IF NOT EXISTS verify_subscription_email AFTER INSERT ON subscription |
494 | FOR EACH ROW |
495 | BEGIN |
496 | UPDATE subscription SET verified = BOOLEAN_FALSE(), last_modified = unixepoch() |
497 | WHERE subscription.pk = NEW.pk AND EXISTS (SELECT 1 FROM list WHERE pk = NEW.list AND verify = BOOLEAN_TRUE()); |
498 | END; |
499 | |
500 | + -- TAG(add_account): Update list subscription entries with 'account' foreign |
501 | + -- key, if addresses match. |
502 | CREATE TRIGGER IF NOT EXISTS add_account AFTER INSERT ON account |
503 | FOR EACH ROW |
504 | BEGIN |
505 | @@ -210,14 +219,17 @@ BEGIN |
506 | WHERE subscription.address = NEW.address; |
507 | END; |
508 | |
509 | + -- TAG(add_account_to_subscription): When adding a new 'subscription', auto |
510 | + -- set 'account' value if there already exists an 'account' entry with the same |
511 | + -- address. |
512 | CREATE TRIGGER IF NOT EXISTS add_account_to_subscription AFTER INSERT ON subscription |
513 | FOR EACH ROW |
514 | + WHEN NEW.account IS NULL AND EXISTS (SELECT 1 FROM account WHERE address = NEW.address) |
515 | BEGIN |
516 | UPDATE subscription |
517 | - SET account = acc.pk, |
518 | + SET account = (SELECT pk FROM account WHERE address = NEW.address), |
519 | last_modified = unixepoch() |
520 | - FROM (SELECT * FROM account) AS acc |
521 | - WHERE subscription.account = acc.address; |
522 | + WHERE subscription.pk = NEW.pk; |
523 | END; |
524 | |
525 | update_last_modified(`list') |