Compare commits

..

2 Commits

Author SHA1 Message Date
iequidoo
fdc2864df4 feat(ffi): Return chat id as data2 for OutgoingCallAccepted, CallEnded events 2026-04-10 10:27:16 -03:00
iequidoo
9cb2077c94 feat: Add EventType::CallMissed and emit it for missed calls (#7840)
Before, only `CallEnded` was emitted for missed calls, or, if a call arrives already being stale,
`IncomingMsg`. Now:
- `CallMissed` is emitted in addition to `CallEnded`.
- `IncomingMsg` is replaced with `CallMissed` for stale calls.
Having only one event type for missed calls should simplify handling them in the apps.

This doesn't emit `CallMissed` for those who aren't allowed to call us. Also, don't emit `CallEnded`
if the caller isn't allowed to call us and the call wasn't accepted, as there's no previous
`IncomingCall` event in this case.
2026-04-10 10:12:48 -03:00
15 changed files with 327 additions and 315 deletions

View File

@@ -6661,6 +6661,7 @@ void dc_event_unref(dc_event_t* event);
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
*
* @param data1 (int) msg_id ID of the message referring to the call
* @param data2 (int) chat_id ID of the chat which the message belongs to
* @param data2 (char*) accept_call_info, text passed to dc_accept_incoming_call()
*/
#define DC_EVENT_OUTGOING_CALL_ACCEPTED 2570
@@ -6672,9 +6673,23 @@ void dc_event_unref(dc_event_t* event);
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
*
* @param data1 (int) msg_id ID of the message referring to the call
* @param data2 (int) chat_id ID of the chat which the message belongs to
*/
#define DC_EVENT_CALL_ENDED 2580
/**
* An incoming call was missed. Only emitted if the caller is allowed to call us. This happens when:
* - A call timed out (not accepted by us on time).
* - A call was canceled by the caller.
* - A stale call message was received, i.e. it is older than the timeout.
*
* This should trigger a UI notification.
*
* @param data1 (int) msg_id ID of the message referring to the call
* @param data2 (int) chat_id ID of the chat which the message belongs to
*/
#define DC_EVENT_CALL_MISSED 2590
/**
* Transport relay added/deleted or default has changed.
* UI should update the list.

View File

@@ -556,6 +556,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::IncomingCallAccepted { .. } => 2560,
EventType::OutgoingCallAccepted { .. } => 2570,
EventType::CallEnded { .. } => 2580,
EventType::CallMissed { .. } => 2590,
EventType::TransportsModified => 2600,
#[allow(unreachable_patterns)]
#[cfg(test)]
@@ -625,6 +626,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::IncomingCall { msg_id, .. }
| EventType::IncomingCallAccepted { msg_id, .. }
| EventType::OutgoingCallAccepted { msg_id, .. }
| EventType::CallMissed { msg_id, .. }
| EventType::CallEnded { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::ChatlistItemChanged { chat_id } => {
chat_id.unwrap_or_default().to_u32() as libc::c_int
@@ -677,10 +679,11 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::ChatModified(_)
| EventType::ChatDeleted { .. }
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::OutgoingCallAccepted { .. }
| EventType::CallEnded { .. }
| EventType::EventChannelOverflow { .. }
| EventType::TransportsModified => 0,
EventType::OutgoingCallAccepted { chat_id, .. }
| EventType::CallEnded { chat_id, .. }
| EventType::CallMissed { chat_id, .. } => chat_id.to_u32() as libc::c_int,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingReaction { msg_id, .. }
@@ -796,7 +799,9 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
let data2 = accept_call_info.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::CallEnded { .. } | EventType::EventChannelOverflow { .. } => ptr::null_mut(),
EventType::CallEnded { .. }
| EventType::CallMissed { .. }
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
comment.to_c_string().unwrap_or_default().into_raw()

View File

@@ -678,7 +678,7 @@ impl CommandApi {
ChatId::new(chat_id).get_fresh_msg_cnt(&ctx).await
}
/// (deprecated) Gets messages to be processed by the bot and returns their IDs.
/// Gets messages to be processed by the bot and returns their IDs.
///
/// Only messages with database ID higher than `last_msg_id` config value
/// are returned. After processing the messages, the bot should
@@ -686,13 +686,6 @@ impl CommandApi {
/// or manually updating the value to avoid getting already
/// processed messages.
///
/// Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
/// even if it is not fully downloaded yet.
/// The bot needs to wait for the message to be fully downloaded.
/// Since this is usually not the desired behavior,
/// bots should instead use the #DC_EVENT_INCOMING_MSG / [`types::events::EventType::IncomingMsg`]
/// event for getting notified about new messages.
///
/// [`markseen_msgs`]: Self::markseen_msgs
async fn get_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
@@ -705,7 +698,7 @@ impl CommandApi {
Ok(msg_ids)
}
/// (deprecated) Waits for messages to be processed by the bot and returns their IDs.
/// Waits for messages to be processed by the bot and returns their IDs.
///
/// This function is similar to [`get_next_msgs`],
/// but waits for internal new message notification before returning.
@@ -716,13 +709,6 @@ impl CommandApi {
/// To shutdown the bot, stopping I/O can be used to interrupt
/// pending or next `wait_next_msgs` call.
///
/// Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
/// even if it is not fully downloaded yet.
/// The bot needs to wait for the message to be fully downloaded.
/// Since this is usually not the desired behavior,
/// bots should instead use the #DC_EVENT_INCOMING_MSG / [`types::events::EventType::IncomingMsg`]
/// event for getting notified about new messages.
///
/// [`get_next_msgs`]: Self::get_next_msgs
async fn wait_next_msgs(&self, account_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;

View File

@@ -463,6 +463,14 @@ pub enum EventType {
chat_id: u32,
},
/// Call missed.
CallMissed {
/// ID of the info message referring to the call.
msg_id: u32,
/// ID of the chat which the message belongs to.
chat_id: u32,
},
/// One or more transports has changed.
///
/// UI should update the list.
@@ -658,6 +666,10 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
chat_id: chat_id.to_u32(),
},
CoreEventType::CallMissed { msg_id, chat_id } => CallMissed {
msg_id: msg_id.to_u32(),
chat_id: chat_id.to_u32(),
},
CoreEventType::TransportsModified => TransportsModified,
#[allow(unreachable_patterns)]

View File

@@ -405,15 +405,7 @@ class Account:
@futuremethod
def wait_next_messages(self) -> list[Message]:
"""(deprecated) Wait for new messages and return a list of them. Meant for bots.
Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
even if it is not fully downloaded yet.
The bot needs to wait for the message to be fully downloaded.
Since this is usually not the desired behavior,
bots should instead use the `EventType.INCOMING_MSG`
event for getting notified about new messages.
"""
"""Wait for new messages and return a list of them."""
next_msg_ids = yield self._rpc.wait_next_msgs.future(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]

View File

@@ -218,10 +218,11 @@ impl Context {
let wait = RINGING_SECONDS;
let context = self.get_weak_context();
task::spawn(Context::emit_end_call_if_unaccepted(
task::spawn(Context::finalize_call_if_unaccepted(
context,
wait.try_into()?,
call.id,
true, // Doesn't matter for outgoing calls
));
Ok(call.id)
@@ -314,39 +315,67 @@ impl Context {
Ok(())
}
async fn emit_end_call_if_unaccepted(
async fn finalize_call_if_unaccepted(
context: WeakContext,
wait: u64,
call_id: MsgId,
can_call_me: bool,
) -> Result<()> {
sleep(Duration::from_secs(wait)).await;
let context = context.upgrade()?;
let Some(mut call) = context.load_call_by_id(call_id).await? else {
warn!(
context,
"emit_end_call_if_unaccepted is called with {call_id} which does not refer to a call."
"finalize_call_if_unaccepted is called with {call_id} which does not refer to a call."
);
return Ok(());
};
if !call.is_accepted() && !call.is_ended() {
let (msg_id, chat_id) = (call_id, call.msg.chat_id);
if call.is_incoming() {
call.mark_as_canceled(&context).await?;
let missed_call_str = stock_str::missed_call(&context);
call.update_text(&context, &missed_call_str).await?;
if can_call_me {
context.emit_event(EventType::CallMissed { msg_id, chat_id });
}
} else {
call.mark_as_ended(&context).await?;
let canceled_call_str = stock_str::canceled_call(&context);
call.update_text(&context, &canceled_call_str).await?;
}
if can_call_me {
context.emit_event(EventType::CallEnded { msg_id, chat_id });
}
context.emit_msgs_changed(call.msg.chat_id, call_id);
context.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
});
}
Ok(())
}
async fn can_call_me(&self, from_id: ContactId) -> Result<bool> {
Ok(match who_can_call_me(self).await? {
WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id)
.await?
.is_some_and(|chat_id_blocked| {
match chat_id_blocked.blocked {
Blocked::Not => true,
Blocked::Yes | Blocked::Request => {
// Do not notify about incoming calls
// from contact requests and blocked contacts.
//
// User can still access the call and accept it
// via the chat in case of contact requests.
false
}
}
}),
WhoCanCallMe::Everybody => ChatIdBlocked::lookup_by_contact(self, from_id)
.await?
.is_none_or(|chat_id_blocked| chat_id_blocked.blocked != Blocked::Yes),
WhoCanCallMe::Nobody => false,
})
}
pub(crate) async fn handle_call_msg(
&self,
call_id: MsgId,
@@ -360,50 +389,33 @@ impl Context {
};
if call.is_incoming() {
if call.is_stale() {
let missed_call_str = stock_str::missed_call(self);
call.update_text(self, &missed_call_str).await?;
self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
let call_str = match call.is_stale() {
true => stock_str::missed_call(self),
false => stock_str::incoming_call(self, call.has_video_initially()),
};
call.update_text(self, &call_str).await?;
let (msg_id, chat_id) = (call_id, call.msg.chat_id);
let can_call_me = self.can_call_me(from_id).await?;
if !can_call_me {
} else if call.is_stale() {
self.emit_event(EventType::CallMissed { msg_id, chat_id });
} else {
let incoming_call_str =
stock_str::incoming_call(self, call.has_video_initially());
call.update_text(self, &incoming_call_str).await?;
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
let can_call_me = match who_can_call_me(self).await? {
WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id)
.await?
.is_some_and(|chat_id_blocked| {
match chat_id_blocked.blocked {
Blocked::Not => true,
Blocked::Yes | Blocked::Request => {
// Do not notify about incoming calls
// from contact requests and blocked contacts.
//
// User can still access the call and accept it
// via the chat in case of contact requests.
false
}
}
}),
WhoCanCallMe::Everybody => ChatIdBlocked::lookup_by_contact(self, from_id)
.await?
.is_none_or(|chat_id_blocked| chat_id_blocked.blocked != Blocked::Yes),
WhoCanCallMe::Nobody => false,
};
if can_call_me {
self.emit_event(EventType::IncomingCall {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
place_call_info: call.place_call_info.to_string(),
has_video: call.has_video_initially(),
});
}
self.emit_event(EventType::IncomingCall {
msg_id,
chat_id,
place_call_info: call.place_call_info.to_string(),
has_video: call.has_video_initially(),
});
}
self.emit_msgs_changed(chat_id, msg_id);
if !call.is_stale() {
let wait = call.remaining_ring_seconds();
let context = self.get_weak_context();
task::spawn(Context::emit_end_call_if_unaccepted(
task::spawn(Context::finalize_call_if_unaccepted(
context,
wait.try_into()?,
call.msg.id,
can_call_me,
));
}
} else {
@@ -455,6 +467,7 @@ impl Context {
return Ok(());
}
let (msg_id, chat_id) = (call_id, call.msg.chat_id);
if !call.is_accepted() {
if call.is_incoming() {
if from_id == ContactId::SELF {
@@ -465,6 +478,9 @@ impl Context {
call.mark_as_canceled(self).await?;
let missed_call_str = stock_str::missed_call(self);
call.update_text(self, &missed_call_str).await?;
if self.can_call_me(from_id).await? {
self.emit_event(EventType::CallMissed { msg_id, chat_id });
}
}
} else {
// outgoing
@@ -482,12 +498,8 @@ impl Context {
call.mark_as_ended(self).await?;
call.update_text_duration(self).await?;
}
self.emit_event(EventType::CallEnded { msg_id, chat_id });
self.emit_msgs_changed(call.msg.chat_id, call_id);
self.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
});
}
_ => {}
}

View File

@@ -5,6 +5,7 @@ use crate::constants::DC_CHAT_ID_TRASH;
use crate::message::MessageState;
use crate::receive_imf::receive_imf;
use crate::test_utils::{TestContext, TestContextManager};
use crate::tools::SystemTime;
struct CallSetup {
pub alice: TestContext,
@@ -490,6 +491,9 @@ async fn test_caller_cancels_call() -> Result<()> {
// Bob receives the ending message
bob.recv_msg_trash(&sent3).await;
assert_text(&bob, bob_call.id, "Missed call").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallMissed { .. }))
.await;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
@@ -502,6 +506,9 @@ async fn test_caller_cancels_call() -> Result<()> {
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Missed call").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallMissed { .. }))
.await;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
@@ -510,6 +517,95 @@ async fn test_caller_cancels_call() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_stale_call() -> Result<()> {
let mut tcm = TestContextManager::new();
for accepted in [false, true] {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
info!(bob, "Alice is accepted: {accepted}.");
if accepted {
bob.create_chat(alice).await;
}
let alice_chat = alice.create_chat(bob).await;
alice
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string(), true)
.await?;
let sent1 = alice.pop_sent_msg().await;
SystemTime::shift(Duration::from_secs(3600));
let bob_call = bob.recv_msg(&sent1).await;
let EventType::MsgsChanged { msg_id, chat_id } = bob
.evtracker
.get_matching(|evt| {
matches!(
evt,
EventType::MsgsChanged { .. }
| EventType::CallMissed { .. }
| EventType::CallEnded { .. }
)
})
.await
else {
unreachable!();
};
assert_eq!(chat_id, bob_call.chat_id);
let msg = Message::load_from_db(bob, msg_id).await?;
assert_eq!(msg.text, stock_str::messages_e2ee_info_msg(bob));
if accepted {
let EventType::CallMissed { msg_id, chat_id } = bob
.evtracker
.get_matching(|evt| {
matches!(
evt,
EventType::CallMissed { .. } | EventType::CallEnded { .. }
)
})
.await
else {
unreachable!();
};
assert_eq!(msg_id, bob_call.id);
assert_eq!(chat_id, bob_call.chat_id);
}
let EventType::MsgsChanged { msg_id, chat_id } = bob
.evtracker
.get_matching(|evt| {
matches!(
evt,
EventType::MsgsChanged { .. }
| EventType::CallMissed { .. }
| EventType::CallEnded { .. }
)
})
.await
else {
unreachable!();
};
assert_eq!(msg_id, bob_call.id);
assert_eq!(chat_id, bob_call.chat_id);
let evt = bob
.evtracker
.get_matching_opt(bob, |evt| {
matches!(
evt,
EventType::CallMissed { .. } | EventType::CallEnded { .. }
)
})
.await;
assert!(evt.is_none());
assert_text(bob, bob_call.id, "Missed call").await?;
assert_eq!(call_state(bob, bob_call.id).await?, CallState::Missed);
// Test that message summary says it is a missed call.
let bob_call_msg = Message::load_from_db(bob, bob_call.id).await?;
let summary = bob_call_msg.get_summary(bob, None).await?;
assert_eq!(summary.text, "🎥 Missed call");
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_is_stale_call() -> Result<()> {
// a call started now is not stale

View File

@@ -4131,56 +4131,61 @@ pub async fn remove_contact_from_chat(
delete_broadcast_secret(context, chat_id).await?;
}
ensure!(
matches!(
chat.typ,
Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast
),
"Cannot remove members from non-group chats."
);
if matches!(
chat.typ,
Chattype::Group | Chattype::OutBroadcast | Chattype::InBroadcast
) {
if !chat.is_self_in_chat(context).await? {
let err_msg = format!(
"Cannot remove contact {contact_id} from chat {chat_id}: self not in group."
);
context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
bail!("{err_msg}");
} else {
let mut sync = Nosync;
if !chat.is_self_in_chat(context).await? {
let err_msg =
format!("Cannot remove contact {contact_id} from chat {chat_id}: self not in group.");
context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
bail!("{err_msg}");
}
if chat.is_promoted() && chat.typ != Chattype::OutBroadcast {
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
} else {
remove_from_chat_contacts_table_without_trace(context, chat_id, contact_id).await?;
}
let mut sync = Nosync;
// We do not return an error if the contact does not exist in the database.
// This allows to delete dangling references to deleted contacts
// in case of the database becoming inconsistent due to a bug.
if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
if chat.is_promoted() {
let addr = contact.get_addr();
let fingerprint = contact.fingerprint().map(|f| f.hex());
if chat.is_promoted() && chat.typ != Chattype::OutBroadcast {
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
} else {
remove_from_chat_contacts_table_without_trace(context, chat_id, contact_id).await?;
}
// We do not return an error if the contact does not exist in the database.
// This allows to delete dangling references to deleted contacts
// in case of the database becoming inconsistent due to a bug.
if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
if chat.is_promoted() {
let addr = contact.get_addr();
let fingerprint = contact.fingerprint().map(|f| f.hex());
let res =
send_member_removal_msg(context, &chat, contact_id, addr, fingerprint.as_deref())
let res = send_member_removal_msg(
context,
&chat,
contact_id,
addr,
fingerprint.as_deref(),
)
.await;
if contact_id == ContactId::SELF {
res?;
} else if let Err(e) = res {
warn!(
context,
"remove_contact_from_chat({chat_id}, {contact_id}): send_msg() failed: {e:#}."
);
if contact_id == ContactId::SELF {
res?;
} else if let Err(e) = res {
warn!(
context,
"remove_contact_from_chat({chat_id}, {contact_id}): send_msg() failed: {e:#}."
);
}
} else {
sync = Sync;
}
}
context.emit_event(EventType::ChatModified(chat_id));
if sync.into() {
chat.sync_contacts(context).await.log_err(context).ok();
}
} else {
sync = Sync;
}
}
context.emit_event(EventType::ChatModified(chat_id));
if sync.into() {
chat.sync_contacts(context).await.log_err(context).ok();
} else {
bail!("Cannot remove members from non-group chats.");
}
Ok(())

View File

@@ -261,7 +261,6 @@ impl Context {
.await?;
send_sync_transports(self).await?;
self.quota.write().await.remove(&removed_transport_id);
self.restart_io_if_running().await;
Ok(())
}

View File

@@ -1142,17 +1142,10 @@ ORDER BY m.timestamp DESC,m.id DESC",
Ok(list)
}
/// (deprecated) Returns a list of messages with database ID higher than requested.
/// Returns a list of messages with database ID higher than requested.
///
/// Blocked contacts and chats are excluded,
/// but self-sent messages and contact requests are included in the results.
///
/// Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
/// even if it is not fully downloaded yet.
/// The bot needs to wait for the message to be fully downloaded.
/// Since this is usually not the desired behavior,
/// bots should instead use the [`EventType::IncomingMsg`]
/// event for getting notified about new messages.
pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> {
let last_msg_id = match self.get_config(Config::LastMsgId).await? {
Some(s) => MsgId::new(s.parse()?),
@@ -1201,7 +1194,7 @@ ORDER BY m.timestamp DESC,m.id DESC",
Ok(list)
}
/// (deprecated) Returns a list of messages with database ID higher than last marked as seen.
/// Returns a list of messages with database ID higher than last marked as seen.
///
/// This function is supposed to be used by bot to request messages
/// that are not processed yet.
@@ -1211,13 +1204,6 @@ ORDER BY m.timestamp DESC,m.id DESC",
/// shortly after notification or notification is manually triggered
/// to interrupt waiting.
/// Notification may be manually triggered by calling [`Self::stop_io`].
///
/// Deprecated 2026-04: This returns the message's id as soon as the first part arrives,
/// even if it is not fully downloaded yet.
/// The bot needs to wait for the message to be fully downloaded.
/// Since this is usually not the desired behavior,
/// bots should instead use the #DC_EVENT_INCOMING_MSG / [`EventType::IncomingMsg`]
/// event for getting notified about new messages.
pub async fn wait_next_msgs(&self) -> Result<Vec<MsgId>> {
self.new_msgs_notify.notified().await;
let list = self.get_next_msgs().await?;

View File

@@ -419,6 +419,14 @@ pub enum EventType {
chat_id: ChatId,
},
/// Call missed.
CallMissed {
/// ID of the message referring to the call.
msg_id: MsgId,
/// ID of the chat which the message belongs to.
chat_id: ChatId,
},
/// One or more transports has changed or another transport is primary now.
///
/// UI should update the list.

View File

@@ -730,19 +730,10 @@ impl Imap {
info!(context, "{message_id:?} is a post-message.");
available_post_msgs.push(message_id.clone());
let is_bot = context.get_config_bool(Config::Bot).await?;
if is_bot && download_limit.is_none_or(|download_limit| size <= download_limit)
{
uids_fetch.push(uid);
uid_message_ids.insert(uid, message_id);
} else {
if download_limit.is_none_or(|download_limit| size <= download_limit) {
// Download later after all the small messages are downloaded,
// so that large messages don't delay receiving small messages
download_later.push(message_id.clone());
}
largest_uid_skipped = Some(uid);
if download_limit.is_none_or(|download_limit| size <= download_limit) {
download_later.push(message_id.clone());
}
largest_uid_skipped = Some(uid);
} else {
info!(context, "{message_id:?} is not a post-message.");
if download_limit.is_none_or(|download_limit| size <= download_limit) {

View File

@@ -14,9 +14,7 @@ use mailparse::SingleInfo;
use num_traits::FromPrimitive;
use regex::Regex;
use crate::chat::{
self, Chat, ChatId, ChatIdBlocked, ChatVisibility, is_contact_in_chat, save_broadcast_secret,
};
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ChatVisibility, save_broadcast_secret};
use crate::config::Config;
use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
use crate::contact::{self, Contact, ContactId, Origin, mark_contact_id_as_verified};
@@ -1071,12 +1069,7 @@ UPDATE msgs SET state=? WHERE
let fresh = received_msg.state == MessageState::InFresh
&& mime_parser.is_system_message != SystemMessage::CallAccepted
&& mime_parser.is_system_message != SystemMessage::CallEnded;
let is_bot = context.get_config_bool(Config::Bot).await?;
let is_pre_message = matches!(mime_parser.pre_message, PreMessageMode::Pre { .. });
let skip_bot_notify = is_bot && is_pre_message;
let important =
mime_parser.incoming && fresh && !is_old_contact_request && !skip_bot_notify;
let important = mime_parser.incoming && fresh && !is_old_contact_request;
for msg_id in &received_msg.msg_ids {
chat_id.emit_msg_event(context, *msg_id, important);
}
@@ -2580,22 +2573,7 @@ WHERE id=?
),
)
.await?;
if context.get_config_bool(Config::Bot).await? {
if original_msg.hidden {
// No need to emit an event about the changed message
} else if !original_msg.chat_id.is_trash() {
let fresh = original_msg.state == MessageState::InFresh;
let important = mime_parser.incoming && fresh;
original_msg
.chat_id
.emit_msg_event(context, original_msg.id, important);
context.new_msgs_notify.notify_one();
}
} else {
context.emit_msgs_changed(original_msg.chat_id, original_msg.id);
}
context.emit_msgs_changed(original_msg.chat_id, original_msg.id);
Ok(())
}
@@ -3155,18 +3133,17 @@ async fn apply_group_changes(
}
}
apply_chat_name_avatar_and_description_changes(
context,
mime_parser,
from_id,
is_from_in_chat,
chat,
&mut send_event_chat_modified,
&mut better_msg,
)
.await?;
if is_from_in_chat {
apply_chat_name_avatar_and_description_changes(
context,
mime_parser,
from_id,
chat,
&mut send_event_chat_modified,
&mut better_msg,
)
.await?;
// Avoid insertion of `from_id` into a group with inappropriate encryption state.
if from_is_key_contact != chat.grpid.is_empty()
&& chat.member_list_is_stale(context).await?
@@ -3340,7 +3317,6 @@ async fn apply_chat_name_avatar_and_description_changes(
context: &Context,
mime_parser: &MimeMessage,
from_id: ContactId,
is_from_in_chat: bool,
chat: &mut Chat,
send_event_chat_modified: &mut bool,
better_msg: &mut Option<String>,
@@ -3369,8 +3345,7 @@ async fn apply_chat_name_avatar_and_description_changes(
let chat_group_name_timestamp = chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
// To provide group name consistency, compare names if timestamps are equal.
if is_from_in_chat
&& (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
&& chat
.id
.update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
@@ -3391,19 +3366,14 @@ async fn apply_chat_name_avatar_and_description_changes(
.get_header(HeaderDef::ChatGroupNameChanged)
.is_some()
{
if is_from_in_chat {
let old_name = &sanitize_single_line(old_name);
better_msg.get_or_insert(
if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
stock_str::msg_broadcast_name_changed(context, old_name, grpname)
} else {
stock_str::msg_grp_name(context, old_name, grpname, from_id).await
},
);
} else {
// Attempt to change group name by non-member, trash it.
*better_msg = Some(String::new());
}
let old_name = &sanitize_single_line(old_name);
better_msg.get_or_insert(
if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
stock_str::msg_broadcast_name_changed(context, old_name, grpname)
} else {
stock_str::msg_grp_name(context, old_name, grpname, from_id).await
},
);
}
}
@@ -3426,8 +3396,7 @@ async fn apply_chat_name_avatar_and_description_changes(
let new_timestamp = timestamp_in_header.unwrap_or(mime_parser.timestamp_sent);
// To provide consistency, compare descriptions if timestamps are equal.
if is_from_in_chat
&& (old_timestamp, &old_description) < (new_timestamp, &new_description)
if (old_timestamp, &old_description) < (new_timestamp, &new_description)
&& chat
.id
.update_timestamp(context, Param::GroupDescriptionTimestamp, new_timestamp)
@@ -3448,13 +3417,8 @@ async fn apply_chat_name_avatar_and_description_changes(
.get_header(HeaderDef::ChatGroupDescriptionChanged)
.is_some()
{
if is_from_in_chat {
better_msg
.get_or_insert(stock_str::msg_chat_description_changed(context, from_id).await);
} else {
// Attempt to change group description by non-member, trash it.
*better_msg = Some(String::new());
}
better_msg
.get_or_insert(stock_str::msg_chat_description_changed(context, from_id).await);
}
}
@@ -3464,46 +3428,39 @@ async fn apply_chat_name_avatar_and_description_changes(
&& value == "group-avatar-changed"
&& let Some(avatar_action) = &mime_parser.group_avatar
{
if is_from_in_chat {
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
better_msg.get_or_insert(
if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
stock_str::msg_broadcast_img_changed(context)
} else {
match avatar_action {
AvatarAction::Delete => {
stock_str::msg_grp_img_deleted(context, from_id).await
}
AvatarAction::Change(_) => {
stock_str::msg_grp_img_changed(context, from_id).await
}
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
better_msg.get_or_insert(
if matches!(chat.typ, Chattype::InBroadcast | Chattype::OutBroadcast) {
stock_str::msg_broadcast_img_changed(context)
} else {
match avatar_action {
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
AvatarAction::Change(_) => {
stock_str::msg_grp_img_changed(context, from_id).await
}
},
);
} else {
// Attempt to change group avatar by non-member, trash it.
*better_msg = Some(String::new());
}
}
},
);
}
if let Some(avatar_action) = &mime_parser.group_avatar
&& is_from_in_chat
&& chat
if let Some(avatar_action) = &mime_parser.group_avatar {
info!(context, "Group-avatar change for {}.", chat.id);
if chat
.param
.update_timestamp(Param::AvatarTimestamp, mime_parser.timestamp_sent)?
{
info!(context, "Group-avatar change for {}.", chat.id);
match avatar_action {
AvatarAction::Change(profile_image) => {
chat.param.set(Param::ProfileImage, profile_image);
}
AvatarAction::Delete => {
chat.param.remove(Param::ProfileImage);
}
};
chat.update_param(context).await?;
*send_event_chat_modified = true;
{
match avatar_action {
AvatarAction::Change(profile_image) => {
chat.param.set(Param::ProfileImage, profile_image);
}
AvatarAction::Delete => {
chat.param.remove(Param::ProfileImage);
}
};
chat.update_param(context).await?;
*send_event_chat_modified = true;
}
}
Ok(())
@@ -3810,12 +3767,10 @@ async fn apply_out_broadcast_changes(
let mut added_removed_id: Option<ContactId> = None;
if from_id == ContactId::SELF {
let is_from_in_chat = true;
apply_chat_name_avatar_and_description_changes(
context,
mime_parser,
from_id,
is_from_in_chat,
chat,
&mut send_event_chat_modified,
&mut better_msg,
@@ -3904,12 +3859,10 @@ async fn apply_in_broadcast_changes(
let mut send_event_chat_modified = false;
let mut better_msg = None;
let is_from_in_chat = is_contact_in_chat(context, chat.id, from_id).await?;
apply_chat_name_avatar_and_description_changes(
context,
mime_parser,
from_id,
is_from_in_chat,
chat,
&mut send_event_chat_modified,
&mut better_msg,

View File

@@ -4378,42 +4378,39 @@ async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = create_group(alice, "Group").await?;
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = create_group(&alice, "Group").await?;
add_contact_to_chat(
alice,
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(bob).await,
alice.add_or_lookup_contact_id(&bob).await,
)
.await?;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
let fiona = &tcm.fiona().await;
let fiona = TestContext::new_fiona().await;
add_contact_to_chat(
alice,
&alice,
alice_chat_id,
alice.add_or_lookup_contact_id(fiona).await,
alice.add_or_lookup_contact_id(&fiona).await,
)
.await?;
let fiona_chat_id = fiona.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
fiona_chat_id.accept(fiona).await?;
fiona_chat_id.accept(&fiona).await?;
SystemTime::shift(Duration::from_secs(60));
chat::set_chat_name(fiona, fiona_chat_id, "Renamed").await?;
// Message about chat name change from non-member is trashed.
bob.recv_msg_trash(&fiona.pop_sent_msg().await).await;
chat::set_chat_name(&fiona, fiona_chat_id, "Renamed").await?;
bob.recv_msg(&fiona.pop_sent_msg().await).await;
// Bob missed the message adding fiona, but mustn't recreate the member list or apply the group
// name change.
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
assert!(is_contact_in_chat(bob, bob_chat_id, ContactId::SELF).await?);
let bob_alice_contact = bob.add_or_lookup_contact_id(alice).await;
assert!(is_contact_in_chat(bob, bob_chat_id, bob_alice_contact).await?);
let chat = Chat::load_from_db(bob, bob_chat_id).await?;
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
let bob_alice_contact = bob.add_or_lookup_contact_id(&alice).await;
assert!(is_contact_in_chat(&bob, bob_chat_id, bob_alice_contact).await?);
let chat = Chat::load_from_db(&bob, bob_chat_id).await?;
assert_eq!(chat.get_name(), "Group");
Ok(())
}

View File

@@ -5,14 +5,12 @@ use pretty_assertions::assert_eq;
use crate::EventType;
use crate::chat;
use crate::chat::send_msg;
use crate::config::Config;
use crate::contact;
use crate::download::{DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, PostMsgMetadata};
use crate::message::{Message, MessageState, Viewtype, delete_msgs, markseen_msgs};
use crate::mimeparser::MimeMessage;
use crate::param::Param;
use crate::reaction::{get_msg_reactions, send_reaction};
use crate::receive_imf::receive_imf;
use crate::summary::assert_summary_texts;
use crate::test_utils::TestContextManager;
use crate::tests::pre_messages::util::{
@@ -797,46 +795,3 @@ async fn test_chatlist_event_on_post_msg_download() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_bot_pre_message_notifications() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
bob.set_config_bool(Config::Bot, true).await?;
let alice_group_id = alice.create_group_with_members("test group", &[&bob]).await;
let (pre_message, post_message, _alice_msg_id) = send_large_file_message(
&alice,
alice_group_id,
Viewtype::File,
&vec![0u8; (PRE_MSG_ATTACHMENT_SIZE_THRESHOLD + 1) as usize],
)
.await?;
// Bob receives pre-message
bob.evtracker.clear_events();
receive_imf(&bob, pre_message.payload().as_bytes(), false).await?;
// Verify Bob does NOT get an IncomingMsg event for the pre-message
assert!(
bob.evtracker
.get_matching_opt(&bob, |e| matches!(e, EventType::IncomingMsg { .. }))
.await
.is_none()
);
// Bob receives post-message
receive_imf(&bob, post_message.payload().as_bytes(), false).await?;
// Verify Bob DOES get an IncomingMsg event for the complete message
bob.evtracker
.get_matching(|e| matches!(e, EventType::IncomingMsg { .. }))
.await;
let msg = bob.get_last_msg().await;
assert_eq!(msg.download_state, DownloadState::Done);
Ok(())
}