1use anyhow::Result;
4use mail_builder::mime::MimePart;
5use serde::{Deserialize, Serialize};
6
7use crate::chat::{self, ChatId};
8use crate::config::Config;
9use crate::constants::Blocked;
10use crate::contact::ContactId;
11use crate::context::Context;
12use crate::log::LogExt;
13use crate::message::{Message, MsgId, Viewtype};
14use crate::mimeparser::SystemMessage;
15use crate::param::Param;
16use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
17use crate::token::Namespace;
18use crate::tools::time;
19use crate::{message, stock_str, token};
20use std::collections::HashSet;
21
22#[derive(Debug, PartialEq)]
24pub(crate) enum Sync {
25 Nosync,
26 Sync,
27}
28
29impl From<Sync> for bool {
30 fn from(sync: Sync) -> bool {
31 match sync {
32 Sync::Nosync => false,
33 Sync::Sync => true,
34 }
35 }
36}
37
38impl From<bool> for Sync {
39 fn from(sync: bool) -> Sync {
40 match sync {
41 false => Sync::Nosync,
42 true => Sync::Sync,
43 }
44 }
45}
46
47#[derive(Debug, Serialize, Deserialize)]
48pub(crate) struct QrTokenData {
49 pub(crate) invitenumber: String,
50 pub(crate) auth: String,
51 pub(crate) grpid: Option<String>,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55pub(crate) enum SyncData {
56 AddQrToken(QrTokenData),
57 DeleteQrToken(QrTokenData),
58 AlterChat {
59 id: chat::SyncId,
60 action: chat::SyncAction,
61 },
62 Config {
63 key: Config,
64 val: String,
65 },
66 SaveMessage {
67 src: String, dest: String, },
70 DeleteMessages {
71 msgs: Vec<String>, },
73}
74
75#[derive(Debug, Serialize, Deserialize)]
76#[serde(untagged)]
77pub(crate) enum SyncDataOrUnknown {
78 SyncData(SyncData),
79 Unknown(serde_json::Value),
80}
81
82#[derive(Debug, Serialize, Deserialize)]
83pub(crate) struct SyncItem {
84 timestamp: i64,
85
86 data: SyncDataOrUnknown,
87}
88
89#[derive(Debug, Deserialize)]
90pub(crate) struct SyncItems {
91 items: Vec<SyncItem>,
92}
93
94impl From<SyncData> for SyncDataOrUnknown {
95 fn from(sync_data: SyncData) -> Self {
96 Self::SyncData(sync_data)
97 }
98}
99
100impl Context {
101 pub(crate) async fn add_sync_item(&self, data: SyncData) -> Result<()> {
106 self.add_sync_item_with_timestamp(data, time()).await
107 }
108
109 async fn add_sync_item_with_timestamp(&self, data: SyncData, timestamp: i64) -> Result<()> {
112 if !self.should_send_sync_msgs().await? {
113 return Ok(());
114 }
115
116 let item = SyncItem {
117 timestamp,
118 data: data.into(),
119 };
120 let item = serde_json::to_string(&item)?;
121 self.sql
122 .execute("INSERT INTO multi_device_sync (item) VALUES(?);", (item,))
123 .await?;
124
125 Ok(())
126 }
127
128 pub(crate) async fn sync_qr_code_tokens(&self, grpid: Option<&str>) -> Result<()> {
133 if !self.should_send_sync_msgs().await? {
134 return Ok(());
135 }
136 if let (Some(invitenumber), Some(auth)) = (
137 token::lookup(self, Namespace::InviteNumber, grpid).await?,
138 token::lookup(self, Namespace::Auth, grpid).await?,
139 ) {
140 self.add_sync_item(SyncData::AddQrToken(QrTokenData {
141 invitenumber,
142 auth,
143 grpid: grpid.map(|s| s.to_string()),
144 }))
145 .await?;
146 }
147 Ok(())
148 }
149
150 pub(crate) async fn sync_qr_code_token_deletion(
154 &self,
155 invitenumber: String,
156 auth: String,
157 ) -> Result<()> {
158 self.add_sync_item(SyncData::DeleteQrToken(QrTokenData {
159 invitenumber,
160 auth,
161 grpid: None,
162 }))
163 .await?;
164 self.scheduler.interrupt_inbox().await;
165 Ok(())
166 }
167
168 pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
174 if let Some((json, ids)) = self.build_sync_json().await? {
175 let chat_id =
176 ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
177 .await?;
178 let mut msg = Message {
179 chat_id,
180 viewtype: Viewtype::Text,
181 text: stock_str::sync_msg_body(self).await,
182 hidden: true,
183 subject: stock_str::sync_msg_subject(self).await,
184 ..Default::default()
185 };
186 msg.param.set_cmd(SystemMessage::MultiDeviceSync);
187 msg.param.set(Param::Arg, json);
188 msg.param.set(Param::Arg2, ids);
189 msg.param.set_int(Param::GuaranteeE2ee, 1);
190 Ok(Some(chat::send_msg(self, chat_id, &mut msg).await?))
191 } else {
192 Ok(None)
193 }
194 }
195
196 pub(crate) async fn build_sync_json(&self) -> Result<Option<(String, String)>> {
199 let (ids, serialized) = self
200 .sql
201 .query_map(
202 "SELECT id, item FROM multi_device_sync ORDER BY id;",
203 (),
204 |row| Ok((row.get::<_, u32>(0)?, row.get::<_, String>(1)?)),
205 |rows| {
206 let mut ids = vec![];
207 let mut serialized = String::default();
208 for row in rows {
209 let (id, item) = row?;
210 ids.push(id);
211 if !serialized.is_empty() {
212 serialized.push_str(",\n");
213 }
214 serialized.push_str(&item);
215 }
216 Ok((ids, serialized))
217 },
218 )
219 .await?;
220
221 if ids.is_empty() {
222 Ok(None)
223 } else {
224 Ok(Some((
225 format!("{{\"items\":[\n{serialized}\n]}}"),
226 ids.iter()
227 .map(|x| x.to_string())
228 .collect::<Vec<String>>()
229 .join(","),
230 )))
231 }
232 }
233
234 pub(crate) fn build_sync_part(&self, json: String) -> MimePart<'static> {
235 MimePart::new("application/json", json).attachment("multi-device-sync.json")
236 }
237
238 pub(crate) fn parse_sync_items(&self, serialized: String) -> Result<SyncItems> {
241 let sync_items: SyncItems = serde_json::from_str(&serialized)?;
242 Ok(sync_items)
243 }
244
245 pub(crate) async fn execute_sync_items(&self, items: &SyncItems) {
256 info!(self, "executing {} sync item(s)", items.items.len());
257 for item in &items.items {
258 match &item.data {
259 SyncDataOrUnknown::SyncData(data) => match data {
260 AddQrToken(token) => self.add_qr_token(token).await,
261 DeleteQrToken(token) => self.delete_qr_token(token).await,
262 AlterChat { id, action } => self.sync_alter_chat(id, action).await,
263 SyncData::Config { key, val } => self.sync_config(key, val).await,
264 SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await,
265 SyncData::DeleteMessages { msgs } => self.sync_message_deletion(msgs).await,
266 },
267 SyncDataOrUnknown::Unknown(data) => {
268 warn!(self, "Ignored unknown sync item: {data}.");
269 Ok(())
270 }
271 }
272 .log_err(self)
273 .ok();
274 }
275
276 if !items.items.is_empty() && !self.get_config_bool(Config::BccSelf).await.unwrap_or(true) {
279 self.set_config_ex(Sync::Nosync, Config::BccSelf, Some("1"))
280 .await
281 .log_err(self)
282 .ok();
283 }
284 }
285
286 async fn add_qr_token(&self, token: &QrTokenData) -> Result<()> {
287 let grpid = token.grpid.as_deref();
288 token::save(self, Namespace::InviteNumber, grpid, &token.invitenumber).await?;
289 token::save(self, Namespace::Auth, grpid, &token.auth).await?;
290 Ok(())
291 }
292
293 async fn delete_qr_token(&self, token: &QrTokenData) -> Result<()> {
294 token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
295 token::delete(self, Namespace::Auth, &token.auth).await?;
296 Ok(())
297 }
298
299 async fn save_message(&self, src_rfc724_mid: &str, dest_rfc724_mid: &String) -> Result<()> {
300 if let Some((src_msg_id, _)) = message::rfc724_mid_exists(self, src_rfc724_mid).await? {
301 chat::save_copy_in_self_talk(self, &src_msg_id, dest_rfc724_mid).await?;
302 }
303 Ok(())
304 }
305
306 async fn sync_message_deletion(&self, msgs: &Vec<String>) -> Result<()> {
307 let mut modified_chat_ids = HashSet::new();
308 let mut msg_ids = Vec::new();
309 for rfc724_mid in msgs {
310 if let Some((msg_id, _)) = message::rfc724_mid_exists(self, rfc724_mid).await? {
311 if let Some(msg) = Message::load_from_db_optional(self, msg_id).await? {
312 message::delete_msg_locally(self, &msg).await?;
313 msg_ids.push(msg.id);
314 modified_chat_ids.insert(msg.chat_id);
315 } else {
316 warn!(self, "Sync message delete: Database entry does not exist.");
317 }
318 } else {
319 warn!(self, "Sync message delete: {rfc724_mid:?} not found.");
320 }
321 }
322 message::delete_msgs_locally_done(self, &msg_ids, modified_chat_ids).await?;
323 Ok(())
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use std::time::Duration;
330
331 use anyhow::bail;
332
333 use super::*;
334 use crate::chat::{remove_contact_from_chat, Chat, ProtectionStatus};
335 use crate::chatlist::Chatlist;
336 use crate::contact::{Contact, Origin};
337 use crate::securejoin::get_securejoin_qr;
338 use crate::test_utils::{self, TestContext, TestContextManager};
339 use crate::tools::SystemTime;
340
341 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
342 async fn test_config_sync_msgs() -> Result<()> {
343 let t = TestContext::new_alice().await;
344 assert_eq!(t.get_config_bool(Config::SyncMsgs).await?, false);
345 assert_eq!(t.get_config_bool(Config::BccSelf).await?, true);
346 assert_eq!(t.should_send_sync_msgs().await?, false);
347
348 t.set_config_bool(Config::SyncMsgs, true).await?;
349 assert_eq!(t.get_config_bool(Config::SyncMsgs).await?, true);
350 assert_eq!(t.get_config_bool(Config::BccSelf).await?, true);
351 assert_eq!(t.should_send_sync_msgs().await?, true);
352
353 t.set_config_bool(Config::BccSelf, false).await?;
354 assert_eq!(t.get_config_bool(Config::SyncMsgs).await?, true);
355 assert_eq!(t.get_config_bool(Config::BccSelf).await?, false);
356 assert_eq!(t.should_send_sync_msgs().await?, false);
357
358 t.set_config_bool(Config::SyncMsgs, false).await?;
359 assert_eq!(t.get_config_bool(Config::SyncMsgs).await?, false);
360 assert_eq!(t.get_config_bool(Config::BccSelf).await?, false);
361 assert_eq!(t.should_send_sync_msgs().await?, false);
362 Ok(())
363 }
364
365 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
366 async fn test_build_sync_json() -> Result<()> {
367 let t = TestContext::new_alice().await;
368 t.set_config_bool(Config::SyncMsgs, true).await?;
369
370 assert!(t.build_sync_json().await?.is_none());
371
372 t.add_sync_item_with_timestamp(
376 SyncData::AlterChat {
377 id: chat::SyncId::ContactAddr("bob@example.net".to_string()),
378 action: chat::SyncAction::SetMuted(chat::MuteDuration::Until(
379 SystemTime::UNIX_EPOCH + Duration::from_millis(42999),
380 )),
381 },
382 1631781315,
383 )
384 .await?;
385
386 t.add_sync_item_with_timestamp(
387 SyncData::AddQrToken(QrTokenData {
388 invitenumber: "testinvite".to_string(),
389 auth: "testauth".to_string(),
390 grpid: Some("group123".to_string()),
391 }),
392 1631781316,
393 )
394 .await?;
395 t.add_sync_item_with_timestamp(
396 SyncData::DeleteQrToken(QrTokenData {
397 invitenumber: "123!?\":.;{}".to_string(),
398 auth: "456".to_string(),
399 grpid: None,
400 }),
401 1631781317,
402 )
403 .await?;
404
405 let (serialized, ids) = t.build_sync_json().await?.unwrap();
406 assert_eq!(
407 serialized,
408 r#"{"items":[
409{"timestamp":1631781315,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":{"SetMuted":{"Until":{"secs_since_epoch":42,"nanos_since_epoch":999000000}}}}}},
410{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"testinvite","auth":"testauth","grpid":"group123"}}},
411{"timestamp":1631781317,"data":{"DeleteQrToken":{"invitenumber":"123!?\":.;{}","auth":"456","grpid":null}}}
412]}"#
413 );
414
415 assert!(t.build_sync_json().await?.is_some());
416 t.sql
417 .execute(
418 &format!("DELETE FROM multi_device_sync WHERE id IN ({ids})"),
419 (),
420 )
421 .await?;
422 assert!(t.build_sync_json().await?.is_none());
423
424 let sync_items = t.parse_sync_items(serialized)?;
425 assert_eq!(sync_items.items.len(), 3);
426
427 Ok(())
428 }
429
430 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
431 async fn test_build_sync_json_sync_msgs_off() -> Result<()> {
432 let t = TestContext::new_alice().await;
433 t.set_config_bool(Config::SyncMsgs, false).await?;
434 t.add_sync_item(SyncData::AddQrToken(QrTokenData {
435 invitenumber: "testinvite".to_string(),
436 auth: "testauth".to_string(),
437 grpid: Some("group123".to_string()),
438 }))
439 .await?;
440 assert!(t.build_sync_json().await?.is_none());
441 Ok(())
442 }
443
444 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
445 async fn test_parse_sync_items() -> Result<()> {
446 let t = TestContext::new_alice().await;
447
448 assert!(t.parse_sync_items(r#"{bad json}"#.to_string()).is_err());
449
450 assert!(t.parse_sync_items(r#"{"badname":[]}"#.to_string()).is_err());
451
452 for bad_item_example in [
453 r#"{"items":[{"timestamp":1631781316,"data":{"BadItem":{"invitenumber":"in","auth":"a","grpid":null}}}]}"#,
454 r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":123}}}]}"#, r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":true}}}]}"#, r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":[]}}}]}"#, r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":{}}}}]}"#, r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","grpid":null}}}]}"#, r#"{"items":[{"timestamp":1631781316,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Burn"}}}]}"#, ] {
461 let sync_items = t.parse_sync_items(bad_item_example.to_string()).unwrap();
462 assert_eq!(sync_items.items.len(), 1);
463 assert!(matches!(sync_items.items[0].timestamp, 1631781316));
464 assert!(matches!(
465 sync_items.items[0].data,
466 SyncDataOrUnknown::Unknown(_)
467 ));
468 }
469
470 let sync_items = t.parse_sync_items(
472 r#"{"items":[{"timestamp":1631781318,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":{"SetMuted":{"Until":{"secs_since_epoch":42,"nanos_since_epoch":999000000}}}}}}]}"#.to_string(),
473 )?;
474 assert_eq!(sync_items.items.len(), 1);
475 let SyncDataOrUnknown::SyncData(AlterChat { id, action }) =
476 &sync_items.items.first().unwrap().data
477 else {
478 bail!("bad item");
479 };
480 assert_eq!(
481 *id,
482 chat::SyncId::ContactAddr("bob@example.net".to_string())
483 );
484 assert_eq!(
485 *action,
486 chat::SyncAction::SetMuted(chat::MuteDuration::Until(
487 SystemTime::UNIX_EPOCH + Duration::from_millis(42999)
488 ))
489 );
490
491 assert_eq!(
493 t.parse_sync_items(r#"{"items":[]}"#.to_string())?
494 .items
495 .len(),
496 0
497 );
498
499 let sync_items = t
501 .parse_sync_items(
502 r#"{"items":[
503{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"yip","grpid":null}}},
504{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip","additional":123,"grpid":null}}}
505]}"#
506 .to_string(),
507 )
508 ?;
509 assert_eq!(sync_items.items.len(), 2);
510
511 let sync_items = t.parse_sync_items(
512 r#"{"items":[
513{"timestamp":1631781318,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip","grpid":null}}}
514],"additional":"field"}"#
515 .to_string(),
516 )?;
517
518 assert_eq!(sync_items.items.len(), 1);
519 if let SyncDataOrUnknown::SyncData(AddQrToken(token)) =
520 &sync_items.items.first().unwrap().data
521 {
522 assert_eq!(token.invitenumber, "in");
523 assert_eq!(token.auth, "yip");
524 assert_eq!(token.grpid, None);
525 } else {
526 bail!("bad item");
527 }
528
529 let sync_items = t.parse_sync_items(
531 r#"{"items":[{"timestamp":1631781319,"data":{"AddQrToken":{"invitenumber":"in","auth":"a"}}}]}"#.to_string(),
532 )
533 ?;
534 assert_eq!(sync_items.items.len(), 1);
535
536 Ok(())
537 }
538
539 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
540 async fn test_execute_sync_items() -> Result<()> {
541 let t = TestContext::new_alice().await;
542
543 assert!(!token::exists(&t, Namespace::Auth, "yip-auth").await?);
544
545 let sync_items = t
546 .parse_sync_items(
547 r#"{"items":[
548{"timestamp":1631781315,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Block"}}},
549{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"yip-in","auth":"a"}}},
550{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"delete unexistent, shall continue"}}},
551{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip-auth"}}},
552{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"foo","grpid":"non-existent"}}},
553{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"directly deleted"}}},
554{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"directly deleted"}}}
555]}"#
556 .to_string(),
557 )
558 ?;
559 t.execute_sync_items(&sync_items).await;
560
561 assert!(
562 Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Unknown)
563 .await?
564 .is_none()
565 );
566 assert!(token::exists(&t, Namespace::InviteNumber, "yip-in").await?);
567 assert!(token::exists(&t, Namespace::Auth, "yip-auth").await?);
568 assert!(!token::exists(&t, Namespace::Auth, "non-existent").await?);
569 assert!(!token::exists(&t, Namespace::Auth, "directly deleted").await?);
570
571 Ok(())
572 }
573
574 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
575 async fn test_send_sync_msg() -> Result<()> {
576 let alice = TestContext::new_alice().await;
577 alice.set_config_bool(Config::SyncMsgs, true).await?;
578 alice
579 .add_sync_item(SyncData::AddQrToken(QrTokenData {
580 invitenumber: "in".to_string(),
581 auth: "testtoken".to_string(),
582 grpid: None,
583 }))
584 .await?;
585 let msg_id = alice.send_sync_msg().await?.unwrap();
586 let msg = Message::load_from_db(&alice, msg_id).await?;
587 let chat = Chat::load_from_db(&alice, msg.chat_id).await?;
588 assert!(chat.is_self_talk());
589
590 assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
593 let chat_id = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
594 let chat = Chat::load_from_db(&alice, chat_id).await?;
595 assert!(chat.is_self_talk());
596 assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
597 let msgs = chat::get_chat_msgs(&alice, chat_id).await?;
598 assert_eq!(msgs.len(), 0);
599
600 let sent_msg = alice.pop_sent_sync_msg().await;
603 let alice2 = TestContext::new_alice().await;
604 alice2.set_config_bool(Config::SyncMsgs, true).await?;
605 alice2.recv_msg_trash(&sent_msg).await;
606 assert!(token::exists(&alice2, token::Namespace::Auth, "testtoken").await?);
607 assert_eq!(Chatlist::try_load(&alice2, 0, None, None).await?.len(), 0);
608
609 let self_contact = alice2.add_or_lookup_contact(&alice2).await;
611 assert!(!self_contact.is_bot());
612
613 let bob = TestContext::new_bob().await;
615 bob.recv_msg(&sent_msg).await;
616 assert!(!token::exists(&bob, token::Namespace::Auth, "testtoken").await?);
617
618 Ok(())
619 }
620
621 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
622 async fn test_send_sync_msg_enables_bccself() -> Result<()> {
623 for (chatmail, sync_message_sent) in
624 [(false, false), (false, true), (true, false), (true, true)]
625 {
626 let alice1 = TestContext::new_alice().await;
627 let alice2 = TestContext::new_alice().await;
628
629 alice1.set_config_bool(Config::SyncMsgs, true).await?;
632 alice2.set_config_bool(Config::SyncMsgs, true).await?;
633
634 if chatmail {
635 alice1.set_config_bool(Config::IsChatmail, true).await?;
636 alice2.set_config_bool(Config::IsChatmail, true).await?;
637 } else {
638 alice2.set_config_bool(Config::BccSelf, false).await?;
639 }
640
641 alice1.set_config_bool(Config::BccSelf, true).await?;
642
643 let sent_msg = if sync_message_sent {
644 alice1
645 .add_sync_item(SyncData::AddQrToken(QrTokenData {
646 invitenumber: "in".to_string(),
647 auth: "testtoken".to_string(),
648 grpid: None,
649 }))
650 .await?;
651 alice1.send_sync_msg().await?.unwrap();
652 alice1.pop_sent_sync_msg().await
653 } else {
654 let chat = alice1.get_self_chat().await;
655 alice1.send_text(chat.id, "Hi").await
656 };
657
658 assert_eq!(alice2.get_config_bool(Config::BccSelf).await?, false);
663
664 alice2.recv_msg_opt(&sent_msg).await;
665 assert_eq!(
666 alice2.get_config_bool(Config::BccSelf).await?,
667 sync_message_sent
672 );
673 }
674 Ok(())
675 }
676
677 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
678 async fn test_bot_no_sync_msgs() -> Result<()> {
679 let mut tcm = TestContextManager::new();
680 let alice = &tcm.alice().await;
681 let bob = &tcm.bob().await;
682 alice.set_config_bool(Config::SyncMsgs, true).await?;
683 let chat_id = alice.create_chat(bob).await.id;
684
685 chat::send_text_msg(alice, chat_id, "hi".to_string()).await?;
686 alice
687 .set_config(Config::Displayname, Some("Alice Human"))
688 .await?;
689 alice.send_sync_msg().await?;
690 alice.pop_sent_sync_msg().await;
691 let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
692 assert_eq!(msg.text, "hi");
693
694 alice.set_config_bool(Config::Bot, true).await?;
695 chat::send_text_msg(alice, chat_id, "hi".to_string()).await?;
696 alice
697 .set_config(Config::Displayname, Some("Alice Bot"))
698 .await?;
699 let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
700 assert_eq!(msg.text, "hi");
701 Ok(())
702 }
703
704 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
705 async fn test_unpromoted_group_qr_sync() -> Result<()> {
706 let mut tcm = TestContextManager::new();
707 let alice = &tcm.alice().await;
708 alice.set_config_bool(Config::SyncMsgs, true).await?;
709 let alice_chatid =
710 chat::create_group_chat(alice, ProtectionStatus::Protected, "the chat").await?;
711 let qr = get_securejoin_qr(alice, Some(alice_chatid)).await?;
712
713 let alice2 = &tcm.alice().await;
715 alice2.set_config_bool(Config::SyncMsgs, true).await?;
716 test_utils::sync(alice, alice2).await;
717
718 let bob = &tcm.bob().await;
719 tcm.exec_securejoin_qr(bob, alice, &qr).await;
720 let msg_id = alice.send_sync_msg().await?;
721 assert!(msg_id.is_some());
725 let sent = alice.pop_sent_sync_msg().await;
726 let msg = alice.parse_msg(&sent).await;
727 let mut sync_items = msg.sync_items.unwrap().items;
728 assert_eq!(sync_items.len(), 1);
729 let data = sync_items.pop().unwrap().data;
730 let SyncDataOrUnknown::SyncData(AddQrToken(_)) = data else {
731 unreachable!();
732 };
733
734 let alice_bob_id = alice.add_or_lookup_contact(bob).await.id;
736 remove_contact_from_chat(alice, alice_chatid, alice_bob_id).await?;
737 alice.pop_sent_msg().await;
738 let sent = alice
739 .send_text(alice_chatid, "Promoting group to another device")
740 .await;
741 alice2.recv_msg(&sent).await;
742
743 let fiona = &tcm.fiona().await;
744 tcm.exec_securejoin_qr(fiona, alice2, &qr).await;
745 let msg = fiona.get_last_msg().await;
746 assert_eq!(msg.text, "Member Me added by alice@example.org.");
747 Ok(())
748 }
749}