From f66adccb334eda5f6ad704d1994203f6023496a8 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Thu, 6 Mar 2025 20:27:58 +0100 Subject: [PATCH 01/21] add call API --- deltachat-ffi/deltachat.h | 109 ++++++ deltachat-ffi/src/lib.rs | 59 ++- deltachat-jsonrpc/src/api/types/events.rs | 25 ++ deltachat-jsonrpc/src/api/types/message.rs | 9 + src/calls.rs | 399 +++++++++++++++++++++ src/events/payload.rs | 24 ++ src/lib.rs | 1 + src/mimefactory.rs | 23 +- src/mimeparser.rs | 26 ++ src/receive_imf.rs | 7 + src/sync.rs | 4 + 11 files changed, 684 insertions(+), 2 deletions(-) create mode 100644 src/calls.rs diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index ee30f2b3b9..e4979e3ac0 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1222,6 +1222,79 @@ void dc_set_webxdc_integration (dc_context_t* context, const char* f uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t chat_id); +/** + * Start an outgoing call. + * This sends a message with all relevant information to the callee, + * who will get informed by an #DC_EVENT_INCOMING_CALL event and rings. + * + * Possible actions during ringing: + * - callee accepts using dc_accept_incoming_call(), caller receives #DC_EVENT_OUTGOING_CALL_ACCEPTED, + * callee's devices receive #DC_EVENT_INCOMING_CALL_ACCEPTED, call starts + * - callee rejects using dc_end_call(), caller receives #DC_EVENT_CALL_ENDED, + * callee's other devices receive #DC_EVENT_CALL_ENDED, done. + * - caller cancels the call using dc_end_call(), callee receives #DC_EVENT_CALL_ENDED, done. + * - after 2 minutes, caller and callee both should end the call. + * this is to prevent endless ringing of callee + * in case caller got offline without being able to send cancellation message. + * + * Actions during the call: + * - callee ends the call using dc_end_call(), caller receives #DC_EVENT_CALL_ENDED + * - caller ends the call using dc_end_call(), callee receives #DC_EVENT_CALL_ENDED + * + * Note, that the events are for updating the call screen, + * possible status messages are added and updated as usual, including the known events. + * In the UI, the sorted chatlist is used as an overview about calls as well as messages. + * To place a call with a contact that has no chat yet, use dc_create_chat_by_contact_id() first. + * + * @memberof dc_context_t + * @param context The context object. + * @param chat_id The chat to place a call for. + * This needs to be a one-to-one chat. + * @return ID of the system message announcing the call. + */ +uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t chat_id); + + +/** + * Accept incoming call. + * + * This implicitly accepts the contact request, if not yet done. + * All affected devices will receive + * either #DC_EVENT_OUTGOING_CALL_ACCEPTED or #DC_EVENT_INCOMING_CALL_ACCEPTED. + * + * @memberof dc_context_t + * @param context The context object. + * @param msg_id The ID of the call to accept. + * This is the ID reported by #DC_EVENT_INCOMING_CALL + * and equal to the ID of the corresponding info message. + * @return 1=success, 0=error + */ + int dc_accept_incoming_call (dc_context_t* context, uint32_t msg_id); + + + /** + * End incoming or outgoing call. + * + * From the view of the caller, a "cancellation", + * from the view of callee, a "rejection". + * If the call was accepted, this is a "hangup". + * + * For accepted calls, + * all participant devices get informed about the ended call via #DC_EVENT_CALL_ENDED. + * For not accepted calls, only the caller will inform the callee. + * + * If the callee rejects, the caller will get an timeout or give up at some point - + * same as for all other reasons the call cannot be established: Device not in reach, device muted, connectivity etc. + * This is to protect privacy of the callee, avoiding to check if callee is online. + * + * @memberof dc_context_t + * @param context The context object. + * @param msg_id the ID of the call. + * @return 1=success, 0=error + */ + int dc_end_call (dc_context_t* context, uint32_t msg_id); + + /** * Save a draft for a chat in the database. * @@ -4541,6 +4614,8 @@ int dc_msg_is_info (const dc_msg_t* msg); * the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL * and also offer a way to fix the encryption, eg. by a button offering a QR scan * - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info` + * - DC_INFO_OUTGOING_CALL (50) - Info-message refers to an outgoing call + * - DC_INFO_INCOMING_CALL (55) - Info-message refers to an incoming call * * For the messages that refer to a CONTACT, * dc_msg_get_info_contact_id() returns the contact ID. @@ -4596,6 +4671,8 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg); #define DC_INFO_PROTECTION_DISABLED 12 #define DC_INFO_INVALID_UNENCRYPTED_MAIL 13 #define DC_INFO_WEBXDC_INFO_MESSAGE 32 +#define DC_INFO_OUTGOING_CALL 50 +#define DC_INFO_INCOMING_CALL 55 /** @@ -6631,6 +6708,38 @@ void dc_event_unref(dc_event_t* event); */ #define DC_EVENT_CHANNEL_OVERFLOW 2400 + + +/** + * Incoming call. + * UI will usually start ringing. + * + * If user takes action, dc_accept_incoming_call() or dc_end_call() should be called. + * + * Otherwise, ringing should end on #DC_EVENT_CALL_ENDED + * or #DC_EVENT_INCOMING_CALL_ACCEPTED + * + * @param data1 (int) msg_id + */ +#define DC_EVENT_INCOMING_CALL 2550 + +/** + * The callee accepted an incoming call on another another device using dc_accept_incoming_call(). + * The caller gets the event #DC_EVENT_OUTGOING_CALL_ACCEPTED at the same time. + */ + #define DC_EVENT_INCOMING_CALL_ACCEPTED 2560 + +/** + * A call placed using dc_place_outgoing_call() was accepted by the callee using dc_accept_incoming_call(). + */ +#define DC_EVENT_OUTGOING_CALL_ACCEPTED 2570 + +/** + * An incoming or outgoing call was ended using dc_end_call(). + */ +#define DC_EVENT_CALL_ENDED 2580 + + /** * @} */ diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 93894829a3..0b4377d072 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -556,6 +556,10 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int EventType::AccountsChanged => 2302, EventType::AccountsItemChanged => 2303, EventType::EventChannelOverflow { .. } => 2400, + EventType::IncomingCall { .. } => 2550, + EventType::IncomingCallAccepted { .. } => 2560, + EventType::OutgoingCallAccepted { .. } => 2570, + EventType::CallEnded { .. } => 2580, #[allow(unreachable_patterns)] #[cfg(test)] _ => unreachable!("This is just to silence a rust_analyzer false-positive"), @@ -619,7 +623,11 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc: EventType::WebxdcRealtimeData { msg_id, .. } | EventType::WebxdcStatusUpdate { msg_id, .. } | EventType::WebxdcRealtimeAdvertisementReceived { msg_id } - | EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int, + | EventType::WebxdcInstanceDeleted { msg_id, .. } + | EventType::IncomingCall { msg_id, .. } + | EventType::IncomingCallAccepted { msg_id, .. } + | EventType::OutgoingCallAccepted { 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 } @@ -671,6 +679,10 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc: | EventType::ChatModified(_) | EventType::ChatDeleted { .. } | EventType::WebxdcRealtimeAdvertisementReceived { .. } + | EventType::IncomingCall { .. } + | EventType::IncomingCallAccepted { .. } + | EventType::OutgoingCallAccepted { .. } + | EventType::CallEnded { .. } | EventType::EventChannelOverflow { .. } => 0, EventType::MsgsChanged { msg_id, .. } | EventType::ReactionsChanged { msg_id, .. } @@ -768,6 +780,10 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut | EventType::AccountsChanged | EventType::AccountsItemChanged | EventType::WebxdcRealtimeAdvertisementReceived { .. } + | EventType::IncomingCall { .. } + | EventType::IncomingCallAccepted { .. } + | EventType::OutgoingCallAccepted { .. } + | EventType::CallEnded { .. } | EventType::EventChannelOverflow { .. } => ptr::null_mut(), EventType::ConfigureProgress { comment, .. } => { if let Some(comment) = comment { @@ -1167,6 +1183,47 @@ pub unsafe extern "C" fn dc_init_webxdc_integration( .unwrap_or(0) } +#[no_mangle] +pub unsafe extern "C" fn dc_place_outgoing_call(context: *mut dc_context_t, chat_id: u32) -> u32 { + if context.is_null() || chat_id == 0 { + eprintln!("ignoring careless call to dc_place_outgoing_call()"); + return 0; + } + let ctx = &*context; + let chat_id = ChatId::new(chat_id); + + block_on(ctx.place_outgoing_call(chat_id)) + .map(|msg_id| msg_id.to_u32()) + .unwrap_or_log_default(ctx, "Failed to place call") +} + +#[no_mangle] +pub unsafe extern "C" fn dc_accept_incoming_call( + context: *mut dc_context_t, + msg_id: u32, +) -> libc::c_int { + if context.is_null() || msg_id == 0 { + eprintln!("ignoring careless call to dc_accept_incoming_call()"); + return 0; + } + let ctx = &*context; + let msg_id = MsgId::new(msg_id); + + block_on(ctx.accept_incoming_call(msg_id)).is_ok() as libc::c_int +} + +#[no_mangle] +pub unsafe extern "C" fn dc_end_call(context: *mut dc_context_t, msg_id: u32) -> libc::c_int { + if context.is_null() || msg_id == 0 { + eprintln!("ignoring careless call to dc_end_call()"); + return 0; + } + let ctx = &*context; + let msg_id = MsgId::new(msg_id); + + block_on(ctx.end_call(msg_id)).is_ok() as libc::c_int +} + #[no_mangle] pub unsafe extern "C" fn dc_set_draft( context: *mut dc_context_t, diff --git a/deltachat-jsonrpc/src/api/types/events.rs b/deltachat-jsonrpc/src/api/types/events.rs index 7472b23025..8344823fa7 100644 --- a/deltachat-jsonrpc/src/api/types/events.rs +++ b/deltachat-jsonrpc/src/api/types/events.rs @@ -417,6 +417,19 @@ pub enum EventType { /// Number of events skipped. n: u64, }, + + /// Incoming call. + IncomingCall { msg_id: u32 }, + + /// Incoming call accepted. + /// This is esp. interesting to stop ringing on other devices. + IncomingCallAccepted { msg_id: u32 }, + + /// Outgoing call accepted. + OutgoingCallAccepted { msg_id: u32 }, + + /// Call ended. + CallEnded { msg_id: u32 }, } impl From for EventType { @@ -567,6 +580,18 @@ impl From for EventType { CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n }, CoreEventType::AccountsChanged => AccountsChanged, CoreEventType::AccountsItemChanged => AccountsItemChanged, + CoreEventType::IncomingCall { msg_id } => IncomingCall { + msg_id: msg_id.to_u32(), + }, + CoreEventType::IncomingCallAccepted { msg_id } => IncomingCallAccepted { + msg_id: msg_id.to_u32(), + }, + CoreEventType::OutgoingCallAccepted { msg_id } => OutgoingCallAccepted { + msg_id: msg_id.to_u32(), + }, + CoreEventType::CallEnded { msg_id } => CallEnded { + msg_id: msg_id.to_u32(), + }, #[allow(unreachable_patterns)] #[cfg(test)] _ => unreachable!("This is just to silence a rust_analyzer false-positive"), diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index 3ad81bd12a..0b3055ebb0 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -434,6 +434,11 @@ pub enum SystemMessageType { /// This message contains a users iroh node address. IrohNodeAddr, + + OutgoingCall, + IncomingCall, + CallAccepted, + CallEnded, } impl From for SystemMessageType { @@ -459,6 +464,10 @@ impl From for SystemMessageType { SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr, SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait, SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout, + SystemMessage::OutgoingCall => SystemMessageType::OutgoingCall, + SystemMessage::IncomingCall => SystemMessageType::IncomingCall, + SystemMessage::CallAccepted => SystemMessageType::CallAccepted, + SystemMessage::CallEnded => SystemMessageType::CallEnded, } } } diff --git a/src/calls.rs b/src/calls.rs new file mode 100644 index 0000000000..ef282bd235 --- /dev/null +++ b/src/calls.rs @@ -0,0 +1,399 @@ +//! # Handle calls. +//! +//! Internally, calls a bound to the user-visible info message initializing the call. +//! This means, the "Call ID" is a "Message ID" currently - similar to webxdc. +//! So, no database changes are needed at this stage. +//! When it comes to relay calls over iroh, we may need a dedicated table, and this may change. +use crate::chat::{send_msg, Chat, ChatId}; +use crate::constants::Chattype; +use crate::context::Context; +use crate::events::EventType; +use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype}; +use crate::mimeparser::{MimeMessage, SystemMessage}; +use crate::param::Param; +use crate::sync::SyncData; +use anyhow::{anyhow, ensure, Result}; + +/// Information about the status of a call. +#[derive(Debug)] +pub struct CallInfo { + /// Incoming our outgoing call? + pub incoming: bool, + + /// Was an incoming call accepted on this device? + /// On other devices, this is never set and for outgoing calls, this is never set. + /// Internally, this is written to Param::Arg. + pub accepted: bool, + + /// Info message referring to the call. + pub msg: Message, +} + +impl Context { + /// Start an outgoing call. + pub async fn place_outgoing_call(&self, chat_id: ChatId) -> Result { + let chat = Chat::load_from_db(self, chat_id).await?; + ensure!(chat.typ == Chattype::Single && !chat.is_self_talk()); + + let mut msg = Message { + viewtype: Viewtype::Text, + text: "Calling...".into(), + ..Default::default() + }; + msg.param.set_cmd(SystemMessage::OutgoingCall); + msg.id = send_msg(self, chat_id, &mut msg).await?; + Ok(msg.id) + } + + /// Accept an incoming call. + pub async fn accept_incoming_call(&self, call_id: MsgId) -> Result<()> { + let call: CallInfo = self.load_call_by_root_id(call_id).await?; + ensure!(call.incoming); + + let chat = Chat::load_from_db(self, call.msg.chat_id).await?; + if chat.is_contact_request() { + chat.id.accept(self).await?; + } + + // mark call as "accepted" (this is just Param::Arg atm) + let mut msg = call.msg.clone(); + msg.param.set_int(Param::Arg, 1); + msg.update_param(self).await?; + + // send an acceptance message around: to the caller as well as to the other devices of the callee + let mut msg = Message { + viewtype: Viewtype::Text, + text: "Call accepted".into(), + ..Default::default() + }; + msg.param.set_cmd(SystemMessage::CallAccepted); + msg.set_quote(self, Some(&call.msg)).await?; + msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?; + self.emit_event(EventType::IncomingCallAccepted { + msg_id: call.msg.id, + }); + Ok(()) + } + + /// Cancel, reject for hangup an incoming or outgoing call. + pub async fn end_call(&self, call_id: MsgId) -> Result<()> { + let call: CallInfo = self.load_call_by_root_id(call_id).await?; + + if call.accepted || !call.incoming { + let mut msg = Message { + viewtype: Viewtype::Text, + text: "Call ended".into(), + ..Default::default() + }; + msg.param.set_cmd(SystemMessage::CallEnded); + msg.set_quote(self, Some(&call.msg)).await?; + msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?; + } else if call.incoming { + // to protect privacy, we do not send a message to others from callee for unaccepted calls + self.add_sync_item(SyncData::RejectIncomingCall { + msg: call.msg.rfc724_mid, + }) + .await?; + self.scheduler.interrupt_inbox().await; + } + + self.emit_event(EventType::CallEnded { + msg_id: call.msg.id, + }); + Ok(()) + } + + pub(crate) async fn handle_call_msg( + &self, + mime_message: &MimeMessage, + call_or_child_id: MsgId, + ) -> Result<()> { + match mime_message.is_system_message { + SystemMessage::IncomingCall => { + let call = self.load_call_by_root_id(call_or_child_id).await?; + if call.incoming { + self.emit_event(EventType::IncomingCall { + msg_id: call.msg.id, + }); + } + } + SystemMessage::CallAccepted => { + let call = self.load_call_by_child_id(call_or_child_id).await?; + if call.incoming { + self.emit_event(EventType::IncomingCallAccepted { + msg_id: call.msg.id, + }); + } else { + self.emit_event(EventType::OutgoingCallAccepted { + msg_id: call.msg.id, + }); + } + } + SystemMessage::CallEnded => { + let call = self.load_call_by_child_id(call_or_child_id).await?; + self.emit_event(EventType::CallEnded { + msg_id: call.msg.id, + }); + } + _ => {} + } + Ok(()) + } + + pub(crate) async fn sync_call_rejection(&self, rfc724_mid: &str) -> Result<()> { + if let Some((msg_id, _)) = rfc724_mid_exists(self, rfc724_mid).await? { + let call = self.load_call_by_root_id(msg_id).await?; + self.emit_event(EventType::CallEnded { + msg_id: call.msg.id, + }); + } + Ok(()) + } + + async fn load_call_by_root_id(&self, call_id: MsgId) -> Result { + let call = Message::load_from_db(self, call_id).await?; + self.load_call_by_message(call) + } + + async fn load_call_by_child_id(&self, child_id: MsgId) -> Result { + let child = Message::load_from_db(self, child_id).await?; + if let Some(call) = child.parent(self).await? { + self.load_call_by_message(call) + } else { + Err(anyhow!("Call parent missing")) + } + } + + fn load_call_by_message(&self, call: Message) -> Result { + ensure!( + call.get_info_type() == SystemMessage::IncomingCall + || call.get_info_type() == SystemMessage::OutgoingCall + ); + + Ok(CallInfo { + incoming: call.get_info_type() == SystemMessage::IncomingCall, + accepted: call.param.get_int(Param::Arg) == Some(1), + msg: call, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + use crate::test_utils::{sync, TestContext, TestContextManager}; + + async fn setup_call() -> Result<( + TestContext, // Alice's 1st device + TestContext, // Alice's 2nd device + Message, // Call message from view of Alice + TestContext, // Bob's 1st device + TestContext, // Bob's 2nd device + Message, // Call message from view of Bob + Message, // Call message from view of Bob's 2nd device + )> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let alice2 = tcm.alice().await; + let bob = tcm.bob().await; + let bob2 = tcm.bob().await; + for t in [&alice, &alice2, &bob, &bob2] { + t.set_config_bool(Config::SyncMsgs, true).await?; + } + + // Alice creates a chat with Bob and places an outgoing call there. + // Alice's other device sees the same message as an outgoing call. + let alice_chat = alice.create_chat(&bob).await; + let test_msg_id = alice.place_outgoing_call(alice_chat.id).await?; + let sent1 = alice.pop_sent_msg().await; + let alice_call = Message::load_from_db(&alice, sent1.sender_msg_id).await?; + assert_eq!(sent1.sender_msg_id, test_msg_id); + assert!(alice_call.is_info()); + assert_eq!(alice_call.get_info_type(), SystemMessage::OutgoingCall); + let info = alice.load_call_by_root_id(alice_call.id).await?; + assert!(!info.accepted); + + let alice2_call = alice2.recv_msg(&sent1).await; + assert!(alice2_call.is_info()); + assert_eq!(alice2_call.get_info_type(), SystemMessage::OutgoingCall); + let info = alice2.load_call_by_root_id(alice2_call.id).await?; + assert!(!info.accepted); + + // Bob receives the message referring to the call on two devices; + // it is an incoming call from the view of Bob + let bob_call = bob.recv_msg(&sent1).await; + bob.evtracker + .get_matching(|evt| matches!(evt, EventType::IncomingCall { .. })) + .await; + assert!(bob_call.is_info()); + assert_eq!(bob_call.get_info_type(), SystemMessage::IncomingCall); + + let bob2_call = bob2.recv_msg(&sent1).await; + assert!(bob2_call.is_info()); + assert_eq!(bob2_call.get_info_type(), SystemMessage::IncomingCall); + + Ok((alice, alice2, alice_call, bob, bob2, bob_call, bob2_call)) + } + + async fn accept_call() -> Result<( + TestContext, + TestContext, + Message, + TestContext, + TestContext, + Message, + )> { + let (alice, alice2, alice_call, bob, bob2, bob_call, bob2_call) = setup_call().await?; + + // Bob accepts the incoming call, this does not add an additional message to the chat + bob.accept_incoming_call(bob_call.id).await?; + bob.evtracker + .get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. })) + .await; + let sent2 = bob.pop_sent_msg().await; + let info = bob.load_call_by_root_id(bob_call.id).await?; + assert!(info.accepted); + + bob2.recv_msg(&sent2).await; + bob2.evtracker + .get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. })) + .await; + let info = bob2.load_call_by_root_id(bob2_call.id).await?; + assert!(!info.accepted); // "accepted" is only true on the device that does the call + + // Alice receives the acceptance message + alice.recv_msg(&sent2).await; + alice + .evtracker + .get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. })) + .await; + + alice2.recv_msg(&sent2).await; + alice2 + .evtracker + .get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. })) + .await; + Ok((alice, alice2, alice_call, bob, bob2, bob_call)) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_accept_call_callee_ends() -> Result<()> { + // Alice calls Bob, Bob accepts + let (alice, alice2, _alice_call, bob, bob2, bob_call) = accept_call().await?; + + // Bob has accepted the call and also ends it + bob.end_call(bob_call.id).await?; + bob.evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + let sent3 = bob.pop_sent_msg().await; + + bob2.recv_msg(&sent3).await; + bob2.evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + + // Alice receives the ending message + alice.recv_msg(&sent3).await; + alice + .evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + + alice2.recv_msg(&sent3).await; + alice2 + .evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_accept_call_caller_ends() -> Result<()> { + // Alice calls Bob, Bob accepts + let (alice, alice2, _alice_call, bob, bob2, bob_call) = accept_call().await?; + + // Bob has accepted the call but Alice ends it + alice.end_call(bob_call.id).await?; + alice + .evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + let sent3 = alice.pop_sent_msg().await; + + alice2.recv_msg(&sent3).await; + alice2 + .evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + + // Bob receives the ending message + bob.recv_msg(&sent3).await; + bob.evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + + bob2.recv_msg(&sent3).await; + bob2.evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_callee_rejects_call() -> Result<()> { + // Alice calls Bob + let (_alice, _alice2, _alice_call, bob, bob2, bob_call, _bob2_call) = setup_call().await?; + + // Bob does not want to talk with Alice. + // To protect Bob's privacy, no message is sent to Alice (who will time out). + // To let Bob close the call window on all devices, a sync message is used instead. + bob.end_call(bob_call.id).await?; + bob.evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + + sync(&bob, &bob2).await; + bob2.evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_caller_cancels_call() -> Result<()> { + // Alice calls Bob + let (alice, alice2, alice_call, bob, bob2, _bob_call, _bob2_call) = setup_call().await?; + + // Alice changes their mind before Bob picks up + alice.end_call(alice_call.id).await?; + alice + .evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + let sent3 = alice.pop_sent_msg().await; + + alice2.recv_msg(&sent3).await; + alice2 + .evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + + // Bob receives the ending message + bob.recv_msg(&sent3).await; + bob.evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + + bob2.recv_msg(&sent3).await; + bob2.evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + + Ok(()) + } +} diff --git a/src/events/payload.rs b/src/events/payload.rs index a5e2f99653..2bcd60bd76 100644 --- a/src/events/payload.rs +++ b/src/events/payload.rs @@ -376,6 +376,30 @@ pub enum EventType { /// This event is emitted from the account whose property changed. AccountsItemChanged, + /// Incoming call. + IncomingCall { + /// ID of the message referring to the call. + msg_id: MsgId, + }, + + /// Incoming call accepted. + IncomingCallAccepted { + /// ID of the message referring to the call. + msg_id: MsgId, + }, + + /// Outgoing call accepted. + OutgoingCallAccepted { + /// ID of the message referring to the call. + msg_id: MsgId, + }, + + /// Call ended. + CallEnded { + /// ID of the message referring to the call. + msg_id: MsgId, + }, + /// Event for using in tests, e.g. as a fence between normally generated events. #[cfg(test)] Test, diff --git a/src/lib.rs b/src/lib.rs index 3c6402cbfe..6a33b23c92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,6 +53,7 @@ pub use events::*; mod aheader; pub mod blob; +pub mod calls; pub mod chat; pub mod chatlist; pub mod config; diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 606d116313..0031344c30 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeSet, HashSet}; use std::io::Cursor; -use anyhow::{Context as _, Result, bail, ensure}; +use anyhow::{anyhow, Context as _, Result, bail, ensure}; use base64::Engine as _; use deltachat_contact_tools::sanitize_bidi_characters; use mail_builder::headers::HeaderType; @@ -1507,6 +1507,27 @@ impl MimeFactory { .into(), )); } + SystemMessage::OutgoingCall => { + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("call").into(), + )); + } + SystemMessage::IncomingCall => { + return Err(anyhow!("Unexpected incoming call rendering.")); + } + SystemMessage::CallAccepted => { + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("call-accepted").into(), + )); + } + SystemMessage::CallEnded => { + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("call-ended").into(), + )); + } _ => {} } diff --git a/src/mimeparser.rs b/src/mimeparser.rs index bc6b38cd2f..e6665a9d87 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -213,6 +213,22 @@ pub enum SystemMessage { /// This message contains a users iroh node address. IrohNodeAddr = 40, + + /// This system message represents an outgoing call. + /// This message is visible to the user as an "info" message. + OutgoingCall = 50, + + /// This system message represents an incoming call. + /// This message is visible to the user as an "info" message. + IncomingCall = 55, + + /// Message indicating that a call was accepted. + /// While the 1:1 call may be established elsewhere, + /// the message is still needed for a multidevice setup, so that other devices stop ringing. + CallAccepted = 56, + + /// Message indicating that a call was ended. + CallEnded = 57, } const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup"; @@ -647,6 +663,16 @@ impl MimeMessage { self.is_system_message = SystemMessage::ChatProtectionDisabled; } else if value == "group-avatar-changed" { self.is_system_message = SystemMessage::GroupImageChanged; + } else if value == "call" { + self.is_system_message = if self.incoming { + SystemMessage::IncomingCall + } else { + SystemMessage::OutgoingCall + }; + } else if value == "call-accepted" { + self.is_system_message = SystemMessage::CallAccepted; + } else if value == "call-ended" { + self.is_system_message = SystemMessage::CallEnded; } } else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() { self.is_system_message = SystemMessage::MemberRemovedFromGroup; diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 77da645eb2..729afc72b5 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1001,6 +1001,13 @@ pub(crate) async fn receive_imf_inner( } context.new_msgs_notify.notify_one(); + if mime_parser.is_system_message == SystemMessage::IncomingCall + || mime_parser.is_system_message == SystemMessage::CallAccepted + || mime_parser.is_system_message == SystemMessage::CallEnded + { + context.handle_call_msg(&mime_parser, insert_msg_id).await?; + } + mime_parser .handle_reports(context, from_id, &mime_parser.parts) .await; diff --git a/src/sync.rs b/src/sync.rs index 90e302f06b..b5a42130b8 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -71,6 +71,9 @@ pub(crate) enum SyncData { DeleteMessages { msgs: Vec, // RFC724 id (i.e. "Message-Id" header) }, + RejectIncomingCall { + msg: String, // RFC724 id (i.e. "Message-Id" header) + }, } #[derive(Debug, Serialize, Deserialize)] @@ -264,6 +267,7 @@ impl Context { SyncData::Config { key, val } => self.sync_config(key, val).await, SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await, SyncData::DeleteMessages { msgs } => self.sync_message_deletion(msgs).await, + SyncData::RejectIncomingCall { msg } => self.sync_call_rejection(msg).await, }, SyncDataOrUnknown::Unknown(data) => { warn!(self, "Ignored unknown sync item: {data}."); From 96dd832ad952004275004330ceffca28267da13e Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Mon, 10 Mar 2025 21:04:44 +0100 Subject: [PATCH 02/21] do not notify the call with a normal notification --- src/calls.rs | 3 +++ src/receive_imf.rs | 14 ++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/calls.rs b/src/calls.rs index ef282bd235..ba3ebc76ff 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -111,6 +111,7 @@ impl Context { match mime_message.is_system_message { SystemMessage::IncomingCall => { let call = self.load_call_by_root_id(call_or_child_id).await?; + self.emit_msgs_changed(call.msg.chat_id, call_or_child_id); if call.incoming { self.emit_event(EventType::IncomingCall { msg_id: call.msg.id, @@ -119,6 +120,7 @@ impl Context { } SystemMessage::CallAccepted => { let call = self.load_call_by_child_id(call_or_child_id).await?; + self.emit_msgs_changed(call.msg.chat_id, call_or_child_id); if call.incoming { self.emit_event(EventType::IncomingCallAccepted { msg_id: call.msg.id, @@ -131,6 +133,7 @@ impl Context { } SystemMessage::CallEnded => { let call = self.load_call_by_child_id(call_or_child_id).await?; + self.emit_msgs_changed(call.msg.chat_id, call_or_child_id); self.emit_event(EventType::CallEnded { msg_id: call.msg.id, }); diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 729afc72b5..a3a0db34e1 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -989,7 +989,12 @@ pub(crate) async fn receive_imf_inner( } } - if received_msg.hidden { + if mime_parser.is_system_message == SystemMessage::IncomingCall + || mime_parser.is_system_message == SystemMessage::CallAccepted + || mime_parser.is_system_message == SystemMessage::CallEnded + { + context.handle_call_msg(&mime_parser, insert_msg_id).await?; + } else if received_msg.hidden { // No need to emit an event about the changed message } else if let Some(replace_chat_id) = replace_chat_id { context.emit_msgs_changed_without_msg_id(replace_chat_id); @@ -1001,13 +1006,6 @@ pub(crate) async fn receive_imf_inner( } context.new_msgs_notify.notify_one(); - if mime_parser.is_system_message == SystemMessage::IncomingCall - || mime_parser.is_system_message == SystemMessage::CallAccepted - || mime_parser.is_system_message == SystemMessage::CallEnded - { - context.handle_call_msg(&mime_parser, insert_msg_id).await?; - } - mime_parser .handle_reports(context, from_id, &mime_parser.parts) .await; From b8696dc0af8fa296f1444e05691ec1286b62e106 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Wed, 12 Mar 2025 14:04:24 +0100 Subject: [PATCH 03/21] do not emit IncomingCall in case the call is stale --- src/calls.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/calls.rs b/src/calls.rs index ba3ebc76ff..f9c033fa4f 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -12,10 +12,22 @@ use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; use crate::sync::SyncData; +use crate::tools::time; use anyhow::{anyhow, ensure, Result}; +/// How long callee's or caller's phone ring. +/// +/// For the callee, this is to prevent endless ringing +/// in case the initial "call" is received, but then the caller went offline. +/// Moreover, this prevents outdated calls to ring +/// in case the initial "call" message arrives delayed. +/// +/// For the caller, this means they should also not wait longer, +/// as the callee won't start the call afterwards. +const RINGING_SECONDS: i64 = 60; + /// Information about the status of a call. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct CallInfo { /// Incoming our outgoing call? pub incoming: bool, @@ -29,6 +41,12 @@ pub struct CallInfo { pub msg: Message, } +impl CallInfo { + fn is_stale_call(&self) -> bool { + time() > self.msg.timestamp_sent + RINGING_SECONDS + } +} + impl Context { /// Start an outgoing call. pub async fn place_outgoing_call(&self, chat_id: ChatId) -> Result { @@ -112,7 +130,7 @@ impl Context { SystemMessage::IncomingCall => { let call = self.load_call_by_root_id(call_or_child_id).await?; self.emit_msgs_changed(call.msg.chat_id, call_or_child_id); - if call.incoming { + if call.incoming && !call.is_stale_call() { self.emit_event(EventType::IncomingCall { msg_id: call.msg.id, }); @@ -399,4 +417,29 @@ mod tests { Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_is_stale_call() -> Result<()> { + // a call started now is not stale + let call_info = CallInfo { + msg: Message { + timestamp_sent: time(), + ..Default::default() + }, + ..Default::default() + }; + assert!(!call_info.is_stale_call()); + + // a call started one hour ago is clearly stale + let call_info = CallInfo { + msg: Message { + timestamp_sent: time() - 3600, + ..Default::default() + }, + ..Default::default() + }; + assert!(call_info.is_stale_call()); + + Ok(()) + } } From c87743611665e2a05beca371c6d74dd77dd6ea5d Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Wed, 12 Mar 2025 14:10:55 +0100 Subject: [PATCH 04/21] clearify scope of events --- deltachat-ffi/deltachat.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index e4979e3ac0..420d1aac7b 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -6726,16 +6726,25 @@ void dc_event_unref(dc_event_t* event); /** * The callee accepted an incoming call on another another device using dc_accept_incoming_call(). * The caller gets the event #DC_EVENT_OUTGOING_CALL_ACCEPTED at the same time. + * + * The event is sent unconditionally when the corresponding message is received. + * UI should only take action in case call UI was opened before, otherwise the event should be ignored. */ #define DC_EVENT_INCOMING_CALL_ACCEPTED 2560 /** * A call placed using dc_place_outgoing_call() was accepted by the callee using dc_accept_incoming_call(). + * + * The event is sent unconditionally when the corresponding message is received. + * UI should only take action in case call UI was opened before, otherwise the event should be ignored. */ #define DC_EVENT_OUTGOING_CALL_ACCEPTED 2570 /** * An incoming or outgoing call was ended using dc_end_call(). + * + * The event is sent unconditionally when the corresponding message is received. + * UI should only take action in case call UI was opened before, otherwise the event should be ignored. */ #define DC_EVENT_CALL_ENDED 2580 From a484325095eb5315c131ba86d9a93269b9540f1e Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Wed, 12 Mar 2025 15:41:36 +0100 Subject: [PATCH 05/21] check for remaining ringing seconds --- src/calls.rs | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/calls.rs b/src/calls.rs index f9c033fa4f..33f0c38694 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -14,6 +14,9 @@ use crate::param::Param; use crate::sync::SyncData; use crate::tools::time; use anyhow::{anyhow, ensure, Result}; +use std::time::Duration; +use tokio::task; +use tokio::time::sleep; /// How long callee's or caller's phone ring. /// @@ -43,7 +46,18 @@ pub struct CallInfo { impl CallInfo { fn is_stale_call(&self) -> bool { - time() > self.msg.timestamp_sent + RINGING_SECONDS + self.remaining_ring_seconds() <= 0 + } + + fn remaining_ring_seconds(&self) -> i64 { + let mut remaining_seconds = self.msg.timestamp_sent + RINGING_SECONDS - time(); + if remaining_seconds < 0 { + remaining_seconds = 0; + } + if remaining_seconds > RINGING_SECONDS { + remaining_seconds = RINGING_SECONDS; + } + return remaining_seconds; } } @@ -121,6 +135,10 @@ impl Context { Ok(()) } + async fn emit_end_call_if_unaccepted(wait: u64) { + sleep(Duration::from_secs(wait)).await; + } + pub(crate) async fn handle_call_msg( &self, mime_message: &MimeMessage, @@ -134,6 +152,8 @@ impl Context { self.emit_event(EventType::IncomingCall { msg_id: call.msg.id, }); + let wait = call.remaining_ring_seconds(); + task::spawn(Context::emit_end_call_if_unaccepted(wait.try_into()?)); } } SystemMessage::CallAccepted => { @@ -429,6 +449,18 @@ mod tests { ..Default::default() }; assert!(!call_info.is_stale_call()); + assert_eq!(call_info.remaining_ring_seconds(), RINGING_SECONDS); + + // call started 5 seconds ago, this is not stale as well + let call_info = CallInfo { + msg: Message { + timestamp_sent: time() - 5, + ..Default::default() + }, + ..Default::default() + }; + assert!(!call_info.is_stale_call()); + assert_eq!(call_info.remaining_ring_seconds(), RINGING_SECONDS - 5); // a call started one hour ago is clearly stale let call_info = CallInfo { @@ -439,6 +471,7 @@ mod tests { ..Default::default() }; assert!(call_info.is_stale_call()); + assert_eq!(call_info.remaining_ring_seconds(), 0); Ok(()) } From 1cbafd54675d99e3b50f01164138a3cb111b3b8f Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Wed, 12 Mar 2025 16:07:38 +0100 Subject: [PATCH 06/21] emit end-call after timeout --- src/calls.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/calls.rs b/src/calls.rs index 33f0c38694..1a0a228bdb 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -135,8 +135,19 @@ impl Context { Ok(()) } - async fn emit_end_call_if_unaccepted(wait: u64) { + async fn emit_end_call_if_unaccepted( + context: Context, + wait: u64, + call_id: MsgId, + ) -> Result<()> { sleep(Duration::from_secs(wait)).await; + let call = context.load_call_by_root_id(call_id).await?; + if !call.accepted { + context.emit_event(EventType::CallEnded { + msg_id: call.msg.id, + }); + } + Ok(()) } pub(crate) async fn handle_call_msg( @@ -153,7 +164,11 @@ impl Context { msg_id: call.msg.id, }); let wait = call.remaining_ring_seconds(); - task::spawn(Context::emit_end_call_if_unaccepted(wait.try_into()?)); + task::spawn(Context::emit_end_call_if_unaccepted( + self.clone(), + wait.try_into()?, + call.msg.id, + )); } } SystemMessage::CallAccepted => { From ff8e73ef87ac9edca4466b37d7a2d73e9f656254 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Wed, 12 Mar 2025 16:13:32 +0100 Subject: [PATCH 07/21] factor out mark_call_as_accepted --- src/calls.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/calls.rs b/src/calls.rs index 1a0a228bdb..a824695570 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -87,10 +87,7 @@ impl Context { chat.id.accept(self).await?; } - // mark call as "accepted" (this is just Param::Arg atm) - let mut msg = call.msg.clone(); - msg.param.set_int(Param::Arg, 1); - msg.update_param(self).await?; + call.msg.clone().mark_call_as_accepted(self).await?; // send an acceptance message around: to the caller as well as to the other devices of the callee let mut msg = Message { @@ -234,6 +231,14 @@ impl Context { } } +impl Message { + async fn mark_call_as_accepted(&mut self, context: &Context) -> Result<()> { + self.param.set_int(Param::Arg, 1); + self.update_param(context).await?; + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; From ec9c51250270f7bcc3565d381c3b9f4c12218f3d Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Wed, 12 Mar 2025 16:22:24 +0100 Subject: [PATCH 08/21] end outgoing calls after one minute --- src/calls.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/calls.rs b/src/calls.rs index a824695570..0220ce7f29 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -67,14 +67,22 @@ impl Context { let chat = Chat::load_from_db(self, chat_id).await?; ensure!(chat.typ == Chattype::Single && !chat.is_self_talk()); - let mut msg = Message { + let mut call = Message { viewtype: Viewtype::Text, text: "Calling...".into(), ..Default::default() }; - msg.param.set_cmd(SystemMessage::OutgoingCall); - msg.id = send_msg(self, chat_id, &mut msg).await?; - Ok(msg.id) + call.param.set_cmd(SystemMessage::OutgoingCall); + call.id = send_msg(self, chat_id, &mut call).await?; + + let wait = RINGING_SECONDS; + task::spawn(Context::emit_end_call_if_unaccepted( + self.clone(), + wait.try_into()?, + call.id, + )); + + Ok(call.id) } /// Accept an incoming call. @@ -176,6 +184,7 @@ impl Context { msg_id: call.msg.id, }); } else { + call.msg.clone().mark_call_as_accepted(self).await?; self.emit_event(EventType::OutgoingCallAccepted { msg_id: call.msg.id, }); From 1f325efbcdb0af0ce02cb0adf78a62e31e625937 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Wed, 12 Mar 2025 16:52:05 +0100 Subject: [PATCH 09/21] test call acceptance --- src/calls.rs | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/calls.rs b/src/calls.rs index 0220ce7f29..af6baef857 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -37,7 +37,6 @@ pub struct CallInfo { /// Was an incoming call accepted on this device? /// On other devices, this is never set and for outgoing calls, this is never set. - /// Internally, this is written to Param::Arg. pub accepted: bool, /// Info message referring to the call. @@ -234,7 +233,7 @@ impl Context { Ok(CallInfo { incoming: call.get_info_type() == SystemMessage::IncomingCall, - accepted: call.param.get_int(Param::Arg) == Some(1), + accepted: call.is_call_accepted()?, msg: call, }) } @@ -242,10 +241,22 @@ impl Context { impl Message { async fn mark_call_as_accepted(&mut self, context: &Context) -> Result<()> { + ensure!( + self.get_info_type() == SystemMessage::IncomingCall + || self.get_info_type() == SystemMessage::OutgoingCall + ); self.param.set_int(Param::Arg, 1); self.update_param(context).await?; Ok(()) } + + fn is_call_accepted(&self) -> Result { + ensure!( + self.get_info_type() == SystemMessage::IncomingCall + || self.get_info_type() == SystemMessage::OutgoingCall + ); + Ok(self.param.get_int(Param::Arg) == Some(1)) + } } #[cfg(test)] @@ -504,4 +515,20 @@ mod tests { Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_mark_call_as_accepted() -> Result<()> { + let (alice, _alice2, alice_call, _bob, _bob2, _bob_call, _bob2_call) = setup_call().await?; + assert!(!alice_call.is_call_accepted()?); + + let mut alice_call = Message::load_from_db(&alice, alice_call.id).await?; + assert!(!alice_call.is_call_accepted()?); + alice_call.mark_call_as_accepted(&alice).await?; + assert!(alice_call.is_call_accepted()?); + + let alice_call = Message::load_from_db(&alice, alice_call.id).await?; + assert!(alice_call.is_call_accepted()?); + + Ok(()) + } } From 15ba54732d4a8f07ba8289fbc06a9509423ca335 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Wed, 12 Mar 2025 17:01:58 +0100 Subject: [PATCH 10/21] make clippy happy --- src/calls.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/calls.rs b/src/calls.rs index af6baef857..79785fb35e 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -49,14 +49,8 @@ impl CallInfo { } fn remaining_ring_seconds(&self) -> i64 { - let mut remaining_seconds = self.msg.timestamp_sent + RINGING_SECONDS - time(); - if remaining_seconds < 0 { - remaining_seconds = 0; - } - if remaining_seconds > RINGING_SECONDS { - remaining_seconds = RINGING_SECONDS; - } - return remaining_seconds; + let remaining_seconds = self.msg.timestamp_sent + RINGING_SECONDS - time(); + remaining_seconds.clamp(0, RINGING_SECONDS) } } From 7fb97882776c428adb5ba78d0378eff4340e00aa Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Wed, 12 Mar 2025 18:13:06 +0100 Subject: [PATCH 11/21] update docs --- deltachat-ffi/deltachat.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 420d1aac7b..48945085f0 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1233,8 +1233,8 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c * - callee rejects using dc_end_call(), caller receives #DC_EVENT_CALL_ENDED, * callee's other devices receive #DC_EVENT_CALL_ENDED, done. * - caller cancels the call using dc_end_call(), callee receives #DC_EVENT_CALL_ENDED, done. - * - after 2 minutes, caller and callee both should end the call. - * this is to prevent endless ringing of callee + * - after 1 minute without action, caller and callee receive #DC_EVENT_CALL_ENDED + * to prevent endless ringing of callee * in case caller got offline without being able to send cancellation message. * * Actions during the call: From ffe16caf7829e72d65c97e598a06e6831372e075 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Wed, 12 Mar 2025 22:27:13 +0100 Subject: [PATCH 12/21] clarify what should happen on multiple calls --- deltachat-ffi/deltachat.h | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 48945085f0..0423009fa1 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1228,12 +1228,15 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c * who will get informed by an #DC_EVENT_INCOMING_CALL event and rings. * * Possible actions during ringing: + * - caller cancels the call using dc_end_call(), callee receives #DC_EVENT_CALL_ENDED. * - callee accepts using dc_accept_incoming_call(), caller receives #DC_EVENT_OUTGOING_CALL_ACCEPTED, * callee's devices receive #DC_EVENT_INCOMING_CALL_ACCEPTED, call starts * - callee rejects using dc_end_call(), caller receives #DC_EVENT_CALL_ENDED, - * callee's other devices receive #DC_EVENT_CALL_ENDED, done. - * - caller cancels the call using dc_end_call(), callee receives #DC_EVENT_CALL_ENDED, done. - * - after 1 minute without action, caller and callee receive #DC_EVENT_CALL_ENDED + * callee's other devices receive #DC_EVENT_CALL_ENDED. + * - callee is already in a call. in this case, + * UI may decide to show a notification instead of ringing. + * otherwise, this is same as timeout. + * - timeout: after 1 minute without action, caller and callee receive #DC_EVENT_CALL_ENDED * to prevent endless ringing of callee * in case caller got offline without being able to send cancellation message. * @@ -1246,6 +1249,9 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c * In the UI, the sorted chatlist is used as an overview about calls as well as messages. * To place a call with a contact that has no chat yet, use dc_create_chat_by_contact_id() first. * + * UI will usually allow only one call at the same time, + * this has to be tracked by UI across profile, the core does not track this. + * * @memberof dc_context_t * @param context The context object. * @param chat_id The chat to place a call for. @@ -6712,14 +6718,20 @@ void dc_event_unref(dc_event_t* event); /** * Incoming call. - * UI will usually start ringing. + * UI will usually start ringing, + * or show a notification if there is already a call in some profile. + * + * Together with this event, + * an info-message is added to the corresponding chat. + * The info-message, however, is _not_ additionally notified using #DC_EVENT_INCOMING_MSG, + * if needed, this has to be done by the UI explicitly. * * If user takes action, dc_accept_incoming_call() or dc_end_call() should be called. * * Otherwise, ringing should end on #DC_EVENT_CALL_ENDED * or #DC_EVENT_INCOMING_CALL_ACCEPTED * - * @param data1 (int) msg_id + * @param data1 (int) msg_id ID of the info-message referring to the call, */ #define DC_EVENT_INCOMING_CALL 2550 From 0c198bcc398df02b7ac34094e78ff37a941184b3 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Thu, 13 Mar 2025 15:01:57 +0100 Subject: [PATCH 13/21] notify about missed calls --- src/calls.rs | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/calls.rs b/src/calls.rs index 79785fb35e..294aac5ddb 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -156,17 +156,23 @@ impl Context { match mime_message.is_system_message { SystemMessage::IncomingCall => { let call = self.load_call_by_root_id(call_or_child_id).await?; - self.emit_msgs_changed(call.msg.chat_id, call_or_child_id); - if call.incoming && !call.is_stale_call() { - self.emit_event(EventType::IncomingCall { - msg_id: call.msg.id, - }); - let wait = call.remaining_ring_seconds(); - task::spawn(Context::emit_end_call_if_unaccepted( - self.clone(), - wait.try_into()?, - call.msg.id, - )); + if call.incoming { + if call.is_stale_call() { + self.emit_incoming_msg(call.msg.chat_id, call_or_child_id); + } else { + self.emit_msgs_changed(call.msg.chat_id, call_or_child_id); + self.emit_event(EventType::IncomingCall { + msg_id: call.msg.id, + }); + let wait = call.remaining_ring_seconds(); + task::spawn(Context::emit_end_call_if_unaccepted( + self.clone(), + wait.try_into()?, + call.msg.id, + )); + } + } else { + self.emit_msgs_changed(call.msg.chat_id, call_or_child_id); } } SystemMessage::CallAccepted => { From c5a1659384ca469ba7371b4d96a9a7276683caf7 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Thu, 13 Mar 2025 18:51:38 +0100 Subject: [PATCH 14/21] update call info message for missed calls --- src/calls.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/calls.rs b/src/calls.rs index 294aac5ddb..49758a1ffa 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -8,7 +8,7 @@ use crate::chat::{send_msg, Chat, ChatId}; use crate::constants::Chattype; use crate::context::Context; use crate::events::EventType; -use crate::message::{rfc724_mid_exists, Message, MsgId, Viewtype}; +use crate::message::{self, rfc724_mid_exists, Message, MsgId, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; use crate::sync::SyncData; @@ -52,6 +52,17 @@ impl CallInfo { let remaining_seconds = self.msg.timestamp_sent + RINGING_SECONDS - time(); remaining_seconds.clamp(0, RINGING_SECONDS) } + + async fn update_text(&self, context: &Context, text: &str) -> Result<()> { + context + .sql + .execute( + "UPDATE msgs SET txt=?, txt_normalized=? WHERE id=?;", + (text, message::normalize_text(text), self.msg.id), + ) + .await?; + Ok(()) + } } impl Context { @@ -158,6 +169,7 @@ impl Context { let call = self.load_call_by_root_id(call_or_child_id).await?; if call.incoming { if call.is_stale_call() { + call.update_text(self, "Missed call").await?; self.emit_incoming_msg(call.msg.chat_id, call_or_child_id); } else { self.emit_msgs_changed(call.msg.chat_id, call_or_child_id); @@ -531,4 +543,17 @@ mod tests { Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_udpate_call_text() -> Result<()> { + let (alice, _alice2, alice_call, _bob, _bob2, _bob_call, _bob2_call) = setup_call().await?; + + let call_info = alice.load_call_by_root_id(alice_call.id).await?; + call_info.update_text(&alice, "foo bar").await?; + + let alice_call = Message::load_from_db(&alice, alice_call.id).await?; + assert_eq!(alice_call.get_text(), "foo bar"); + + Ok(()) + } } From 13c90c5a999a9eb2f90802536daa064669312fcc Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Fri, 14 Mar 2025 19:09:18 +0100 Subject: [PATCH 15/21] show all call state in the same info-message --- src/calls.rs | 15 +++------------ src/receive_imf.rs | 22 ++++++++++++++++++---- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/calls.rs b/src/calls.rs index 49758a1ffa..10ff250a86 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -13,7 +13,7 @@ use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; use crate::sync::SyncData; use crate::tools::time; -use anyhow::{anyhow, ensure, Result}; +use anyhow::{ensure, Result}; use std::time::Duration; use tokio::task; use tokio::time::sleep; @@ -188,7 +188,7 @@ impl Context { } } SystemMessage::CallAccepted => { - let call = self.load_call_by_child_id(call_or_child_id).await?; + let call = self.load_call_by_root_id(call_or_child_id).await?; self.emit_msgs_changed(call.msg.chat_id, call_or_child_id); if call.incoming { self.emit_event(EventType::IncomingCallAccepted { @@ -202,7 +202,7 @@ impl Context { } } SystemMessage::CallEnded => { - let call = self.load_call_by_child_id(call_or_child_id).await?; + let call = self.load_call_by_root_id(call_or_child_id).await?; self.emit_msgs_changed(call.msg.chat_id, call_or_child_id); self.emit_event(EventType::CallEnded { msg_id: call.msg.id, @@ -228,15 +228,6 @@ impl Context { self.load_call_by_message(call) } - async fn load_call_by_child_id(&self, child_id: MsgId) -> Result { - let child = Message::load_from_db(self, child_id).await?; - if let Some(call) = child.parent(self).await? { - self.load_call_by_message(call) - } else { - Err(anyhow!("Call parent missing")) - } - } - fn load_call_by_message(&self, call: Message) -> Result { ensure!( call.get_info_type() == SystemMessage::IncomingCall diff --git a/src/receive_imf.rs b/src/receive_imf.rs index a3a0db34e1..ca67bc3936 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -989,10 +989,7 @@ pub(crate) async fn receive_imf_inner( } } - if mime_parser.is_system_message == SystemMessage::IncomingCall - || mime_parser.is_system_message == SystemMessage::CallAccepted - || mime_parser.is_system_message == SystemMessage::CallEnded - { + if mime_parser.is_system_message == SystemMessage::IncomingCall { context.handle_call_msg(&mime_parser, insert_msg_id).await?; } else if received_msg.hidden { // No need to emit an event about the changed message @@ -1985,6 +1982,23 @@ async fn add_parts( handle_edit_delete(context, mime_parser, from_id).await?; + if mime_parser.is_system_message == SystemMessage::CallAccepted + || mime_parser.is_system_message == SystemMessage::CallEnded + { + // TODO: chat_id = DC_CHAT_ID_TRASH; + if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) { + if let Some(call) = + message::get_by_rfc724_mids(context, &parse_message_ids(field)).await? + { + context.handle_call_msg(mime_parser, call.get_id()).await?; + } else { + warn!(context, "Call: Cannot load parent.") + } + } else { + warn!(context, "Call: Not a reply.") + } + } + let is_reaction = mime_parser.parts.iter().any(|part| part.is_reaction); let hidden = is_reaction; let mut parts = mime_parser.parts.iter().peekable(); From b5277554683fc474cd09da170405c71f64b84d42 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Tue, 8 Apr 2025 15:36:04 +0200 Subject: [PATCH 16/21] adapt to new info_contact_id api --- src/message.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/message.rs b/src/message.rs index e385ce9d5c..319b0cfad3 100644 --- a/src/message.rs +++ b/src/message.rs @@ -972,6 +972,10 @@ impl Message { | SystemMessage::WebxdcStatusUpdate | SystemMessage::WebxdcInfoMessage | SystemMessage::IrohNodeAddr + | SystemMessage::OutgoingCall + | SystemMessage::IncomingCall + | SystemMessage::CallAccepted + | SystemMessage::CallEnded | SystemMessage::Unknown => Ok(None), } } From 5222e69cf27a56292e68d2302e0d1b47b3a2fb83 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Mon, 16 Jun 2025 14:52:47 +0200 Subject: [PATCH 17/21] document msg_id for all call events --- deltachat-ffi/deltachat.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 0423009fa1..4d829fcc6a 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -6731,7 +6731,7 @@ void dc_event_unref(dc_event_t* event); * Otherwise, ringing should end on #DC_EVENT_CALL_ENDED * or #DC_EVENT_INCOMING_CALL_ACCEPTED * - * @param data1 (int) msg_id ID of the info-message referring to the call, + * @param data1 (int) msg_id ID of the info-message referring to the call */ #define DC_EVENT_INCOMING_CALL 2550 @@ -6741,6 +6741,8 @@ void dc_event_unref(dc_event_t* event); * * The event is sent unconditionally when the corresponding message is received. * UI should only take action in case call UI was opened before, otherwise the event should be ignored. + * + * @param data1 (int) msg_id ID of the info-message referring to the call */ #define DC_EVENT_INCOMING_CALL_ACCEPTED 2560 @@ -6749,6 +6751,8 @@ void dc_event_unref(dc_event_t* event); * * The event is sent unconditionally when the corresponding message is received. * UI should only take action in case call UI was opened before, otherwise the event should be ignored. + * + * @param data1 (int) msg_id ID of the info-message referring to the call */ #define DC_EVENT_OUTGOING_CALL_ACCEPTED 2570 @@ -6757,6 +6761,8 @@ void dc_event_unref(dc_event_t* event); * * The event is sent unconditionally when the corresponding message is received. * UI should only take action in case call UI was opened before, otherwise the event should be ignored. + * + * @param data1 (int) msg_id ID of the info-message referring to the call */ #define DC_EVENT_CALL_ENDED 2580 From 5b043e2bb20ca0b7c21f6227989bae5a72710dbc Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Mon, 16 Jun 2025 15:39:07 +0200 Subject: [PATCH 18/21] set WebrtcRoom for calls --- deltachat-ffi/deltachat.h | 5 ++++- src/calls.rs | 27 +++++++++++++++++++++++++-- src/message.rs | 6 ++---- src/mimefactory.rs | 3 +++ src/mimeparser.rs | 18 ++++++++++-------- 5 files changed, 44 insertions(+), 15 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 4d829fcc6a..aea2b640b3 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1244,6 +1244,8 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c * - callee ends the call using dc_end_call(), caller receives #DC_EVENT_CALL_ENDED * - caller ends the call using dc_end_call(), callee receives #DC_EVENT_CALL_ENDED * + * The call URL is avauilable at dc_msg_get_videochat_url(). + * * Note, that the events are for updating the call screen, * possible status messages are added and updated as usual, including the known events. * In the UI, the sorted chatlist is used as an overview about calls as well as messages. @@ -6731,7 +6733,8 @@ void dc_event_unref(dc_event_t* event); * Otherwise, ringing should end on #DC_EVENT_CALL_ENDED * or #DC_EVENT_INCOMING_CALL_ACCEPTED * - * @param data1 (int) msg_id ID of the info-message referring to the call + * @param data1 (int) msg_id ID of the info-message referring to the call. + * The call URL is avauilable at dc_msg_get_videochat_url(). */ #define DC_EVENT_INCOMING_CALL 2550 diff --git a/src/calls.rs b/src/calls.rs index 10ff250a86..f4732f60e5 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -5,6 +5,7 @@ //! So, no database changes are needed at this stage. //! When it comes to relay calls over iroh, we may need a dedicated table, and this may change. use crate::chat::{send_msg, Chat, ChatId}; +use crate::config::Config; use crate::constants::Chattype; use crate::context::Context; use crate::events::EventType; @@ -12,8 +13,8 @@ use crate::message::{self, rfc724_mid_exists, Message, MsgId, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; use crate::sync::SyncData; -use crate::tools::time; -use anyhow::{ensure, Result}; +use crate::tools::{create_id, time}; +use anyhow::{bail, ensure, Result}; use std::time::Duration; use tokio::task; use tokio::time::sleep; @@ -71,12 +72,24 @@ impl Context { let chat = Chat::load_from_db(self, chat_id).await?; ensure!(chat.typ == Chattype::Single && !chat.is_self_talk()); + let instance = if let Some(instance) = self.get_config(Config::WebrtcInstance).await? { + if !instance.is_empty() { + instance + } else { + bail!("webrtc_instance is empty"); + } + } else { + bail!("webrtc_instance not set"); + }; + let instance = Message::create_webrtc_instance(&instance, &create_id()); + let mut call = Message { viewtype: Viewtype::Text, text: "Calling...".into(), ..Default::default() }; call.param.set_cmd(SystemMessage::OutgoingCall); + call.param.set(Param::WebrtcRoom, &instance); call.id = send_msg(self, chat_id, &mut call).await?; let wait = RINGING_SECONDS; @@ -284,6 +297,8 @@ mod tests { let bob2 = tcm.bob().await; for t in [&alice, &alice2, &bob, &bob2] { t.set_config_bool(Config::SyncMsgs, true).await?; + t.set_config(Config::WebrtcInstance, Some("https://foo.bar")) + .await?; } // Alice creates a chat with Bob and places an outgoing call there. @@ -295,12 +310,17 @@ mod tests { assert_eq!(sent1.sender_msg_id, test_msg_id); assert!(alice_call.is_info()); assert_eq!(alice_call.get_info_type(), SystemMessage::OutgoingCall); + let alice_url = alice_call.get_videochat_url().unwrap(); + assert!(alice_url.starts_with("https://foo.bar/")); let info = alice.load_call_by_root_id(alice_call.id).await?; assert!(!info.accepted); let alice2_call = alice2.recv_msg(&sent1).await; assert!(alice2_call.is_info()); assert_eq!(alice2_call.get_info_type(), SystemMessage::OutgoingCall); + let alice2_url = alice2_call.get_videochat_url().unwrap(); + assert!(alice2_url.starts_with("https://foo.bar/")); + assert_eq!(alice_url, alice2_url); let info = alice2.load_call_by_root_id(alice2_call.id).await?; assert!(!info.accepted); @@ -312,6 +332,9 @@ mod tests { .await; assert!(bob_call.is_info()); assert_eq!(bob_call.get_info_type(), SystemMessage::IncomingCall); + let bob_url = bob_call.get_videochat_url().unwrap(); + assert!(bob_url.starts_with("https://foo.bar/")); + assert_eq!(alice_url, bob_url); let bob2_call = bob2.recv_msg(&sent1).await; assert!(bob2_call.is_info()); diff --git a/src/message.rs b/src/message.rs index 319b0cfad3..9f4b87524f 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1077,10 +1077,8 @@ impl Message { /// Returns videochat URL if the message is a videochat invitation. pub fn get_videochat_url(&self) -> Option { - if self.viewtype == Viewtype::VideochatInvitation { - if let Some(instance) = self.param.get(Param::WebrtcRoom) { - return Some(Message::parse_webrtc_instance(instance).1); - } + if let Some(instance) = self.param.get(Param::WebrtcRoom) { + return Some(Message::parse_webrtc_instance(instance).1); } None } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 0031344c30..ebf2e52bac 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1552,6 +1552,9 @@ impl MimeFactory { "Chat-Content", mail_builder::headers::raw::Raw::new("videochat-invitation").into(), )); + } + + if msg.param.exists(Param::WebrtcRoom) { headers.push(( "Chat-Webrtc-Room", mail_builder::headers::raw::Raw::new( diff --git a/src/mimeparser.rs b/src/mimeparser.rs index e6665a9d87..82cdd1d5e8 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -695,16 +695,18 @@ impl MimeMessage { } fn parse_videochat_headers(&mut self) { - if let Some(value) = self.get_header(HeaderDef::ChatContent) { - if value == "videochat-invitation" { - let instance = self - .get_header(HeaderDef::ChatWebrtcRoom) - .map(|s| s.to_string()); - if let Some(part) = self.parts.first_mut() { + let is_videochat_invite = + self.get_header(HeaderDef::ChatContent).unwrap_or_default() == "videochat-invitation"; + let instance = self + .get_header(HeaderDef::ChatWebrtcRoom) + .map(|s| s.to_string()); + if let Some(part) = self.parts.first_mut() { + // + if let Some(instance) = instance { + if is_videochat_invite { part.typ = Viewtype::VideochatInvitation; - part.param - .set(Param::WebrtcRoom, instance.unwrap_or_default()); } + part.param.set(Param::WebrtcRoom, instance); } } } From 9475c18a69c66ccbfa47c345689a1beb03e44ea7 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Wed, 18 Jun 2025 19:55:45 +0200 Subject: [PATCH 19/21] relay place_call_info and accept_call_info --- deltachat-ffi/deltachat.h | 14 ++- deltachat-ffi/src/lib.rs | 34 +++++-- deltachat-jsonrpc/src/api/types/events.rs | 33 +++++-- src/calls.rs | 109 +++++++++++++++------- src/events/payload.rs | 6 ++ src/headerdef.rs | 1 + src/message.rs | 6 +- src/mimefactory.rs | 11 +++ src/mimeparser.rs | 20 ++-- src/param.rs | 3 + 10 files changed, 176 insertions(+), 61 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index aea2b640b3..26e9ea0b86 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1244,8 +1244,6 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c * - callee ends the call using dc_end_call(), caller receives #DC_EVENT_CALL_ENDED * - caller ends the call using dc_end_call(), callee receives #DC_EVENT_CALL_ENDED * - * The call URL is avauilable at dc_msg_get_videochat_url(). - * * Note, that the events are for updating the call screen, * possible status messages are added and updated as usual, including the known events. * In the UI, the sorted chatlist is used as an overview about calls as well as messages. @@ -1258,9 +1256,11 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c * @param context The context object. * @param chat_id The chat to place a call for. * This needs to be a one-to-one chat. + * @param place_call_info any data that other devices receives + * in #DC_EVENT_INCOMING_CALL. * @return ID of the system message announcing the call. */ -uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t chat_id); +uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t chat_id, const char* place_call_info); /** @@ -1275,9 +1275,11 @@ uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t ch * @param msg_id The ID of the call to accept. * This is the ID reported by #DC_EVENT_INCOMING_CALL * and equal to the ID of the corresponding info message. + * @param accept_call_info any data that other devices receives + * in #DC_EVENT_OUTGOING_CALL_ACCEPTED or #DC_EVENT_INCOMING_CALL_ACCEPTED. * @return 1=success, 0=error */ - int dc_accept_incoming_call (dc_context_t* context, uint32_t msg_id); + int dc_accept_incoming_call (dc_context_t* context, uint32_t msg_id, const char* accept_call_info); /** @@ -6734,7 +6736,7 @@ void dc_event_unref(dc_event_t* event); * or #DC_EVENT_INCOMING_CALL_ACCEPTED * * @param data1 (int) msg_id ID of the info-message referring to the call. - * The call URL is avauilable at dc_msg_get_videochat_url(). + * @param data2 (char*) place_call_info, text passed to dc_place_outgoing_call() */ #define DC_EVENT_INCOMING_CALL 2550 @@ -6746,6 +6748,7 @@ void dc_event_unref(dc_event_t* event); * UI should only take action in case call UI was opened before, otherwise the event should be ignored. * * @param data1 (int) msg_id ID of the info-message referring to the call + * @param data2 (char*) accept_call_info, text passed to dc_place_outgoing_call() */ #define DC_EVENT_INCOMING_CALL_ACCEPTED 2560 @@ -6756,6 +6759,7 @@ void dc_event_unref(dc_event_t* event); * UI should only take action in case call UI was opened before, otherwise the event should be ignored. * * @param data1 (int) msg_id ID of the info-message referring to the call + * @param data2 (char*) accept_call_info, text passed to dc_accept_incoming_call() */ #define DC_EVENT_OUTGOING_CALL_ACCEPTED 2570 diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 0b4377d072..408213f57c 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -780,11 +780,22 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut | EventType::AccountsChanged | EventType::AccountsItemChanged | EventType::WebxdcRealtimeAdvertisementReceived { .. } - | EventType::IncomingCall { .. } - | EventType::IncomingCallAccepted { .. } - | EventType::OutgoingCallAccepted { .. } - | EventType::CallEnded { .. } - | EventType::EventChannelOverflow { .. } => ptr::null_mut(), + | EventType::IncomingCall { + place_call_info, .. + } => { + let data2 = place_call_info.to_c_string().unwrap_or_default(); + data2.into_raw() + } + EventType::IncomingCallAccepted { + accept_call_info, .. + } + | EventType::OutgoingCallAccepted { + accept_call_info, .. + } => { + let data2 = accept_call_info.to_c_string().unwrap_or_default(); + data2.into_raw() + } + EventType::CallEnded { .. } | EventType::EventChannelOverflow { .. } => ptr::null_mut(), EventType::ConfigureProgress { comment, .. } => { if let Some(comment) = comment { comment.to_c_string().unwrap_or_default().into_raw() @@ -1184,15 +1195,20 @@ pub unsafe extern "C" fn dc_init_webxdc_integration( } #[no_mangle] -pub unsafe extern "C" fn dc_place_outgoing_call(context: *mut dc_context_t, chat_id: u32) -> u32 { +pub unsafe extern "C" fn dc_place_outgoing_call( + context: *mut dc_context_t, + chat_id: u32, + place_call_info: *const libc::c_char, +) -> u32 { if context.is_null() || chat_id == 0 { eprintln!("ignoring careless call to dc_place_outgoing_call()"); return 0; } let ctx = &*context; let chat_id = ChatId::new(chat_id); + let place_call_info = to_string_lossy(place_call_info); - block_on(ctx.place_outgoing_call(chat_id)) + block_on(ctx.place_outgoing_call(chat_id, place_call_info)) .map(|msg_id| msg_id.to_u32()) .unwrap_or_log_default(ctx, "Failed to place call") } @@ -1201,6 +1217,7 @@ pub unsafe extern "C" fn dc_place_outgoing_call(context: *mut dc_context_t, chat pub unsafe extern "C" fn dc_accept_incoming_call( context: *mut dc_context_t, msg_id: u32, + accept_call_info: *const libc::c_char, ) -> libc::c_int { if context.is_null() || msg_id == 0 { eprintln!("ignoring careless call to dc_accept_incoming_call()"); @@ -1208,8 +1225,9 @@ pub unsafe extern "C" fn dc_accept_incoming_call( } let ctx = &*context; let msg_id = MsgId::new(msg_id); + let accept_call_info = to_string_lossy(accept_call_info); - block_on(ctx.accept_incoming_call(msg_id)).is_ok() as libc::c_int + block_on(ctx.accept_incoming_call(msg_id, accept_call_info)).is_ok() as libc::c_int } #[no_mangle] diff --git a/deltachat-jsonrpc/src/api/types/events.rs b/deltachat-jsonrpc/src/api/types/events.rs index 8344823fa7..0172099e07 100644 --- a/deltachat-jsonrpc/src/api/types/events.rs +++ b/deltachat-jsonrpc/src/api/types/events.rs @@ -419,14 +419,23 @@ pub enum EventType { }, /// Incoming call. - IncomingCall { msg_id: u32 }, + IncomingCall { + msg_id: u32, + place_call_info: String, + }, /// Incoming call accepted. /// This is esp. interesting to stop ringing on other devices. - IncomingCallAccepted { msg_id: u32 }, + IncomingCallAccepted { + msg_id: u32, + accept_call_info: String, + }, /// Outgoing call accepted. - OutgoingCallAccepted { msg_id: u32 }, + OutgoingCallAccepted { + msg_id: u32, + accept_call_info: String, + }, /// Call ended. CallEnded { msg_id: u32 }, @@ -580,14 +589,26 @@ impl From for EventType { CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n }, CoreEventType::AccountsChanged => AccountsChanged, CoreEventType::AccountsItemChanged => AccountsItemChanged, - CoreEventType::IncomingCall { msg_id } => IncomingCall { + CoreEventType::IncomingCall { + msg_id, + place_call_info, + } => IncomingCall { msg_id: msg_id.to_u32(), + place_call_info, }, - CoreEventType::IncomingCallAccepted { msg_id } => IncomingCallAccepted { + CoreEventType::IncomingCallAccepted { + msg_id, + accept_call_info, + } => IncomingCallAccepted { msg_id: msg_id.to_u32(), + accept_call_info, }, - CoreEventType::OutgoingCallAccepted { msg_id } => OutgoingCallAccepted { + CoreEventType::OutgoingCallAccepted { + msg_id, + accept_call_info, + } => OutgoingCallAccepted { msg_id: msg_id.to_u32(), + accept_call_info, }, CoreEventType::CallEnded { msg_id } => CallEnded { msg_id: msg_id.to_u32(), diff --git a/src/calls.rs b/src/calls.rs index f4732f60e5..8a67162d5e 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -5,16 +5,16 @@ //! So, no database changes are needed at this stage. //! When it comes to relay calls over iroh, we may need a dedicated table, and this may change. use crate::chat::{send_msg, Chat, ChatId}; -use crate::config::Config; use crate::constants::Chattype; use crate::context::Context; use crate::events::EventType; +use crate::headerdef::HeaderDef; use crate::message::{self, rfc724_mid_exists, Message, MsgId, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; use crate::sync::SyncData; -use crate::tools::{create_id, time}; -use anyhow::{bail, ensure, Result}; +use crate::tools::time; +use anyhow::{ensure, Result}; use std::time::Duration; use tokio::task; use tokio::time::sleep; @@ -40,6 +40,12 @@ pub struct CallInfo { /// On other devices, this is never set and for outgoing calls, this is never set. pub accepted: bool, + /// User-defined text as given to place_outgoing_call() + pub place_call_info: String, + + /// User-defined text as given to accept_incoming_call() + pub accept_call_info: String, + /// Info message referring to the call. pub msg: Message, } @@ -68,28 +74,21 @@ impl CallInfo { impl Context { /// Start an outgoing call. - pub async fn place_outgoing_call(&self, chat_id: ChatId) -> Result { + pub async fn place_outgoing_call( + &self, + chat_id: ChatId, + place_call_info: String, + ) -> Result { let chat = Chat::load_from_db(self, chat_id).await?; ensure!(chat.typ == Chattype::Single && !chat.is_self_talk()); - let instance = if let Some(instance) = self.get_config(Config::WebrtcInstance).await? { - if !instance.is_empty() { - instance - } else { - bail!("webrtc_instance is empty"); - } - } else { - bail!("webrtc_instance not set"); - }; - let instance = Message::create_webrtc_instance(&instance, &create_id()); - let mut call = Message { viewtype: Viewtype::Text, text: "Calling...".into(), ..Default::default() }; call.param.set_cmd(SystemMessage::OutgoingCall); - call.param.set(Param::WebrtcRoom, &instance); + call.param.set(Param::WebrtcRoom, &place_call_info); call.id = send_msg(self, chat_id, &mut call).await?; let wait = RINGING_SECONDS; @@ -103,7 +102,11 @@ impl Context { } /// Accept an incoming call. - pub async fn accept_incoming_call(&self, call_id: MsgId) -> Result<()> { + pub async fn accept_incoming_call( + &self, + call_id: MsgId, + accept_call_info: String, + ) -> Result<()> { let call: CallInfo = self.load_call_by_root_id(call_id).await?; ensure!(call.incoming); @@ -112,7 +115,10 @@ impl Context { chat.id.accept(self).await?; } - call.msg.clone().mark_call_as_accepted(self).await?; + call.msg + .clone() + .mark_call_as_accepted(self, accept_call_info.to_string()) + .await?; // send an acceptance message around: to the caller as well as to the other devices of the callee let mut msg = Message { @@ -121,10 +127,13 @@ impl Context { ..Default::default() }; msg.param.set_cmd(SystemMessage::CallAccepted); + msg.param + .set(Param::WebrtcAccepted, accept_call_info.to_string()); msg.set_quote(self, Some(&call.msg)).await?; msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?; self.emit_event(EventType::IncomingCallAccepted { msg_id: call.msg.id, + accept_call_info, }); Ok(()) } @@ -188,6 +197,7 @@ impl Context { self.emit_msgs_changed(call.msg.chat_id, call_or_child_id); self.emit_event(EventType::IncomingCall { msg_id: call.msg.id, + place_call_info: call.place_call_info.to_string(), }); let wait = call.remaining_ring_seconds(); task::spawn(Context::emit_end_call_if_unaccepted( @@ -206,11 +216,19 @@ impl Context { if call.incoming { self.emit_event(EventType::IncomingCallAccepted { msg_id: call.msg.id, + accept_call_info: call.accept_call_info, }); } else { - call.msg.clone().mark_call_as_accepted(self).await?; + let accept_call_info = mime_message + .get_header(HeaderDef::ChatWebrtcAccepted) + .unwrap_or_default(); + call.msg + .clone() + .mark_call_as_accepted(self, accept_call_info.to_string()) + .await?; self.emit_event(EventType::OutgoingCallAccepted { msg_id: call.msg.id, + accept_call_info: accept_call_info.to_string(), }); } } @@ -250,18 +268,33 @@ impl Context { Ok(CallInfo { incoming: call.get_info_type() == SystemMessage::IncomingCall, accepted: call.is_call_accepted()?, + place_call_info: call + .param + .get(Param::WebrtcRoom) + .unwrap_or_default() + .to_string(), + accept_call_info: call + .param + .get(Param::WebrtcAccepted) + .unwrap_or_default() + .to_string(), msg: call, }) } } impl Message { - async fn mark_call_as_accepted(&mut self, context: &Context) -> Result<()> { + async fn mark_call_as_accepted( + &mut self, + context: &Context, + accept_call_info: String, + ) -> Result<()> { ensure!( self.get_info_type() == SystemMessage::IncomingCall || self.get_info_type() == SystemMessage::OutgoingCall ); self.param.set_int(Param::Arg, 1); + self.param.set(Param::WebrtcAccepted, accept_call_info); self.update_param(context).await?; Ok(()) } @@ -297,32 +330,29 @@ mod tests { let bob2 = tcm.bob().await; for t in [&alice, &alice2, &bob, &bob2] { t.set_config_bool(Config::SyncMsgs, true).await?; - t.set_config(Config::WebrtcInstance, Some("https://foo.bar")) - .await?; } // Alice creates a chat with Bob and places an outgoing call there. // Alice's other device sees the same message as an outgoing call. let alice_chat = alice.create_chat(&bob).await; - let test_msg_id = alice.place_outgoing_call(alice_chat.id).await?; + let test_msg_id = alice + .place_outgoing_call(alice_chat.id, "place_info".to_string()) + .await?; let sent1 = alice.pop_sent_msg().await; let alice_call = Message::load_from_db(&alice, sent1.sender_msg_id).await?; assert_eq!(sent1.sender_msg_id, test_msg_id); assert!(alice_call.is_info()); assert_eq!(alice_call.get_info_type(), SystemMessage::OutgoingCall); - let alice_url = alice_call.get_videochat_url().unwrap(); - assert!(alice_url.starts_with("https://foo.bar/")); let info = alice.load_call_by_root_id(alice_call.id).await?; assert!(!info.accepted); + assert_eq!(info.place_call_info, "place_info"); let alice2_call = alice2.recv_msg(&sent1).await; assert!(alice2_call.is_info()); assert_eq!(alice2_call.get_info_type(), SystemMessage::OutgoingCall); - let alice2_url = alice2_call.get_videochat_url().unwrap(); - assert!(alice2_url.starts_with("https://foo.bar/")); - assert_eq!(alice_url, alice2_url); let info = alice2.load_call_by_root_id(alice2_call.id).await?; assert!(!info.accepted); + assert_eq!(info.place_call_info, "place_info"); // Bob receives the message referring to the call on two devices; // it is an incoming call from the view of Bob @@ -332,13 +362,16 @@ mod tests { .await; assert!(bob_call.is_info()); assert_eq!(bob_call.get_info_type(), SystemMessage::IncomingCall); - let bob_url = bob_call.get_videochat_url().unwrap(); - assert!(bob_url.starts_with("https://foo.bar/")); - assert_eq!(alice_url, bob_url); + let info = bob.load_call_by_root_id(bob_call.id).await?; + assert!(!info.accepted); + assert_eq!(info.place_call_info, "place_info"); let bob2_call = bob2.recv_msg(&sent1).await; assert!(bob2_call.is_info()); assert_eq!(bob2_call.get_info_type(), SystemMessage::IncomingCall); + let info = bob2.load_call_by_root_id(bob2_call.id).await?; + assert!(!info.accepted); + assert_eq!(info.place_call_info, "place_info"); Ok((alice, alice2, alice_call, bob, bob2, bob_call, bob2_call)) } @@ -354,13 +387,16 @@ mod tests { let (alice, alice2, alice_call, bob, bob2, bob_call, bob2_call) = setup_call().await?; // Bob accepts the incoming call, this does not add an additional message to the chat - bob.accept_incoming_call(bob_call.id).await?; + bob.accept_incoming_call(bob_call.id, "accepted_info".to_string()) + .await?; bob.evtracker .get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. })) .await; let sent2 = bob.pop_sent_msg().await; let info = bob.load_call_by_root_id(bob_call.id).await?; assert!(info.accepted); + assert_eq!(info.place_call_info, "place_info"); + assert_eq!(info.accept_call_info, "accepted_info"); bob2.recv_msg(&sent2).await; bob2.evtracker @@ -375,12 +411,17 @@ mod tests { .evtracker .get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. })) .await; + let info = alice.load_call_by_root_id(alice_call.id).await?; + assert!(info.accepted); + assert_eq!(info.place_call_info, "place_info"); + assert_eq!(info.accept_call_info, "accepted_info"); alice2.recv_msg(&sent2).await; alice2 .evtracker .get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. })) .await; + Ok((alice, alice2, alice_call, bob, bob2, bob_call)) } @@ -549,7 +590,9 @@ mod tests { let mut alice_call = Message::load_from_db(&alice, alice_call.id).await?; assert!(!alice_call.is_call_accepted()?); - alice_call.mark_call_as_accepted(&alice).await?; + alice_call + .mark_call_as_accepted(&alice, "accepted_info".to_string()) + .await?; assert!(alice_call.is_call_accepted()?); let alice_call = Message::load_from_db(&alice, alice_call.id).await?; diff --git a/src/events/payload.rs b/src/events/payload.rs index 2bcd60bd76..ca5f67e3b0 100644 --- a/src/events/payload.rs +++ b/src/events/payload.rs @@ -380,18 +380,24 @@ pub enum EventType { IncomingCall { /// ID of the message referring to the call. msg_id: MsgId, + /// User-defined info as passed to place_outgoing_call() + place_call_info: String, }, /// Incoming call accepted. IncomingCallAccepted { /// ID of the message referring to the call. msg_id: MsgId, + /// User-defined info as passed to accept_incoming_call() + accept_call_info: String, }, /// Outgoing call accepted. OutgoingCallAccepted { /// ID of the message referring to the call. msg_id: MsgId, + /// User-defined info as passed to accept_incoming_call() + accept_call_info: String, }, /// Call ended. diff --git a/src/headerdef.rs b/src/headerdef.rs index 330a4d9ba0..1669c91c12 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -86,6 +86,7 @@ pub enum HeaderDef { ChatDispositionNotificationTo, ChatWebrtcRoom, + ChatWebrtcAccepted, /// This message deletes the messages listed in the value by rfc724_mid. ChatDelete, diff --git a/src/message.rs b/src/message.rs index 9f4b87524f..319b0cfad3 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1077,8 +1077,10 @@ impl Message { /// Returns videochat URL if the message is a videochat invitation. pub fn get_videochat_url(&self) -> Option { - if let Some(instance) = self.param.get(Param::WebrtcRoom) { - return Some(Message::parse_webrtc_instance(instance).1); + if self.viewtype == Viewtype::VideochatInvitation { + if let Some(instance) = self.param.get(Param::WebrtcRoom) { + return Some(Message::parse_webrtc_instance(instance).1); + } } None } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index ebf2e52bac..b0c9b84d92 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1565,6 +1565,17 @@ impl MimeFactory { ) .into(), )); + } else if msg.param.exists(Param::WebrtcAccepted) { + headers.push(( + "Chat-Webrtc-Accepted", + mail_builder::headers::raw::Raw::new( + msg.param + .get(Param::WebrtcAccepted) + .unwrap_or_default() + .to_string(), + ) + .into(), + )); } if msg.viewtype == Viewtype::Voice diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 82cdd1d5e8..2fdb14662a 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -695,18 +695,24 @@ impl MimeMessage { } fn parse_videochat_headers(&mut self) { - let is_videochat_invite = - self.get_header(HeaderDef::ChatContent).unwrap_or_default() == "videochat-invitation"; - let instance = self + let content = self + .get_header(HeaderDef::ChatContent) + .unwrap_or_default() + .to_string(); + let room = self .get_header(HeaderDef::ChatWebrtcRoom) .map(|s| s.to_string()); + let accepted = self + .get_header(HeaderDef::ChatWebrtcAccepted) + .map(|s| s.to_string()); if let Some(part) = self.parts.first_mut() { - // - if let Some(instance) = instance { - if is_videochat_invite { + if let Some(room) = room { + if content == "videochat-invitation" { part.typ = Viewtype::VideochatInvitation; } - part.param.set(Param::WebrtcRoom, instance); + part.param.set(Param::WebrtcRoom, room); + } else if let Some(accepted) = accepted { + part.param.set(Param::WebrtcAccepted, accepted); } } } diff --git a/src/param.rs b/src/param.rs index 9e0433a256..4cfecdf706 100644 --- a/src/param.rs +++ b/src/param.rs @@ -120,6 +120,9 @@ pub enum Param { /// For Messages WebrtcRoom = b'V', + /// For Messages + WebrtcAccepted = b'7', + /// For Messages: space-separated list of messaged IDs of forwarded copies. /// /// This is used when a [crate::message::Message] is in the From 3a626f9676904c519ec5b6f381e16c3aefab496f Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Thu, 19 Jun 2025 00:31:49 +0200 Subject: [PATCH 20/21] fix ffi --- deltachat-ffi/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 408213f57c..398052df9e 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -779,7 +779,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut | EventType::ChatlistChanged | EventType::AccountsChanged | EventType::AccountsItemChanged - | EventType::WebxdcRealtimeAdvertisementReceived { .. } + | EventType::WebxdcRealtimeAdvertisementReceived { .. } => ptr::null_mut(), | EventType::IncomingCall { place_call_info, .. } => { From e6605465d781af6d3e2feecb81bc5473bb1d3503 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Mon, 7 Jul 2025 12:50:46 +0200 Subject: [PATCH 21/21] make clippy happy --- deltachat-ffi/src/lib.rs | 2 +- src/calls.rs | 8 ++++---- src/mimefactory.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 398052df9e..d4a10568f0 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -780,7 +780,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut | EventType::AccountsChanged | EventType::AccountsItemChanged | EventType::WebxdcRealtimeAdvertisementReceived { .. } => ptr::null_mut(), - | EventType::IncomingCall { + EventType::IncomingCall { place_call_info, .. } => { let data2 = place_call_info.to_c_string().unwrap_or_default(); diff --git a/src/calls.rs b/src/calls.rs index 8a67162d5e..eaefae6b7f 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -4,17 +4,17 @@ //! This means, the "Call ID" is a "Message ID" currently - similar to webxdc. //! So, no database changes are needed at this stage. //! When it comes to relay calls over iroh, we may need a dedicated table, and this may change. -use crate::chat::{send_msg, Chat, ChatId}; +use crate::chat::{Chat, ChatId, send_msg}; use crate::constants::Chattype; use crate::context::Context; use crate::events::EventType; use crate::headerdef::HeaderDef; -use crate::message::{self, rfc724_mid_exists, Message, MsgId, Viewtype}; +use crate::message::{self, Message, MsgId, Viewtype, rfc724_mid_exists}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; use crate::sync::SyncData; use crate::tools::time; -use anyhow::{ensure, Result}; +use anyhow::{Result, ensure}; use std::time::Duration; use tokio::task; use tokio::time::sleep; @@ -312,7 +312,7 @@ impl Message { mod tests { use super::*; use crate::config::Config; - use crate::test_utils::{sync, TestContext, TestContextManager}; + use crate::test_utils::{TestContext, TestContextManager, sync}; async fn setup_call() -> Result<( TestContext, // Alice's 1st device diff --git a/src/mimefactory.rs b/src/mimefactory.rs index b0c9b84d92..2e92f88d3b 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeSet, HashSet}; use std::io::Cursor; -use anyhow::{anyhow, Context as _, Result, bail, ensure}; +use anyhow::{Context as _, Result, anyhow, bail, ensure}; use base64::Engine as _; use deltachat_contact_tools::sanitize_bidi_characters; use mail_builder::headers::HeaderType;