@@ -209,6 +209,30 @@ impl ChatId {
209
209
self == DC_CHAT_ID_ALLDONE_HINT
210
210
}
211
211
212
+ /// Returns [`ChatId`] of a chat that `msg` belongs to.
213
+ ///
214
+ /// Checks that `msg` is assigned to the right chat.
215
+ pub ( crate ) fn lookup_by_message ( msg : & Message ) -> Option < Self > {
216
+ if msg. chat_id == DC_CHAT_ID_TRASH {
217
+ return None ;
218
+ }
219
+ if msg. download_state != DownloadState :: Done
220
+ // TODO (2023-09-12): Added for backward compatibility with versions that did not have
221
+ // `DownloadState::Undecipherable`. Remove eventually with the comment in
222
+ // `MimeMessage::from_bytes()`.
223
+ || msg
224
+ . error
225
+ . as_ref ( )
226
+ . filter ( |e| e. starts_with ( "Decrypting failed:" ) )
227
+ . is_some ( )
228
+ {
229
+ // If `msg` is not fully downloaded or undecipherable, it may have been assigned to the
230
+ // wrong chat (they often get assigned to the 1:1 chat with the sender).
231
+ return None ;
232
+ }
233
+ Some ( msg. chat_id )
234
+ }
235
+
212
236
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id`
213
237
/// if it exists and is not blocked.
214
238
///
@@ -374,6 +398,7 @@ impl ChatId {
374
398
375
399
pub ( crate ) async fn block_ex ( self , context : & Context , sync : sync:: Sync ) -> Result < ( ) > {
376
400
let chat = Chat :: load_from_db ( context, self ) . await ?;
401
+ let mut delete = false ;
377
402
378
403
match chat. typ {
379
404
Chattype :: Broadcast => {
@@ -392,7 +417,7 @@ impl ChatId {
392
417
}
393
418
Chattype :: Group => {
394
419
info ! ( context, "Can't block groups yet, deleting the chat." ) ;
395
- self . delete ( context ) . await ? ;
420
+ delete = true ;
396
421
}
397
422
Chattype :: Mailinglist => {
398
423
if self . set_blocked ( context, Blocked :: Yes ) . await ? {
@@ -408,6 +433,9 @@ impl ChatId {
408
433
. log_err ( context)
409
434
. ok ( ) ;
410
435
}
436
+ if delete {
437
+ self . delete ( context) . await ?;
438
+ }
411
439
Ok ( ( ) )
412
440
}
413
441
@@ -1124,47 +1152,46 @@ impl ChatId {
1124
1152
Ok ( self . get_param ( context) . await ?. exists ( Param :: Devicetalk ) )
1125
1153
}
1126
1154
1127
- async fn parent_query < T , F > ( self , context : & Context , fields : & str , f : F ) -> Result < Option < T > >
1155
+ async fn parent_query < T , F > (
1156
+ self ,
1157
+ context : & Context ,
1158
+ fields : & str ,
1159
+ state_out_min : MessageState ,
1160
+ f : F ,
1161
+ ) -> Result < Option < T > >
1128
1162
where
1129
1163
F : Send + FnOnce ( & rusqlite:: Row ) -> rusqlite:: Result < T > ,
1130
1164
T : Send + ' static ,
1131
1165
{
1132
1166
let sql = & context. sql ;
1133
- // Do not reply to not fully downloaded messages. Such a message could be a group chat
1134
- // message that we assigned to 1:1 chat.
1135
1167
let query = format ! (
1136
1168
"SELECT {fields} \
1137
- FROM msgs WHERE chat_id=? AND state NOT IN (?, ?) AND NOT hidden AND download_state={} \
1169
+ FROM msgs \
1170
+ WHERE chat_id=? \
1171
+ AND ((state BETWEEN {} AND {}) OR (state >= {})) \
1172
+ AND NOT hidden \
1173
+ AND download_state={} \
1138
1174
ORDER BY timestamp DESC, id DESC \
1139
1175
LIMIT 1;",
1140
- DownloadState :: Done as u32 ,
1176
+ MessageState :: InFresh as u32 ,
1177
+ MessageState :: InSeen as u32 ,
1178
+ state_out_min as u32 ,
1179
+ // Do not reply to not fully downloaded messages. Such a message could be a group chat
1180
+ // message that we assigned to 1:1 chat.
1181
+ DownloadState :: Done as u32 ,
1141
1182
) ;
1142
- let row = sql
1143
- . query_row_optional (
1144
- & query,
1145
- (
1146
- self ,
1147
- MessageState :: OutPreparing ,
1148
- MessageState :: OutDraft ,
1149
- // We don't filter `OutPending` and `OutFailed` messages because the new message
1150
- // for which `parent_query()` is done may assume that it will be received in a
1151
- // context affected by those messages, e.g. they could add new members to a
1152
- // group and the new message will contain them in "To:". Anyway recipients must
1153
- // be prepared to orphaned references.
1154
- ) ,
1155
- f,
1156
- )
1157
- . await ?;
1158
- Ok ( row)
1183
+ sql. query_row_optional ( & query, ( self , ) , f) . await
1159
1184
}
1160
1185
1161
1186
async fn get_parent_mime_headers (
1162
1187
self ,
1163
1188
context : & Context ,
1189
+ state_out_min : MessageState ,
1164
1190
) -> Result < Option < ( String , String , String ) > > {
1165
1191
self . parent_query (
1166
1192
context,
1167
1193
"rfc724_mid, mime_in_reply_to, mime_references" ,
1194
+ state_out_min,
1168
1195
|row : & rusqlite:: Row | {
1169
1196
let rfc724_mid: String = row. get ( 0 ) ?;
1170
1197
let mime_in_reply_to: String = row. get ( 1 ) ?;
@@ -1741,7 +1768,15 @@ impl Chat {
1741
1768
// we do not set In-Reply-To/References in this case.
1742
1769
if !self . is_self_talk ( ) {
1743
1770
if let Some ( ( parent_rfc724_mid, parent_in_reply_to, parent_references) ) =
1744
- self . id . get_parent_mime_headers ( context) . await ?
1771
+ // We don't filter `OutPending` and `OutFailed` messages because the new message for
1772
+ // which `parent_query()` is done may assume that it will be received in a context
1773
+ // affected by those messages, e.g. they could add new members to a group and the
1774
+ // new message will contain them in "To:". Anyway recipients must be prepared to
1775
+ // orphaned references.
1776
+ self
1777
+ . id
1778
+ . get_parent_mime_headers ( context, MessageState :: OutPending )
1779
+ . await ?
1745
1780
{
1746
1781
// "In-Reply-To:" is not changed if it is set manually.
1747
1782
// This does not affect "References:" header, it will contain "default parent" (the
@@ -1959,10 +1994,26 @@ impl Chat {
1959
1994
Ok ( r)
1960
1995
}
1961
1996
Chattype :: Broadcast | Chattype :: Group | Chattype :: Mailinglist => {
1962
- if self . grpid . is_empty ( ) {
1963
- return Ok ( None ) ;
1997
+ if ! self . grpid . is_empty ( ) {
1998
+ return Ok ( Some ( SyncId :: Grpid ( self . grpid . clone ( ) ) ) ) ;
1964
1999
}
1965
- Ok ( Some ( SyncId :: Grpid ( self . grpid . clone ( ) ) ) )
2000
+
2001
+ let Some ( ( parent_rfc724_mid, parent_in_reply_to, _) ) = self
2002
+ . id
2003
+ . get_parent_mime_headers ( context, MessageState :: OutDelivered )
2004
+ . await ?
2005
+ else {
2006
+ warn ! (
2007
+ context,
2008
+ "Chat::get_sync_id({}): No good message identifying the chat found." ,
2009
+ self . id
2010
+ ) ;
2011
+ return Ok ( None ) ;
2012
+ } ;
2013
+ Ok ( Some ( SyncId :: Msgids ( vec ! [
2014
+ parent_in_reply_to,
2015
+ parent_rfc724_mid,
2016
+ ] ) ) )
1966
2017
}
1967
2018
}
1968
2019
}
@@ -4242,8 +4293,8 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String])
4242
4293
pub ( crate ) enum SyncId {
4243
4294
ContactAddr ( String ) ,
4244
4295
Grpid ( String ) ,
4245
- // NOTE: Ad-hoc groups lack an identifier that can be used across devices so
4246
- // block/mute/etc. actions on them are not synchronized to other devices.
4296
+ /// "Message-ID"-s, from oldest to latest. Used for ad-hoc groups.
4297
+ Msgids ( Vec < String > ) ,
4247
4298
}
4248
4299
4249
4300
/// An action synchronised to other devices.
@@ -4266,12 +4317,9 @@ impl Context {
4266
4317
pub ( crate ) async fn sync_alter_chat ( & self , id : & SyncId , action : & SyncAction ) -> Result < ( ) > {
4267
4318
let chat_id = match id {
4268
4319
SyncId :: ContactAddr ( addr) => {
4269
- let Some ( contact_id) =
4270
- Contact :: lookup_id_by_addr_ex ( self , addr, Origin :: Unknown , None ) . await ?
4271
- else {
4272
- warn ! ( self , "sync_alter_chat: No contact for addr '{addr}'." ) ;
4273
- return Ok ( ( ) ) ;
4274
- } ;
4320
+ let contact_id = Contact :: lookup_id_by_addr_ex ( self , addr, Origin :: Unknown , None )
4321
+ . await ?
4322
+ . with_context ( || format ! ( "No contact for addr '{addr}'" ) ) ?;
4275
4323
match action {
4276
4324
SyncAction :: Block => {
4277
4325
return contact:: set_blocked ( self , Nosync , contact_id, true ) . await
@@ -4281,22 +4329,26 @@ impl Context {
4281
4329
}
4282
4330
_ => ( ) ,
4283
4331
}
4284
- let Some ( chat_id) = ChatId :: lookup_by_contact ( self , contact_id) . await ? else {
4285
- warn ! ( self , "sync_alter_chat: No chat for addr '{addr}'." ) ;
4286
- return Ok ( ( ) ) ;
4287
- } ;
4288
- chat_id
4332
+ ChatId :: lookup_by_contact ( self , contact_id)
4333
+ . await ?
4334
+ . with_context ( || format ! ( "No chat for addr '{addr}'" ) ) ?
4289
4335
}
4290
4336
SyncId :: Grpid ( grpid) => {
4291
4337
if let SyncAction :: CreateBroadcast ( name) = action {
4292
4338
create_broadcast_list_ex ( self , Nosync , grpid. clone ( ) , name. clone ( ) ) . await ?;
4293
4339
return Ok ( ( ) ) ;
4294
4340
}
4295
- let Some ( ( chat_id, ..) ) = get_chat_id_by_grpid ( self , grpid) . await ? else {
4296
- warn ! ( self , "sync_alter_chat: No chat for grpid '{grpid}'." ) ;
4297
- return Ok ( ( ) ) ;
4298
- } ;
4299
- chat_id
4341
+ get_chat_id_by_grpid ( self , grpid)
4342
+ . await ?
4343
+ . with_context ( || format ! ( "No chat for grpid '{grpid}'" ) ) ?
4344
+ . 0
4345
+ }
4346
+ SyncId :: Msgids ( msgids) => {
4347
+ let msg = message:: get_latest_by_rfc724_mids ( self , msgids)
4348
+ . await ?
4349
+ . with_context ( || format ! ( "No message found for Message-IDs {msgids:?}" ) ) ?;
4350
+ ChatId :: lookup_by_message ( & msg)
4351
+ . with_context ( || format ! ( "No chat found for Message-IDs {msgids:?}" ) ) ?
4300
4352
}
4301
4353
} ;
4302
4354
match action {
@@ -6941,6 +6993,51 @@ mod tests {
6941
6993
Ok ( ( ) )
6942
6994
}
6943
6995
6996
+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 2 ) ]
6997
+ async fn test_sync_adhoc_grp ( ) -> Result < ( ) > {
6998
+ let alice0 = & TestContext :: new_alice ( ) . await ;
6999
+ let alice1 = & TestContext :: new_alice ( ) . await ;
7000
+ for a in [ alice0, alice1] {
7001
+ a. set_config_bool ( Config :: SyncMsgs , true ) . await ?;
7002
+ }
7003
+
7004
+ let mut chat_ids = Vec :: new ( ) ;
7005
+ for a in [ alice0, alice1] {
7006
+ let msg = receive_imf (
7007
+ a,
7008
+ b"Subject: =?utf-8?q?Message_from_alice=40example=2Eorg?=\r \n \
7009
+ From: alice@example.org\r \n \
7010
+ To: <bob@example.net>, <fiona@example.org> \r \n \
7011
+ Date: Mon, 2 Dec 2023 16:59:39 +0000\r \n \
7012
+ Message-ID: <Mr.alices_original_mail@example.org>\r \n \
7013
+ Chat-Version: 1.0\r \n \
7014
+ \r \n \
7015
+ hi\r \n ",
7016
+ false ,
7017
+ )
7018
+ . await ?
7019
+ . unwrap ( ) ;
7020
+ chat_ids. push ( msg. chat_id ) ;
7021
+ }
7022
+ let chat1 = Chat :: load_from_db ( alice1, chat_ids[ 1 ] ) . await ?;
7023
+ assert_eq ! ( chat1. typ, Chattype :: Group ) ;
7024
+ assert ! ( chat1. grpid. is_empty( ) ) ;
7025
+
7026
+ // Test synchronisation on chat blocking because it causes chat deletion currently and thus
7027
+ // requires generating a sync message in advance.
7028
+ chat_ids[ 0 ] . block ( alice0) . await ?;
7029
+ sync ( alice0, alice1) . await ;
7030
+ assert ! ( Chat :: load_from_db( alice1, chat_ids[ 1 ] ) . await . is_err( ) ) ;
7031
+ assert ! (
7032
+ !alice1
7033
+ . sql
7034
+ . exists( "SELECT COUNT(*) FROM chats WHERE id=?" , ( chat_ids[ 1 ] , ) )
7035
+ . await ?
7036
+ ) ;
7037
+
7038
+ Ok ( ( ) )
7039
+ }
7040
+
6944
7041
/// Tests syncing of chat visibility on a self-chat. This way we test:
6945
7042
/// - Self-chat synchronisation.
6946
7043
/// - That sync messages don't unarchive the self-chat.
0 commit comments