deltachat/
sync.rs

1//! # Synchronize items between devices.
2
3use 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 as _, warn};
13use crate::login_param::EnteredLoginParam;
14use crate::message::{Message, MsgId, Viewtype};
15use crate::mimeparser::SystemMessage;
16use crate::param::Param;
17use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
18use crate::token::Namespace;
19use crate::tools::time;
20use crate::transport::{ConfiguredLoginParamJson, sync_transports};
21use crate::{message, stock_str, token};
22use std::collections::HashSet;
23
24/// Whether to send device sync messages. Aimed for usage in the internal API.
25#[derive(Debug, PartialEq)]
26pub(crate) enum Sync {
27    Nosync,
28    Sync,
29}
30
31impl From<Sync> for bool {
32    fn from(sync: Sync) -> bool {
33        match sync {
34            Sync::Nosync => false,
35            Sync::Sync => true,
36        }
37    }
38}
39
40impl From<bool> for Sync {
41    fn from(sync: bool) -> Sync {
42        match sync {
43            false => Sync::Nosync,
44            true => Sync::Sync,
45        }
46    }
47}
48
49#[derive(Debug, Serialize, Deserialize)]
50pub(crate) struct QrTokenData {
51    pub(crate) invitenumber: String,
52    pub(crate) auth: String,
53    pub(crate) grpid: Option<String>,
54}
55
56#[derive(Debug, Serialize, Deserialize)]
57pub(crate) struct TransportData {
58    /// Configured login parameters.
59    pub(crate) configured: ConfiguredLoginParamJson,
60
61    /// Login parameters entered by the user.
62    ///
63    /// They can be used to reconfigure the transport.
64    pub(crate) entered: EnteredLoginParam,
65
66    /// Timestamp of when the transport was last time (re)configured.
67    pub(crate) timestamp: i64,
68}
69
70#[derive(Debug, Serialize, Deserialize)]
71pub(crate) struct RemovedTransportData {
72    /// Address of the removed transport.
73    pub(crate) addr: String,
74
75    /// Timestamp of when the transport was removed.
76    pub(crate) timestamp: i64,
77}
78
79#[derive(Debug, Serialize, Deserialize)]
80pub(crate) enum SyncData {
81    AddQrToken(QrTokenData),
82    DeleteQrToken(QrTokenData),
83    AlterChat {
84        id: chat::SyncId,
85        action: chat::SyncAction,
86    },
87    Config {
88        key: Config,
89        val: String,
90    },
91    SaveMessage {
92        src: String,  // RFC724 id (i.e. "Message-Id" header)
93        dest: String, // RFC724 id (i.e. "Message-Id" header)
94    },
95    DeleteMessages {
96        msgs: Vec<String>, // RFC724 id (i.e. "Message-Id" header)
97    },
98
99    /// Update transport configuration.
100    ///
101    /// This message contains a list of all added transports
102    /// together with their addition timestamp,
103    /// and all removed transports together with
104    /// the removal timestamp.
105    ///
106    /// In case of a tie, addition and removal timestamps
107    /// being the same, removal wins.
108    /// It is more likely that transport is added
109    /// and then removed within a second,
110    /// but unlikely the other way round
111    /// as adding new transport takes time
112    /// to run configuration.
113    Transports {
114        /// Active transports.
115        transports: Vec<TransportData>,
116
117        /// Removed transports with the timestamp of removal.
118        removed_transports: Vec<RemovedTransportData>,
119    },
120}
121
122#[derive(Debug, Serialize, Deserialize)]
123#[serde(untagged)]
124pub(crate) enum SyncDataOrUnknown {
125    SyncData(SyncData),
126    Unknown(serde_json::Value),
127}
128
129#[derive(Debug, Serialize, Deserialize)]
130pub(crate) struct SyncItem {
131    timestamp: i64,
132
133    data: SyncDataOrUnknown,
134}
135
136#[derive(Debug, Deserialize)]
137pub(crate) struct SyncItems {
138    items: Vec<SyncItem>,
139}
140
141impl From<SyncData> for SyncDataOrUnknown {
142    fn from(sync_data: SyncData) -> Self {
143        Self::SyncData(sync_data)
144    }
145}
146
147impl Context {
148    /// Adds an item to the list of items that should be synchronized to other devices.
149    ///
150    /// NB: Private and `pub(crate)` functions shouldn't call this unless `Sync::Sync` is explicitly
151    /// passed to them. This way it's always clear whether the code performs synchronisation.
152    pub(crate) async fn add_sync_item(&self, data: SyncData) -> Result<()> {
153        self.add_sync_item_with_timestamp(data, time()).await
154    }
155
156    /// Adds item and timestamp to the list of items that should be synchronized to other devices.
157    /// If device synchronization is disabled, the function does nothing.
158    async fn add_sync_item_with_timestamp(&self, data: SyncData, timestamp: i64) -> Result<()> {
159        if !self.should_send_sync_msgs().await? {
160            return Ok(());
161        }
162
163        let item = SyncItem {
164            timestamp,
165            data: data.into(),
166        };
167        let item = serde_json::to_string(&item)?;
168        self.sql
169            .execute("INSERT INTO multi_device_sync (item) VALUES(?);", (item,))
170            .await?;
171
172        Ok(())
173    }
174
175    /// Adds most recent qr-code tokens for the given group or self-contact to the list of items to
176    /// be synced. If device synchronization is disabled,
177    /// no tokens exist or the chat is unpromoted, the function does nothing.
178    /// The caller should call `SchedulerState::interrupt_inbox()` on its own to trigger sending.
179    pub(crate) async fn sync_qr_code_tokens(&self, grpid: Option<&str>) -> Result<()> {
180        if !self.should_send_sync_msgs().await? {
181            return Ok(());
182        }
183        if let (Some(invitenumber), Some(auth)) = (
184            token::lookup(self, Namespace::InviteNumber, grpid).await?,
185            token::lookup(self, Namespace::Auth, grpid).await?,
186        ) {
187            self.add_sync_item(SyncData::AddQrToken(QrTokenData {
188                invitenumber,
189                auth,
190                grpid: grpid.map(|s| s.to_string()),
191            }))
192            .await?;
193        }
194        Ok(())
195    }
196
197    /// Adds deleted qr-code token to the list of items to be synced
198    /// so that the token also gets deleted on the other devices.
199    /// This interrupts SMTP on its own.
200    pub(crate) async fn sync_qr_code_token_deletion(
201        &self,
202        invitenumber: String,
203        auth: String,
204    ) -> Result<()> {
205        self.add_sync_item(SyncData::DeleteQrToken(QrTokenData {
206            invitenumber,
207            auth,
208            grpid: None,
209        }))
210        .await?;
211        self.scheduler.interrupt_inbox().await;
212        Ok(())
213    }
214
215    /// Sends out a self-sent message with items to be synchronized, if any.
216    ///
217    /// Mustn't be called from multiple tasks in parallel to avoid sending the same sync items twice
218    /// because sync items are removed from the db only after successful sending. We guarantee this
219    /// by calling `send_sync_msg()` only from the inbox loop.
220    pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
221        if let Some((json, ids)) = self.build_sync_json().await? {
222            let chat_id =
223                ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
224                    .await?;
225            let mut msg = Message {
226                chat_id,
227                viewtype: Viewtype::Text,
228                text: stock_str::sync_msg_body(self).await,
229                hidden: true,
230                subject: stock_str::sync_msg_subject(self).await,
231                ..Default::default()
232            };
233            msg.param.set_cmd(SystemMessage::MultiDeviceSync);
234            msg.param.set(Param::Arg, json);
235            msg.param.set(Param::Arg2, ids);
236            msg.param.set_int(Param::GuaranteeE2ee, 1);
237            Ok(Some(chat::send_msg(self, chat_id, &mut msg).await?))
238        } else {
239            Ok(None)
240        }
241    }
242
243    /// Copies all sync items to a JSON string and clears the sync-table.
244    /// Returns the JSON string and a comma-separated string of the IDs used.
245    pub(crate) async fn build_sync_json(&self) -> Result<Option<(String, String)>> {
246        let (ids, serialized) = self
247            .sql
248            .query_map(
249                "SELECT id, item FROM multi_device_sync ORDER BY id;",
250                (),
251                |row| {
252                    let id: u32 = row.get(0)?;
253                    let item: String = row.get(1)?;
254                    Ok((id, item))
255                },
256                |rows| {
257                    let mut ids = vec![];
258                    let mut serialized = String::default();
259                    for row in rows {
260                        let (id, item) = row?;
261                        ids.push(id);
262                        if !serialized.is_empty() {
263                            serialized.push_str(",\n");
264                        }
265                        serialized.push_str(&item);
266                    }
267                    Ok((ids, serialized))
268                },
269            )
270            .await?;
271
272        if ids.is_empty() {
273            Ok(None)
274        } else {
275            Ok(Some((
276                format!("{{\"items\":[\n{serialized}\n]}}"),
277                ids.iter()
278                    .map(|x| x.to_string())
279                    .collect::<Vec<String>>()
280                    .join(","),
281            )))
282        }
283    }
284
285    pub(crate) fn build_sync_part(&self, json: String) -> MimePart<'static> {
286        MimePart::new("application/json", json).attachment("multi-device-sync.json")
287    }
288
289    /// Takes a JSON string created by `build_sync_json()`
290    /// and construct `SyncItems` from it.
291    pub(crate) fn parse_sync_items(&self, serialized: String) -> Result<SyncItems> {
292        let sync_items: SyncItems = serde_json::from_str(&serialized)?;
293        Ok(sync_items)
294    }
295
296    /// Executes sync items sent by other device.
297    ///
298    /// CAVE: When changing the code to handle other sync items,
299    /// take care that does not result in calls to `add_sync_item()`
300    /// as otherwise we would add in a dead-loop between two devices
301    /// sending message back and forth.
302    ///
303    /// If an error is returned, the caller shall not try over because some sync items could be
304    /// already executed. Sync items are considered independent and executed in the given order but
305    /// regardless of whether executing of the previous items succeeded.
306    pub(crate) async fn execute_sync_items(&self, items: &SyncItems, timestamp_sent: i64) {
307        info!(self, "executing {} sync item(s)", items.items.len());
308        for item in &items.items {
309            // Limit the timestamp to ensure it is not in the future.
310            //
311            // `sent_timestamp` should be already corrected
312            // if the `Date` header is in the future.
313            let timestamp = std::cmp::min(item.timestamp, timestamp_sent);
314
315            match &item.data {
316                SyncDataOrUnknown::SyncData(data) => match data {
317                    AddQrToken(token) => self.add_qr_token(token, timestamp).await,
318                    DeleteQrToken(token) => self.delete_qr_token(token, timestamp).await,
319                    AlterChat { id, action } => self.sync_alter_chat(id, action).await,
320                    SyncData::Config { key, val } => self.sync_config(key, val).await,
321                    SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await,
322                    SyncData::DeleteMessages { msgs } => self.sync_message_deletion(msgs).await,
323                    SyncData::Transports {
324                        transports,
325                        removed_transports,
326                    } => sync_transports(self, transports, removed_transports).await,
327                },
328                SyncDataOrUnknown::Unknown(data) => {
329                    warn!(self, "Ignored unknown sync item: {data}.");
330                    Ok(())
331                }
332            }
333            .log_err(self)
334            .ok();
335        }
336
337        // Since there was a sync message, we know that there is a second device.
338        // Set BccSelf to true if it isn't already.
339        if !items.items.is_empty() && !self.get_config_bool(Config::BccSelf).await.unwrap_or(true) {
340            self.set_config_ex(Sync::Nosync, Config::BccSelf, Some("1"))
341                .await
342                .log_err(self)
343                .ok();
344        }
345    }
346
347    async fn add_qr_token(&self, token: &QrTokenData, timestamp: i64) -> Result<()> {
348        let grpid = token.grpid.as_deref();
349        token::save(
350            self,
351            Namespace::InviteNumber,
352            grpid,
353            &token.invitenumber,
354            timestamp,
355        )
356        .await?;
357        token::save(self, Namespace::Auth, grpid, &token.auth, timestamp).await?;
358        Ok(())
359    }
360
361    async fn delete_qr_token(&self, token: &QrTokenData, timestamp: i64) -> Result<()> {
362        self.sql
363            .execute(
364                "DELETE FROM tokens
365                 WHERE foreign_key IN
366                 (SELECT foreign_key FROM tokens
367                  WHERE token=? OR token=? AND timestamp <= ?)",
368                (&token.invitenumber, &token.auth, timestamp),
369            )
370            .await?;
371        Ok(())
372    }
373
374    async fn save_message(&self, src_rfc724_mid: &str, dest_rfc724_mid: &String) -> Result<()> {
375        if let Some(src_msg_id) = message::rfc724_mid_exists(self, src_rfc724_mid).await? {
376            chat::save_copy_in_self_talk(self, src_msg_id, dest_rfc724_mid).await?;
377        }
378        Ok(())
379    }
380
381    async fn sync_message_deletion(&self, msgs: &Vec<String>) -> Result<()> {
382        let mut modified_chat_ids = HashSet::new();
383        let mut msg_ids = Vec::new();
384        for rfc724_mid in msgs {
385            if let Some(msg_id) = message::rfc724_mid_exists(self, rfc724_mid).await? {
386                if let Some(msg) = Message::load_from_db_optional(self, msg_id).await? {
387                    message::delete_msg_locally(self, &msg).await?;
388                    msg_ids.push(msg.id);
389                    modified_chat_ids.insert(msg.chat_id);
390                } else {
391                    warn!(self, "Sync message delete: Database entry does not exist.");
392                }
393            } else {
394                warn!(self, "Sync message delete: {rfc724_mid:?} not found.");
395            }
396        }
397        message::delete_msgs_locally_done(self, &msg_ids, modified_chat_ids).await?;
398        Ok(())
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use std::time::Duration;
405
406    use anyhow::bail;
407
408    use super::*;
409    use crate::chat::{Chat, remove_contact_from_chat};
410    use crate::chatlist::Chatlist;
411    use crate::contact::{Contact, Origin};
412    use crate::securejoin::get_securejoin_qr;
413    use crate::test_utils::{self, TestContext, TestContextManager};
414    use crate::tools::SystemTime;
415
416    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
417    async fn test_config_sync_msgs() -> Result<()> {
418        let t = TestContext::new_alice().await;
419        assert_eq!(t.get_config_bool(Config::SyncMsgs).await?, false);
420        assert_eq!(t.get_config_bool(Config::BccSelf).await?, true);
421        assert_eq!(t.should_send_sync_msgs().await?, false);
422
423        t.set_config_bool(Config::SyncMsgs, true).await?;
424        assert_eq!(t.get_config_bool(Config::SyncMsgs).await?, true);
425        assert_eq!(t.get_config_bool(Config::BccSelf).await?, true);
426        assert_eq!(t.should_send_sync_msgs().await?, true);
427
428        t.set_config_bool(Config::BccSelf, false).await?;
429        assert_eq!(t.get_config_bool(Config::SyncMsgs).await?, true);
430        assert_eq!(t.get_config_bool(Config::BccSelf).await?, false);
431        assert_eq!(t.should_send_sync_msgs().await?, false);
432
433        t.set_config_bool(Config::SyncMsgs, false).await?;
434        assert_eq!(t.get_config_bool(Config::SyncMsgs).await?, false);
435        assert_eq!(t.get_config_bool(Config::BccSelf).await?, false);
436        assert_eq!(t.should_send_sync_msgs().await?, false);
437        Ok(())
438    }
439
440    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
441    async fn test_build_sync_json() -> Result<()> {
442        let t = TestContext::new_alice().await;
443        t.set_config_bool(Config::SyncMsgs, true).await?;
444
445        assert!(t.build_sync_json().await?.is_none());
446
447        // Having one test on `SyncData::AlterChat` is sufficient here as
448        // `chat::SyncAction::SetMuted` introduces enums inside items and `SystemTime`. Let's avoid
449        // in-depth testing of the serialiser here which is an external crate.
450        t.add_sync_item_with_timestamp(
451            SyncData::AlterChat {
452                id: chat::SyncId::ContactAddr("bob@example.net".to_string()),
453                action: chat::SyncAction::SetMuted(chat::MuteDuration::Until(
454                    SystemTime::UNIX_EPOCH + Duration::from_millis(42999),
455                )),
456            },
457            1631781315,
458        )
459        .await?;
460
461        t.add_sync_item_with_timestamp(
462            SyncData::AddQrToken(QrTokenData {
463                invitenumber: "testinvite".to_string(),
464                auth: "testauth".to_string(),
465                grpid: Some("group123".to_string()),
466            }),
467            1631781316,
468        )
469        .await?;
470        t.add_sync_item_with_timestamp(
471            SyncData::DeleteQrToken(QrTokenData {
472                invitenumber: "123!?\":.;{}".to_string(),
473                auth: "456".to_string(),
474                grpid: None,
475            }),
476            1631781317,
477        )
478        .await?;
479
480        let (serialized, ids) = t.build_sync_json().await?.unwrap();
481        assert_eq!(
482            serialized,
483            r#"{"items":[
484{"timestamp":1631781315,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":{"SetMuted":{"Until":{"secs_since_epoch":42,"nanos_since_epoch":999000000}}}}}},
485{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"testinvite","auth":"testauth","grpid":"group123"}}},
486{"timestamp":1631781317,"data":{"DeleteQrToken":{"invitenumber":"123!?\":.;{}","auth":"456","grpid":null}}}
487]}"#
488        );
489
490        assert!(t.build_sync_json().await?.is_some());
491        t.sql
492            .execute(
493                &format!("DELETE FROM multi_device_sync WHERE id IN ({ids})"),
494                (),
495            )
496            .await?;
497        assert!(t.build_sync_json().await?.is_none());
498
499        let sync_items = t.parse_sync_items(serialized)?;
500        assert_eq!(sync_items.items.len(), 3);
501
502        Ok(())
503    }
504
505    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
506    async fn test_build_sync_json_sync_msgs_off() -> Result<()> {
507        let t = TestContext::new_alice().await;
508        t.set_config_bool(Config::SyncMsgs, false).await?;
509        t.add_sync_item(SyncData::AddQrToken(QrTokenData {
510            invitenumber: "testinvite".to_string(),
511            auth: "testauth".to_string(),
512            grpid: Some("group123".to_string()),
513        }))
514        .await?;
515        assert!(t.build_sync_json().await?.is_none());
516        Ok(())
517    }
518
519    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
520    async fn test_parse_sync_items() -> Result<()> {
521        let t = TestContext::new_alice().await;
522
523        assert!(t.parse_sync_items(r#"{bad json}"#.to_string()).is_err());
524
525        assert!(t.parse_sync_items(r#"{"badname":[]}"#.to_string()).is_err());
526
527        for bad_item_example in [
528            r#"{"items":[{"timestamp":1631781316,"data":{"BadItem":{"invitenumber":"in","auth":"a","grpid":null}}}]}"#,
529            r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":123}}}]}"#, // `123` is invalid for `String`
530            r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":true}}}]}"#, // `true` is invalid for `String`
531            r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":[]}}}]}"#, // `[]` is invalid for `String`
532            r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":{}}}}]}"#, // `{}` is invalid for `String`
533            r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","grpid":null}}}]}"#, // missing field
534            r#"{"items":[{"timestamp":1631781316,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Burn"}}}]}"#, // Unknown enum value
535        ] {
536            let sync_items = t.parse_sync_items(bad_item_example.to_string()).unwrap();
537            assert_eq!(sync_items.items.len(), 1);
538            assert!(matches!(sync_items.items[0].timestamp, 1631781316));
539            assert!(matches!(
540                sync_items.items[0].data,
541                SyncDataOrUnknown::Unknown(_)
542            ));
543        }
544
545        // Test enums inside items and SystemTime
546        let sync_items = t.parse_sync_items(
547            r#"{"items":[{"timestamp":1631781318,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":{"SetMuted":{"Until":{"secs_since_epoch":42,"nanos_since_epoch":999000000}}}}}}]}"#.to_string(),
548        )?;
549        assert_eq!(sync_items.items.len(), 1);
550        let SyncDataOrUnknown::SyncData(AlterChat { id, action }) =
551            &sync_items.items.first().unwrap().data
552        else {
553            bail!("bad item");
554        };
555        assert_eq!(
556            *id,
557            chat::SyncId::ContactAddr("bob@example.net".to_string())
558        );
559        assert_eq!(
560            *action,
561            chat::SyncAction::SetMuted(chat::MuteDuration::Until(
562                SystemTime::UNIX_EPOCH + Duration::from_millis(42999)
563            ))
564        );
565
566        // empty item list is okay
567        assert_eq!(
568            t.parse_sync_items(r#"{"items":[]}"#.to_string())?
569                .items
570                .len(),
571            0
572        );
573
574        // to allow forward compatibility, additional fields should not break parsing
575        let sync_items = t
576            .parse_sync_items(
577                r#"{"items":[
578{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"yip","grpid":null}}},
579{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip","additional":123,"grpid":null}}}
580]}"#
581                .to_string(),
582            )
583            ?;
584        assert_eq!(sync_items.items.len(), 2);
585
586        let sync_items = t.parse_sync_items(
587            r#"{"items":[
588{"timestamp":1631781318,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip","grpid":null}}}
589],"additional":"field"}"#
590                .to_string(),
591        )?;
592
593        assert_eq!(sync_items.items.len(), 1);
594        if let SyncDataOrUnknown::SyncData(AddQrToken(token)) =
595            &sync_items.items.first().unwrap().data
596        {
597            assert_eq!(token.invitenumber, "in");
598            assert_eq!(token.auth, "yip");
599            assert_eq!(token.grpid, None);
600        } else {
601            bail!("bad item");
602        }
603
604        // to allow backward compatibility, missing `Option<>` should not break parsing
605        let sync_items = t.parse_sync_items(
606               r#"{"items":[{"timestamp":1631781319,"data":{"AddQrToken":{"invitenumber":"in","auth":"a"}}}]}"#.to_string(),
607           )
608           ?;
609        assert_eq!(sync_items.items.len(), 1);
610
611        Ok(())
612    }
613
614    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
615    async fn test_execute_sync_items() -> Result<()> {
616        let t = TestContext::new_alice().await;
617
618        assert!(!token::exists(&t, Namespace::Auth, "yip-auth").await?);
619
620        let timestamp_sent = time();
621        let sync_items = t
622            .parse_sync_items(
623                r#"{"items":[
624{"timestamp":1631781315,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Block"}}},
625{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"yip-in","auth":"a"}}},
626{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"delete unexistent, shall continue"}}},
627{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip-auth"}}},
628{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"foo","grpid":"non-existent"}}},
629{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"directly deleted"}}},
630{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"directly deleted"}}}
631]}"#
632                .to_string(),
633            )
634            ?;
635        t.execute_sync_items(&sync_items, timestamp_sent).await;
636
637        assert!(
638            Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Unknown)
639                .await?
640                .is_none()
641        );
642        assert!(!token::exists(&t, Namespace::InviteNumber, "yip-in").await?);
643        assert!(!token::exists(&t, Namespace::Auth, "yip-auth").await?);
644        assert!(!token::exists(&t, Namespace::Auth, "non-existent").await?);
645        assert!(!token::exists(&t, Namespace::Auth, "directly deleted").await?);
646
647        Ok(())
648    }
649
650    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
651    async fn test_send_sync_msg() -> Result<()> {
652        let alice = TestContext::new_alice().await;
653        alice.set_config_bool(Config::SyncMsgs, true).await?;
654        alice
655            .add_sync_item(SyncData::AddQrToken(QrTokenData {
656                invitenumber: "in".to_string(),
657                auth: "testtoken".to_string(),
658                grpid: None,
659            }))
660            .await?;
661        let msg_id = alice.send_sync_msg().await?.unwrap();
662        let msg = Message::load_from_db(&alice, msg_id).await?;
663        let chat = Chat::load_from_db(&alice, msg.chat_id).await?;
664        assert!(chat.is_self_talk());
665
666        // check that the used self-talk is not visible to the user
667        // but that creation will still work (in this case, the chat is empty)
668        assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
669        let chat_id = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
670        let chat = Chat::load_from_db(&alice, chat_id).await?;
671        assert!(chat.is_self_talk());
672        assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
673        let msgs = chat::get_chat_msgs(&alice, chat_id).await?;
674        assert_eq!(msgs.len(), 0);
675
676        // let alice's other device receive and execute the sync message,
677        // also here, self-talk should stay hidden
678        let sent_msg = alice.pop_sent_sync_msg().await;
679        let alice2 = TestContext::new_alice().await;
680        alice2.set_config_bool(Config::SyncMsgs, true).await?;
681        alice2.recv_msg_trash(&sent_msg).await;
682        assert!(token::exists(&alice2, token::Namespace::Auth, "testtoken").await?);
683        assert_eq!(Chatlist::try_load(&alice2, 0, None, None).await?.len(), 0);
684
685        // Sync messages are "auto-generated", but they mustn't make the self-contact a bot.
686        let self_contact = alice2.add_or_lookup_contact(&alice2).await;
687        assert!(!self_contact.is_bot());
688
689        // the same sync message sent to bob must not be executed
690        let bob = TestContext::new_bob().await;
691        bob.recv_msg_trash(&sent_msg).await;
692        assert!(!token::exists(&bob, token::Namespace::Auth, "testtoken").await?);
693
694        Ok(())
695    }
696
697    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
698    async fn test_send_sync_msg_enables_bccself() -> Result<()> {
699        for (chatmail, sync_message_sent) in
700            [(false, false), (false, true), (true, false), (true, true)]
701        {
702            let alice1 = TestContext::new_alice().await;
703            let alice2 = TestContext::new_alice().await;
704
705            // SyncMsgs defaults to true on real devices, but in tests it defaults to false,
706            // so we need to enable it
707            alice1.set_config_bool(Config::SyncMsgs, true).await?;
708            alice2.set_config_bool(Config::SyncMsgs, true).await?;
709
710            alice1.set_config_bool(Config::IsChatmail, chatmail).await?;
711            alice2.set_config_bool(Config::IsChatmail, chatmail).await?;
712
713            alice1.set_config_bool(Config::BccSelf, true).await?;
714            alice2.set_config_bool(Config::BccSelf, false).await?;
715
716            let sent_msg = if sync_message_sent {
717                alice1
718                    .add_sync_item(SyncData::AddQrToken(QrTokenData {
719                        invitenumber: "in".to_string(),
720                        auth: "testtoken".to_string(),
721                        grpid: None,
722                    }))
723                    .await?;
724                alice1.send_sync_msg().await?.unwrap();
725                alice1.pop_sent_sync_msg().await
726            } else {
727                let chat = alice1.get_self_chat().await;
728                alice1.send_text(chat.id, "Hi").await
729            };
730
731            // On chatmail accounts, BccSelf defaults to false.
732            // When receiving a sync message from another device,
733            // there obviously is a multi-device-setup, and BccSelf
734            // should be enabled.
735            assert_eq!(alice2.get_config_bool(Config::BccSelf).await?, false);
736
737            alice2.recv_msg_opt(&sent_msg).await;
738            assert_eq!(
739                alice2.get_config_bool(Config::BccSelf).await?,
740                // BccSelf should be enabled when receiving a sync message,
741                // but not when receiving another outgoing message
742                // because we might have forgotten it and it then it might have been forwarded to us again
743                // (though of course this is very unlikely).
744                sync_message_sent
745            );
746        }
747        Ok(())
748    }
749
750    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
751    async fn test_bot_no_sync_msgs() -> Result<()> {
752        let mut tcm = TestContextManager::new();
753        let alice = &tcm.alice().await;
754        let bob = &tcm.bob().await;
755        alice.set_config_bool(Config::SyncMsgs, true).await?;
756        let chat_id = alice.create_chat(bob).await.id;
757
758        chat::send_text_msg(alice, chat_id, "hi".to_string()).await?;
759        alice
760            .set_config(Config::Displayname, Some("Alice Human"))
761            .await?;
762        alice.send_sync_msg().await?;
763        alice.pop_sent_sync_msg().await;
764        let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
765        assert_eq!(msg.text, "hi");
766
767        alice.set_config_bool(Config::Bot, true).await?;
768        chat::send_text_msg(alice, chat_id, "hi".to_string()).await?;
769        alice
770            .set_config(Config::Displayname, Some("Alice Bot"))
771            .await?;
772        let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
773        assert_eq!(msg.text, "hi");
774        Ok(())
775    }
776
777    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
778    async fn test_unpromoted_group_qr_sync() -> Result<()> {
779        let mut tcm = TestContextManager::new();
780        let alice = &tcm.alice().await;
781        alice.set_config_bool(Config::SyncMsgs, true).await?;
782        let alice_chatid = chat::create_group(alice, "the chat").await?;
783        let qr = get_securejoin_qr(alice, Some(alice_chatid)).await?;
784
785        // alice2 syncs the QR code token.
786        let alice2 = &tcm.alice().await;
787        alice2.set_config_bool(Config::SyncMsgs, true).await?;
788        test_utils::sync(alice, alice2).await;
789
790        let bob = &tcm.bob().await;
791        tcm.exec_securejoin_qr(bob, alice, &qr).await;
792        let msg_id = alice.send_sync_msg().await?;
793        // Core <= v1.143 doesn't sync QR code tokens immediately, so current Core does that when a
794        // group is promoted for compatibility (because the group could be created by older Core).
795        // TODO: assert!(msg_id.is_none());
796        assert!(msg_id.is_some());
797        let sent = alice.pop_sent_sync_msg().await;
798        let msg = alice.parse_msg(&sent).await;
799        let mut sync_items = msg.sync_items.unwrap().items;
800        assert_eq!(sync_items.len(), 1);
801        let data = sync_items.pop().unwrap().data;
802        let SyncDataOrUnknown::SyncData(AddQrToken(_)) = data else {
803            unreachable!();
804        };
805
806        // Remove Bob because alice2 doesn't have their key.
807        let alice_bob_id = alice.add_or_lookup_contact(bob).await.id;
808        remove_contact_from_chat(alice, alice_chatid, alice_bob_id).await?;
809        alice.pop_sent_msg().await;
810        let sent = alice
811            .send_text(alice_chatid, "Promoting group to another device")
812            .await;
813        alice2.recv_msg(&sent).await;
814
815        let fiona = &tcm.fiona().await;
816        tcm.exec_securejoin_qr(fiona, alice2, &qr).await;
817        let msg = fiona.get_last_msg().await;
818        assert_eq!(msg.text, "Member Me added by alice@example.org.");
819        Ok(())
820    }
821}