diff --git a/src/chat.rs b/src/chat.rs index 17353eb7a..ac9405ea8 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -228,29 +228,31 @@ impl ChatId { /// /// This sends the message with the protected status change to the chat, /// notifying the user on this device as well as the other users in the chat. - /// If `promoted` is false this means the chat only exists on this device so far - /// and does not need to be sent out. - /// In this case an local info message is added to the chat. + /// + /// If `promote` is false this means, the message must not be sent out + /// and only a local info message should be added to the chat. + /// This is used when protection is enabled implicitly or when a chat is not yet promoted. pub(crate) async fn add_protection_msg( self, context: &Context, protect: ProtectionStatus, - promoted: bool, + promote: bool, from_id: u32, ) -> Result<(), Error> { let msg_text = context.stock_protection_msg(protect, from_id).await; + let cmd = match protect { + ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled, + ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled, + }; - if promoted { + if promote { let mut msg = Message::default(); msg.viewtype = Viewtype::Text; msg.text = Some(msg_text); - msg.param.set_cmd(match protect { - ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled, - ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled, - }); + msg.param.set_cmd(cmd); send_msg(context, self, &mut msg).await?; } else { - add_info_msg(context, self, msg_text).await; + add_info_msg_with_cmd(context, self, msg_text, cmd).await?; } Ok(()) @@ -2899,18 +2901,22 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul /// Adds an informational message to chat. /// /// For example, it can be a message showing that a member was added to a group. -pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl AsRef) { +pub(crate) async fn add_info_msg_with_cmd( + context: &Context, + chat_id: ChatId, + text: impl AsRef, + cmd: SystemMessage, +) -> Result { let rfc724_mid = dc_create_outgoing_rfc724_mid(None, "@device"); - let ephemeral_timer = match chat_id.get_ephemeral_timer(context).await { - Err(e) => { - warn!(context, "Could not get timer for info msg: {}", e); - return; - } - Ok(ephemeral_timer) => ephemeral_timer, - }; + let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?; - if let Err(e) = context.sql.execute( - "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid,ephemeral_timer) VALUES (?,?,?, ?,?,?, ?,?,?);", + let mut param = Params::new(); + if cmd != SystemMessage::Unknown { + param.set_cmd(cmd) + } + + context.sql.execute( + "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid,ephemeral_timer, param) VALUES (?,?,?, ?,?,?, ?,?,?, ?);", paramsv![ chat_id, DC_CONTACT_ID_INFO, @@ -2920,22 +2926,25 @@ pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl MessageState::InNoticed, text.as_ref().to_string(), rfc724_mid, - ephemeral_timer + ephemeral_timer, + param.to_string(), ] - ).await { - warn!(context, "Could not add info msg: {}", e); - return; - } + ).await?; let row_id = context .sql .get_rowid(context, "msgs", "rfc724_mid", &rfc724_mid) .await .unwrap_or_default(); - context.emit_event(EventType::MsgsChanged { - chat_id, - msg_id: MsgId::new(row_id), - }); + let msg_id = MsgId::new(row_id); + context.emit_event(EventType::MsgsChanged { chat_id, msg_id }); + Ok(msg_id) +} + +pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: impl AsRef) { + if let Err(e) = add_info_msg_with_cmd(context, chat_id, text, SystemMessage::Unknown).await { + warn!(context, "Could not add info msg: {}", e); + } } #[cfg(test)] @@ -3590,4 +3599,116 @@ mod tests { false ); } + + #[async_std::test] + async fn test_add_info_msg() { + let t = TestContext::new().await; + let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo") + .await + .unwrap(); + add_info_msg(&t.ctx, chat_id, "foo info").await; + + let msg = t.get_last_msg(chat_id).await; + assert_eq!(msg.get_chat_id(), chat_id); + assert_eq!(msg.get_viewtype(), Viewtype::Text); + assert_eq!(msg.get_text().unwrap(), "foo info"); + assert!(msg.is_info()); + assert_eq!(msg.get_info_type(), SystemMessage::Unknown); + } + + #[async_std::test] + async fn test_add_info_msg_with_cmd() { + let t = TestContext::new().await; + let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo") + .await + .unwrap(); + let msg_id = add_info_msg_with_cmd( + &t.ctx, + chat_id, + "foo bar info", + SystemMessage::EphemeralTimerChanged, + ) + .await + .unwrap(); + + let msg = Message::load_from_db(&t.ctx, msg_id).await.unwrap(); + assert_eq!(msg.get_chat_id(), chat_id); + assert_eq!(msg.get_viewtype(), Viewtype::Text); + assert_eq!(msg.get_text().unwrap(), "foo bar info"); + assert!(msg.is_info()); + assert_eq!(msg.get_info_type(), SystemMessage::EphemeralTimerChanged); + + let msg2 = t.get_last_msg(chat_id).await; + assert_eq!(msg.get_id(), msg2.get_id()); + } + + #[async_std::test] + async fn test_set_protection() { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t.ctx, ProtectionStatus::Unprotected, "foo") + .await + .unwrap(); + let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); + assert!(!chat.is_protected()); + assert!(chat.is_unpromoted()); + + // enable protection on unpromoted chat, the info-message is added via add_info_msg() + chat_id + .set_protection(&t.ctx, ProtectionStatus::Protected) + .await + .unwrap(); + + let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); + assert!(chat.is_protected()); + assert!(chat.is_unpromoted()); + + let msgs = get_chat_msgs(&t.ctx, chat_id, 0, None).await; + assert_eq!(msgs.len(), 1); + + let msg = t.get_last_msg(chat_id).await; + assert!(msg.is_info()); + assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled); + assert_eq!(msg.get_state(), MessageState::InNoticed); + + // disable protection again, still unpromoted + chat_id + .set_protection(&t.ctx, ProtectionStatus::Unprotected) + .await + .unwrap(); + + let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); + assert!(!chat.is_protected()); + assert!(chat.is_unpromoted()); + + let msg = t.get_last_msg(chat_id).await; + assert!(msg.is_info()); + assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionDisabled); + assert_eq!(msg.get_state(), MessageState::InNoticed); + + // send a message, this switches to promoted state + send_text_msg(&t.ctx, chat_id, "hi!".to_string()) + .await + .unwrap(); + + let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); + assert!(!chat.is_protected()); + assert!(!chat.is_unpromoted()); + + let msgs = get_chat_msgs(&t.ctx, chat_id, 0, None).await; + assert_eq!(msgs.len(), 3); + + // enable protection on promoted chat, the info-message is sent via send_msg() this time + chat_id + .set_protection(&t.ctx, ProtectionStatus::Protected) + .await + .unwrap(); + let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); + assert!(chat.is_protected()); + assert!(!chat.is_unpromoted()); + + let msg = t.get_last_msg(chat_id).await; + assert!(msg.is_info()); + assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled); + assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat + } } diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index 0173bd33a..8956a0d24 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -1245,8 +1245,10 @@ async fn create_or_lookup_group( recreate_member_list = true; if create_protected == ProtectionStatus::Protected { + // set from_id=0 as it is not clear that the sender of this random group message + // actually really has enabled chat-protection at some point. chat_id - .add_protection_msg(context, ProtectionStatus::Protected, false, from_id) + .add_protection_msg(context, ProtectionStatus::Protected, false, 0) .await?; } } diff --git a/src/test_utils.rs b/src/test_utils.rs index 28d4a902d..6e902a267 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -9,13 +9,15 @@ use async_std::path::PathBuf; use async_std::sync::RwLock; use tempfile::{tempdir, TempDir}; -use crate::chat::ChatId; +use crate::chat; +use crate::chat::{ChatId, ChatItem}; use crate::config::Config; use crate::context::Context; use crate::dc_receive_imf::dc_receive_imf; use crate::dc_tools::EmailAddress; use crate::job::Action; use crate::key::{self, DcKey}; +use crate::message::Message; use crate::mimeparser::MimeMessage; use crate::param::{Param, Params}; @@ -187,6 +189,19 @@ impl TestContext { .await .unwrap(); } + + /// Get the most recent message of a chat. + /// + /// Panics on errors or if the most recent message is a marker. + pub async fn get_last_msg(&self, chat_id: ChatId) -> Message { + let msgs = chat::get_chat_msgs(&self.ctx, chat_id, 0, None).await; + let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() { + msg_id + } else { + panic!("Wrong item type"); + }; + Message::load_from_db(&self.ctx, *msg_id).await.unwrap() + } } /// A raw message as it was scheduled to be sent.