mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 09:26:29 +03:00
refactor: Move chat-related code from sync to chat module
- Reduce cross-module dependencies. - Stop bloating the `sync` module while implementing synchronisation of more entities. - Now there's the only `ChatId` :)
This commit is contained in:
240
src/chat.rs
240
src/chat.rs
@@ -39,7 +39,7 @@ use crate::receive_imf::ReceivedMsg;
|
|||||||
use crate::smtp::send_msg_to_smtp;
|
use crate::smtp::send_msg_to_smtp;
|
||||||
use crate::sql;
|
use crate::sql;
|
||||||
use crate::stock_str;
|
use crate::stock_str;
|
||||||
use crate::sync::{self, ChatAction, Sync::*, SyncData};
|
use crate::sync::{self, Sync::*, SyncData};
|
||||||
use crate::tools::{
|
use crate::tools::{
|
||||||
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
|
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
|
||||||
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
|
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
|
||||||
@@ -400,7 +400,7 @@ impl ChatId {
|
|||||||
|
|
||||||
if sync.into() {
|
if sync.into() {
|
||||||
// NB: For a 1:1 chat this currently triggers `Contact::block()` on other devices.
|
// NB: For a 1:1 chat this currently triggers `Contact::block()` on other devices.
|
||||||
chat.sync(context, ChatAction::Block)
|
chat.sync(context, SyncAction::Block)
|
||||||
.await
|
.await
|
||||||
.log_err(context)
|
.log_err(context)
|
||||||
.ok();
|
.ok();
|
||||||
@@ -421,7 +421,7 @@ impl ChatId {
|
|||||||
// TODO: For a 1:1 chat this currently triggers `Contact::unblock()` on other devices.
|
// TODO: For a 1:1 chat this currently triggers `Contact::unblock()` on other devices.
|
||||||
// Maybe we should unblock the contact locally too, this would also resolve discrepancy
|
// Maybe we should unblock the contact locally too, this would also resolve discrepancy
|
||||||
// with `block()` which also blocks the contact.
|
// with `block()` which also blocks the contact.
|
||||||
chat.sync(context, ChatAction::Unblock)
|
chat.sync(context, SyncAction::Unblock)
|
||||||
.await
|
.await
|
||||||
.log_err(context)
|
.log_err(context)
|
||||||
.ok();
|
.ok();
|
||||||
@@ -472,7 +472,7 @@ impl ChatId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if sync.into() {
|
if sync.into() {
|
||||||
chat.sync(context, ChatAction::Accept)
|
chat.sync(context, SyncAction::Accept)
|
||||||
.await
|
.await
|
||||||
.log_err(context)
|
.log_err(context)
|
||||||
.ok();
|
.ok();
|
||||||
@@ -637,7 +637,7 @@ impl ChatId {
|
|||||||
|
|
||||||
if sync.into() {
|
if sync.into() {
|
||||||
let chat = Chat::load_from_db(context, self).await?;
|
let chat = Chat::load_from_db(context, self).await?;
|
||||||
chat.sync(context, ChatAction::SetVisibility(visibility))
|
chat.sync(context, SyncAction::SetVisibility(visibility))
|
||||||
.await
|
.await
|
||||||
.log_err(context)
|
.log_err(context)
|
||||||
.ok();
|
.ok();
|
||||||
@@ -1932,18 +1932,18 @@ impl Chat {
|
|||||||
Ok(msg.id)
|
Ok(msg.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a `ChatAction` synchronising chat contacts to other devices.
|
/// Sends a `SyncAction` synchronising chat contacts to other devices.
|
||||||
pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> {
|
pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> {
|
||||||
let mut addrs = Vec::new();
|
let mut addrs = Vec::new();
|
||||||
for contact_id in get_chat_contacts(context, self.id).await? {
|
for contact_id in get_chat_contacts(context, self.id).await? {
|
||||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||||
addrs.push(contact.get_addr().to_string());
|
addrs.push(contact.get_addr().to_string());
|
||||||
}
|
}
|
||||||
self.sync(context, ChatAction::SetContacts(addrs)).await
|
self.sync(context, SyncAction::SetContacts(addrs)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns chat id for the purpose of synchronisation across devices.
|
/// Returns chat id for the purpose of synchronisation across devices.
|
||||||
async fn get_sync_id(&self, context: &Context) -> Result<Option<sync::ChatId>> {
|
async fn get_sync_id(&self, context: &Context) -> Result<Option<SyncId>> {
|
||||||
match self.typ {
|
match self.typ {
|
||||||
Chattype::Single => {
|
Chattype::Single => {
|
||||||
let mut r = None;
|
let mut r = None;
|
||||||
@@ -1955,7 +1955,7 @@ impl Chat {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||||
r = Some(sync::ChatId::ContactAddr(contact.get_addr().to_string()));
|
r = Some(SyncId::ContactAddr(contact.get_addr().to_string()));
|
||||||
}
|
}
|
||||||
Ok(r)
|
Ok(r)
|
||||||
}
|
}
|
||||||
@@ -1963,13 +1963,13 @@ impl Chat {
|
|||||||
if self.grpid.is_empty() {
|
if self.grpid.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
Ok(Some(sync::ChatId::Grpid(self.grpid.clone())))
|
Ok(Some(SyncId::Grpid(self.grpid.clone())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Synchronises a chat action to other devices.
|
/// Synchronises a chat action to other devices.
|
||||||
pub(crate) async fn sync(&self, context: &Context, action: ChatAction) -> Result<()> {
|
pub(crate) async fn sync(&self, context: &Context, action: SyncAction) -> Result<()> {
|
||||||
if let Some(id) = self.get_sync_id(context).await? {
|
if let Some(id) = self.get_sync_id(context).await? {
|
||||||
sync(context, id, action).await?;
|
sync(context, id, action).await?;
|
||||||
}
|
}
|
||||||
@@ -1977,7 +1977,7 @@ impl Chat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sync(context: &Context, id: sync::ChatId, action: ChatAction) -> Result<()> {
|
async fn sync(context: &Context, id: SyncId, action: SyncAction) -> Result<()> {
|
||||||
context
|
context
|
||||||
.add_sync_item(SyncData::AlterChat { id, action })
|
.add_sync_item(SyncData::AlterChat { id, action })
|
||||||
.await?;
|
.await?;
|
||||||
@@ -3290,8 +3290,8 @@ async fn create_broadcast_list_ex(
|
|||||||
|
|
||||||
context.emit_msgs_changed_without_ids();
|
context.emit_msgs_changed_without_ids();
|
||||||
if sync.into() {
|
if sync.into() {
|
||||||
let id = sync::ChatId::Grpid(grpid);
|
let id = SyncId::Grpid(grpid);
|
||||||
let action = ChatAction::CreateBroadcast(chat_name);
|
let action = SyncAction::CreateBroadcast(chat_name);
|
||||||
self::sync(context, id, action).await.log_err(context).ok();
|
self::sync(context, id, action).await.log_err(context).ok();
|
||||||
}
|
}
|
||||||
Ok(chat_id)
|
Ok(chat_id)
|
||||||
@@ -3568,7 +3568,7 @@ pub(crate) async fn set_muted_ex(
|
|||||||
context.emit_event(EventType::ChatModified(chat_id));
|
context.emit_event(EventType::ChatModified(chat_id));
|
||||||
if sync.into() {
|
if sync.into() {
|
||||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||||
chat.sync(context, ChatAction::SetMuted(duration))
|
chat.sync(context, SyncAction::SetMuted(duration))
|
||||||
.await
|
.await
|
||||||
.log_err(context)
|
.log_err(context)
|
||||||
.ok();
|
.ok();
|
||||||
@@ -4221,15 +4221,34 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String])
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A cross-device chat id used for synchronisation.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub(crate) enum SyncId {
|
||||||
|
ContactAddr(String),
|
||||||
|
Grpid(String),
|
||||||
|
// NOTE: Ad-hoc groups lack an identifier that can be used across devices so
|
||||||
|
// block/mute/etc. actions on them are not synchronized to other devices.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An action synchronised to other devices.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub(crate) enum SyncAction {
|
||||||
|
Block,
|
||||||
|
Unblock,
|
||||||
|
Accept,
|
||||||
|
SetVisibility(ChatVisibility),
|
||||||
|
SetMuted(MuteDuration),
|
||||||
|
/// Create broadcast list with the given name.
|
||||||
|
CreateBroadcast(String),
|
||||||
|
/// Set chat contacts by their addresses.
|
||||||
|
SetContacts(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
/// Executes [`SyncData::AlterChat`] item sent by other device.
|
/// Executes [`SyncData::AlterChat`] item sent by other device.
|
||||||
pub(crate) async fn sync_alter_chat(
|
pub(crate) async fn sync_alter_chat(&self, id: &SyncId, action: &SyncAction) -> Result<()> {
|
||||||
&self,
|
|
||||||
id: &sync::ChatId,
|
|
||||||
action: &ChatAction,
|
|
||||||
) -> Result<()> {
|
|
||||||
let chat_id = match id {
|
let chat_id = match id {
|
||||||
sync::ChatId::ContactAddr(addr) => {
|
SyncId::ContactAddr(addr) => {
|
||||||
let Some(contact_id) =
|
let Some(contact_id) =
|
||||||
Contact::lookup_id_by_addr_ex(self, addr, Origin::Unknown, None).await?
|
Contact::lookup_id_by_addr_ex(self, addr, Origin::Unknown, None).await?
|
||||||
else {
|
else {
|
||||||
@@ -4237,10 +4256,10 @@ impl Context {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
match action {
|
match action {
|
||||||
ChatAction::Block => {
|
SyncAction::Block => {
|
||||||
return contact::set_blocked(self, Nosync, contact_id, true).await
|
return contact::set_blocked(self, Nosync, contact_id, true).await
|
||||||
}
|
}
|
||||||
ChatAction::Unblock => {
|
SyncAction::Unblock => {
|
||||||
return contact::set_blocked(self, Nosync, contact_id, false).await
|
return contact::set_blocked(self, Nosync, contact_id, false).await
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
@@ -4251,8 +4270,8 @@ impl Context {
|
|||||||
};
|
};
|
||||||
chat_id
|
chat_id
|
||||||
}
|
}
|
||||||
sync::ChatId::Grpid(grpid) => {
|
SyncId::Grpid(grpid) => {
|
||||||
if let ChatAction::CreateBroadcast(name) = action {
|
if let SyncAction::CreateBroadcast(name) = action {
|
||||||
create_broadcast_list_ex(self, Nosync, grpid.clone(), name.clone()).await?;
|
create_broadcast_list_ex(self, Nosync, grpid.clone(), name.clone()).await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -4264,15 +4283,15 @@ impl Context {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
match action {
|
match action {
|
||||||
ChatAction::Block => chat_id.block_ex(self, Nosync).await,
|
SyncAction::Block => chat_id.block_ex(self, Nosync).await,
|
||||||
ChatAction::Unblock => chat_id.unblock_ex(self, Nosync).await,
|
SyncAction::Unblock => chat_id.unblock_ex(self, Nosync).await,
|
||||||
ChatAction::Accept => chat_id.accept_ex(self, Nosync).await,
|
SyncAction::Accept => chat_id.accept_ex(self, Nosync).await,
|
||||||
ChatAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await,
|
SyncAction::SetVisibility(v) => chat_id.set_visibility_ex(self, Nosync, *v).await,
|
||||||
ChatAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await,
|
SyncAction::SetMuted(duration) => set_muted_ex(self, Nosync, chat_id, *duration).await,
|
||||||
ChatAction::CreateBroadcast(_) => {
|
SyncAction::CreateBroadcast(_) => {
|
||||||
Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request."))
|
Err(anyhow!("sync_alter_chat({id:?}, {action:?}): Bad request."))
|
||||||
}
|
}
|
||||||
ChatAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
|
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4286,6 +4305,7 @@ mod tests {
|
|||||||
use crate::message::delete_msgs;
|
use crate::message::delete_msgs;
|
||||||
use crate::receive_imf::receive_imf;
|
use crate::receive_imf::receive_imf;
|
||||||
use crate::test_utils::{TestContext, TestContextManager};
|
use crate::test_utils::{TestContext, TestContextManager};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
@@ -6764,4 +6784,160 @@ mod tests {
|
|||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_sync_alter_chat() -> Result<()> {
|
||||||
|
let alices = [
|
||||||
|
TestContext::new_alice().await,
|
||||||
|
TestContext::new_alice().await,
|
||||||
|
];
|
||||||
|
for a in &alices {
|
||||||
|
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||||
|
}
|
||||||
|
let bob = TestContext::new_bob().await;
|
||||||
|
|
||||||
|
let ba_chat = bob.create_chat(&alices[0]).await;
|
||||||
|
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||||
|
let a0b_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
|
||||||
|
alices[1].recv_msg(&sent_msg).await;
|
||||||
|
let ab_contact_ids = [
|
||||||
|
alices[0].add_or_lookup_contact(&bob).await.id,
|
||||||
|
alices[1].add_or_lookup_contact(&bob).await.id,
|
||||||
|
];
|
||||||
|
|
||||||
|
async fn sync(alices: &[TestContext]) -> Result<()> {
|
||||||
|
let sync_msg = alices.get(0).unwrap().pop_sent_msg().await;
|
||||||
|
alices.get(1).unwrap().recv_msg(&sync_msg).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Request);
|
||||||
|
a0b_chat_id.accept(&alices[0]).await?;
|
||||||
|
sync(&alices).await?;
|
||||||
|
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
|
||||||
|
a0b_chat_id.block(&alices[0]).await?;
|
||||||
|
sync(&alices).await?;
|
||||||
|
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Yes);
|
||||||
|
a0b_chat_id.unblock(&alices[0]).await?;
|
||||||
|
sync(&alices).await?;
|
||||||
|
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
|
||||||
|
|
||||||
|
// Unblocking a 1:1 chat doesn't unblock the contact currently.
|
||||||
|
Contact::unblock(&alices[0], ab_contact_ids[0]).await?;
|
||||||
|
|
||||||
|
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
||||||
|
Contact::block(&alices[0], ab_contact_ids[0]).await?;
|
||||||
|
sync(&alices).await?;
|
||||||
|
assert!(alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
||||||
|
Contact::unblock(&alices[0], ab_contact_ids[0]).await?;
|
||||||
|
sync(&alices).await?;
|
||||||
|
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
||||||
|
|
||||||
|
// Test accepting and blocking groups. This way we test:
|
||||||
|
// - Group chats synchronisation.
|
||||||
|
// - That blocking a group deletes it on other devices.
|
||||||
|
let fiona = TestContext::new_fiona().await;
|
||||||
|
let fiona_grp_chat_id = fiona
|
||||||
|
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&alices[0]])
|
||||||
|
.await;
|
||||||
|
let sent_msg = fiona.send_text(fiona_grp_chat_id, "hi").await;
|
||||||
|
let a0_grp_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
|
||||||
|
let a1_grp_chat_id = alices[1].recv_msg(&sent_msg).await.chat_id;
|
||||||
|
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
|
||||||
|
assert_eq!(a1_grp_chat.blocked, Blocked::Request);
|
||||||
|
a0_grp_chat_id.accept(&alices[0]).await?;
|
||||||
|
sync(&alices).await?;
|
||||||
|
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
|
||||||
|
assert_eq!(a1_grp_chat.blocked, Blocked::Not);
|
||||||
|
a0_grp_chat_id.block(&alices[0]).await?;
|
||||||
|
sync(&alices).await?;
|
||||||
|
assert!(Chat::load_from_db(&alices[1], a1_grp_chat_id)
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
assert!(
|
||||||
|
!alices[1]
|
||||||
|
.sql
|
||||||
|
.exists("SELECT COUNT(*) FROM chats WHERE id=?", (a1_grp_chat_id,))
|
||||||
|
.await?
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test syncing of chat visibility on a self-chat. This way we test:
|
||||||
|
// - Self-chat synchronisation.
|
||||||
|
// - That sync messages don't unarchive the self-chat.
|
||||||
|
let a0self_chat_id = alices[0].get_self_chat().await.id;
|
||||||
|
assert_eq!(
|
||||||
|
alices[1].get_self_chat().await.get_visibility(),
|
||||||
|
ChatVisibility::Normal
|
||||||
|
);
|
||||||
|
let mut visibilities =
|
||||||
|
ChatVisibility::iter().chain(std::iter::once(ChatVisibility::Normal));
|
||||||
|
visibilities.next();
|
||||||
|
for v in visibilities {
|
||||||
|
a0self_chat_id.set_visibility(&alices[0], v).await?;
|
||||||
|
sync(&alices).await?;
|
||||||
|
for a in &alices {
|
||||||
|
assert_eq!(a.get_self_chat().await.get_visibility(), v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
alices[1].get_chat(&bob).await.mute_duration,
|
||||||
|
MuteDuration::NotMuted
|
||||||
|
);
|
||||||
|
let mute_durations = [
|
||||||
|
MuteDuration::Forever,
|
||||||
|
MuteDuration::Until(SystemTime::now() + Duration::from_secs(42)),
|
||||||
|
MuteDuration::NotMuted,
|
||||||
|
];
|
||||||
|
for m in mute_durations {
|
||||||
|
set_muted(&alices[0], a0b_chat_id, m).await?;
|
||||||
|
sync(&alices).await?;
|
||||||
|
let m = match m {
|
||||||
|
MuteDuration::Until(time) => MuteDuration::Until(
|
||||||
|
SystemTime::UNIX_EPOCH
|
||||||
|
+ Duration::from_secs(
|
||||||
|
time.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_ => m,
|
||||||
|
};
|
||||||
|
assert_eq!(alices[1].get_chat(&bob).await.mute_duration, m);
|
||||||
|
}
|
||||||
|
|
||||||
|
let a0_broadcast_id = create_broadcast_list(&alices[0]).await?;
|
||||||
|
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
|
||||||
|
set_chat_name(&alices[0], a0_broadcast_id, "Broadcast list 42").await?;
|
||||||
|
sync(&alices).await?;
|
||||||
|
let a1_broadcast_id = get_chat_id_by_grpid(&alices[1], &a0_broadcast_chat.grpid)
|
||||||
|
.await?
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
let a1_broadcast_chat = Chat::load_from_db(&alices[1], a1_broadcast_id).await?;
|
||||||
|
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
|
||||||
|
// TODO: Implement synchronisation of `set_chat_name()`.
|
||||||
|
// assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
|
||||||
|
assert!(get_chat_contacts(&alices[1], a1_broadcast_id)
|
||||||
|
.await?
|
||||||
|
.is_empty());
|
||||||
|
add_contact_to_chat(&alices[0], a0_broadcast_id, ab_contact_ids[0]).await?;
|
||||||
|
sync(&alices).await?;
|
||||||
|
assert_eq!(
|
||||||
|
get_chat_contacts(&alices[1], a1_broadcast_id).await?,
|
||||||
|
vec![ab_contact_ids[1]]
|
||||||
|
);
|
||||||
|
let sent_msg = alices[1].send_text(a1_broadcast_id, "hi").await;
|
||||||
|
let msg = bob.recv_msg(&sent_msg).await;
|
||||||
|
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||||
|
assert_eq!(chat.get_type(), Chattype::Mailinglist);
|
||||||
|
// TODO: It doesn't work now for some reason, `msg.chat_id == DC_CHAT_ID_TRASH`.
|
||||||
|
// let msg = alices[0].recv_msg(&sent_msg).await;
|
||||||
|
// assert_eq!(msg.chat_id, a0_broadcast_id);
|
||||||
|
remove_contact_from_chat(&alices[0], a0_broadcast_id, ab_contact_ids[0]).await?;
|
||||||
|
sync(&alices).await?;
|
||||||
|
assert!(get_chat_contacts(&alices[1], a1_broadcast_id)
|
||||||
|
.await?
|
||||||
|
.is_empty());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1480,12 +1480,12 @@ WHERE type=? AND id IN (
|
|||||||
|
|
||||||
if sync.into() {
|
if sync.into() {
|
||||||
let action = match new_blocking {
|
let action = match new_blocking {
|
||||||
true => sync::ChatAction::Block,
|
true => chat::SyncAction::Block,
|
||||||
false => sync::ChatAction::Unblock,
|
false => chat::SyncAction::Unblock,
|
||||||
};
|
};
|
||||||
context
|
context
|
||||||
.add_sync_item(SyncData::AlterChat {
|
.add_sync_item(SyncData::AlterChat {
|
||||||
id: sync::ChatId::ContactAddr(contact.addr.clone()),
|
id: chat::SyncId::ContactAddr(contact.addr.clone()),
|
||||||
action,
|
action,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
212
src/sync.rs
212
src/sync.rs
@@ -5,7 +5,7 @@ use lettre_email::mime::{self};
|
|||||||
use lettre_email::PartBuilder;
|
use lettre_email::PartBuilder;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::chat::{self, Chat, ChatVisibility};
|
use crate::chat::{self, Chat, ChatId};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::Blocked;
|
use crate::constants::Blocked;
|
||||||
use crate::contact::ContactId;
|
use crate::contact::ContactId;
|
||||||
@@ -42,32 +42,14 @@ pub(crate) struct QrTokenData {
|
|||||||
pub(crate) grpid: Option<String>,
|
pub(crate) grpid: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
|
||||||
pub(crate) enum ChatId {
|
|
||||||
ContactAddr(String),
|
|
||||||
Grpid(String),
|
|
||||||
// NOTE: Ad-hoc groups lack an identifier that can be used across devices so
|
|
||||||
// block/mute/etc. actions on them are not synchronized to other devices.
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
|
||||||
pub(crate) enum ChatAction {
|
|
||||||
Block,
|
|
||||||
Unblock,
|
|
||||||
Accept,
|
|
||||||
SetVisibility(ChatVisibility),
|
|
||||||
SetMuted(chat::MuteDuration),
|
|
||||||
/// Create broadcast list with the given name.
|
|
||||||
CreateBroadcast(String),
|
|
||||||
/// Set chat contacts by their addresses.
|
|
||||||
SetContacts(Vec<String>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub(crate) enum SyncData {
|
pub(crate) enum SyncData {
|
||||||
AddQrToken(QrTokenData),
|
AddQrToken(QrTokenData),
|
||||||
DeleteQrToken(QrTokenData),
|
DeleteQrToken(QrTokenData),
|
||||||
AlterChat { id: ChatId, action: ChatAction },
|
AlterChat {
|
||||||
|
id: chat::SyncId,
|
||||||
|
action: chat::SyncAction,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -109,7 +91,7 @@ impl Context {
|
|||||||
/// Adds most recent qr-code tokens for a given chat to the list of items to be synced.
|
/// Adds most recent qr-code tokens for a given chat to the list of items to be synced.
|
||||||
/// If device synchronization is disabled,
|
/// If device synchronization is disabled,
|
||||||
/// no tokens exist or the chat is unpromoted, the function does nothing.
|
/// no tokens exist or the chat is unpromoted, the function does nothing.
|
||||||
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<chat::ChatId>) -> Result<()> {
|
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<ChatId>) -> Result<()> {
|
||||||
if !self.get_config_bool(Config::SyncMsgs).await? {
|
if !self.get_config_bool(Config::SyncMsgs).await? {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -160,7 +142,7 @@ impl Context {
|
|||||||
pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
|
pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
|
||||||
if let Some((json, ids)) = self.build_sync_json().await? {
|
if let Some((json, ids)) = self.build_sync_json().await? {
|
||||||
let chat_id =
|
let chat_id =
|
||||||
chat::ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
|
ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
|
||||||
.await?;
|
.await?;
|
||||||
let mut msg = Message {
|
let mut msg = Message {
|
||||||
chat_id,
|
chat_id,
|
||||||
@@ -300,12 +282,10 @@ mod tests {
|
|||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use anyhow::bail;
|
use anyhow::bail;
|
||||||
use strum::IntoEnumIterator;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::chat::{Chat, ProtectionStatus};
|
use crate::chat::Chat;
|
||||||
use crate::chatlist::Chatlist;
|
use crate::chatlist::Chatlist;
|
||||||
use crate::constants::Chattype;
|
|
||||||
use crate::contact::{Contact, Origin};
|
use crate::contact::{Contact, Origin};
|
||||||
use crate::test_utils::TestContext;
|
use crate::test_utils::TestContext;
|
||||||
use crate::token::Namespace;
|
use crate::token::Namespace;
|
||||||
@@ -328,13 +308,13 @@ mod tests {
|
|||||||
|
|
||||||
assert!(t.build_sync_json().await?.is_none());
|
assert!(t.build_sync_json().await?.is_none());
|
||||||
|
|
||||||
// Having one test on `SyncData::AlterChat` is sufficient here as `ChatAction::SetMuted`
|
// Having one test on `SyncData::AlterChat` is sufficient here as
|
||||||
// introduces enums inside items and `SystemTime`. Let's avoid in-depth testing of the
|
// `chat::SyncAction::SetMuted` introduces enums inside items and `SystemTime`. Let's avoid
|
||||||
// serialiser here which is an external crate.
|
// in-depth testing of the serialiser here which is an external crate.
|
||||||
t.add_sync_item_with_timestamp(
|
t.add_sync_item_with_timestamp(
|
||||||
SyncData::AlterChat {
|
SyncData::AlterChat {
|
||||||
id: ChatId::ContactAddr("bob@example.net".to_string()),
|
id: chat::SyncId::ContactAddr("bob@example.net".to_string()),
|
||||||
action: ChatAction::SetMuted(chat::MuteDuration::Until(
|
action: chat::SyncAction::SetMuted(chat::MuteDuration::Until(
|
||||||
SystemTime::UNIX_EPOCH + Duration::from_millis(42999),
|
SystemTime::UNIX_EPOCH + Duration::from_millis(42999),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
@@ -447,10 +427,13 @@ mod tests {
|
|||||||
let AlterChat { id, action } = &sync_items.items.get(0).unwrap().data else {
|
let AlterChat { id, action } = &sync_items.items.get(0).unwrap().data else {
|
||||||
bail!("bad item");
|
bail!("bad item");
|
||||||
};
|
};
|
||||||
assert_eq!(*id, ChatId::ContactAddr("bob@example.net".to_string()));
|
assert_eq!(
|
||||||
|
*id,
|
||||||
|
chat::SyncId::ContactAddr("bob@example.net".to_string())
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*action,
|
*action,
|
||||||
ChatAction::SetMuted(chat::MuteDuration::Until(
|
chat::SyncAction::SetMuted(chat::MuteDuration::Until(
|
||||||
SystemTime::UNIX_EPOCH + Duration::from_millis(42999)
|
SystemTime::UNIX_EPOCH + Duration::from_millis(42999)
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
@@ -555,7 +538,7 @@ mod tests {
|
|||||||
// check that the used self-talk is not visible to the user
|
// 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)
|
// 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);
|
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 0);
|
||||||
let chat_id = chat::ChatId::create_for_contact(&alice, ContactId::SELF).await?;
|
let chat_id = ChatId::create_for_contact(&alice, ContactId::SELF).await?;
|
||||||
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
||||||
assert!(chat.is_self_talk());
|
assert!(chat.is_self_talk());
|
||||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
||||||
@@ -578,161 +561,4 @@ mod tests {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
||||||
async fn test_alter_chat() -> Result<()> {
|
|
||||||
let alices = [
|
|
||||||
TestContext::new_alice().await,
|
|
||||||
TestContext::new_alice().await,
|
|
||||||
];
|
|
||||||
for a in &alices {
|
|
||||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
|
||||||
}
|
|
||||||
let bob = TestContext::new_bob().await;
|
|
||||||
|
|
||||||
let ba_chat = bob.create_chat(&alices[0]).await;
|
|
||||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
|
||||||
let a0b_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
|
|
||||||
alices[1].recv_msg(&sent_msg).await;
|
|
||||||
let ab_contact_ids = [
|
|
||||||
alices[0].add_or_lookup_contact(&bob).await.id,
|
|
||||||
alices[1].add_or_lookup_contact(&bob).await.id,
|
|
||||||
];
|
|
||||||
|
|
||||||
async fn sync(alices: &[TestContext]) -> Result<()> {
|
|
||||||
let sync_msg = alices.get(0).unwrap().pop_sent_msg().await;
|
|
||||||
alices.get(1).unwrap().recv_msg(&sync_msg).await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Request);
|
|
||||||
a0b_chat_id.accept(&alices[0]).await?;
|
|
||||||
sync(&alices).await?;
|
|
||||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
|
|
||||||
a0b_chat_id.block(&alices[0]).await?;
|
|
||||||
sync(&alices).await?;
|
|
||||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Yes);
|
|
||||||
a0b_chat_id.unblock(&alices[0]).await?;
|
|
||||||
sync(&alices).await?;
|
|
||||||
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
|
|
||||||
|
|
||||||
// Unblocking a 1:1 chat doesn't unblock the contact currently.
|
|
||||||
Contact::unblock(&alices[0], ab_contact_ids[0]).await?;
|
|
||||||
|
|
||||||
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
|
||||||
Contact::block(&alices[0], ab_contact_ids[0]).await?;
|
|
||||||
sync(&alices).await?;
|
|
||||||
assert!(alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
|
||||||
Contact::unblock(&alices[0], ab_contact_ids[0]).await?;
|
|
||||||
sync(&alices).await?;
|
|
||||||
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
|
|
||||||
|
|
||||||
// Test accepting and blocking groups. This way we test:
|
|
||||||
// - Group chats synchronisation.
|
|
||||||
// - That blocking a group deletes it on other devices.
|
|
||||||
let fiona = TestContext::new_fiona().await;
|
|
||||||
let fiona_grp_chat_id = fiona
|
|
||||||
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&alices[0]])
|
|
||||||
.await;
|
|
||||||
let sent_msg = fiona.send_text(fiona_grp_chat_id, "hi").await;
|
|
||||||
let a0_grp_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
|
|
||||||
let a1_grp_chat_id = alices[1].recv_msg(&sent_msg).await.chat_id;
|
|
||||||
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
|
|
||||||
assert_eq!(a1_grp_chat.blocked, Blocked::Request);
|
|
||||||
a0_grp_chat_id.accept(&alices[0]).await?;
|
|
||||||
sync(&alices).await?;
|
|
||||||
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
|
|
||||||
assert_eq!(a1_grp_chat.blocked, Blocked::Not);
|
|
||||||
a0_grp_chat_id.block(&alices[0]).await?;
|
|
||||||
sync(&alices).await?;
|
|
||||||
assert!(Chat::load_from_db(&alices[1], a1_grp_chat_id)
|
|
||||||
.await
|
|
||||||
.is_err());
|
|
||||||
assert!(
|
|
||||||
!alices[1]
|
|
||||||
.sql
|
|
||||||
.exists("SELECT COUNT(*) FROM chats WHERE id=?", (a1_grp_chat_id,))
|
|
||||||
.await?
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test syncing of chat visibility on a self-chat. This way we test:
|
|
||||||
// - Self-chat synchronisation.
|
|
||||||
// - That sync messages don't unarchive the self-chat.
|
|
||||||
let a0self_chat_id = alices[0].get_self_chat().await.id;
|
|
||||||
assert_eq!(
|
|
||||||
alices[1].get_self_chat().await.get_visibility(),
|
|
||||||
ChatVisibility::Normal
|
|
||||||
);
|
|
||||||
let mut visibilities =
|
|
||||||
ChatVisibility::iter().chain(std::iter::once(ChatVisibility::Normal));
|
|
||||||
visibilities.next();
|
|
||||||
for v in visibilities {
|
|
||||||
a0self_chat_id.set_visibility(&alices[0], v).await?;
|
|
||||||
sync(&alices).await?;
|
|
||||||
for a in &alices {
|
|
||||||
assert_eq!(a.get_self_chat().await.get_visibility(), v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use chat::MuteDuration;
|
|
||||||
assert_eq!(
|
|
||||||
alices[1].get_chat(&bob).await.mute_duration,
|
|
||||||
MuteDuration::NotMuted
|
|
||||||
);
|
|
||||||
let mute_durations = [
|
|
||||||
MuteDuration::Forever,
|
|
||||||
MuteDuration::Until(SystemTime::now() + Duration::from_secs(42)),
|
|
||||||
MuteDuration::NotMuted,
|
|
||||||
];
|
|
||||||
for m in mute_durations {
|
|
||||||
chat::set_muted(&alices[0], a0b_chat_id, m).await?;
|
|
||||||
sync(&alices).await?;
|
|
||||||
let m = match m {
|
|
||||||
MuteDuration::Until(time) => MuteDuration::Until(
|
|
||||||
SystemTime::UNIX_EPOCH
|
|
||||||
+ Duration::from_secs(
|
|
||||||
time.duration_since(SystemTime::UNIX_EPOCH)?.as_secs(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_ => m,
|
|
||||||
};
|
|
||||||
assert_eq!(alices[1].get_chat(&bob).await.mute_duration, m);
|
|
||||||
}
|
|
||||||
|
|
||||||
let a0_broadcast_id = chat::create_broadcast_list(&alices[0]).await?;
|
|
||||||
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
|
|
||||||
chat::set_chat_name(&alices[0], a0_broadcast_id, "Broadcast list 42").await?;
|
|
||||||
sync(&alices).await?;
|
|
||||||
let a1_broadcast_id = chat::get_chat_id_by_grpid(&alices[1], &a0_broadcast_chat.grpid)
|
|
||||||
.await?
|
|
||||||
.unwrap()
|
|
||||||
.0;
|
|
||||||
let a1_broadcast_chat = Chat::load_from_db(&alices[1], a1_broadcast_id).await?;
|
|
||||||
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
|
|
||||||
// TODO: Implement synchronisation of `chat::set_chat_name()`.
|
|
||||||
// assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
|
|
||||||
assert!(chat::get_chat_contacts(&alices[1], a1_broadcast_id)
|
|
||||||
.await?
|
|
||||||
.is_empty());
|
|
||||||
chat::add_contact_to_chat(&alices[0], a0_broadcast_id, ab_contact_ids[0]).await?;
|
|
||||||
sync(&alices).await?;
|
|
||||||
assert_eq!(
|
|
||||||
chat::get_chat_contacts(&alices[1], a1_broadcast_id).await?,
|
|
||||||
vec![ab_contact_ids[1]]
|
|
||||||
);
|
|
||||||
let sent_msg = alices[1].send_text(a1_broadcast_id, "hi").await;
|
|
||||||
let msg = bob.recv_msg(&sent_msg).await;
|
|
||||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
|
||||||
assert_eq!(chat.get_type(), Chattype::Mailinglist);
|
|
||||||
// TODO: It doesn't work now for some reason, `msg.chat_id == DC_CHAT_ID_TRASH`.
|
|
||||||
// let msg = alices[0].recv_msg(&sent_msg).await;
|
|
||||||
// assert_eq!(msg.chat_id, a0_broadcast_id);
|
|
||||||
chat::remove_contact_from_chat(&alices[0], a0_broadcast_id, ab_contact_ids[0]).await?;
|
|
||||||
sync(&alices).await?;
|
|
||||||
assert!(chat::get_chat_contacts(&alices[1], a1_broadcast_id)
|
|
||||||
.await?
|
|
||||||
.is_empty());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user