mirror of
https://github.com/chatmail/core.git
synced 2026-04-23 00:16:34 +03:00
feat: Sync chat Blocked state across devices (#4817)
This commit is contained in:
125
src/sync.rs
125
src/sync.rs
@@ -5,7 +5,7 @@ use lettre_email::mime::{self};
|
||||
use lettre_email::PartBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{Chat, ChatId};
|
||||
use crate::chat::{self, Chat};
|
||||
use crate::config::Config;
|
||||
use crate::constants::Blocked;
|
||||
use crate::contact::ContactId;
|
||||
@@ -13,10 +13,10 @@ use crate::context::Context;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::sync::SyncData::{AddQrToken, DeleteQrToken};
|
||||
use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
|
||||
use crate::token::Namespace;
|
||||
use crate::tools::time;
|
||||
use crate::{chat, stock_str, token};
|
||||
use crate::{stock_str, token};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct QrTokenData {
|
||||
@@ -25,10 +25,40 @@ pub(crate) struct QrTokenData {
|
||||
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,
|
||||
// TODO: Actually unblocking a chat is not a public API. `Contact::unblock()` is what a user
|
||||
// does actually, but it doesn't call `chat::ChatId::unblock()`. So, unblocking chats sync
|
||||
// doesn't work now, but let it be implemented on chats nevertheless. The straightforward fix is
|
||||
// to call `chat::ChatId::unblock()` in a context of user action.
|
||||
//
|
||||
// But it still works if a message is sent to a blocked contact because
|
||||
// `chat::ChatId::unblock()` is called then.
|
||||
Unblock,
|
||||
|
||||
Accept,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct AlterChatData {
|
||||
pub(crate) id: ChatId,
|
||||
pub(crate) action: ChatAction,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) enum SyncData {
|
||||
AddQrToken(QrTokenData),
|
||||
DeleteQrToken(QrTokenData),
|
||||
AlterChat(AlterChatData),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -67,7 +97,7 @@ impl Context {
|
||||
/// 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<()> {
|
||||
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<chat::ChatId>) -> Result<()> {
|
||||
if !self.get_config_bool(Config::SyncMsgs).await? {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -118,7 +148,7 @@ impl Context {
|
||||
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, ContactId::SELF, Blocked::Yes)
|
||||
chat::ChatId::create_for_contact_with_blocked(self, ContactId::SELF, Blocked::Yes)
|
||||
.await?;
|
||||
let mut msg = Message {
|
||||
chat_id,
|
||||
@@ -215,7 +245,7 @@ impl Context {
|
||||
/// 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<()> {
|
||||
async fn execute_sync_items_inner(&self, items: &SyncItems) -> Result<()> {
|
||||
info!(self, "executing {} sync item(s)", items.items.len());
|
||||
for item in &items.items {
|
||||
match &item.data {
|
||||
@@ -243,10 +273,16 @@ impl Context {
|
||||
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
|
||||
token::delete(self, Namespace::Auth, &token.auth).await?;
|
||||
}
|
||||
AlterChat(data) => self.sync_alter_chat(data).await?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Executes sync items sent by other device.
|
||||
pub(crate) async fn execute_sync_items(&self, items: &SyncItems) -> Result<()> {
|
||||
self.nosync().execute_sync_items_inner(items).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -256,6 +292,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::chat::Chat;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::token::Namespace;
|
||||
|
||||
@@ -277,6 +314,18 @@ mod tests {
|
||||
|
||||
assert!(t.build_sync_json().await?.is_none());
|
||||
|
||||
// Having one test on `SyncData::AlterChat` is sufficient here as `AlterChatData` introduces
|
||||
// enums inside items. Let's avoid in-depth testing of the serialiser here which is an
|
||||
// external crate.
|
||||
t.add_sync_item_with_timestamp(
|
||||
SyncData::AlterChat(AlterChatData {
|
||||
id: ChatId::ContactAddr("bob@example.net".to_string()),
|
||||
action: ChatAction::Block,
|
||||
}),
|
||||
1631781315,
|
||||
)
|
||||
.await?;
|
||||
|
||||
t.add_sync_item_with_timestamp(
|
||||
SyncData::AddQrToken(QrTokenData {
|
||||
invitenumber: "testinvite".to_string(),
|
||||
@@ -300,6 +349,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
serialized,
|
||||
r#"{"items":[
|
||||
{"timestamp":1631781315,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Block"}}},
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"testinvite","auth":"testauth","grpid":"group123"}}},
|
||||
{"timestamp":1631781317,"data":{"DeleteQrToken":{"invitenumber":"123!?\":.;{}","auth":"456","grpid":null}}}
|
||||
]}"#
|
||||
@@ -310,7 +360,7 @@ mod tests {
|
||||
assert!(t.build_sync_json().await?.is_none());
|
||||
|
||||
let sync_items = t.parse_sync_items(serialized)?;
|
||||
assert_eq!(sync_items.items.len(), 2);
|
||||
assert_eq!(sync_items.items.len(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -368,6 +418,22 @@ mod tests {
|
||||
)
|
||||
.is_err()); // missing field
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781318,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Burn"}}}]}"#.to_string(),
|
||||
)
|
||||
.is_err()); // Unknown enum value
|
||||
|
||||
// Test enums inside items
|
||||
let sync_items = t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781318,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Block"}}}]}"#.to_string(),
|
||||
)?;
|
||||
assert_eq!(sync_items.items.len(), 1);
|
||||
let AlterChat(AlterChatData { id, action }) = &sync_items.items.get(0).unwrap().data else {
|
||||
bail!("bad item");
|
||||
};
|
||||
assert_eq!(*id, ChatId::ContactAddr("bob@example.net".to_string()));
|
||||
assert_eq!(*action, ChatAction::Block);
|
||||
|
||||
// empty item list is okay
|
||||
assert_eq!(
|
||||
t.parse_sync_items(r#"{"items":[]}"#.to_string())?
|
||||
@@ -423,6 +489,7 @@ mod tests {
|
||||
let sync_items = t
|
||||
.parse_sync_items(
|
||||
r#"{"items":[
|
||||
{"timestamp":1631781315,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Block"}}},
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"yip-in","auth":"a"}}},
|
||||
{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"delete unexistent, shall continue"}}},
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip-auth"}}},
|
||||
@@ -435,6 +502,11 @@ mod tests {
|
||||
?;
|
||||
t.execute_sync_items(&sync_items).await?;
|
||||
|
||||
assert!(
|
||||
Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Unknown)
|
||||
.await?
|
||||
.is_none()
|
||||
);
|
||||
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-existent").await);
|
||||
@@ -462,7 +534,7 @@ mod tests {
|
||||
// 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, ContactId::SELF).await?;
|
||||
let chat_id = chat::ChatId::create_for_contact(&alice, ContactId::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);
|
||||
@@ -485,4 +557,41 @@ mod tests {
|
||||
|
||||
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;
|
||||
|
||||
async fn sync(alices: &[TestContext]) -> Result<()> {
|
||||
alices.get(0).unwrap().send_sync_msg().await?.unwrap();
|
||||
let sent_msg = alices.get(0).unwrap().pop_sent_msg().await;
|
||||
alices.get(1).unwrap().recv_msg(&sent_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);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user