mirror of
https://github.com/chatmail/core.git
synced 2026-05-22 16:26:31 +03:00
add multi-device sync (#2669)
* add basic multi-device-sync functions * generate json * add context.parse_sync_items() * add context.execute_sync_items() * piggyback sync-commands message, add body for human-readable part * avoid double json renderings * mimeparser parses incoming .json sync-files * do not piggyback sync-files * execute sync items * return status of send_sync_msg() * send sync messages as multipart/report * add a per-item-timestamp and also allow adding other per-item-fields in the future * if the self-chat does not exist, create it blocked/hidden * create tokens closer to real qr-code needs * respect bcc_self setting, add test for that * sync qr code tokens after promoting groups * send sync-messages only if an experimental switch is set * trigger send_sync_msg() after sending messages and after creating/redraw/revive qr-code * add DC_STR_* constants to deltachat.h * adapt to refactored qr module as of #2729 * tweak test * use SendSyncMsgs config name instead of SendExperimentalSyncMsgs - we can remove or rename the config nevertheless, but have the option to keep it without renaming * tweak docs * remove currently unused effective timestamp calculation * clarify when send_sync_msg() is called * make sure, sync-messages are encrypted and are sent by SELF * tweak docs, fix typos
This commit is contained in:
@@ -5959,6 +5959,21 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// `%1$s` will be replaced by human-readable date and time.
|
/// `%1$s` will be replaced by human-readable date and time.
|
||||||
#define DC_STR_DOWNLOAD_AVAILABILITY 100
|
#define DC_STR_DOWNLOAD_AVAILABILITY 100
|
||||||
|
|
||||||
|
/// "Multi Device Synchronization"
|
||||||
|
///
|
||||||
|
/// Used in subjects of outgoing sync messages.
|
||||||
|
#define DC_STR_SYNC_MSG_SUBJECT 101
|
||||||
|
|
||||||
|
/// "This message is used to synchronize data between your devices."
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// Used as message text of outgoing sync messages.
|
||||||
|
/// The text is visible in non-dc-muas or in outdated Delta Chat versions,
|
||||||
|
/// the default text therefore adds the following hint:
|
||||||
|
/// "If you see this message in Delta Chat,
|
||||||
|
/// please update your Delta Chat apps on all devices."
|
||||||
|
#define DC_STR_SYNC_MSG_BODY 102
|
||||||
|
|
||||||
/// "Incoming Messages"
|
/// "Incoming Messages"
|
||||||
///
|
///
|
||||||
/// Used as a headline in the connectivity view.
|
/// Used as a headline in the connectivity view.
|
||||||
|
|||||||
@@ -386,6 +386,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
sendsticker <file> [<text>]\n\
|
sendsticker <file> [<text>]\n\
|
||||||
sendfile <file> [<text>]\n\
|
sendfile <file> [<text>]\n\
|
||||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||||
|
sendsyncmsg\n\
|
||||||
videochat\n\
|
videochat\n\
|
||||||
draft [<text>]\n\
|
draft [<text>]\n\
|
||||||
devicemsg <text>\n\
|
devicemsg <text>\n\
|
||||||
@@ -895,6 +896,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
}));
|
}));
|
||||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||||
}
|
}
|
||||||
|
"sendsyncmsg" => match context.send_sync_msg().await? {
|
||||||
|
Some(msg_id) => println!("sync message sent as {}.", msg_id),
|
||||||
|
None => println!("sync message not needed."),
|
||||||
|
},
|
||||||
"videochat" => {
|
"videochat" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ const DB_COMMANDS: [&str; 10] = [
|
|||||||
"housekeeping",
|
"housekeeping",
|
||||||
];
|
];
|
||||||
|
|
||||||
const CHAT_COMMANDS: [&str; 34] = [
|
const CHAT_COMMANDS: [&str; 35] = [
|
||||||
"listchats",
|
"listchats",
|
||||||
"listarchived",
|
"listarchived",
|
||||||
"chat",
|
"chat",
|
||||||
@@ -188,6 +188,7 @@ const CHAT_COMMANDS: [&str; 34] = [
|
|||||||
"sendimage",
|
"sendimage",
|
||||||
"sendfile",
|
"sendfile",
|
||||||
"sendhtml",
|
"sendhtml",
|
||||||
|
"sendsyncmsg",
|
||||||
"videochat",
|
"videochat",
|
||||||
"draft",
|
"draft",
|
||||||
"listmedia",
|
"listmedia",
|
||||||
|
|||||||
22
src/chat.rs
22
src/chat.rs
@@ -171,6 +171,17 @@ impl ChatId {
|
|||||||
/// This should be used when **a user action** creates a chat 1:1, it ensures the chat
|
/// This should be used when **a user action** creates a chat 1:1, it ensures the chat
|
||||||
/// exists and is unblocked and scales the [`Contact`]'s origin.
|
/// exists and is unblocked and scales the [`Contact`]'s origin.
|
||||||
pub async fn create_for_contact(context: &Context, contact_id: u32) -> Result<Self> {
|
pub async fn create_for_contact(context: &Context, contact_id: u32) -> Result<Self> {
|
||||||
|
ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Not).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as `create_for_contact()` with an additional `create_blocked` parameter
|
||||||
|
/// that is used in case the chat does not exist.
|
||||||
|
/// If the chat exists already, `create_blocked` is ignored.
|
||||||
|
pub(crate) async fn create_for_contact_with_blocked(
|
||||||
|
context: &Context,
|
||||||
|
contact_id: u32,
|
||||||
|
create_blocked: Blocked,
|
||||||
|
) -> Result<Self> {
|
||||||
let chat_id = match ChatIdBlocked::lookup_by_contact(context, contact_id).await? {
|
let chat_id = match ChatIdBlocked::lookup_by_contact(context, contact_id).await? {
|
||||||
Some(chat) => {
|
Some(chat) => {
|
||||||
if chat.blocked != Blocked::Not {
|
if chat.blocked != Blocked::Not {
|
||||||
@@ -182,7 +193,10 @@ impl ChatId {
|
|||||||
if Contact::real_exists_by_id(context, contact_id).await?
|
if Contact::real_exists_by_id(context, contact_id).await?
|
||||||
|| contact_id == DC_CONTACT_ID_SELF
|
|| contact_id == DC_CONTACT_ID_SELF
|
||||||
{
|
{
|
||||||
let chat_id = ChatId::get_for_contact(context, contact_id).await?;
|
let chat_id =
|
||||||
|
ChatIdBlocked::get_for_contact(context, contact_id, create_blocked)
|
||||||
|
.await
|
||||||
|
.map(|chat| chat.id)?;
|
||||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat).await?;
|
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat).await?;
|
||||||
chat_id
|
chat_id
|
||||||
} else {
|
} else {
|
||||||
@@ -1191,6 +1205,10 @@ impl Chat {
|
|||||||
msg.param.set_int(Param::AttachGroupImage, 1);
|
msg.param.set_int(Param::AttachGroupImage, 1);
|
||||||
self.param.remove(Param::Unpromoted);
|
self.param.remove(Param::Unpromoted);
|
||||||
self.update_param(context).await?;
|
self.update_param(context).await?;
|
||||||
|
// send_sync_msg() is called (usually) a moment later at Job::send_msg_to_smtp()
|
||||||
|
// when the group-creation message is actually sent though SMTP -
|
||||||
|
// this makes sure, the other devices are aware of grpid that is used in the sync-message.
|
||||||
|
context.sync_qr_code_tokens(Some(self.id)).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset encrypt error state eg. for forwarding
|
// reset encrypt error state eg. for forwarding
|
||||||
@@ -2383,6 +2401,8 @@ pub(crate) async fn add_contact_to_chat_ex(
|
|||||||
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
||||||
chat.param.remove(Param::Unpromoted);
|
chat.param.remove(Param::Unpromoted);
|
||||||
chat.update_param(context).await?;
|
chat.update_param(context).await?;
|
||||||
|
context.sync_qr_code_tokens(Some(chat_id)).await?;
|
||||||
|
context.send_sync_msg().await?;
|
||||||
}
|
}
|
||||||
let self_addr = context
|
let self_addr = context
|
||||||
.get_config(Config::ConfiguredAddr)
|
.get_config(Config::ConfiguredAddr)
|
||||||
|
|||||||
@@ -175,6 +175,11 @@ pub enum Config {
|
|||||||
/// 0 = no limit.
|
/// 0 = no limit.
|
||||||
#[strum(props(default = "0"))]
|
#[strum(props(default = "0"))]
|
||||||
DownloadLimit,
|
DownloadLimit,
|
||||||
|
|
||||||
|
/// Send sync messages, requires `BccSelf` to be set as well.
|
||||||
|
/// In a future versions, this switch may be removed.
|
||||||
|
#[strum(props(default = "0"))]
|
||||||
|
SendSyncMsgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ impl Context {
|
|||||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
|
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
|
||||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
|
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
|
||||||
let bcc_self = self.get_config_int(Config::BccSelf).await?;
|
let bcc_self = self.get_config_int(Config::BccSelf).await?;
|
||||||
|
let send_sync_msgs = self.get_config_int(Config::SendSyncMsgs).await?;
|
||||||
|
|
||||||
let prv_key_cnt = self
|
let prv_key_cnt = self
|
||||||
.sql
|
.sql
|
||||||
@@ -392,6 +393,7 @@ impl Context {
|
|||||||
self.get_config_int(Config::KeyGenType).await?.to_string(),
|
self.get_config_int(Config::KeyGenType).await?.to_string(),
|
||||||
);
|
);
|
||||||
res.insert("bcc_self", bcc_self.to_string());
|
res.insert("bcc_self", bcc_self.to_string());
|
||||||
|
res.insert("send_sync_msgs", send_sync_msgs.to_string());
|
||||||
res.insert("private_key_count", prv_key_cnt.to_string());
|
res.insert("private_key_count", prv_key_cnt.to_string());
|
||||||
res.insert("public_key_count", pub_key_cnt.to_string());
|
res.insert("public_key_count", pub_key_cnt.to_string());
|
||||||
res.insert("fingerprint", fingerprint_str);
|
res.insert("fingerprint", fingerprint_str);
|
||||||
|
|||||||
@@ -233,6 +233,20 @@ pub(crate) async fn dc_receive_imf_inner(
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(ref sync_items) = mime_parser.sync_items {
|
||||||
|
if from_id == DC_CONTACT_ID_SELF {
|
||||||
|
if mime_parser.was_encrypted() {
|
||||||
|
if let Err(err) = context.execute_sync_items(sync_items).await {
|
||||||
|
warn!(context, "receive_imf cannot execute sync items: {}", err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!(context, "sync items are not encrypted.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!(context, "sync items not sent by self.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(avatar_action) = &mime_parser.user_avatar {
|
if let Some(avatar_action) = &mime_parser.user_avatar {
|
||||||
if from_id != 0
|
if from_id != 0
|
||||||
&& context
|
&& context
|
||||||
@@ -692,6 +706,10 @@ async fn add_parts(
|
|||||||
state = MessageState::OutDelivered;
|
state = MessageState::OutDelivered;
|
||||||
to_id = to_ids.get_index(0).cloned().unwrap_or_default();
|
to_id = to_ids.get_index(0).cloned().unwrap_or_default();
|
||||||
|
|
||||||
|
let self_sent = from_id == DC_CONTACT_ID_SELF
|
||||||
|
&& to_ids.len() == 1
|
||||||
|
&& to_ids.contains(&DC_CONTACT_ID_SELF);
|
||||||
|
|
||||||
// handshake may mark contacts as verified and must be processed before chats are created
|
// handshake may mark contacts as verified and must be processed before chats are created
|
||||||
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
|
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
|
||||||
is_dc_message = MessengerMessage::Yes; // avoid discarding by show_emails setting
|
is_dc_message = MessengerMessage::Yes; // avoid discarding by show_emails setting
|
||||||
@@ -710,6 +728,10 @@ async fn add_parts(
|
|||||||
return Ok(DC_CHAT_ID_TRASH);
|
return Ok(DC_CHAT_ID_TRASH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if mime_parser.sync_items.is_some() && self_sent {
|
||||||
|
is_dc_message = MessengerMessage::Yes;
|
||||||
|
allow_creation = true;
|
||||||
|
*hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the message is outgoing AND there is no Received header AND it's not in the sentbox,
|
// If the message is outgoing AND there is no Received header AND it's not in the sentbox,
|
||||||
@@ -775,7 +797,11 @@ async fn add_parts(
|
|||||||
}
|
}
|
||||||
if chat_id.is_none() && allow_creation {
|
if chat_id.is_none() && allow_creation {
|
||||||
let create_blocked = if !Contact::is_blocked_load(context, to_id).await? {
|
let create_blocked = if !Contact::is_blocked_load(context, to_id).await? {
|
||||||
Blocked::Not
|
if self_sent && *hidden {
|
||||||
|
Blocked::Manually
|
||||||
|
} else {
|
||||||
|
Blocked::Not
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Blocked::Request
|
Blocked::Request
|
||||||
};
|
};
|
||||||
@@ -794,9 +820,6 @@ async fn add_parts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let self_sent = from_id == DC_CONTACT_ID_SELF
|
|
||||||
&& to_ids.len() == 1
|
|
||||||
&& to_ids.contains(&DC_CONTACT_ID_SELF);
|
|
||||||
|
|
||||||
if chat_id.is_none() && self_sent {
|
if chat_id.is_none() && self_sent {
|
||||||
// from_id==to_id==DC_CONTACT_ID_SELF - this is a self-sent messages,
|
// from_id==to_id==DC_CONTACT_ID_SELF - this is a self-sent messages,
|
||||||
|
|||||||
13
src/job.rs
13
src/job.rs
@@ -433,6 +433,13 @@ impl Job {
|
|||||||
}
|
}
|
||||||
// now also delete the generated file
|
// now also delete the generated file
|
||||||
dc_delete_file(context, filename).await;
|
dc_delete_file(context, filename).await;
|
||||||
|
|
||||||
|
// finally, create another send-job if there are items to be synced.
|
||||||
|
// triggering sync-job after msg-send-job guarantees, the recipient has grpid etc.
|
||||||
|
// once the sync message arrives.
|
||||||
|
// if there are no items to sync, this function returns fast.
|
||||||
|
context.send_sync_msg().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1001,6 +1008,12 @@ pub async fn send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<Job
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
|
||||||
|
if let Err(err) = context.delete_sync_ids(sync_ids).await {
|
||||||
|
error!(context, "Failed to delete sync ids: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if attach_selfavatar {
|
if attach_selfavatar {
|
||||||
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
|
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
|
||||||
error!(context, "Failed to set selfavatar timestamp: {:?}", err);
|
error!(context, "Failed to set selfavatar timestamp: {:?}", err);
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pub mod securejoin;
|
|||||||
mod simplify;
|
mod simplify;
|
||||||
mod smtp;
|
mod smtp;
|
||||||
pub mod stock_str;
|
pub mod stock_str;
|
||||||
|
mod sync;
|
||||||
mod token;
|
mod token;
|
||||||
mod update_helper;
|
mod update_helper;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
|||||||
@@ -68,6 +68,13 @@ pub struct MimeFactory<'a> {
|
|||||||
references: String,
|
references: String,
|
||||||
req_mdn: bool,
|
req_mdn: bool,
|
||||||
last_added_location_id: u32,
|
last_added_location_id: u32,
|
||||||
|
|
||||||
|
/// If the created mime-structure contains sync-items,
|
||||||
|
/// the IDs of these items are listed here.
|
||||||
|
/// The IDs are returned via `RenderedEmail`
|
||||||
|
/// and must be deleted if the message is actually queued for sending.
|
||||||
|
sync_ids_to_delete: Option<String>,
|
||||||
|
|
||||||
attach_selfavatar: bool,
|
attach_selfavatar: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +87,12 @@ pub struct RenderedEmail {
|
|||||||
pub is_gossiped: bool,
|
pub is_gossiped: bool,
|
||||||
pub last_added_location_id: u32,
|
pub last_added_location_id: u32,
|
||||||
|
|
||||||
|
/// A comma-separated string of sync-IDs that are used by the rendered email
|
||||||
|
/// and must be deleted once the message is actually queued for sending
|
||||||
|
/// (deletion must be done by `delete_sync_ids()`).
|
||||||
|
/// If the rendered email is not queued for sending, the IDs must not be deleted.
|
||||||
|
pub sync_ids_to_delete: Option<String>,
|
||||||
|
|
||||||
/// Message ID (Message in the sense of Email)
|
/// Message ID (Message in the sense of Email)
|
||||||
pub rfc724_mid: String,
|
pub rfc724_mid: String,
|
||||||
pub subject: String,
|
pub subject: String,
|
||||||
@@ -205,6 +218,7 @@ impl<'a> MimeFactory<'a> {
|
|||||||
references,
|
references,
|
||||||
req_mdn,
|
req_mdn,
|
||||||
last_added_location_id: 0,
|
last_added_location_id: 0,
|
||||||
|
sync_ids_to_delete: None,
|
||||||
attach_selfavatar,
|
attach_selfavatar,
|
||||||
};
|
};
|
||||||
Ok(factory)
|
Ok(factory)
|
||||||
@@ -249,6 +263,7 @@ impl<'a> MimeFactory<'a> {
|
|||||||
references: String::default(),
|
references: String::default(),
|
||||||
req_mdn: false,
|
req_mdn: false,
|
||||||
last_added_location_id: 0,
|
last_added_location_id: 0,
|
||||||
|
sync_ids_to_delete: None,
|
||||||
attach_selfavatar: false,
|
attach_selfavatar: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -603,12 +618,20 @@ impl<'a> MimeFactory<'a> {
|
|||||||
main_part
|
main_part
|
||||||
} else {
|
} else {
|
||||||
// Multiple parts, render as multipart.
|
// Multiple parts, render as multipart.
|
||||||
parts.into_iter().fold(
|
let part_holder = if self.msg.param.get_cmd() == SystemMessage::MultiDeviceSync {
|
||||||
PartBuilder::new()
|
PartBuilder::new().header((
|
||||||
.message_type(MimeMultipartType::Mixed)
|
"Content-Type".to_string(),
|
||||||
.child(main_part.build()),
|
"multipart/report; report-type=multi-device-sync".to_string(),
|
||||||
|message, part| message.child(part.build()),
|
))
|
||||||
)
|
} else {
|
||||||
|
PartBuilder::new().message_type(MimeMultipartType::Mixed)
|
||||||
|
};
|
||||||
|
|
||||||
|
parts
|
||||||
|
.into_iter()
|
||||||
|
.fold(part_holder.child(main_part.build()), |message, part| {
|
||||||
|
message.child(part.build())
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let outer_message = if is_encrypted {
|
let outer_message = if is_encrypted {
|
||||||
@@ -729,6 +752,7 @@ impl<'a> MimeFactory<'a> {
|
|||||||
is_encrypted,
|
is_encrypted,
|
||||||
is_gossiped,
|
is_gossiped,
|
||||||
last_added_location_id,
|
last_added_location_id,
|
||||||
|
sync_ids_to_delete: self.sync_ids_to_delete,
|
||||||
rfc724_mid,
|
rfc724_mid,
|
||||||
subject: subject_str,
|
subject: subject_str,
|
||||||
})
|
})
|
||||||
@@ -873,7 +897,7 @@ impl<'a> MimeFactory<'a> {
|
|||||||
"ephemeral-timer-changed".to_string(),
|
"ephemeral-timer-changed".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
SystemMessage::LocationOnly => {
|
SystemMessage::LocationOnly | SystemMessage::MultiDeviceSync => {
|
||||||
// This should prevent automatic replies,
|
// This should prevent automatic replies,
|
||||||
// such as non-delivery reports.
|
// such as non-delivery reports.
|
||||||
//
|
//
|
||||||
@@ -1103,6 +1127,15 @@ impl<'a> MimeFactory<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we do not piggyback sync-files to other self-sent-messages
|
||||||
|
// to not risk files becoming too larger and being skipped by download-on-demand.
|
||||||
|
if command == SystemMessage::MultiDeviceSync && self.is_e2ee_guaranteed() {
|
||||||
|
let json = self.msg.param.get(Param::Arg).unwrap_or_default();
|
||||||
|
let ids = self.msg.param.get(Param::Arg2).unwrap_or_default();
|
||||||
|
parts.push(context.build_sync_part(json.to_string()).await);
|
||||||
|
self.sync_ids_to_delete = Some(ids.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
if self.attach_selfavatar {
|
if self.attach_selfavatar {
|
||||||
match context.get_config(Config::Selfavatar).await? {
|
match context.get_config(Config::Selfavatar).await? {
|
||||||
Some(path) => match build_selfavatar_file(context, &path) {
|
Some(path) => match build_selfavatar_file(context, &path) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ use crate::param::{Param, Params};
|
|||||||
use crate::peerstate::Peerstate;
|
use crate::peerstate::Peerstate;
|
||||||
use crate::simplify::simplify;
|
use crate::simplify::simplify;
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
|
use crate::sync::SyncItems;
|
||||||
|
|
||||||
/// A parsed MIME message.
|
/// A parsed MIME message.
|
||||||
///
|
///
|
||||||
@@ -61,6 +62,7 @@ pub struct MimeMessage {
|
|||||||
pub is_system_message: SystemMessage,
|
pub is_system_message: SystemMessage,
|
||||||
pub location_kml: Option<location::Kml>,
|
pub location_kml: Option<location::Kml>,
|
||||||
pub message_kml: Option<location::Kml>,
|
pub message_kml: Option<location::Kml>,
|
||||||
|
pub(crate) sync_items: Option<SyncItems>,
|
||||||
pub(crate) user_avatar: Option<AvatarAction>,
|
pub(crate) user_avatar: Option<AvatarAction>,
|
||||||
pub(crate) group_avatar: Option<AvatarAction>,
|
pub(crate) group_avatar: Option<AvatarAction>,
|
||||||
pub(crate) mdn_reports: Vec<Report>,
|
pub(crate) mdn_reports: Vec<Report>,
|
||||||
@@ -124,6 +126,10 @@ pub enum SystemMessage {
|
|||||||
// Chat protection state changed
|
// Chat protection state changed
|
||||||
ChatProtectionEnabled = 11,
|
ChatProtectionEnabled = 11,
|
||||||
ChatProtectionDisabled = 12,
|
ChatProtectionDisabled = 12,
|
||||||
|
|
||||||
|
/// Self-sent-message that contains only json used for multi-device-sync;
|
||||||
|
/// if possible, we attach that to other messages as for locations.
|
||||||
|
MultiDeviceSync = 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SystemMessage {
|
impl Default for SystemMessage {
|
||||||
@@ -279,6 +285,7 @@ impl MimeMessage {
|
|||||||
is_system_message: SystemMessage::Unknown,
|
is_system_message: SystemMessage::Unknown,
|
||||||
location_kml: None,
|
location_kml: None,
|
||||||
message_kml: None,
|
message_kml: None,
|
||||||
|
sync_items: None,
|
||||||
user_avatar: None,
|
user_avatar: None,
|
||||||
group_avatar: None,
|
group_avatar: None,
|
||||||
failure_report: None,
|
failure_report: None,
|
||||||
@@ -813,6 +820,12 @@ impl MimeMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some("multi-device-sync") => {
|
||||||
|
if let Some(second) = mail.subparts.get(1) {
|
||||||
|
self.add_single_part_if_known(context, second, is_related)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
if let Some(first) = mail.subparts.get(0) {
|
if let Some(first) = mail.subparts.get(0) {
|
||||||
any_part_added = self
|
any_part_added = self
|
||||||
@@ -999,7 +1012,20 @@ impl MimeMessage {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else if filename == "multi-device-sync.json" {
|
||||||
|
let serialized = String::from_utf8_lossy(decoded_data)
|
||||||
|
.parse()
|
||||||
|
.unwrap_or_default();
|
||||||
|
self.sync_items = context
|
||||||
|
.parse_sync_items(serialized)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
warn!(context, "failed to parse sync data: {}", err);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* we have a regular file attachment,
|
/* we have a regular file attachment,
|
||||||
write decoded data to new blob object */
|
write decoded data to new blob object */
|
||||||
|
|
||||||
|
|||||||
12
src/qr.rs
12
src/qr.rs
@@ -391,6 +391,10 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
|||||||
} => {
|
} => {
|
||||||
token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?;
|
token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?;
|
||||||
token::delete(context, token::Namespace::Auth, &authcode).await?;
|
token::delete(context, token::Namespace::Auth, &authcode).await?;
|
||||||
|
context
|
||||||
|
.sync_qr_code_token_deletion(invitenumber, authcode)
|
||||||
|
.await?;
|
||||||
|
context.send_sync_msg().await?;
|
||||||
}
|
}
|
||||||
Qr::WithdrawVerifyGroup {
|
Qr::WithdrawVerifyGroup {
|
||||||
invitenumber,
|
invitenumber,
|
||||||
@@ -399,6 +403,10 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
|||||||
} => {
|
} => {
|
||||||
token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?;
|
token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?;
|
||||||
token::delete(context, token::Namespace::Auth, &authcode).await?;
|
token::delete(context, token::Namespace::Auth, &authcode).await?;
|
||||||
|
context
|
||||||
|
.sync_qr_code_token_deletion(invitenumber, authcode)
|
||||||
|
.await?;
|
||||||
|
context.send_sync_msg().await?;
|
||||||
}
|
}
|
||||||
Qr::ReviveVerifyContact {
|
Qr::ReviveVerifyContact {
|
||||||
invitenumber,
|
invitenumber,
|
||||||
@@ -407,6 +415,8 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
|||||||
} => {
|
} => {
|
||||||
token::save(context, token::Namespace::InviteNumber, None, &invitenumber).await?;
|
token::save(context, token::Namespace::InviteNumber, None, &invitenumber).await?;
|
||||||
token::save(context, token::Namespace::Auth, None, &authcode).await?;
|
token::save(context, token::Namespace::Auth, None, &authcode).await?;
|
||||||
|
context.sync_qr_code_tokens(None).await?;
|
||||||
|
context.send_sync_msg().await?;
|
||||||
}
|
}
|
||||||
Qr::ReviveVerifyGroup {
|
Qr::ReviveVerifyGroup {
|
||||||
invitenumber,
|
invitenumber,
|
||||||
@@ -425,6 +435,8 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
token::save(context, token::Namespace::Auth, chat_id, &authcode).await?;
|
token::save(context, token::Namespace::Auth, chat_id, &authcode).await?;
|
||||||
|
context.sync_qr_code_tokens(chat_id).await?;
|
||||||
|
context.send_sync_msg().await?;
|
||||||
}
|
}
|
||||||
_ => bail!("qr code {:?} does not contain config", qr),
|
_ => bail!("qr code {:?} does not contain config", qr),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ use crate::token;
|
|||||||
mod bobstate;
|
mod bobstate;
|
||||||
mod qrinvite;
|
mod qrinvite;
|
||||||
|
|
||||||
|
use crate::token::Namespace;
|
||||||
use bobstate::{BobHandshakeStage, BobState, BobStateHandle};
|
use bobstate::{BobHandshakeStage, BobState, BobStateHandle};
|
||||||
use qrinvite::QrInvite;
|
use qrinvite::QrInvite;
|
||||||
|
|
||||||
@@ -171,8 +172,11 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> R
|
|||||||
|
|
||||||
// invitenumber will be used to allow starting the handshake,
|
// invitenumber will be used to allow starting the handshake,
|
||||||
// auth will be used to verify the fingerprint
|
// auth will be used to verify the fingerprint
|
||||||
let invitenumber = token::lookup_or_new(context, token::Namespace::InviteNumber, group).await;
|
let sync_token = token::lookup(context, Namespace::InviteNumber, group)
|
||||||
let auth = token::lookup_or_new(context, token::Namespace::Auth, group).await;
|
.await?
|
||||||
|
.is_none();
|
||||||
|
let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, group).await;
|
||||||
|
let auth = token::lookup_or_new(context, Namespace::Auth, group).await;
|
||||||
let self_addr = match context.get_config(Config::ConfiguredAddr).await {
|
let self_addr = match context.get_config(Config::ConfiguredAddr).await {
|
||||||
Ok(Some(addr)) => addr,
|
Ok(Some(addr)) => addr,
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
@@ -208,7 +212,9 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> R
|
|||||||
let chat = Chat::load_from_db(context, group).await?;
|
let chat = Chat::load_from_db(context, group).await?;
|
||||||
let group_name = chat.get_name();
|
let group_name = chat.get_name();
|
||||||
let group_name_urlencoded = utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
|
let group_name_urlencoded = utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
|
||||||
|
if sync_token {
|
||||||
|
context.sync_qr_code_tokens(Some(chat.id)).await?;
|
||||||
|
}
|
||||||
format!(
|
format!(
|
||||||
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
|
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
|
||||||
fingerprint.hex(),
|
fingerprint.hex(),
|
||||||
@@ -220,6 +226,9 @@ pub async fn dc_get_securejoin_qr(context: &Context, group: Option<ChatId>) -> R
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// parameters used: a=n=i=s=
|
// parameters used: a=n=i=s=
|
||||||
|
if sync_token {
|
||||||
|
context.sync_qr_code_tokens(None).await?;
|
||||||
|
}
|
||||||
format!(
|
format!(
|
||||||
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
|
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
|
||||||
fingerprint.hex(),
|
fingerprint.hex(),
|
||||||
|
|||||||
@@ -487,6 +487,16 @@ paramsv![]
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
if dbversion < 80 {
|
||||||
|
info!(context, "[migration] v80");
|
||||||
|
sql.execute_migration(
|
||||||
|
r#"CREATE TABLE multi_device_sync (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
item TEXT DEFAULT '');"#,
|
||||||
|
80,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
recalc_fingerprints,
|
recalc_fingerprints,
|
||||||
|
|||||||
@@ -276,6 +276,15 @@ pub enum StockMessage {
|
|||||||
#[strum(props(fallback = "Download maximum available until %1$s"))]
|
#[strum(props(fallback = "Download maximum available until %1$s"))]
|
||||||
DownloadAvailability = 100,
|
DownloadAvailability = 100,
|
||||||
|
|
||||||
|
#[strum(props(fallback = "Multi Device Synchronization"))]
|
||||||
|
SyncMsgSubject = 101,
|
||||||
|
|
||||||
|
#[strum(props(
|
||||||
|
fallback = "This message is used to synchronize data between your devices.\n\n\
|
||||||
|
👉 If you see this message in Delta Chat, please update your Delta Chat apps on all devices."
|
||||||
|
))]
|
||||||
|
SyncMsgBody = 102,
|
||||||
|
|
||||||
#[strum(props(fallback = "Incoming Messages"))]
|
#[strum(props(fallback = "Incoming Messages"))]
|
||||||
IncomingMessages = 103,
|
IncomingMessages = 103,
|
||||||
|
|
||||||
@@ -624,6 +633,16 @@ pub(crate) async fn ac_setup_msg_body(context: &Context) -> String {
|
|||||||
translated(context, StockMessage::AcSetupMsgBody).await
|
translated(context, StockMessage::AcSetupMsgBody).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stock string: `Multi Device Synchronization`.
|
||||||
|
pub(crate) async fn sync_msg_subject(context: &Context) -> String {
|
||||||
|
translated(context, StockMessage::SyncMsgSubject).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stock string: `This message is used to synchronize data betweeen your devices.`.
|
||||||
|
pub(crate) async fn sync_msg_body(context: &Context) -> String {
|
||||||
|
translated(context, StockMessage::SyncMsgBody).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Stock string: `Cannot login as \"%1$s\". Please check...`.
|
/// Stock string: `Cannot login as \"%1$s\". Please check...`.
|
||||||
pub(crate) async fn cannot_login(context: &Context, user: impl AsRef<str>) -> String {
|
pub(crate) async fn cannot_login(context: &Context, user: impl AsRef<str>) -> String {
|
||||||
translated(context, StockMessage::CannotLogin)
|
translated(context, StockMessage::CannotLogin)
|
||||||
|
|||||||
509
src/sync.rs
Normal file
509
src/sync.rs
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
//! # Synchronize items between devices.
|
||||||
|
|
||||||
|
use crate::chat::{Chat, ChatId};
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::constants::{Blocked, Viewtype, DC_CONTACT_ID_SELF};
|
||||||
|
use crate::context::Context;
|
||||||
|
use crate::dc_tools::time;
|
||||||
|
use crate::message::{Message, MsgId};
|
||||||
|
use crate::mimeparser::SystemMessage;
|
||||||
|
use crate::param::Param;
|
||||||
|
use crate::sync::SyncData::{AddQrToken, DeleteQrToken};
|
||||||
|
use crate::token::Namespace;
|
||||||
|
use crate::{chat, stock_str, token};
|
||||||
|
use anyhow::Result;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use lettre_email::mime::{self};
|
||||||
|
use lettre_email::PartBuilder;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub(crate) struct QrTokenData {
|
||||||
|
pub(crate) invitenumber: String,
|
||||||
|
pub(crate) auth: String,
|
||||||
|
pub(crate) grpid: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub(crate) enum SyncData {
|
||||||
|
AddQrToken(QrTokenData),
|
||||||
|
DeleteQrToken(QrTokenData),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub(crate) struct SyncItem {
|
||||||
|
timestamp: i64,
|
||||||
|
data: SyncData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct SyncItems {
|
||||||
|
items: Vec<SyncItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
/// Checks if sync messages shall be sent.
|
||||||
|
/// Receiving sync messages is currently always enabled;
|
||||||
|
/// the messages are force-encrypted anyway.
|
||||||
|
async fn is_sync_sending_enabled(&self) -> Result<bool> {
|
||||||
|
self.get_config_bool(Config::SendSyncMsgs).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an item to the list of items that should be synchronized to other devices.
|
||||||
|
pub(crate) async fn add_sync_item(&self, data: SyncData) -> Result<()> {
|
||||||
|
self.add_sync_item_with_timestamp(data, time()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds item and timestamp to the list of items that should be synchronized to other devices.
|
||||||
|
/// If device synchronization is disabled, the function does nothing.
|
||||||
|
async fn add_sync_item_with_timestamp(&self, data: SyncData, timestamp: i64) -> Result<()> {
|
||||||
|
if !self.is_sync_sending_enabled().await? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let item = SyncItem { timestamp, data };
|
||||||
|
let item = serde_json::to_string(&item)?;
|
||||||
|
self.sql
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO multi_device_sync (item) VALUES(?);",
|
||||||
|
paramsv![item],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds most recent qr-code tokens for a given chat to the list of items to be synced.
|
||||||
|
/// If device synchronization is disabled,
|
||||||
|
/// no tokens exist or the chat is unpromoted, the function does nothing.
|
||||||
|
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<ChatId>) -> Result<()> {
|
||||||
|
if !self.is_sync_sending_enabled().await? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (Some(invitenumber), Some(auth)) = (
|
||||||
|
token::lookup(self, Namespace::InviteNumber, chat_id).await?,
|
||||||
|
token::lookup(self, Namespace::Auth, chat_id).await?,
|
||||||
|
) {
|
||||||
|
let grpid = if let Some(chat_id) = chat_id {
|
||||||
|
let chat = Chat::load_from_db(self, chat_id).await?;
|
||||||
|
if !chat.is_promoted() {
|
||||||
|
info!(
|
||||||
|
self,
|
||||||
|
"group '{}' not yet promoted, do not sync tokens yet.", chat.grpid
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Some(chat.grpid)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
self.add_sync_item(SyncData::AddQrToken(QrTokenData {
|
||||||
|
invitenumber,
|
||||||
|
auth,
|
||||||
|
grpid,
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add deleted qr-code token to the list of items to be synced
|
||||||
|
// so that the token also gets deleted on the other devices.
|
||||||
|
pub(crate) async fn sync_qr_code_token_deletion(
|
||||||
|
&self,
|
||||||
|
invitenumber: String,
|
||||||
|
auth: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.add_sync_item(SyncData::DeleteQrToken(QrTokenData {
|
||||||
|
invitenumber,
|
||||||
|
auth,
|
||||||
|
grpid: None,
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends out a self-sent message with items to be synchronized, if any.
|
||||||
|
pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
|
||||||
|
if let Some((json, ids)) = self.build_sync_json().await? {
|
||||||
|
let chat_id = ChatId::create_for_contact_with_blocked(
|
||||||
|
self,
|
||||||
|
DC_CONTACT_ID_SELF,
|
||||||
|
Blocked::Manually,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let mut msg = Message {
|
||||||
|
chat_id,
|
||||||
|
viewtype: Viewtype::Text,
|
||||||
|
text: Some(stock_str::sync_msg_body(self).await),
|
||||||
|
hidden: true,
|
||||||
|
subject: stock_str::sync_msg_subject(self).await,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
msg.param.set_cmd(SystemMessage::MultiDeviceSync);
|
||||||
|
msg.param.set(Param::Arg, json);
|
||||||
|
msg.param.set(Param::Arg2, ids);
|
||||||
|
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||||
|
Ok(Some(chat::send_msg(self, chat_id, &mut msg).await?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies all sync items to a JSON string and clears the sync-table.
|
||||||
|
/// Returns the JSON string and a comma-separated string of the IDs used.
|
||||||
|
pub(crate) async fn build_sync_json(&self) -> Result<Option<(String, String)>> {
|
||||||
|
let (ids, serialized) = self
|
||||||
|
.sql
|
||||||
|
.query_map(
|
||||||
|
"SELECT id, item FROM multi_device_sync ORDER BY id;",
|
||||||
|
paramsv![],
|
||||||
|
|row| Ok((row.get::<_, u32>(0)?, row.get::<_, String>(1)?)),
|
||||||
|
|rows| {
|
||||||
|
let mut ids = vec![];
|
||||||
|
let mut serialized = String::default();
|
||||||
|
for row in rows {
|
||||||
|
let (id, item) = row?;
|
||||||
|
ids.push(id);
|
||||||
|
if !serialized.is_empty() {
|
||||||
|
serialized.push_str(",\n");
|
||||||
|
}
|
||||||
|
serialized.push_str(&item);
|
||||||
|
}
|
||||||
|
Ok((ids, serialized))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if ids.is_empty() {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Ok(Some((
|
||||||
|
format!("{{\"items\":[\n{}\n]}}", serialized),
|
||||||
|
ids.iter().map(|x| x.to_string()).join(","),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn build_sync_part(&self, json: String) -> PartBuilder {
|
||||||
|
PartBuilder::new()
|
||||||
|
.content_type(&"application/json".parse::<mime::Mime>().unwrap())
|
||||||
|
.header((
|
||||||
|
"Content-Disposition",
|
||||||
|
"attachment; filename=\"multi-device-sync.json\"",
|
||||||
|
))
|
||||||
|
.body(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes IDs as returned by `build_sync_json()`.
|
||||||
|
pub(crate) async fn delete_sync_ids(&self, ids: String) -> Result<()> {
|
||||||
|
self.sql
|
||||||
|
.execute(
|
||||||
|
format!("DELETE FROM multi_device_sync WHERE id IN ({});", ids),
|
||||||
|
paramsv![],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes a JSON string created by `build_sync_json()`
|
||||||
|
/// and construct `SyncItems` from it.
|
||||||
|
pub(crate) async fn parse_sync_items(&self, serialized: String) -> Result<SyncItems> {
|
||||||
|
let sync_items: SyncItems = serde_json::from_str(&serialized)?;
|
||||||
|
Ok(sync_items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute sync items.
|
||||||
|
///
|
||||||
|
/// CAVE: When changing the code to handle other sync items,
|
||||||
|
/// take care that does not result in calls to `add_sync_item()`
|
||||||
|
/// as otherwise we would add in a dead-loop between two devices
|
||||||
|
/// sending message back and forth.
|
||||||
|
///
|
||||||
|
/// If an error is returned, the caller shall not try over.
|
||||||
|
/// Therefore, errors should only be returned on database errors or so.
|
||||||
|
/// If eg. just an item cannot be deleted,
|
||||||
|
/// that should not hold off the other items to be executed.
|
||||||
|
pub(crate) async fn execute_sync_items(&self, items: &SyncItems) -> Result<()> {
|
||||||
|
info!(self, "executing {} sync item(s)", items.items.len());
|
||||||
|
for item in &items.items {
|
||||||
|
match &item.data {
|
||||||
|
AddQrToken(token) => {
|
||||||
|
let chat_id = if let Some(grpid) = &token.grpid {
|
||||||
|
if let Some((chat_id, _, _)) =
|
||||||
|
chat::get_chat_id_by_grpid(self, grpid).await?
|
||||||
|
{
|
||||||
|
Some(chat_id)
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
self,
|
||||||
|
"Ignoring token for nonexistent/deleted group '{}'.", grpid
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
token::save(self, Namespace::InviteNumber, chat_id, &token.invitenumber)
|
||||||
|
.await?;
|
||||||
|
token::save(self, Namespace::Auth, chat_id, &token.auth).await?;
|
||||||
|
}
|
||||||
|
DeleteQrToken(token) => {
|
||||||
|
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
|
||||||
|
token::delete(self, Namespace::Auth, &token.auth).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::chat::Chat;
|
||||||
|
use crate::chatlist::Chatlist;
|
||||||
|
use crate::test_utils::TestContext;
|
||||||
|
use crate::token::Namespace;
|
||||||
|
use anyhow::bail;
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_is_sync_sending_enabled() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
assert!(!t.is_sync_sending_enabled().await?);
|
||||||
|
t.set_config_bool(Config::SendSyncMsgs, true).await?;
|
||||||
|
assert!(t.is_sync_sending_enabled().await?);
|
||||||
|
t.set_config_bool(Config::SendSyncMsgs, false).await?;
|
||||||
|
assert!(!t.is_sync_sending_enabled().await?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_build_sync_json() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
t.set_config_bool(Config::SendSyncMsgs, true).await?;
|
||||||
|
|
||||||
|
assert!(t.build_sync_json().await?.is_none());
|
||||||
|
|
||||||
|
t.add_sync_item_with_timestamp(
|
||||||
|
SyncData::AddQrToken(QrTokenData {
|
||||||
|
invitenumber: "testinvite".to_string(),
|
||||||
|
auth: "testauth".to_string(),
|
||||||
|
grpid: Some("group123".to_string()),
|
||||||
|
}),
|
||||||
|
1631781316,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
t.add_sync_item_with_timestamp(
|
||||||
|
SyncData::DeleteQrToken(QrTokenData {
|
||||||
|
invitenumber: "123!?\":.;{}".to_string(),
|
||||||
|
auth: "456".to_string(),
|
||||||
|
grpid: None,
|
||||||
|
}),
|
||||||
|
1631781317,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (serialized, ids) = t.build_sync_json().await?.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
serialized,
|
||||||
|
r#"{"items":[
|
||||||
|
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"testinvite","auth":"testauth","grpid":"group123"}}},
|
||||||
|
{"timestamp":1631781317,"data":{"DeleteQrToken":{"invitenumber":"123!?\":.;{}","auth":"456","grpid":null}}}
|
||||||
|
]}"#
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(t.build_sync_json().await?.is_some());
|
||||||
|
t.delete_sync_ids(ids).await?;
|
||||||
|
assert!(t.build_sync_json().await?.is_none());
|
||||||
|
|
||||||
|
let sync_items = t.parse_sync_items(serialized).await?;
|
||||||
|
assert_eq!(sync_items.items.len(), 2);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_build_sync_json_sync_msgs_off() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
t.set_config_bool(Config::SendSyncMsgs, false).await?;
|
||||||
|
t.add_sync_item(SyncData::AddQrToken(QrTokenData {
|
||||||
|
invitenumber: "testinvite".to_string(),
|
||||||
|
auth: "testauth".to_string(),
|
||||||
|
grpid: Some("group123".to_string()),
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
assert!(t.build_sync_json().await?.is_none());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_parse_sync_items() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
|
||||||
|
assert!(t
|
||||||
|
.parse_sync_items(r#"{bad json}"#.to_string())
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
assert!(t
|
||||||
|
.parse_sync_items(r#"{"badname":[]}"#.to_string())
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
assert!(t.parse_sync_items(
|
||||||
|
r#"{"items":[{"timestamp":1631781316,"data":{"BadItem":{"invitenumber":"in","auth":"a","grpid":null}}}]}"#
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.await.is_err());
|
||||||
|
|
||||||
|
assert!(t.parse_sync_items(
|
||||||
|
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":123}}}]}"#.to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()); // `123` is invalid for `String`
|
||||||
|
|
||||||
|
assert!(t.parse_sync_items(
|
||||||
|
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":true}}}]}"#.to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()); // `true` is invalid for `String`
|
||||||
|
|
||||||
|
assert!(t.parse_sync_items(
|
||||||
|
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":[]}}}]}"#.to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()); // `[]` is invalid for `String`
|
||||||
|
|
||||||
|
assert!(t.parse_sync_items(
|
||||||
|
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":{}}}}]}"#.to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()); // `{}` is invalid for `String`
|
||||||
|
|
||||||
|
assert!(t.parse_sync_items(
|
||||||
|
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","grpid":null}}}]}"#.to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()); // missing field
|
||||||
|
|
||||||
|
// empty item list is okay
|
||||||
|
assert_eq!(
|
||||||
|
t.parse_sync_items(r#"{"items":[]}"#.to_string())
|
||||||
|
.await?
|
||||||
|
.items
|
||||||
|
.len(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// to allow forward compatibility, additional fields should not break parsing
|
||||||
|
let sync_items = t
|
||||||
|
.parse_sync_items(
|
||||||
|
r#"{"items":[
|
||||||
|
{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"yip","grpid":null}}},
|
||||||
|
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip","additional":123,"grpid":null}}}
|
||||||
|
]}"#
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(sync_items.items.len(), 2);
|
||||||
|
|
||||||
|
let sync_items = t
|
||||||
|
.parse_sync_items(
|
||||||
|
r#"{"items":[
|
||||||
|
{"timestamp":1631781318,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip","grpid":null}}}
|
||||||
|
],"additional":"field"}"#
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(sync_items.items.len(), 1);
|
||||||
|
if let AddQrToken(token) = &sync_items.items.get(0).unwrap().data {
|
||||||
|
assert_eq!(token.invitenumber, "in");
|
||||||
|
assert_eq!(token.auth, "yip");
|
||||||
|
assert_eq!(token.grpid, None);
|
||||||
|
} else {
|
||||||
|
bail!("bad item");
|
||||||
|
}
|
||||||
|
|
||||||
|
// to allow backward compatibility, missing `Option<>` should not break parsing
|
||||||
|
let sync_items = t.parse_sync_items(
|
||||||
|
r#"{"items":[{"timestamp":1631781319,"data":{"AddQrToken":{"invitenumber":"in","auth":"a"}}}]}"#.to_string(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(sync_items.items.len(), 1);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_execute_sync_items() -> Result<()> {
|
||||||
|
let t = TestContext::new_alice().await;
|
||||||
|
|
||||||
|
assert!(!token::exists(&t, Namespace::Auth, "yip-auth").await);
|
||||||
|
|
||||||
|
let sync_items = t
|
||||||
|
.parse_sync_items(
|
||||||
|
r#"{"items":[
|
||||||
|
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"yip-in","auth":"a"}}},
|
||||||
|
{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"delete unexistant, shall continue"}}},
|
||||||
|
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip-auth"}}},
|
||||||
|
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"foo","grpid":"non-existant"}}},
|
||||||
|
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"directly deleted"}}},
|
||||||
|
{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"directly deleted"}}}
|
||||||
|
]}"#
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
t.execute_sync_items(&sync_items).await?;
|
||||||
|
|
||||||
|
assert!(token::exists(&t, Namespace::InviteNumber, "yip-in").await);
|
||||||
|
assert!(token::exists(&t, Namespace::Auth, "yip-auth").await);
|
||||||
|
assert!(!token::exists(&t, Namespace::Auth, "non-existant").await);
|
||||||
|
assert!(!token::exists(&t, Namespace::Auth, "directly deleted").await);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_send_sync_msg() -> Result<()> {
|
||||||
|
let alice = TestContext::new_alice().await;
|
||||||
|
alice.set_config_bool(Config::SendSyncMsgs, true).await?;
|
||||||
|
alice
|
||||||
|
.add_sync_item(SyncData::AddQrToken(QrTokenData {
|
||||||
|
invitenumber: "in".to_string(),
|
||||||
|
auth: "testtoken".to_string(),
|
||||||
|
grpid: None,
|
||||||
|
}))
|
||||||
|
.await?;
|
||||||
|
let msg_id = alice.send_sync_msg().await?.unwrap();
|
||||||
|
let msg = Message::load_from_db(&alice, msg_id).await?;
|
||||||
|
let chat = Chat::load_from_db(&alice, msg.chat_id).await?;
|
||||||
|
assert!(chat.is_self_talk());
|
||||||
|
|
||||||
|
// check that the used self-talk is not visible to the user
|
||||||
|
// but that creation will still work (in this case, the chat is empty)
|
||||||
|
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
||||||
|
let chat_id = ChatId::create_for_contact(&alice, DC_CONTACT_ID_SELF).await?;
|
||||||
|
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
||||||
|
assert!(chat.is_self_talk());
|
||||||
|
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
||||||
|
let msgs = chat::get_chat_msgs(&alice, chat_id, 0, None).await?;
|
||||||
|
assert_eq!(msgs.len(), 0);
|
||||||
|
|
||||||
|
// let alice's other device receive and execute the sync message,
|
||||||
|
// also here, self-talk should stay hidden
|
||||||
|
let sent_msg = alice.pop_sent_msg().await;
|
||||||
|
let alice2 = TestContext::new_alice().await;
|
||||||
|
alice2.recv_msg(&sent_msg).await;
|
||||||
|
assert!(token::exists(&alice2, token::Namespace::Auth, "testtoken").await);
|
||||||
|
assert_eq!(Chatlist::try_load(&alice2, 0, None, None).await?.len(), 0);
|
||||||
|
|
||||||
|
// the same sync message sent to bob must not be executed
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
bob.recv_msg(&sent_msg).await;
|
||||||
|
assert!(!token::exists(&bob, token::Namespace::Auth, "testtoken").await);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user