refine call states (#7179)

- sync declined calls from callee to caller, as usual in all larger
messengers
- introduce the call states "Missed call", "Declined call" and
"Cancelled all" ("Ended call" is gone)
- allow calling end_call()/accept_call() for already ended/accepted
calls, in practise, handling all cornercases is tricky in UI - and the
state needs anyways to be tracked.
- track and show the call duration

the duration calculation depends on local time, but it is displayed only
coarse and is not needed for any state. this can be improved as needed,
timestamps of the corresponding messages are probably better at some
point. or ending device sends its view of the time around. but for the
first throw, it seems good enough

if we finally want that set of states, it can be exposed to a json-info
in a subsequent call, so that the UI can render it more nicely. fallback
strings as follows will stay for now to make adaption in other UI easy,
and for debugging:

<img width="320" alt="IMG_0154"
src="https://github.com/user-attachments/assets/09a89bfb-66f4-4184-b05c-e8040b96cf44"
/>

successor of https://github.com/chatmail/core/pull/6650
This commit is contained in:
bjoern
2025-09-08 15:48:35 +02:00
committed by GitHub
parent b6ab13f1de
commit ab8aedf06e
9 changed files with 276 additions and 199 deletions

View File

@@ -1216,21 +1216,21 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
/**
* Start an outgoing call.
* This sends a message with all relevant information to the callee,
* This sends a message of type #DC_MSG_CALL with all relevant information to the callee,
* 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 receives #DC_EVENT_CALL_ENDED and has a "Missed Call"
*
* - 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 after 1 minute timeout.
* callee's other devices receive #DC_EVENT_CALL_ENDED
* - callee declines using dc_end_call():
* caller receives #DC_EVENT_CALL_ENDED and has a "Declinced Call".
* callee's other devices receive #DC_EVENT_CALL_ENDED and have a "Cancelled Call",
*
* - callee is already in a call:
* in this case, UI may decide to show a notification instead of ringing.
@@ -1240,7 +1240,9 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
* 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
* in case caller got offline without being able to send cancellation message.
* for caller, this is a "Cancelled Call";
* for callee, this is a "Missed Call"
*
* Actions during the call:
*
@@ -1276,13 +1278,15 @@ uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t ch
* All affected devices will receive
* either #DC_EVENT_OUTGOING_CALL_ACCEPTED or #DC_EVENT_INCOMING_CALL_ACCEPTED.
*
* If the call is already accepted or ended, nothing happens.
*
* @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 equals to the ID of the corresponding info message.
* @param accept_call_info any data that other devices receive
* in #DC_EVENT_OUTGOING_CALL_ACCEPTED or #DC_EVENT_INCOMING_CALL_ACCEPTED.
* in #DC_EVENT_OUTGOING_CALL_ACCEPTED.
* @return 1=success, 0=error
*/
int dc_accept_incoming_call (dc_context_t* context, uint32_t msg_id, const char* accept_call_info);
@@ -1291,17 +1295,13 @@ uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t ch
/**
* End incoming or outgoing call.
*
* From the view of the caller, a "cancellation",
* from the view of callee, a "rejection".
* For unaccepted calls ended by the caller, this is a "cancellation".
* Unaccepted calls ended by the callee are a "decline".
* 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.
* All participant devices get informed about the ended call via #DC_EVENT_CALL_ENDED.
*
* If the callee rejects, the caller will get a 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.
* If the call is already ended, nothing happens.
*
* @memberof dc_context_t
* @param context The context object.
@@ -6724,29 +6724,27 @@ void dc_event_unref(dc_event_t* event);
* 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.
* a message of type #DC_MSG_CALL is added to the corresponding chat;
* this message is announced and updated by the usual even as #DC_EVENT_MSGS_CHANGED.
*
* 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 ID of the info-message referring to the call.
* @param data1 (int) msg_id ID of the message referring to the call.
* @param data2 (char*) place_call_info, text passed to dc_place_outgoing_call()
*/
#define DC_EVENT_INCOMING_CALL 2550
/**
* The callee accepted an incoming call on another device using dc_accept_incoming_call().
* The callee accepted an incoming call on this or 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.
*
* @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()
* @param data1 (int) msg_id ID of the message referring to the call
*/
#define DC_EVENT_INCOMING_CALL_ACCEPTED 2560
@@ -6756,7 +6754,7 @@ 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
* @param data1 (int) msg_id ID of the 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
@@ -6768,7 +6766,7 @@ 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
* @param data1 (int) msg_id ID of the message referring to the call
*/
#define DC_EVENT_CALL_ENDED 2580

View File

@@ -779,6 +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::IncomingCallAccepted { .. }
| EventType::WebxdcRealtimeAdvertisementReceived { .. } => ptr::null_mut(),
EventType::IncomingCall {
place_call_info, ..
@@ -786,10 +787,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
let data2 = place_call_info.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::IncomingCallAccepted {
accept_call_info, ..
}
| EventType::OutgoingCallAccepted {
EventType::OutgoingCallAccepted {
accept_call_info, ..
} => {
let data2 = accept_call_info.to_c_string().unwrap_or_default();

View File

@@ -430,8 +430,6 @@ pub enum EventType {
IncomingCallAccepted {
/// ID of the info message referring to the call.
msg_id: u32,
/// User-defined info passed to dc_accept_incoming_call()
accept_call_info: String,
},
/// Outgoing call accepted.
@@ -604,12 +602,8 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
place_call_info,
},
CoreEventType::IncomingCallAccepted {
msg_id,
accept_call_info,
} => IncomingCallAccepted {
CoreEventType::IncomingCallAccepted { msg_id } => IncomingCallAccepted {
msg_id: msg_id.to_u32(),
accept_call_info,
},
CoreEventType::OutgoingCallAccepted {
msg_id,

View File

@@ -1,17 +1,17 @@
//! # Handle calls.
//!
//! Internally, calls are bound to the user-visible info message initializing the call.
//! This means, the "Call ID" is a "Message ID" currently - similar to webxdc.
//! Internally, calls are bound a user-visible message initializing the call.
//! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs.
use crate::chat::{Chat, ChatId, send_msg};
use crate::constants::Chattype;
use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::message::{self, Message, MsgId, Viewtype, rfc724_mid_exists};
use crate::log::info;
use crate::message::{self, Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
use crate::sync::SyncData;
use crate::tools::time;
use anyhow::{Result, ensure};
use std::time::Duration;
@@ -29,29 +29,30 @@ use tokio::time::sleep;
/// as the callee won't start the call afterwards.
const RINGING_SECONDS: i64 = 60;
/// For persisting parameters in the call, we use Param::Arg*
const CALL_ACCEPTED_TIMESTAMP: Param = Param::Arg;
const CALL_ENDED_TIMESTAMP: Param = Param::Arg4;
/// Information about the status of a call.
#[derive(Debug, Default)]
pub struct CallInfo {
/// Incoming or outgoing call?
pub is_incoming: bool,
/// Was an incoming call accepted on this device?
/// For privacy reasons, only for accepted incoming calls, callee sends a message to caller on `end_call()`.
/// On other devices and for outgoing calls, `is_accepted` is never set.
pub is_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.
/// Message referring to the call.
/// Data are persisted along with the message using Param::Arg*
pub msg: Message,
}
impl CallInfo {
fn is_stale_call(&self) -> bool {
fn is_incoming(&self) -> bool {
self.msg.from_id != ContactId::SELF
}
fn is_stale(&self) -> bool {
self.remaining_ring_seconds() <= 0
}
@@ -70,6 +71,60 @@ impl CallInfo {
.await?;
Ok(())
}
async fn update_text_duration(&self, context: &Context) -> Result<()> {
let minutes = self.get_duration_seconds() / 60;
let duration = match minutes {
0 => "<1 minute".to_string(),
1 => "1 minute".to_string(),
n => format!("{} minutes", n),
};
if self.is_incoming() {
self.update_text(context, &format!("Incoming call\n{duration}"))
.await?;
} else {
self.update_text(context, &format!("Outgoing call\n{duration}"))
.await?;
}
Ok(())
}
/// Mark calls as accepted.
/// This is needed for all devices where a stale-timer runs, to prevent accepted calls being terminated as stale.
async fn mark_as_accepted(&mut self, context: &Context) -> Result<()> {
self.msg.param.set_i64(CALL_ACCEPTED_TIMESTAMP, time());
self.msg.update_param(context).await?;
Ok(())
}
fn is_accepted(&self) -> bool {
self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP)
}
async fn mark_as_ended(&mut self, context: &Context) -> Result<()> {
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, time());
self.msg.update_param(context).await?;
Ok(())
}
fn is_ended(&self) -> bool {
self.msg.param.exists(CALL_ENDED_TIMESTAMP)
}
fn get_duration_seconds(&self) -> i64 {
if let (Some(start), Some(end)) = (
self.msg.param.get_i64(CALL_ACCEPTED_TIMESTAMP),
self.msg.param.get_i64(CALL_ENDED_TIMESTAMP),
) {
let seconds = end - start;
if seconds <= 0 {
return 1;
}
return seconds;
}
0
}
}
impl Context {
@@ -84,7 +139,7 @@ impl Context {
let mut call = Message {
viewtype: Viewtype::Call,
text: "Calling...".into(),
text: "Outgoing call".into(),
..Default::default()
};
call.param.set(Param::WebrtcRoom, &place_call_info);
@@ -107,22 +162,22 @@ impl Context {
accept_call_info: String,
) -> Result<()> {
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
ensure!(call.is_incoming);
ensure!(call.is_incoming());
if call.is_accepted() || call.is_ended() {
info!(self, "Call already accepted/ended");
return Ok(());
}
call.mark_as_accepted(self).await?;
let chat = Chat::load_from_db(self, call.msg.chat_id).await?;
if chat.is_contact_request() {
chat.id.accept(self).await?;
}
call.update_text(self, "Call accepted").await?;
call.msg
.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 {
viewtype: Viewtype::Text,
text: "Call accepted".into(),
text: "[Call accepted]".into(),
..Default::default()
};
msg.param.set_cmd(SystemMessage::CallAccepted);
@@ -133,36 +188,39 @@ impl Context {
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,
});
self.emit_msgs_changed(call.msg.chat_id, call_id);
Ok(())
}
/// Cancel, reject or hangup an incoming or outgoing call.
/// Cancel, decline or hangup an incoming or outgoing call.
pub async fn end_call(&self, call_id: MsgId) -> Result<()> {
let call: CallInfo = self.load_call_by_id(call_id).await?;
call.update_text(self, "Call ended").await?;
if call.is_accepted || !call.is_incoming {
let mut msg = Message {
viewtype: Viewtype::Text,
text: "Call ended".into(),
..Default::default()
};
msg.param.set_cmd(SystemMessage::CallEnded);
msg.hidden = true;
msg.set_quote(self, Some(&call.msg)).await?;
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
} else if call.is_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;
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
if call.is_ended() {
info!(self, "Call already ended");
return Ok(());
}
call.mark_as_ended(self).await?;
if !call.is_accepted() {
if call.is_incoming() {
call.update_text(self, "Declined call").await?;
} else {
call.update_text(self, "Cancelled call").await?;
}
} else {
call.update_text_duration(self).await?;
}
let mut msg = Message {
viewtype: Viewtype::Text,
text: "[Call ended]".into(),
..Default::default()
};
msg.param.set_cmd(SystemMessage::CallEnded);
msg.hidden = true;
msg.set_quote(self, Some(&call.msg)).await?;
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
self.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
@@ -177,8 +235,15 @@ impl Context {
call_id: MsgId,
) -> Result<()> {
sleep(Duration::from_secs(wait)).await;
let call = context.load_call_by_id(call_id).await?;
if !call.is_accepted {
let mut call = context.load_call_by_id(call_id).await?;
if !call.is_accepted() && !call.is_ended() {
call.mark_as_ended(&context).await?;
if call.is_incoming() {
call.update_text(&context, "Missed call").await?;
} else {
call.update_text(&context, "Cancelled call").await?;
}
context.emit_msgs_changed(call.msg.chat_id, call_id);
context.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
});
@@ -188,16 +253,18 @@ impl Context {
pub(crate) async fn handle_call_msg(
&self,
mime_message: &MimeMessage,
call_id: MsgId,
mime_message: &MimeMessage,
from_id: ContactId,
) -> Result<()> {
if mime_message.is_call() {
let call = self.load_call_by_id(call_id).await?;
if call.is_incoming {
if call.is_stale_call() {
if call.is_incoming() {
if call.is_stale() {
call.update_text(self, "Missed call").await?;
self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
} else {
call.update_text(self, "Incoming call").await?;
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
self.emit_event(EventType::IncomingCall {
msg_id: call.msg.id,
@@ -211,27 +278,28 @@ impl Context {
));
}
} else {
call.update_text(self, "Outgoing call").await?;
self.emit_msgs_changed(call.msg.chat_id, call_id);
}
} else {
match mime_message.is_system_message {
SystemMessage::CallAccepted => {
let call = self.load_call_by_id(call_id).await?;
call.update_text(self, "Call accepted").await?;
let mut call = self.load_call_by_id(call_id).await?;
if call.is_ended() || call.is_accepted() {
info!(self, "CallAccepted received for accepted/ended call");
return Ok(());
}
call.mark_as_accepted(self).await?;
self.emit_msgs_changed(call.msg.chat_id, call_id);
if call.is_incoming {
if call.is_incoming() {
self.emit_event(EventType::IncomingCallAccepted {
msg_id: call.msg.id,
accept_call_info: call.accept_call_info,
});
} else {
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(),
@@ -239,8 +307,33 @@ impl Context {
}
}
SystemMessage::CallEnded => {
let call = self.load_call_by_id(call_id).await?;
call.update_text(self, "Call ended").await?;
let mut call = self.load_call_by_id(call_id).await?;
if call.is_ended() {
// may happen eg. if a a message is missed
info!(self, "CallEnded received for ended call");
return Ok(());
}
call.mark_as_ended(self).await?;
if !call.is_accepted() {
if call.is_incoming() {
if from_id == ContactId::SELF {
call.update_text(self, "Declined call").await?;
} else {
call.update_text(self, "Missed call").await?;
}
} else {
// outgoing
if from_id == ContactId::SELF {
call.update_text(self, "Cancelled call").await?;
} else {
call.update_text(self, "Declined call").await?;
}
}
} else {
call.update_text_duration(self).await?;
}
self.emit_msgs_changed(call.msg.chat_id, call_id);
self.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
@@ -252,16 +345,6 @@ impl Context {
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_id(msg_id).await?;
call.update_text(self, "Call ended").await?;
self.emit_event(EventType::CallEnded { msg_id });
self.emit_msgs_changed(call.msg.chat_id, msg_id);
}
Ok(())
}
async fn load_call_by_id(&self, call_id: MsgId) -> Result<CallInfo> {
let call = Message::load_from_db(self, call_id).await?;
self.load_call_by_message(call)
@@ -271,8 +354,6 @@ impl Context {
ensure!(call.viewtype == Viewtype::Call);
Ok(CallInfo {
is_incoming: call.get_from_id() != ContactId::SELF,
is_accepted: call.is_call_accepted()?,
place_call_info: call
.param
.get(Param::WebrtcRoom)
@@ -288,24 +369,5 @@ impl Context {
}
}
impl Message {
async fn mark_call_as_accepted(
&mut self,
context: &Context,
accept_call_info: String,
) -> Result<()> {
ensure!(self.viewtype == Viewtype::Call);
self.param.set_int(Param::Arg, 1);
self.param.set(Param::WebrtcAccepted, accept_call_info);
self.update_param(context).await?;
Ok(())
}
fn is_call_accepted(&self) -> Result<bool> {
ensure!(self.viewtype == Viewtype::Call);
Ok(self.param.get_int(Param::Arg) == Some(1))
}
}
#[cfg(test)]
mod calls_tests;

View File

@@ -1,6 +1,6 @@
use super::*;
use crate::config::Config;
use crate::test_utils::{TestContext, TestContextManager, sync};
use crate::test_utils::{TestContext, TestContextManager};
struct CallSetup {
pub alice: TestContext,
@@ -13,6 +13,11 @@ struct CallSetup {
pub bob2_call: Message,
}
async fn assert_text(t: &TestContext, call_id: MsgId, text: &str) -> Result<()> {
assert_eq!(Message::load_from_db(t, call_id).await?.text, text);
Ok(())
}
async fn setup_call() -> Result<CallSetup> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
@@ -27,7 +32,7 @@ async fn setup_call() -> Result<CallSetup> {
// 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, "place_info".to_string())
.place_outgoing_call(alice_chat.id, "place-info-123".to_string())
.await?;
let sent1 = alice.pop_sent_msg().await;
assert_eq!(sent1.sender_msg_id, test_msg_id);
@@ -37,9 +42,10 @@ async fn setup_call() -> Result<CallSetup> {
assert!(!m.is_info());
assert_eq!(m.viewtype, Viewtype::Call);
let info = t.load_call_by_id(m.id).await?;
assert!(!info.is_incoming);
assert!(!info.is_accepted);
assert_eq!(info.place_call_info, "place_info");
assert!(!info.is_incoming());
assert!(!info.is_accepted());
assert_eq!(info.place_call_info, "place-info-123");
assert_text(t, m.id, "Outgoing call").await?;
}
// Bob receives the message referring to the call on two devices;
@@ -53,9 +59,10 @@ async fn setup_call() -> Result<CallSetup> {
.get_matching(|evt| matches!(evt, EventType::IncomingCall { .. }))
.await;
let info = t.load_call_by_id(m.id).await?;
assert!(info.is_incoming);
assert!(!info.is_accepted);
assert_eq!(info.place_call_info, "place_info");
assert!(info.is_incoming());
assert!(!info.is_accepted());
assert_eq!(info.place_call_info, "place-info-123");
assert_text(t, m.id, "Incoming call").await?;
}
Ok(CallSetup {
@@ -83,48 +90,45 @@ async fn accept_call() -> Result<CallSetup> {
} = setup_call().await?;
// Bob accepts the incoming call
bob.accept_incoming_call(bob_call.id, "accepted_info".to_string())
bob.accept_incoming_call(bob_call.id, "accept-info-456".to_string())
.await?;
assert_text(&bob, bob_call.id, "Incoming call").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
.await;
let sent2 = bob.pop_sent_msg().await;
let info = bob.load_call_by_id(bob_call.id).await?;
assert!(info.is_accepted);
assert_eq!(info.place_call_info, "place_info");
assert_eq!(info.accept_call_info, "accepted_info");
assert!(info.is_accepted());
assert_eq!(info.place_call_info, "place-info-123");
bob2.recv_msg_trash(&sent2).await;
assert_eq!(
Message::load_from_db(&bob, bob_call.id).await?.text,
"Call accepted"
);
assert_text(&bob, bob_call.id, "Incoming call").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
.await;
let info = bob2.load_call_by_id(bob2_call.id).await?;
assert!(!info.is_accepted); // "accepted" is only true on the device that does the call
assert!(info.is_accepted());
// Alice receives the acceptance message
alice.recv_msg_trash(&sent2).await;
assert_eq!(
Message::load_from_db(&alice, alice_call.id).await?.text,
"Call accepted"
);
alice
assert_text(&alice, alice_call.id, "Outgoing call").await?;
let ev = alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
.await;
assert_eq!(
ev,
EventType::OutgoingCallAccepted {
msg_id: alice2_call.id,
accept_call_info: "accept-info-456".to_string()
}
);
let info = alice.load_call_by_id(alice_call.id).await?;
assert!(info.is_accepted);
assert_eq!(info.place_call_info, "place_info");
assert_eq!(info.accept_call_info, "accepted_info");
assert!(info.is_accepted());
assert_eq!(info.place_call_info, "place-info-123");
alice2.recv_msg_trash(&sent2).await;
assert_eq!(
Message::load_from_db(&alice2, alice2_call.id).await?.text,
"Call accepted"
);
assert_text(&alice2, alice2_call.id, "Outgoing call").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
@@ -142,11 +146,6 @@ async fn accept_call() -> Result<CallSetup> {
})
}
async fn assert_is_call_ended(t: &TestContext, call_id: MsgId) -> Result<()> {
assert_eq!(Message::load_from_db(t, call_id).await?.text, "Call ended");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_accept_call_callee_ends() -> Result<()> {
// Alice calls Bob, Bob accepts
@@ -164,28 +163,28 @@ async fn test_accept_call_callee_ends() -> Result<()> {
// Bob has accepted the call and also ends it
bob.end_call(bob_call.id).await?;
assert_is_call_ended(&bob, bob_call.id).await?;
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = bob.pop_sent_msg().await;
bob2.recv_msg_trash(&sent3).await;
assert_is_call_ended(&bob2, bob2_call.id).await?;
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
// Alice receives the ending message
alice.recv_msg_trash(&sent3).await;
assert_is_call_ended(&alice, alice_call.id).await?;
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
alice2.recv_msg_trash(&sent3).await;
assert_is_call_ended(&alice2, alice2_call.id).await?;
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
@@ -199,6 +198,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
// Alice calls Bob, Bob accepts
let CallSetup {
alice,
alice_call,
alice2,
alice2_call,
bob,
@@ -209,7 +209,8 @@ async fn test_accept_call_caller_ends() -> Result<()> {
} = accept_call().await?;
// Bob has accepted the call but Alice ends it
alice.end_call(bob_call.id).await?;
alice.end_call(alice_call.id).await?;
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
@@ -217,7 +218,7 @@ async fn test_accept_call_caller_ends() -> Result<()> {
let sent3 = alice.pop_sent_msg().await;
alice2.recv_msg_trash(&sent3).await;
assert_is_call_ended(&alice2, alice2_call.id).await?;
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
@@ -225,13 +226,13 @@ async fn test_accept_call_caller_ends() -> Result<()> {
// Bob receives the ending message
bob.recv_msg_trash(&sent3).await;
assert_is_call_ended(&bob, bob_call.id).await?;
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
bob2.recv_msg_trash(&sent3).await;
assert_is_call_ended(&bob2, bob2_call.id).await?;
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
@@ -243,6 +244,10 @@ async fn test_accept_call_caller_ends() -> Result<()> {
async fn test_callee_rejects_call() -> Result<()> {
// Alice calls Bob
let CallSetup {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
@@ -250,21 +255,36 @@ async fn test_callee_rejects_call() -> Result<()> {
..
} = 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 has accepted Alice before, but does not want to talk with Alice
bob_call.chat_id.accept(&bob).await?;
bob.end_call(bob_call.id).await?;
assert_is_call_ended(&bob, bob_call.id).await?;
assert_text(&bob, bob_call.id, "Declined call").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = bob.pop_sent_msg().await;
sync(&bob, &bob2).await;
assert_is_call_ended(&bob2, bob2_call.id).await?;
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Declined call").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
// Alice receives decline message
alice.recv_msg_trash(&sent3).await;
assert_text(&alice, alice_call.id, "Declined call").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Declined call").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
Ok(())
}
@@ -285,7 +305,7 @@ async fn test_caller_cancels_call() -> Result<()> {
// Alice changes their mind before Bob picks up
alice.end_call(alice_call.id).await?;
assert_is_call_ended(&alice, alice_call.id).await?;
assert_text(&alice, alice_call.id, "Cancelled call").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
@@ -293,7 +313,7 @@ async fn test_caller_cancels_call() -> Result<()> {
let sent3 = alice.pop_sent_msg().await;
alice2.recv_msg_trash(&sent3).await;
assert_is_call_ended(&alice2, alice2_call.id).await?;
assert_text(&alice2, alice2_call.id, "Cancelled call").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
@@ -301,13 +321,13 @@ async fn test_caller_cancels_call() -> Result<()> {
// Bob receives the ending message
bob.recv_msg_trash(&sent3).await;
assert_is_call_ended(&bob, bob_call.id).await?;
assert_text(&bob, bob_call.id, "Missed call").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
bob2.recv_msg_trash(&sent3).await;
assert_is_call_ended(&bob2, bob2_call.id).await?;
assert_text(&bob2, bob2_call.id, "Missed call").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
@@ -325,7 +345,7 @@ async fn test_is_stale_call() -> Result<()> {
},
..Default::default()
};
assert!(!call_info.is_stale_call());
assert!(!call_info.is_stale());
let remaining_seconds = call_info.remaining_ring_seconds();
assert!(remaining_seconds == RINGING_SECONDS || remaining_seconds == RINGING_SECONDS - 1);
@@ -337,7 +357,7 @@ async fn test_is_stale_call() -> Result<()> {
},
..Default::default()
};
assert!(!call_info.is_stale_call());
assert!(!call_info.is_stale());
let remaining_seconds = call_info.remaining_ring_seconds();
assert!(remaining_seconds == RINGING_SECONDS - 5 || remaining_seconds == RINGING_SECONDS - 6);
@@ -349,28 +369,32 @@ async fn test_is_stale_call() -> Result<()> {
},
..Default::default()
};
assert!(call_info.is_stale_call());
assert!(call_info.is_stale());
assert_eq!(call_info.remaining_ring_seconds(), 0);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mark_call_as_accepted() -> Result<()> {
async fn test_mark_calls() -> Result<()> {
let CallSetup {
alice, alice_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, "accepted_info".to_string())
.await?;
assert!(alice_call.is_call_accepted()?);
let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?;
assert!(!call_info.is_accepted());
assert!(!call_info.is_ended());
call_info.mark_as_accepted(&alice).await?;
assert!(call_info.is_accepted());
assert!(!call_info.is_ended());
let alice_call = Message::load_from_db(&alice, alice_call.id).await?;
assert!(alice_call.is_call_accepted()?);
let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?;
assert!(call_info.is_accepted());
assert!(!call_info.is_ended());
call_info.mark_as_ended(&alice).await?;
assert!(call_info.is_accepted());
assert!(call_info.is_ended());
Ok(())
}

View File

@@ -388,8 +388,6 @@ pub enum EventType {
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.

View File

@@ -1575,6 +1575,9 @@ impl MimeFactory {
"Chat-Content",
mail_builder::headers::raw::Raw::new("call").into(),
));
placeholdertext = Some(
"[This is a 'Call'. The sender uses an experiment not supported on your version yet]".to_string(),
);
}
if msg.param.exists(Param::WebrtcRoom) {

View File

@@ -1001,7 +1001,9 @@ pub(crate) async fn receive_imf_inner(
}
if mime_parser.is_call() {
context.handle_call_msg(&mime_parser, insert_msg_id).await?;
context
.handle_call_msg(insert_msg_id, &mime_parser, from_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 {
@@ -1989,7 +1991,9 @@ async fn add_parts(
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?;
context
.handle_call_msg(call.get_id(), mime_parser, from_id)
.await?;
} else {
warn!(context, "Call: Cannot load parent.")
}

View File

@@ -71,9 +71,6 @@ pub(crate) enum SyncData {
DeleteMessages {
msgs: Vec<String>, // RFC724 id (i.e. "Message-Id" header)
},
RejectIncomingCall {
msg: String, // RFC724 id (i.e. "Message-Id" header)
},
}
#[derive(Debug, Serialize, Deserialize)]
@@ -267,7 +264,6 @@ 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}.");