5
5
//! - `id` - status update serial number
6
6
//! - `msg_id` - ID of the message in the `msgs` table
7
7
//! - `update_item` - JSON representation of the status update
8
+ //! - `uid` - "id" field of the update, used for deduplication
8
9
//!
9
10
//! Status updates are scheduled for sending by adding a record
10
11
//! to `smtp_status_updates_table` SQL table.
@@ -37,6 +38,7 @@ use crate::mimefactory::wrapped_base64_encode;
37
38
use crate :: mimeparser:: SystemMessage ;
38
39
use crate :: param:: Param ;
39
40
use crate :: param:: Params ;
41
+ use crate :: tools:: create_id;
40
42
use crate :: tools:: strip_rtlo_characters;
41
43
use crate :: tools:: { create_smeared_timestamp, get_abs_path} ;
42
44
@@ -178,6 +180,13 @@ pub struct StatusUpdateItem {
178
180
/// for a voting app.
179
181
#[ serde( skip_serializing_if = "Option::is_none" ) ]
180
182
pub summary : Option < String > ,
183
+
184
+ /// Unique ID for deduplication.
185
+ /// This can be used if the message is sent over multiple transports.
186
+ ///
187
+ /// If there is no ID, message is always considered to be unique.
188
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
189
+ pub uid : Option < String > ,
181
190
}
182
191
183
192
/// Update items as passed to the UIs.
@@ -317,7 +326,14 @@ impl Context {
317
326
timestamp : i64 ,
318
327
can_info_msg : bool ,
319
328
from_id : ContactId ,
320
- ) -> Result < StatusUpdateSerial > {
329
+ ) -> Result < Option < StatusUpdateSerial > > {
330
+ let Some ( status_update_serial) = self
331
+ . write_status_update_inner ( & instance. id , & status_update_item)
332
+ . await ?
333
+ else {
334
+ return Ok ( None ) ;
335
+ } ;
336
+
321
337
if can_info_msg {
322
338
if let Some ( ref info) = status_update_item. info {
323
339
if let Some ( info_msg_id) =
@@ -376,34 +392,49 @@ impl Context {
376
392
self . emit_msgs_changed ( instance. chat_id , instance. id ) ;
377
393
}
378
394
379
- let status_update_serial = self
380
- . write_status_update_inner ( & instance. id , status_update_item)
381
- . await ?;
382
-
383
395
if instance. viewtype == Viewtype :: Webxdc {
384
396
self . emit_event ( EventType :: WebxdcStatusUpdate {
385
397
msg_id : instance. id ,
386
398
status_update_serial,
387
399
} ) ;
388
400
}
389
401
390
- Ok ( status_update_serial)
402
+ Ok ( Some ( status_update_serial) )
391
403
}
392
404
405
+ /// Inserts a status update item into `msgs_status_updates` table.
406
+ ///
407
+ /// Returns serial ID of the status update if a new item is inserted.
393
408
pub ( crate ) async fn write_status_update_inner (
394
409
& self ,
395
410
instance_id : & MsgId ,
396
- status_update_item : StatusUpdateItem ,
397
- ) -> Result < StatusUpdateSerial > {
398
- let rowid = self
411
+ status_update_item : & StatusUpdateItem ,
412
+ ) -> Result < Option < StatusUpdateSerial > > {
413
+ let uid = status_update_item. uid . as_deref ( ) ;
414
+ let Some ( rowid) = self
399
415
. sql
400
- . insert (
401
- "INSERT INTO msgs_status_updates (msg_id, update_item) VALUES(?, ?);" ,
402
- ( instance_id, serde_json:: to_string ( & status_update_item) ?) ,
416
+ . query_row_optional (
417
+ "INSERT INTO msgs_status_updates (msg_id, update_item, uid) VALUES(?, ?, ?)
418
+ ON CONFLICT (uid) DO NOTHING
419
+ RETURNING id" ,
420
+ (
421
+ instance_id,
422
+ serde_json:: to_string ( & status_update_item) ?,
423
+ uid,
424
+ ) ,
425
+ |row| {
426
+ let id: u32 = row. get ( 0 ) ?;
427
+ Ok ( id)
428
+ } ,
403
429
)
404
- . await ?;
430
+ . await ?
431
+ else {
432
+ let uid = uid. unwrap_or ( "-" ) ;
433
+ info ! ( self , "Ignoring duplicate status update with uid={uid}" ) ;
434
+ return Ok ( None ) ;
435
+ } ;
405
436
let status_update_serial = StatusUpdateSerial ( u32:: try_from ( rowid) ?) ;
406
- Ok ( status_update_serial)
437
+ Ok ( Some ( status_update_serial) )
407
438
}
408
439
409
440
/// Returns the update_item with `status_update_serial` from the webxdc with message id `msg_id`.
@@ -449,7 +480,7 @@ impl Context {
449
480
pub async fn send_webxdc_status_update_struct (
450
481
& self ,
451
482
instance_msg_id : MsgId ,
452
- status_update : StatusUpdateItem ,
483
+ mut status_update : StatusUpdateItem ,
453
484
descr : & str ,
454
485
) -> Result < ( ) > {
455
486
let mut instance = Message :: load_from_db ( self , instance_msg_id) . await ?;
@@ -467,6 +498,7 @@ impl Context {
467
498
MessageState :: Undefined | MessageState :: OutPreparing | MessageState :: OutDraft
468
499
) ;
469
500
501
+ status_update. uid = Some ( create_id ( ) ) ;
470
502
let status_update_serial: StatusUpdateSerial = self
471
503
. create_status_update_record (
472
504
& mut instance,
@@ -475,7 +507,8 @@ impl Context {
475
507
send_now,
476
508
ContactId :: SELF ,
477
509
)
478
- . await ?;
510
+ . await ?
511
+ . context ( "Failed to create status update" ) ?;
479
512
480
513
if send_now {
481
514
self . sql . insert (
@@ -655,7 +688,10 @@ impl Context {
655
688
let ( update_item_str, serial) = row;
656
689
let update_item = StatusUpdateItemAndSerial
657
690
{
658
- item : serde_json:: from_str ( & update_item_str) ?,
691
+ item : StatusUpdateItem {
692
+ uid : None , // Erase UIDs, apps, bots and tests don't need to know them.
693
+ ..serde_json:: from_str ( & update_item_str) ?
694
+ } ,
659
695
serial,
660
696
max_serial,
661
697
} ;
@@ -1348,18 +1384,39 @@ mod tests {
1348
1384
info : None ,
1349
1385
document : None ,
1350
1386
summary : None ,
1387
+ uid : Some ( "iecie2Ze" . to_string ( ) ) ,
1351
1388
} ,
1352
1389
1640178619 ,
1353
1390
true ,
1354
1391
ContactId :: SELF ,
1355
1392
)
1356
- . await ?;
1393
+ . await ?
1394
+ . unwrap ( ) ;
1357
1395
assert_eq ! (
1358
1396
t. get_webxdc_status_updates( instance. id, StatusUpdateSerial ( 0 ) )
1359
1397
. await ?,
1360
1398
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
1361
1399
) ;
1362
1400
1401
+ // Update with duplicate update ID is received.
1402
+ // Whatever the payload is, update should be ignored just because ID is duplicate.
1403
+ let update_id1_duplicate = t
1404
+ . create_status_update_record (
1405
+ & mut instance,
1406
+ StatusUpdateItem {
1407
+ payload : json ! ( { "nothing" : "this should be ignored" } ) ,
1408
+ info : None ,
1409
+ document : None ,
1410
+ summary : None ,
1411
+ uid : Some ( "iecie2Ze" . to_string ( ) ) ,
1412
+ } ,
1413
+ 1640178619 ,
1414
+ true ,
1415
+ ContactId :: SELF ,
1416
+ )
1417
+ . await ?;
1418
+ assert_eq ! ( update_id1_duplicate, None ) ;
1419
+
1363
1420
assert ! ( t
1364
1421
. send_webxdc_status_update( instance. id, "\n \n \n " , "" )
1365
1422
. await
@@ -1384,15 +1441,17 @@ mod tests {
1384
1441
info : None ,
1385
1442
document : None ,
1386
1443
summary : None ,
1444
+ uid : None ,
1387
1445
} ,
1388
1446
1640178619 ,
1389
1447
true ,
1390
1448
ContactId :: SELF ,
1391
1449
)
1392
- . await ?;
1450
+ . await ?
1451
+ . unwrap ( ) ;
1393
1452
assert_eq ! (
1394
1453
t. get_webxdc_status_updates( instance. id, update_id1) . await ?,
1395
- r#"[{"payload":{"foo2":"bar2"},"serial":2 ,"max_serial":2 }]"#
1454
+ r#"[{"payload":{"foo2":"bar2"},"serial":3 ,"max_serial":3 }]"#
1396
1455
) ;
1397
1456
t. create_status_update_record (
1398
1457
& mut instance,
@@ -1401,6 +1460,7 @@ mod tests {
1401
1460
info : None ,
1402
1461
document : None ,
1403
1462
summary : None ,
1463
+ uid : None ,
1404
1464
} ,
1405
1465
1640178619 ,
1406
1466
true ,
@@ -1410,9 +1470,9 @@ mod tests {
1410
1470
assert_eq ! (
1411
1471
t. get_webxdc_status_updates( instance. id, StatusUpdateSerial ( 0 ) )
1412
1472
. await ?,
1413
- r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":3 },
1414
- {"payload":{"foo2":"bar2"},"serial":2 ,"max_serial":3 },
1415
- {"payload":true,"serial":3 ,"max_serial":3 }]"#
1473
+ r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":4 },
1474
+ {"payload":{"foo2":"bar2"},"serial":3 ,"max_serial":4 },
1475
+ {"payload":true,"serial":4 ,"max_serial":4 }]"#
1416
1476
) ;
1417
1477
1418
1478
t. send_webxdc_status_update (
@@ -1423,8 +1483,8 @@ mod tests {
1423
1483
. await ?;
1424
1484
assert_eq ! (
1425
1485
t. get_webxdc_status_updates( instance. id, update_id2) . await ?,
1426
- r#"[{"payload":true,"serial":3 ,"max_serial":4 },
1427
- {"payload":1,"serial":4 ,"max_serial":4 }]"#
1486
+ r#"[{"payload":true,"serial":4 ,"max_serial":5 },
1487
+ {"payload":1,"serial":5 ,"max_serial":5 }]"#
1428
1488
) ;
1429
1489
1430
1490
Ok ( ( ) )
@@ -1654,6 +1714,8 @@ mod tests {
1654
1714
1655
1715
#[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
1656
1716
async fn test_render_webxdc_status_update_object_range ( ) -> Result < ( ) > {
1717
+ use regex:: Regex ;
1718
+
1657
1719
let t = TestContext :: new_alice ( ) . await ;
1658
1720
let chat_id = create_group_chat ( & t, ProtectionStatus :: Unprotected , "a chat" ) . await ?;
1659
1721
let instance = send_webxdc_instance ( & t, chat_id) . await ?;
@@ -1672,7 +1734,13 @@ mod tests {
1672
1734
)
1673
1735
. await ?
1674
1736
. unwrap ( ) ;
1675
- assert_eq ! ( json, "{\" updates\" :[{\" payload\" :2},\n {\" payload\" :3}]}" ) ;
1737
+ let json = Regex :: new ( r#""uid":"[^"]*""# )
1738
+ . unwrap ( )
1739
+ . replace_all ( & json, "XXX" ) ;
1740
+ assert_eq ! (
1741
+ json,
1742
+ "{\" updates\" :[{\" payload\" :2,XXX},\n {\" payload\" :3,XXX}]}"
1743
+ ) ;
1676
1744
1677
1745
assert_eq ! (
1678
1746
t. sql
0 commit comments