diff --git a/src/chat.rs b/src/chat.rs index 546f069d76..c0455a5895 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2697,7 +2697,9 @@ async fn prepare_send_msg( .unwrap_or_default(), _ => false, }; - if let Some(reason) = chat.why_cant_send_ex(context, &skip_fn).await? { + if msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate { + // Already checked in `send_webxdc_status_update_struct()`. + } else if let Some(reason) = chat.why_cant_send_ex(context, &skip_fn).await? { bail!("Cannot send to {chat_id}: {reason}"); } @@ -4680,7 +4682,7 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> { // note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it) chatlist_events::emit_chatlist_item_changed(context, msg.chat_id); - if msg.viewtype == Viewtype::Webxdc { + if msg.viewtype == Viewtype::Webxdc && msg.chat_typ != Chattype::OutBroadcast { let conn_fn = |conn: &mut rusqlite::Connection| { let range = conn.query_row( "SELECT IFNULL(min(id), 1), IFNULL(max(id), 0) \ diff --git a/src/constants.rs b/src/constants.rs index 28eb4c0fed..2180fc7b87 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -107,12 +107,14 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9); IntoStaticStr, Serialize, Deserialize, + Default, )] #[repr(u32)] pub enum Chattype { /// A 1:1 chat, i.e. a normal chat with a single contact. /// /// Created by [`ChatId::create_for_contact`]. + #[default] Single = 100, /// Group chat. diff --git a/src/message.rs b/src/message.rs index c34a43093f..118b900435 100644 --- a/src/message.rs +++ b/src/message.rs @@ -453,6 +453,7 @@ pub struct Message { pub(crate) is_dc_message: MessengerMessage, pub(crate) original_msg_id: MsgId, pub(crate) mime_modified: bool, + pub(crate) chat_typ: Chattype, pub(crate) chat_visibility: ChatVisibility, pub(crate) chat_blocked: Blocked, pub(crate) location_id: u32, @@ -527,6 +528,7 @@ impl Message { m.param AS param, m.hidden AS hidden, m.location_id AS location, + c.type AS chat_typ, c.archived AS visibility, c.blocked AS blocked FROM msgs m @@ -586,6 +588,10 @@ impl Message { param: row.get::<_, String>("param")?.parse().unwrap_or_default(), hidden: row.get("hidden")?, location_id: row.get("location")?, + // This is safe: see `ChatId::delete_ex()`, `None` chat type can't happen. + chat_typ: row + .get::<_, Option<_>>("chat_typ")? + .unwrap_or(Chattype::Single), chat_visibility: row.get::<_, Option<_>>("visibility")?.unwrap_or_default(), chat_blocked: row .get::<_, Option>("blocked")? diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 8fa2a5632d..c271e4349f 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1589,10 +1589,7 @@ impl MimeFactory { } } - if chat.typ == Chattype::Group - || chat.typ == Chattype::OutBroadcast - || chat.typ == Chattype::InBroadcast - { + if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast { headers.push(( "Chat-Group-Name", mail_builder::headers::text::Text::new(chat.name.to_string()).into(), @@ -1603,7 +1600,11 @@ impl MimeFactory { mail_builder::headers::text::Text::new(ts.to_string()).into(), )); } - + } + if chat.typ == Chattype::Group + || chat.typ == Chattype::OutBroadcast + || chat.typ == Chattype::InBroadcast + { match command { SystemMessage::MemberRemovedFromGroup => { let email_to_remove = msg.param.get(Param::Arg).unwrap_or_default(); @@ -2036,14 +2037,15 @@ impl MimeFactory { HeaderDef::IrohGossipTopic.get_headername(), mail_builder::headers::raw::Raw::new(topic).into(), )); - if let (Some(json), _) = context - .render_webxdc_status_update_object( - msg.id, - StatusUpdateSerial::MIN, - StatusUpdateSerial::MAX, - None, - ) - .await? + if msg.chat_typ != Chattype::OutBroadcast + && let (Some(json), _) = context + .render_webxdc_status_update_object( + msg.id, + StatusUpdateSerial::MIN, + StatusUpdateSerial::MAX, + None, + ) + .await? { parts.push(context.build_status_update_part(&json)); } diff --git a/src/webxdc.rs b/src/webxdc.rs index 29184d23ff..183ea4aba8 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -34,7 +34,7 @@ use serde_json::Value; use sha2::{Digest, Sha256}; use tokio::{fs::File, io::BufReader}; -use crate::chat::{self, Chat}; +use crate::chat::{self, CantSendReason, Chat}; use crate::constants::Chattype; use crate::contact::ContactId; use crate::context::Context; @@ -536,9 +536,14 @@ impl Context { let chat = Chat::load_from_db(self, chat_id) .await .with_context(|| format!("Failed to load chat {chat_id} from the database"))?; - if let Some(reason) = chat.why_cant_send(self).await.with_context(|| { - format!("Failed to check if webxdc update can be sent to chat {chat_id}") - })? { + let skip_fn = |reason: &CantSendReason| *reason == CantSendReason::InBroadcast; + if let Some(reason) = chat + .why_cant_send_ex(self, &skip_fn) + .await + .with_context(|| { + format!("Failed to check if webxdc update can be sent to chat {chat_id}") + })? + { bail!("Cannot send to {chat_id}: {reason}."); } diff --git a/src/webxdc/webxdc_tests.rs b/src/webxdc/webxdc_tests.rs index cf72c459dd..fffd0df841 100644 --- a/src/webxdc/webxdc_tests.rs +++ b/src/webxdc/webxdc_tests.rs @@ -11,7 +11,9 @@ use crate::chat::{ use crate::chatlist::Chatlist; use crate::config::Config; use crate::ephemeral; +use crate::imex::{BackupProvider, get_backup}; use crate::receive_imf::receive_imf; +use crate::securejoin::get_securejoin_qr; use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager}; use crate::tools::{self, SystemTime}; use crate::{message, sql}; @@ -1588,6 +1590,93 @@ async fn test_webxdc_no_internet_access() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_in_broadcast_send_status_update() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let alice_chat_id = create_broadcast(alice, "bc".to_string()).await?; + let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?; + let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await; + + let mut alice_instance = create_webxdc_instance( + alice, + "minimal.xdc", + include_bytes!("../../test-data/webxdc/minimal.xdc"), + )?; + alice_chat_id + .set_draft(alice, Some(&mut alice_instance)) + .await?; + alice_instance = alice_chat_id.get_draft(alice).await?.unwrap(); + alice + .send_webxdc_status_update(alice_instance.id, r#"{"payload":41}"#) + .await?; + send_msg(alice, alice_chat_id, &mut alice_instance).await?; + let sent_msg = alice.pop_sent_msg().await; + let bob_instance = bob.recv_msg(&sent_msg).await; + assert_eq!(bob_instance.chat_id, bob_chat_id); + + let provider = BackupProvider::prepare(bob).await?; + let bob2 = &tcm.unconfigured().await; + get_backup(bob2, provider.qr()).await?; + + bob.send_webxdc_status_update(bob_instance.id, r#"{"payload":42}"#) + .await?; + bob.flush_status_updates().await?; + // Don't wait to make sure that status updates are sent immediately, otherwise the check for + // Alice below isn't reliable. + let sent_msg = bob.pop_sent_msg_opt(Duration::ZERO).await.unwrap(); + + alice.recv_msg_trash(&sent_msg).await; + assert_eq!( + alice + .get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":41,"serial":1,"max_serial":2}, +{"payload":42,"serial":2,"max_serial":2}]"# + ); + + bob2.recv_msg_trash(&sent_msg).await; + assert_eq!( + bob2.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[{"payload":41,"serial":1,"max_serial":2}, +{"payload":42,"serial":2,"max_serial":2}]"# + ); + + // Non-subscriber's status updates are rejected. + let alice_bob_id = alice.add_or_lookup_contact_id(bob).await; + remove_contact_from_chat(alice, alice_chat_id, alice_bob_id).await?; + alice.pop_sent_msg().await; + let status = + helper_send_receive_status_update(bob, alice, &bob_instance, &alice_instance).await?; + assert_eq!( + status, + r#"[{"payload":41,"serial":1,"max_serial":2}, +{"payload":42,"serial":2,"max_serial":2}]"# + ); + + // Subscribers' status updates are confidential and shalln't be re-sent. So initial status + // updates aren't re-sent too. + let fiona = &tcm.fiona().await; + let fiona_chat_id = tcm.exec_securejoin_qr(fiona, alice, &qr).await; + resend_msgs(alice, &[alice_instance.id]).await?; + let sent1 = alice.pop_sent_msg().await; + alice.flush_status_updates().await?; + assert!(alice.pop_sent_msg_opt(Duration::ZERO).await.is_none()); + let fiona_instance = fiona.recv_msg(&sent1).await; + assert_eq!(fiona_instance.chat_id, fiona_chat_id); + assert_eq!(fiona_instance.chat_typ, Chattype::InBroadcast); + assert_eq!( + fiona + .get_webxdc_status_updates(fiona_instance.id, StatusUpdateSerial(0)) + .await?, + r#"[]"# + ); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_webxdc_chatlist_summary() -> Result<()> { let t = TestContext::new_alice().await;