From 4d1a9c2aa1ce2b4755b7b04d99f639a5bdc16d21 Mon Sep 17 00:00:00 2001 From: iequidoo Date: Wed, 8 Nov 2023 21:53:28 -0300 Subject: [PATCH] feat: Sync chat contacts across devices (#4953) Sync chat contacts across devices for broadcast lists and groups. This needs the corresponding chat to exist on other devices which is not the case for unpromoted groups, so it fails for them now but it's only a warning and will work once creation of unpromoted groups is synchronised too. --- src/chat.rs | 79 +++++++++++++++++++++++++++++++++++++++++++--- src/receive_imf.rs | 16 +--------- src/securejoin.rs | 11 +++++-- src/sync.rs | 2 ++ 4 files changed, 86 insertions(+), 22 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index b6dfab702..5cca50adf 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,6 +1,6 @@ //! # Chat module. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::convert::{TryFrom, TryInto}; use std::fmt; use std::path::{Path, PathBuf}; @@ -21,7 +21,7 @@ use crate::constants::{ Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, }; -use crate::contact::{self, Contact, ContactId, Origin, VerifiedStatus}; +use crate::contact::{self, Contact, ContactAddress, ContactId, Origin, VerifiedStatus}; use crate::context::Context; use crate::debug_logging::maybe_set_logging_xdc; use crate::download::DownloadState; @@ -1916,6 +1916,17 @@ impl Chat { Ok(msg.id) } + /// Sends a `ChatAction` synchronising chat contacts to other devices. + pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> { + let mut addrs = Vec::new(); + for contact_id in get_chat_contacts(context, self.id).await? { + let contact = Contact::get_by_id(context, contact_id).await?; + addrs.push(contact.get_addr().to_string()); + } + self.add_sync_item(context, ChatAction::SetContacts(addrs)) + .await + } + /// Returns chat id for the purpose of synchronisation across devices. async fn get_sync_id(&self, context: &Context) -> Result> { match self.typ { @@ -3235,6 +3246,28 @@ pub async fn create_broadcast_list(context: &Context) -> Result { Ok(chat_id) } +/// Set chat contacts in the `chats_contacts` table. +pub(crate) async fn update_chat_contacts_table( + context: &Context, + id: ChatId, + contacts: &HashSet, +) -> Result<()> { + context + .sql + .transaction(move |transaction| { + transaction.execute("DELETE FROM chats_contacts WHERE chat_id=?", (id,))?; + for contact_id in contacts { + transaction.execute( + "INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)", + (id, contact_id), + )?; + } + Ok(()) + }) + .await?; + Ok(()) +} + /// Adds contacts to the `chats_contacts` table. pub(crate) async fn add_to_chat_contacts_table( context: &Context, @@ -3280,12 +3313,13 @@ pub async fn add_contact_to_chat( chat_id: ChatId, contact_id: ContactId, ) -> Result<()> { - add_contact_to_chat_ex(context, chat_id, contact_id, false).await?; + add_contact_to_chat_ex(context, Sync, chat_id, contact_id, false).await?; Ok(()) } pub(crate) async fn add_contact_to_chat_ex( context: &Context, + mut sync: sync::Sync, chat_id: ChatId, contact_id: ContactId, from_handshake: bool, @@ -3367,8 +3401,12 @@ pub(crate) async fn add_contact_to_chat_ex( msg.param.set(Param::Arg, contact_addr); msg.param.set_int(Param::Arg2, from_handshake.into()); msg.id = send_msg(context, chat_id, &mut msg).await?; + sync = Nosync; } context.emit_event(EventType::ChatModified(chat_id)); + if sync.into() { + chat.sync_contacts(context).await?; + } Ok(true) } @@ -3508,6 +3546,7 @@ pub async fn remove_contact_from_chat( context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone())); bail!("{}", err_msg); } else { + let mut sync = Nosync; // We do not return an error if the contact does not exist in the database. // This allows to delete dangling references to deleted contacts // in case of the database becoming inconsistent due to a bug. @@ -3528,6 +3567,8 @@ pub async fn remove_contact_from_chat( msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup); msg.param.set(Param::Arg, contact.get_addr()); msg.id = send_msg(context, chat_id, &mut msg).await?; + } else { + sync = Sync; } } // we remove the member from the chat after constructing the @@ -3542,6 +3583,9 @@ pub async fn remove_contact_from_chat( // check/encryption logic. remove_from_chat_contacts_table(context, chat_id, contact_id).await?; context.emit_event(EventType::ChatModified(chat_id)); + if sync.into() { + chat.sync_contacts(context).await?; + } } } else { bail!("Cannot remove members from non-group chats."); @@ -4096,6 +4140,30 @@ pub(crate) async fn update_msg_text_and_timestamp( Ok(()) } +/// Set chat contacts by their addresses creating the corresponding contacts if necessary. +async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String]) -> Result<()> { + let chat = Chat::load_from_db(context, id).await?; + ensure!( + chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast, + "{id} is not a group/broadcast", + ); + let mut contacts = HashSet::new(); + for addr in addrs { + let contact_addr = ContactAddress::new(addr)?; + let contact = Contact::add_or_lookup(context, "", contact_addr, Origin::Hidden) + .await? + .0; + contacts.insert(contact); + } + let contacts_old = HashSet::::from_iter(get_chat_contacts(context, id).await?); + if contacts == contacts_old { + return Ok(()); + } + update_chat_contacts_table(context, id, &contacts).await?; + context.emit_event(EventType::ChatModified(id)); + Ok(()) +} + impl Context { /// Executes [`SyncData::AlterChat`] item sent by other device. pub(crate) async fn sync_alter_chat( @@ -4140,6 +4208,7 @@ impl Context { ChatAction::Accept => chat_id.accept_ex(self, Nosync).await, ChatAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await, ChatAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await, + ChatAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await, } .ok(); Ok(()) @@ -4365,7 +4434,7 @@ mod tests { let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo") .await .unwrap(); - let added = add_contact_to_chat_ex(&t, chat_id, ContactId::SELF, false) + let added = add_contact_to_chat_ex(&t, Nosync, chat_id, ContactId::SELF, false) .await .unwrap(); assert_eq!(added, false); @@ -4715,7 +4784,7 @@ mod tests { // adding or removing contacts from one-to-one-chats result in an error let claire = Contact::create(&ctx, "", "claire@foo.de").await.unwrap(); - let added = add_contact_to_chat_ex(&ctx, chat.id, claire, false).await; + let added = add_contact_to_chat_ex(&ctx, Nosync, chat.id, claire, false).await; assert!(added.is_err()); assert_eq!(get_chat_contacts(&ctx, chat.id).await.unwrap().len(), 1); diff --git a/src/receive_imf.rs b/src/receive_imf.rs index cca73384a..463a2483b 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1923,21 +1923,7 @@ async fn apply_group_changes( } if new_members != chat_contacts { - let new_members_ref = &new_members; - context - .sql - .transaction(move |transaction| { - transaction - .execute("DELETE FROM chats_contacts WHERE chat_id=?", (chat_id,))?; - for contact_id in new_members_ref { - transaction.execute( - "INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)", - (chat_id, contact_id), - )?; - } - Ok(()) - }) - .await?; + chat::update_chat_contacts_table(context, chat_id, &new_members).await?; chat_contacts = new_members; send_event_chat_modified = true; } diff --git a/src/securejoin.rs b/src/securejoin.rs index 71336ac5a..8cad9d840 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -21,6 +21,7 @@ use crate::param::Param; use crate::peerstate::{Peerstate, PeerstateKeyType}; use crate::qr::check_qr; use crate::stock_str; +use crate::sync::Sync::*; use crate::token; use crate::tools::time; @@ -443,8 +444,14 @@ pub(crate) async fn handle_securejoin_handshake( match chat::get_chat_id_by_grpid(context, field_grpid).await? { Some((group_chat_id, _, _)) => { secure_connection_established(context, contact_id, group_chat_id).await?; - chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true) - .await?; + chat::add_contact_to_chat_ex( + context, + Nosync, + group_chat_id, + contact_id, + true, + ) + .await?; } None => bail!("Chat {} not found", &field_grpid), } diff --git a/src/sync.rs b/src/sync.rs index 87a425d83..673a46e10 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -56,6 +56,8 @@ pub(crate) enum ChatAction { Accept, SetVisibility(ChatVisibility), SetMuted(chat::MuteDuration), + /// Set chat contacts by their addresses. + SetContacts(Vec), } #[derive(Debug, Serialize, Deserialize)]