From bd6c9908e455be7704e7fde101d7394caa18dc76 Mon Sep 17 00:00:00 2001 From: Hocuri Date: Mon, 1 Jun 2026 10:48:01 +0200 Subject: [PATCH] fix: Update the channel title after joining if the QR code included a wrong title (#8260) Fix https://github.com/chatmail/core/issues/8250. Not sure why anyone would tamper with the link, but maybe the name gets ellipsized because it is very long, or maybe we decide to ellipsize more aggressively in the future. The fix is easy enough, just set the GroupNameTimestamp when creating the channel, so that `HeaderDef::ChatGroupNameTimestamp` gets set when sending a message and the logic in `apply_chat_name_avatar_and_description_changes()` recognizes the incoming name's timestamp as newer than `chat_group_name_timestamp` (which is 0 right after joining a chat). The test is somewhat hacky and depends on the title being the last parameter in the invite code URL, but this is fine; if we ever change that, then the test will fail and we need to modify it. This also removes some wrong doc comments. Since groups are unpromoted until sending a first message, the same fix isn't needed for groups. --- deltachat-jsonrpc/src/api.rs | 3 --- .../src/deltachat_rpc_client/account.py | 3 --- src/chat.rs | 16 +++++++------- src/chat/chat_tests.rs | 21 ++++++++++++++++--- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index e70c2771b..da3cb9db8 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -1106,9 +1106,6 @@ impl CommandApi { /// because the word "channel" already appears a lot in the code, /// which would make it hard to grep for it. /// - /// After creation, the chat contains no recipients and is in _unpromoted_ state; - /// see [`CommandApi::create_group_chat`] for more information on the unpromoted state. - /// /// Returns the created chat's id. async fn create_broadcast(&self, account_id: u32, chat_name: String) -> Result { let ctx = self.get_context(account_id).await?; diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index db24c8ff2..8308291cc 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -340,9 +340,6 @@ class Account: because the word "channel" already appears a lot in the code, which would make it hard to grep for it. - After creation, the chat contains no recipients and is in _unpromoted_ state; - see `create_group()` for more information on the unpromoted state. - Returns the created chat. """ return Chat(self, self._rpc.create_broadcast(self.id, name)) diff --git a/src/chat.rs b/src/chat.rs index 11197d261..376b87e1e 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -32,6 +32,7 @@ use crate::debug_logging::maybe_set_logging_xdc; use crate::download::{ DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, PRE_MSG_SIZE_WARNING_THRESHOLD, }; +use crate::ensure_and_debug_assert_eq; use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers}; use crate::events::EventType; use crate::key::{Fingerprint, self_fingerprint}; @@ -1782,9 +1783,8 @@ impl Chat { ); bail!("Cannot set message, contact for {} not found.", self.id); } - } else if matches!(self.typ, Chattype::Group | Chattype::OutBroadcast) - && self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 - { + } else if self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 { + ensure_and_debug_assert_eq!(self.typ, Chattype::Group,); msg.param.set_int(Param::AttachChatAvatarAndDescription, 1); self.param .remove(Param::Unpromoted) @@ -3626,9 +3626,6 @@ pub(crate) async fn create_group_ex( /// because the word "channel" already appears a lot in the code, /// which would make it hard to grep for it. /// -/// After creation, the chat contains no recipients and is in _unpromoted_ state; -/// see [`create_group`] for more information on the unpromoted state. -/// /// Returns the created chat's id. pub async fn create_broadcast(context: &Context, chat_name: String) -> Result { let grpid = create_id(); @@ -3660,17 +3657,20 @@ pub(crate) async fn create_out_broadcast_ex( |row| row.get(0), )?; ensure!(cnt == 0, "{cnt} chats exist with grpid {grpid}"); + let mut params: Params = Params::new(); + params.update_timestamp(Param::GroupNameTimestamp, time())?; t.execute( "INSERT INTO chats - (type, name, name_normalized, grpid, created_timestamp) - VALUES(?, ?, ?, ?, ?)", + (type, name, name_normalized, grpid, created_timestamp, param) + VALUES(?, ?, ?, ?, ?, ?)", ( Chattype::OutBroadcast, &chat_name, normalize_text(&chat_name), &grpid, timestamp, + params.to_string(), ), )?; let chat_id = ChatId::new(t.last_insert_rowid().try_into()?); diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 813d29012..381a6e9d0 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -9,6 +9,7 @@ use crate::headerdef::HeaderDef; use crate::imex::{ImexMode, has_backup, imex}; use crate::message::{Message, MessengerMessage, delete_msgs}; use crate::mimeparser::{self, MimeMessage}; +use crate::qr::{Qr, check_qr}; use crate::receive_imf::receive_imf; use crate::securejoin::{get_securejoin_qr, join_securejoin}; use crate::test_utils; @@ -2922,10 +2923,24 @@ async fn test_broadcast_change_name() -> Result<()> { let fiona = &tcm.fiona().await; let broadcast_id = create_broadcast(alice, "Channel".to_string()).await?; - let qr = get_securejoin_qr(alice, Some(broadcast_id)).await.unwrap(); + let mut qr = get_securejoin_qr(alice, Some(broadcast_id)).await.unwrap(); + // Something goes wrong with the title, e.g. maybe it gets ellipsized + // Note that the title always comes at the end for human readability + qr += "+modified+title"; + + { + tcm.section("Alice invites Bob to her channel"); + let Qr::AskJoinBroadcast { name, .. } = check_qr(bob, &qr).await? else { + panic!(); + }; + assert_eq!(name, "Channel modified title"); + + // The channel's name gets fixed after actually joining the channel: + let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await; + let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?; + assert_eq!(bob_chat.name, "Channel"); + } - tcm.section("Alice invites Bob to her channel"); - tcm.exec_securejoin_qr(bob, alice, &qr).await; tcm.section("Alice invites Fiona to her channel"); tcm.exec_securejoin_qr(fiona, alice, &qr).await;