mirror of
https://github.com/chatmail/core.git
synced 2026-05-13 20:06:30 +03:00
Compare commits
2 Commits
dc09/del-t
...
iequidoo/C
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdc2864df4 | ||
|
|
9cb2077c94 |
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
112
src/calls.rs
112
src/calls.rs
@@ -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,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
91
src/chat.rs
91
src/chat.rs
@@ -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(())
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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.
|
||||
|
||||
15
src/imap.rs
15
src/imap.rs
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user