mirror of
https://github.com/chatmail/core.git
synced 2026-05-02 21:06:31 +03:00
feat: Resend the last 10 messages to new broadcast member (#7678)
Messages are sent and encrypted only to the new member. This way we at least postpone spreading the information that the new member joined: even if the server operator is a broadcast member, they can't know that immediately.
This commit is contained in:
@@ -22,7 +22,7 @@ use std::sync::{Arc, LazyLock, Mutex};
|
|||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration};
|
use deltachat::chat::{ChatId, ChatMsgsFilter, ChatVisibility, GetChatMsgsOptions, MuteDuration};
|
||||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||||
use deltachat::contact::{Contact, ContactId, Origin};
|
use deltachat::contact::{Contact, ContactId, Origin};
|
||||||
use deltachat::context::{Context, ContextBuilder};
|
use deltachat::context::{Context, ContextBuilder};
|
||||||
@@ -1345,9 +1345,10 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
|
|||||||
chat::get_chat_msgs_ex(
|
chat::get_chat_msgs_ex(
|
||||||
ctx,
|
ctx,
|
||||||
ChatId::new(chat_id),
|
ChatId::new(chat_id),
|
||||||
MessageListOptions {
|
GetChatMsgsOptions {
|
||||||
info_only,
|
filter: ChatMsgsFilter::info_only(info_only),
|
||||||
add_daymarker,
|
add_daymarker,
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use deltachat::calls::ice_servers;
|
|||||||
use deltachat::chat::{
|
use deltachat::chat::{
|
||||||
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
|
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
|
||||||
get_chat_msgs_ex, markfresh_chat, marknoticed_all_chats, marknoticed_chat,
|
get_chat_msgs_ex, markfresh_chat, marknoticed_all_chats, marknoticed_chat,
|
||||||
remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
remove_contact_from_chat, Chat, ChatId, ChatItem, ChatMsgsFilter, GetChatMsgsOptions,
|
||||||
};
|
};
|
||||||
use deltachat::chatlist::Chatlist;
|
use deltachat::chatlist::Chatlist;
|
||||||
use deltachat::config::{get_all_ui_config_keys, Config};
|
use deltachat::config::{get_all_ui_config_keys, Config};
|
||||||
@@ -1382,9 +1382,10 @@ impl CommandApi {
|
|||||||
let msg = get_chat_msgs_ex(
|
let msg = get_chat_msgs_ex(
|
||||||
&ctx,
|
&ctx,
|
||||||
ChatId::new(chat_id),
|
ChatId::new(chat_id),
|
||||||
MessageListOptions {
|
GetChatMsgsOptions {
|
||||||
info_only,
|
filter: ChatMsgsFilter::info_only(info_only),
|
||||||
add_daymarker,
|
add_daymarker,
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -1428,9 +1429,10 @@ impl CommandApi {
|
|||||||
let msg = get_chat_msgs_ex(
|
let msg = get_chat_msgs_ex(
|
||||||
&ctx,
|
&ctx,
|
||||||
ChatId::new(chat_id),
|
ChatId::new(chat_id),
|
||||||
MessageListOptions {
|
GetChatMsgsOptions {
|
||||||
info_only,
|
filter: ChatMsgsFilter::info_only(info_only),
|
||||||
add_daymarker,
|
add_daymarker,
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ use std::str::FromStr;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{bail, ensure, Result};
|
use anyhow::{bail, ensure, Result};
|
||||||
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
|
use deltachat::chat::{
|
||||||
|
self, Chat, ChatId, ChatItem, ChatVisibility, GetChatMsgsOptions, MuteDuration,
|
||||||
|
};
|
||||||
use deltachat::chatlist::*;
|
use deltachat::chatlist::*;
|
||||||
use deltachat::constants::*;
|
use deltachat::constants::*;
|
||||||
use deltachat::contact::*;
|
use deltachat::contact::*;
|
||||||
@@ -622,9 +624,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
let msglist = chat::get_chat_msgs_ex(
|
let msglist = chat::get_chat_msgs_ex(
|
||||||
&context,
|
&context,
|
||||||
sel_chat.get_id(),
|
sel_chat.get_id(),
|
||||||
chat::MessageListOptions {
|
GetChatMsgsOptions {
|
||||||
info_only: false,
|
|
||||||
add_daymarker: true,
|
add_daymarker: true,
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
200
src/chat.rs
200
src/chat.rs
@@ -23,8 +23,9 @@ use crate::chatlist_events;
|
|||||||
use crate::color::str_to_color;
|
use crate::color::str_to_color;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::{
|
use crate::constants::{
|
||||||
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_CHAT_ID_LAST_SPECIAL,
|
self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
|
||||||
DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX, TIMESTAMP_SENT_TOLERANCE,
|
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX,
|
||||||
|
TIMESTAMP_SENT_TOLERANCE,
|
||||||
};
|
};
|
||||||
use crate::contact::{self, Contact, ContactId, Origin};
|
use crate::contact::{self, Contact, ContactId, Origin};
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
@@ -34,7 +35,7 @@ use crate::download::{
|
|||||||
};
|
};
|
||||||
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
||||||
use crate::events::EventType;
|
use crate::events::EventType;
|
||||||
use crate::key::self_fingerprint;
|
use crate::key::{Fingerprint, self_fingerprint};
|
||||||
use crate::location;
|
use crate::location;
|
||||||
use crate::log::{LogExt, warn};
|
use crate::log::{LogExt, warn};
|
||||||
use crate::logged_debug_assert;
|
use crate::logged_debug_assert;
|
||||||
@@ -2753,7 +2754,7 @@ async fn prepare_send_msg(
|
|||||||
}
|
}
|
||||||
chat.prepare_msg_raw(context, msg, update_msg_id).await?;
|
chat.prepare_msg_raw(context, msg, update_msg_id).await?;
|
||||||
|
|
||||||
let row_ids = create_send_msg_jobs(context, msg)
|
let row_ids = create_send_msg_jobs(context, msg, None)
|
||||||
.await
|
.await
|
||||||
.context("Failed to create send jobs")?;
|
.context("Failed to create send jobs")?;
|
||||||
if !row_ids.is_empty() {
|
if !row_ids.is_empty() {
|
||||||
@@ -2823,7 +2824,14 @@ async fn render_mime_message_and_pre_message(
|
|||||||
/// Returns row ids if `smtp` table jobs were created or an empty `Vec` otherwise.
|
/// Returns row ids if `smtp` table jobs were created or an empty `Vec` otherwise.
|
||||||
///
|
///
|
||||||
/// The caller has to interrupt SMTP loop or otherwise process new rows.
|
/// The caller has to interrupt SMTP loop or otherwise process new rows.
|
||||||
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
|
///
|
||||||
|
/// * `row_id` - Actual Message ID, if `Some`. This is to avoid updating the `msgs` row, in which
|
||||||
|
/// case `msg.id` is fake (`u32::MAX`);
|
||||||
|
pub(crate) async fn create_send_msg_jobs(
|
||||||
|
context: &Context,
|
||||||
|
msg: &mut Message,
|
||||||
|
row_id: Option<MsgId>,
|
||||||
|
) -> Result<Vec<i64>> {
|
||||||
let cmd = msg.param.get_cmd();
|
let cmd = msg.param.get_cmd();
|
||||||
if cmd == SystemMessage::GroupNameChanged || cmd == SystemMessage::GroupDescriptionChanged {
|
if cmd == SystemMessage::GroupNameChanged || cmd == SystemMessage::GroupDescriptionChanged {
|
||||||
msg.chat_id
|
msg.chat_id
|
||||||
@@ -2840,7 +2848,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
|||||||
}
|
}
|
||||||
|
|
||||||
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
|
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
|
||||||
let mimefactory = match MimeFactory::from_msg(context, msg.clone()).await {
|
let mimefactory = match MimeFactory::from_msg(context, msg.clone(), row_id).await {
|
||||||
Ok(mf) => mf,
|
Ok(mf) => mf,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Mark message as failed
|
// Mark message as failed
|
||||||
@@ -3102,42 +3110,61 @@ async fn donation_request_maybe(context: &Context) -> Result<()> {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Chat message list request options.
|
/// Controls which messages [`get_chat_msgs_ex`] returns.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Default, PartialEq)]
|
||||||
pub struct MessageListOptions {
|
pub enum ChatMsgsFilter {
|
||||||
/// Return only info messages.
|
/// All messages.
|
||||||
pub info_only: bool,
|
#[default]
|
||||||
|
All,
|
||||||
|
/// Info messages.
|
||||||
|
Info,
|
||||||
|
/// Non-info non-system messages.
|
||||||
|
NonInfoNonSystem,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChatMsgsFilter {
|
||||||
|
/// Returns filter capturing only info messages or all messages.
|
||||||
|
pub fn info_only(arg: bool) -> Self {
|
||||||
|
match arg {
|
||||||
|
true => Self::Info,
|
||||||
|
false => Self::All,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`get_chat_msgs_ex`] options.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct GetChatMsgsOptions {
|
||||||
|
/// Which messages to return.
|
||||||
|
pub filter: ChatMsgsFilter,
|
||||||
|
|
||||||
/// Add day markers before each date regarding the local timezone.
|
/// Add day markers before each date regarding the local timezone.
|
||||||
pub add_daymarker: bool,
|
pub add_daymarker: bool,
|
||||||
|
|
||||||
|
/// If `Some(n)`, return up to `n` last (by timestamp) messages.
|
||||||
|
pub n_last: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns all messages belonging to the chat.
|
/// Returns all messages belonging to the chat.
|
||||||
pub async fn get_chat_msgs(context: &Context, chat_id: ChatId) -> Result<Vec<ChatItem>> {
|
pub async fn get_chat_msgs(context: &Context, chat_id: ChatId) -> Result<Vec<ChatItem>> {
|
||||||
get_chat_msgs_ex(
|
get_chat_msgs_ex(context, chat_id, Default::default()).await
|
||||||
context,
|
|
||||||
chat_id,
|
|
||||||
MessageListOptions {
|
|
||||||
info_only: false,
|
|
||||||
add_daymarker: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns messages belonging to the chat according to the given options.
|
/// Returns messages belonging to the chat according to the given options.
|
||||||
|
/// Older messages go first.
|
||||||
#[expect(clippy::arithmetic_side_effects)]
|
#[expect(clippy::arithmetic_side_effects)]
|
||||||
pub async fn get_chat_msgs_ex(
|
pub async fn get_chat_msgs_ex(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
chat_id: ChatId,
|
chat_id: ChatId,
|
||||||
options: MessageListOptions,
|
options: GetChatMsgsOptions,
|
||||||
) -> Result<Vec<ChatItem>> {
|
) -> Result<Vec<ChatItem>> {
|
||||||
let MessageListOptions {
|
let GetChatMsgsOptions {
|
||||||
info_only,
|
filter,
|
||||||
add_daymarker,
|
add_daymarker,
|
||||||
|
n_last,
|
||||||
} = options;
|
} = options;
|
||||||
let process_row = if info_only {
|
let process_row = |row: &rusqlite::Row| {
|
||||||
|row: &rusqlite::Row| {
|
if filter != ChatMsgsFilter::All {
|
||||||
// is_info logic taken from Message.is_info()
|
// is_info logic taken from Message.is_info()
|
||||||
let params = row.get::<_, String>("param")?;
|
let params = row.get::<_, String>("param")?;
|
||||||
let (from_id, to_id) = (
|
let (from_id, to_id) = (
|
||||||
@@ -3157,15 +3184,13 @@ pub async fn get_chat_msgs_ex(
|
|||||||
Ok((
|
Ok((
|
||||||
row.get::<_, i64>("timestamp")?,
|
row.get::<_, i64>("timestamp")?,
|
||||||
row.get::<_, MsgId>("id")?,
|
row.get::<_, MsgId>("id")?,
|
||||||
!is_info_msg,
|
is_info_msg == (filter == ChatMsgsFilter::Info),
|
||||||
))
|
))
|
||||||
}
|
} else {
|
||||||
} else {
|
|
||||||
|row: &rusqlite::Row| {
|
|
||||||
Ok((
|
Ok((
|
||||||
row.get::<_, i64>("timestamp")?,
|
row.get::<_, i64>("timestamp")?,
|
||||||
row.get::<_, MsgId>("id")?,
|
row.get::<_, MsgId>("id")?,
|
||||||
false,
|
true,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -3174,8 +3199,8 @@ pub async fn get_chat_msgs_ex(
|
|||||||
// let sqlite execute an ORDER BY clause.
|
// let sqlite execute an ORDER BY clause.
|
||||||
let mut sorted_rows = Vec::new();
|
let mut sorted_rows = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let (ts, curr_id, exclude_message): (i64, MsgId, bool) = row?;
|
let (ts, curr_id, include): (i64, MsgId, bool) = row?;
|
||||||
if !exclude_message {
|
if include {
|
||||||
sorted_rows.push((ts, curr_id));
|
sorted_rows.push((ts, curr_id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3202,21 +3227,27 @@ pub async fn get_chat_msgs_ex(
|
|||||||
Ok(ret)
|
Ok(ret)
|
||||||
};
|
};
|
||||||
|
|
||||||
let items = if info_only {
|
let n_last_subst = match n_last {
|
||||||
|
Some(n) => format!("ORDER BY timestamp DESC, id DESC LIMIT {n}"),
|
||||||
|
None => "".to_string(),
|
||||||
|
};
|
||||||
|
let items = if filter != ChatMsgsFilter::All {
|
||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.query_map(
|
.query_map(
|
||||||
// GLOB is used here instead of LIKE because it is case-sensitive
|
&format!("
|
||||||
"SELECT m.id AS id, m.timestamp AS timestamp, m.param AS param, m.from_id AS from_id, m.to_id AS to_id
|
SELECT m.id AS id, m.timestamp AS timestamp, m.param AS param, m.from_id AS from_id, m.to_id AS to_id
|
||||||
FROM msgs m
|
FROM msgs m
|
||||||
WHERE m.chat_id=?
|
WHERE m.chat_id=?
|
||||||
AND m.hidden=0
|
AND m.hidden=0
|
||||||
AND (
|
AND ?=(
|
||||||
m.param GLOB '*\nS=*' OR param GLOB 'S=*'
|
m.param GLOB '*\nS=*' OR param GLOB 'S=*'
|
||||||
OR m.from_id == ?
|
OR m.from_id == ?
|
||||||
OR m.to_id == ?
|
OR m.to_id == ?
|
||||||
);",
|
)
|
||||||
(chat_id, ContactId::INFO, ContactId::INFO),
|
{n_last_subst}"
|
||||||
|
),
|
||||||
|
(chat_id, filter == ChatMsgsFilter::Info, ContactId::INFO, ContactId::INFO),
|
||||||
process_row,
|
process_row,
|
||||||
process_rows,
|
process_rows,
|
||||||
)
|
)
|
||||||
@@ -3225,10 +3256,14 @@ pub async fn get_chat_msgs_ex(
|
|||||||
context
|
context
|
||||||
.sql
|
.sql
|
||||||
.query_map(
|
.query_map(
|
||||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
&format!(
|
||||||
FROM msgs m
|
"
|
||||||
WHERE m.chat_id=?
|
SELECT m.id AS id, m.timestamp AS timestamp
|
||||||
AND m.hidden=0;",
|
FROM msgs m
|
||||||
|
WHERE m.chat_id=?
|
||||||
|
AND m.hidden=0
|
||||||
|
{n_last_subst}"
|
||||||
|
),
|
||||||
(chat_id,),
|
(chat_id,),
|
||||||
process_row,
|
process_row,
|
||||||
process_rows,
|
process_rows,
|
||||||
@@ -4009,6 +4044,29 @@ pub(crate) async fn add_contact_to_chat_ex(
|
|||||||
if sync.into() {
|
if sync.into() {
|
||||||
chat.sync_contacts(context).await.log_err(context).ok();
|
chat.sync_contacts(context).await.log_err(context).ok();
|
||||||
}
|
}
|
||||||
|
let resend_last_msgs = || async {
|
||||||
|
let items = get_chat_msgs_ex(
|
||||||
|
context,
|
||||||
|
chat.id,
|
||||||
|
GetChatMsgsOptions {
|
||||||
|
filter: ChatMsgsFilter::NonInfoNonSystem,
|
||||||
|
n_last: Some(constants::N_MSGS_TO_NEW_BROADCAST_MEMBER),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let msgs: Vec<_> = items
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|i| match i {
|
||||||
|
ChatItem::Message { msg_id } => Some(msg_id),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
resend_msgs_ex(context, &msgs, contact.fingerprint()).await
|
||||||
|
};
|
||||||
|
if chat.typ == Chattype::OutBroadcast {
|
||||||
|
resend_last_msgs().await.log_err(context).ok();
|
||||||
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4566,7 +4624,10 @@ pub async fn forward_msgs_2ctx(
|
|||||||
chat.prepare_msg_raw(ctx_dst, &mut msg, None).await?;
|
chat.prepare_msg_raw(ctx_dst, &mut msg, None).await?;
|
||||||
|
|
||||||
curr_timestamp += 1;
|
curr_timestamp += 1;
|
||||||
if !create_send_msg_jobs(ctx_dst, &mut msg).await?.is_empty() {
|
if !create_send_msg_jobs(ctx_dst, &mut msg, None)
|
||||||
|
.await?
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
ctx_dst.scheduler.interrupt_smtp().await;
|
ctx_dst.scheduler.interrupt_smtp().await;
|
||||||
}
|
}
|
||||||
created_msgs.push(msg.id);
|
created_msgs.push(msg.id);
|
||||||
@@ -4675,10 +4736,28 @@ pub(crate) async fn save_copy_in_self_talk(
|
|||||||
Ok(msg.rfc724_mid)
|
Ok(msg.rfc724_mid)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resends given messages with the same Message-ID.
|
/// Resends given messages to members of the corresponding chats.
|
||||||
///
|
///
|
||||||
/// This is primarily intended to make existing webxdcs available to new chat members.
|
/// This is primarily intended to make existing webxdcs available to new chat members.
|
||||||
pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||||
|
resend_msgs_ex(context, msg_ids, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resends given messages to a contact with fingerprint `to_fingerprint` or, if it's `None`, to
|
||||||
|
/// members of the corresponding chats.
|
||||||
|
///
|
||||||
|
/// NB: Actually `to_fingerprint` is only passed for `OutBroadcast` chats when a new member is
|
||||||
|
/// added. Currently webxdc status updates are re-sent to all broadcast members instead of the
|
||||||
|
/// requested contact, but this will be changed soon: webxdc status updates won't be re-sent at all
|
||||||
|
/// as they may contain confidential data sent by subscribers to the owner. Of course this may also
|
||||||
|
/// happen without resending subscribers' status updates if a webxdc app is "unsafe" for use in
|
||||||
|
/// broadcasts on its own, but this is a separate problem.
|
||||||
|
pub(crate) async fn resend_msgs_ex(
|
||||||
|
context: &Context,
|
||||||
|
msg_ids: &[MsgId],
|
||||||
|
to_fingerprint: Option<Fingerprint>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let to_fingerprint = to_fingerprint.map(|f| f.hex());
|
||||||
let mut msgs: Vec<Message> = Vec::new();
|
let mut msgs: Vec<Message> = Vec::new();
|
||||||
for msg_id in msg_ids {
|
for msg_id in msg_ids {
|
||||||
let msg = Message::load_from_db(context, *msg_id).await?;
|
let msg = Message::load_from_db(context, *msg_id).await?;
|
||||||
@@ -4697,11 +4776,25 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
|||||||
| MessageState::OutFailed
|
| MessageState::OutFailed
|
||||||
| MessageState::OutDelivered
|
| MessageState::OutDelivered
|
||||||
| MessageState::OutMdnRcvd => {
|
| MessageState::OutMdnRcvd => {
|
||||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?
|
// Broadcast owners shouldn't see spinners on messages being auto-re-sent to new
|
||||||
|
// subscribers (otherwise big channel owners will see spinners most of the time).
|
||||||
|
if to_fingerprint.is_none() {
|
||||||
|
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
msg_state => bail!("Unexpected message state {msg_state}"),
|
msg_state => bail!("Unexpected message state {msg_state}"),
|
||||||
}
|
}
|
||||||
if create_send_msg_jobs(context, &mut msg).await?.is_empty() {
|
let mut row_id = None;
|
||||||
|
if let Some(to_fingerprint) = &to_fingerprint {
|
||||||
|
msg.param.set(Param::Arg4, to_fingerprint.clone());
|
||||||
|
// Avoid updating the `msgs` row.
|
||||||
|
row_id = Some(msg.id);
|
||||||
|
msg.id = MsgId::new(u32::MAX);
|
||||||
|
}
|
||||||
|
if create_send_msg_jobs(context, &mut msg, row_id)
|
||||||
|
.await?
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4712,7 +4805,8 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
|||||||
chat_id: msg.chat_id,
|
chat_id: msg.chat_id,
|
||||||
msg_id: msg.id,
|
msg_id: msg.id,
|
||||||
});
|
});
|
||||||
// note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
|
// The event only matters if the message is last in the chat.
|
||||||
|
// But it's probably too expensive check, and UIs anyways need to debounce.
|
||||||
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
|
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
|
||||||
|
|
||||||
if msg.viewtype == Viewtype::Webxdc {
|
if msg.viewtype == Viewtype::Webxdc {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::Event;
|
use crate::Event;
|
||||||
use crate::chatlist::get_archived_cnt;
|
use crate::chatlist::get_archived_cnt;
|
||||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
|
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS, N_MSGS_TO_NEW_BROADCAST_MEMBER};
|
||||||
use crate::ephemeral::Timer;
|
use crate::ephemeral::Timer;
|
||||||
use crate::headerdef::HeaderDef;
|
use crate::headerdef::HeaderDef;
|
||||||
use crate::imex::{ImexMode, has_backup, imex};
|
use crate::imex::{ImexMode, has_backup, imex};
|
||||||
@@ -2947,6 +2947,56 @@ async fn test_broadcast_change_name() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_broadcast_resend_to_new_member() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
let bob = &tcm.bob().await;
|
||||||
|
let fiona = &tcm.fiona().await;
|
||||||
|
|
||||||
|
let alice_bc_id = create_broadcast(alice, "Channel".to_string()).await?;
|
||||||
|
let qr = get_securejoin_qr(alice, Some(alice_bc_id)).await.unwrap();
|
||||||
|
|
||||||
|
tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||||
|
let mut alice_msg_ids = Vec::new();
|
||||||
|
for i in 0..(N_MSGS_TO_NEW_BROADCAST_MEMBER + 1) {
|
||||||
|
alice_msg_ids.push(
|
||||||
|
alice
|
||||||
|
.send_text(alice_bc_id, &i.to_string())
|
||||||
|
.await
|
||||||
|
.sender_msg_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let fiona_bc_id = tcm.exec_securejoin_qr(fiona, alice, &qr).await;
|
||||||
|
for msg_id in alice_msg_ids {
|
||||||
|
assert_eq!(msg_id.get_state(alice).await?, MessageState::OutDelivered);
|
||||||
|
}
|
||||||
|
for i in 0..N_MSGS_TO_NEW_BROADCAST_MEMBER {
|
||||||
|
let rev_order = false;
|
||||||
|
let resent_msg = alice
|
||||||
|
.pop_sent_msg_ex(rev_order, Duration::ZERO)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let fiona_msg = fiona.recv_msg(&resent_msg).await;
|
||||||
|
assert_eq!(fiona_msg.chat_id, fiona_bc_id);
|
||||||
|
assert_eq!(fiona_msg.text, (i + 1).to_string());
|
||||||
|
assert!(resent_msg.recipients.contains("fiona@example.net"));
|
||||||
|
assert!(!resent_msg.recipients.contains("bob@"));
|
||||||
|
// The message is undecryptable for Bob, he mustn't be able to know yet that somebody joined
|
||||||
|
// the broadcast even if he is a postman in this land. E.g. Fiona may leave after fetching
|
||||||
|
// the news, Bob won't know about that.
|
||||||
|
assert!(
|
||||||
|
MimeMessage::from_bytes(bob, resent_msg.payload().as_bytes())
|
||||||
|
.await?
|
||||||
|
.decryption_error
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
|
bob.recv_msg_trash(&resent_msg).await;
|
||||||
|
}
|
||||||
|
assert!(alice.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// - Alice has multiple devices
|
/// - Alice has multiple devices
|
||||||
/// - Alice creates a broadcast and sends a message into it
|
/// - Alice creates a broadcast and sends a message into it
|
||||||
/// - Alice's second device sees the broadcast
|
/// - Alice's second device sees the broadcast
|
||||||
|
|||||||
@@ -244,6 +244,9 @@ Here is what to do:
|
|||||||
|
|
||||||
If you have any questions, please send an email to delta@merlinux.eu or ask at https://support.delta.chat/."#;
|
If you have any questions, please send an email to delta@merlinux.eu or ask at https://support.delta.chat/."#;
|
||||||
|
|
||||||
|
/// How many recent messages should be re-sent to a new broadcast member.
|
||||||
|
pub(crate) const N_MSGS_TO_NEW_BROADCAST_MEMBER: usize = 10;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
|
|||||||
@@ -194,8 +194,16 @@ fn new_address_with_name(name: &str, address: String) -> Address<'static> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MimeFactory {
|
impl MimeFactory {
|
||||||
|
/// Returns `MimeFactory` for rendering `msg`.
|
||||||
|
///
|
||||||
|
/// * `row_id` - Actual Message ID, if `Some`. This is to avoid updating the `msgs` row, in
|
||||||
|
/// which case `msg.id` is fake (`u32::MAX`);
|
||||||
#[expect(clippy::arithmetic_side_effects)]
|
#[expect(clippy::arithmetic_side_effects)]
|
||||||
pub async fn from_msg(context: &Context, msg: Message) -> Result<MimeFactory> {
|
pub async fn from_msg(
|
||||||
|
context: &Context,
|
||||||
|
msg: Message,
|
||||||
|
row_id: Option<MsgId>,
|
||||||
|
) -> Result<MimeFactory> {
|
||||||
let now = time();
|
let now = time();
|
||||||
let chat = Chat::load_from_db(context, msg.chat_id).await?;
|
let chat = Chat::load_from_db(context, msg.chat_id).await?;
|
||||||
let attach_profile_data = Self::should_attach_profile_data(&msg);
|
let attach_profile_data = Self::should_attach_profile_data(&msg);
|
||||||
@@ -505,12 +513,13 @@ impl MimeFactory {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let msg_id = row_id.unwrap_or(msg.id);
|
||||||
let (in_reply_to, references) = context
|
let (in_reply_to, references) = context
|
||||||
.sql
|
.sql
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT mime_in_reply_to, IFNULL(mime_references, '')
|
"SELECT mime_in_reply_to, IFNULL(mime_references, '')
|
||||||
FROM msgs WHERE id=?",
|
FROM msgs WHERE id=?",
|
||||||
(msg.id,),
|
(msg_id,),
|
||||||
|row| {
|
|row| {
|
||||||
let in_reply_to: String = row.get(0)?;
|
let in_reply_to: String = row.get(0)?;
|
||||||
let references: String = row.get(1)?;
|
let references: String = row.get(1)?;
|
||||||
@@ -2225,18 +2234,18 @@ fn should_encrypt_symmetrically(msg: &Message, chat: &Chat) -> bool {
|
|||||||
/// rather than all recipients.
|
/// rather than all recipients.
|
||||||
/// This function returns the fingerprint of the recipient the message should be sent to.
|
/// This function returns the fingerprint of the recipient the message should be sent to.
|
||||||
fn must_have_only_one_recipient<'a>(msg: &'a Message, chat: &Chat) -> Option<Result<&'a str>> {
|
fn must_have_only_one_recipient<'a>(msg: &'a Message, chat: &Chat) -> Option<Result<&'a str>> {
|
||||||
if chat.typ == Chattype::OutBroadcast
|
if chat.typ != Chattype::OutBroadcast {
|
||||||
&& matches!(
|
None
|
||||||
msg.param.get_cmd(),
|
} else if let Some(fp) = msg.param.get(Param::Arg4) {
|
||||||
SystemMessage::MemberRemovedFromGroup | SystemMessage::MemberAddedToGroup
|
Some(Ok(fp))
|
||||||
)
|
} else if matches!(
|
||||||
{
|
msg.param.get_cmd(),
|
||||||
let Some(fp) = msg.param.get(Param::Arg4) else {
|
SystemMessage::MemberRemovedFromGroup | SystemMessage::MemberAddedToGroup
|
||||||
return Some(Err(format_err!("Missing removed/added member")));
|
) {
|
||||||
};
|
Some(Err(format_err!("Missing removed/added member")))
|
||||||
return Some(Ok(fp));
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn build_body_file(context: &Context, msg: &Message) -> Result<MimePart<'static>> {
|
async fn build_body_file(context: &Context, msg: &Message) -> Result<MimePart<'static>> {
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ async fn test_subject_mdn() {
|
|||||||
chat::send_msg(&t, new_msg.chat_id, &mut new_msg)
|
chat::send_msg(&t, new_msg.chat_id, &mut new_msg)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap();
|
let mf = MimeFactory::from_msg(&t, new_msg, None).await.unwrap();
|
||||||
// The subject string should not be "Re: message opened"
|
// The subject string should not be "Re: message opened"
|
||||||
assert_eq!("Re: Hello, Bob", mf.subject_str(&t).await.unwrap());
|
assert_eq!("Re: Hello, Bob", mf.subject_str(&t).await.unwrap());
|
||||||
}
|
}
|
||||||
@@ -412,7 +412,7 @@ async fn first_subject_str(t: TestContext) -> String {
|
|||||||
new_msg.chat_id = chat_id;
|
new_msg.chat_id = chat_id;
|
||||||
chat::send_msg(&t, chat_id, &mut new_msg).await.unwrap();
|
chat::send_msg(&t, chat_id, &mut new_msg).await.unwrap();
|
||||||
|
|
||||||
let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap();
|
let mf = MimeFactory::from_msg(&t, new_msg, None).await.unwrap();
|
||||||
|
|
||||||
mf.subject_str(&t).await.unwrap()
|
mf.subject_str(&t).await.unwrap()
|
||||||
}
|
}
|
||||||
@@ -500,7 +500,7 @@ async fn msg_to_subject_str_inner(
|
|||||||
chat::send_msg(&t, new_msg.chat_id, &mut new_msg)
|
chat::send_msg(&t, new_msg.chat_id, &mut new_msg)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mf = MimeFactory::from_msg(&t, new_msg).await.unwrap();
|
let mf = MimeFactory::from_msg(&t, new_msg, None).await.unwrap();
|
||||||
mf.subject_str(&t).await.unwrap()
|
mf.subject_str(&t).await.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,7 +545,7 @@ async fn test_render_reply() {
|
|||||||
.await;
|
.await;
|
||||||
chat::send_msg(&t, msg.chat_id, &mut msg).await.unwrap();
|
chat::send_msg(&t, msg.chat_id, &mut msg).await.unwrap();
|
||||||
|
|
||||||
let mimefactory = MimeFactory::from_msg(&t, msg).await.unwrap();
|
let mimefactory = MimeFactory::from_msg(&t, msg, None).await.unwrap();
|
||||||
|
|
||||||
let recipients = mimefactory.recipients();
|
let recipients = mimefactory.recipients();
|
||||||
assert_eq!(recipients, vec!["charlie@example.com"]);
|
assert_eq!(recipients, vec!["charlie@example.com"]);
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ use tempfile::{TempDir, tempdir};
|
|||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
use tokio::{fs, task};
|
use tokio::{fs, task};
|
||||||
|
|
||||||
use crate::chat::{
|
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, add_to_chat_contacts_table, create_group};
|
||||||
self, Chat, ChatId, ChatIdBlocked, MessageListOptions, add_to_chat_contacts_table, create_group,
|
|
||||||
};
|
|
||||||
use crate::chatlist::Chatlist;
|
use crate::chatlist::Chatlist;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::{Blocked, Chattype};
|
use crate::constants::{Blocked, Chattype};
|
||||||
@@ -275,16 +273,17 @@ impl TestContextManager {
|
|||||||
|
|
||||||
let chat_id = join_securejoin(&joiner.ctx, qr).await.unwrap();
|
let chat_id = join_securejoin(&joiner.ctx, qr).await.unwrap();
|
||||||
|
|
||||||
loop {
|
for _ in 0..2 {
|
||||||
let mut something_sent = false;
|
let mut something_sent = false;
|
||||||
if let Some(sent) = joiner.pop_sent_msg_opt(Duration::ZERO).await {
|
let rev_order = false;
|
||||||
|
if let Some(sent) = joiner.pop_sent_msg_ex(rev_order, Duration::ZERO).await {
|
||||||
for inviter in inviters {
|
for inviter in inviters {
|
||||||
inviter.recv_msg_opt(&sent).await;
|
inviter.recv_msg_opt(&sent).await;
|
||||||
}
|
}
|
||||||
something_sent = true;
|
something_sent = true;
|
||||||
}
|
}
|
||||||
for inviter in inviters {
|
for inviter in inviters {
|
||||||
if let Some(sent) = inviter.pop_sent_msg_opt(Duration::ZERO).await {
|
if let Some(sent) = inviter.pop_sent_msg_ex(rev_order, Duration::ZERO).await {
|
||||||
joiner.recv_msg_opt(&sent).await;
|
joiner.recv_msg_opt(&sent).await;
|
||||||
something_sent = true;
|
something_sent = true;
|
||||||
}
|
}
|
||||||
@@ -623,25 +622,35 @@ impl TestContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn pop_sent_msg_opt(&self, timeout: Duration) -> Option<SentMessage<'_>> {
|
pub async fn pop_sent_msg_opt(&self, timeout: Duration) -> Option<SentMessage<'_>> {
|
||||||
|
let rev_order = true;
|
||||||
|
self.pop_sent_msg_ex(rev_order, timeout).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn pop_sent_msg_ex(
|
||||||
|
&self,
|
||||||
|
rev_order: bool,
|
||||||
|
timeout: Duration,
|
||||||
|
) -> Option<SentMessage<'_>> {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
let mut query = "
|
||||||
|
SELECT id, msg_id, mime, recipients
|
||||||
|
FROM smtp
|
||||||
|
ORDER BY id"
|
||||||
|
.to_string();
|
||||||
|
if rev_order {
|
||||||
|
query += " DESC";
|
||||||
|
}
|
||||||
let (rowid, msg_id, payload, recipients) = loop {
|
let (rowid, msg_id, payload, recipients) = loop {
|
||||||
let row = self
|
let row = self
|
||||||
.ctx
|
.ctx
|
||||||
.sql
|
.sql
|
||||||
.query_row_optional(
|
.query_row_optional(&query, (), |row| {
|
||||||
r#"
|
let rowid: i64 = row.get(0)?;
|
||||||
SELECT id, msg_id, mime, recipients
|
let msg_id: MsgId = row.get(1)?;
|
||||||
FROM smtp
|
let mime: String = row.get(2)?;
|
||||||
ORDER BY id DESC"#,
|
let recipients: String = row.get(3)?;
|
||||||
(),
|
Ok((rowid, msg_id, mime, recipients))
|
||||||
|row| {
|
})
|
||||||
let rowid: i64 = row.get(0)?;
|
|
||||||
let msg_id: MsgId = row.get(1)?;
|
|
||||||
let mime: String = row.get(2)?;
|
|
||||||
let recipients: String = row.get(3)?;
|
|
||||||
Ok((rowid, msg_id, mime, recipients))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.expect("query_row_optional failed");
|
.expect("query_row_optional failed");
|
||||||
if let Some(row) = row {
|
if let Some(row) = row {
|
||||||
@@ -1083,16 +1092,9 @@ impl TestContext {
|
|||||||
async fn display_chat(&self, chat_id: ChatId) -> String {
|
async fn display_chat(&self, chat_id: ChatId) -> String {
|
||||||
let mut res = String::new();
|
let mut res = String::new();
|
||||||
|
|
||||||
let msglist = chat::get_chat_msgs_ex(
|
let msglist = chat::get_chat_msgs_ex(self, chat_id, Default::default())
|
||||||
self,
|
.await
|
||||||
chat_id,
|
.unwrap();
|
||||||
MessageListOptions {
|
|
||||||
info_only: false,
|
|
||||||
add_daymarker: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let msglist: Vec<MsgId> = msglist
|
let msglist: Vec<MsgId> = msglist
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|x| match x {
|
.filter_map(|x| match x {
|
||||||
|
|||||||
Reference in New Issue
Block a user