mirror of
https://github.com/chatmail/core.git
synced 2026-04-26 09:56:35 +03:00
feat: add call ringing API (#6650)
this PR adds a "ringing" api that can be used for calls later. see deltachat.h for details about the API; jsonrpc is left out until things are settled for the needs of android/iOS UI using this PR already successfully are https://github.com/deltachat/deltachat-ios/pull/2638 and https://github.com/deltachat/deltachat-android/pull/3785 ; the "payload" passed forth and back is optimised for https://github.com/deltachat/calls-webapp --------- Co-authored-by: l <link2xt@testrun.org>
This commit is contained in:
@@ -1215,6 +1215,103 @@ 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:
|
||||
*
|
||||
* - 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 after 1 minute timeout.
|
||||
* 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
|
||||
*
|
||||
* Actions during the call:
|
||||
*
|
||||
* - caller ends the call using dc_end_call():
|
||||
* callee receives #DC_EVENT_CALL_ENDED
|
||||
*
|
||||
* - callee ends the call using dc_end_call():
|
||||
* caller 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.
|
||||
*
|
||||
* 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.
|
||||
* This needs to be a one-to-one chat.
|
||||
* @param place_call_info any data that other devices receive
|
||||
* 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, const char* place_call_info);
|
||||
|
||||
|
||||
/**
|
||||
* 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 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.
|
||||
* @return 1=success, 0=error
|
||||
*/
|
||||
int dc_accept_incoming_call (dc_context_t* context, uint32_t msg_id, const char* accept_call_info);
|
||||
|
||||
|
||||
/**
|
||||
* 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 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.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
@@ -4546,6 +4643,8 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
||||
* 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_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted"
|
||||
* - DC_INFO_OUTGOING_CALL (60) - Info-message refers to an outgoing call
|
||||
* - DC_INFO_INCOMING_CALL (65) - 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.
|
||||
@@ -4602,6 +4701,8 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
|
||||
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
|
||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||
#define DC_INFO_CHAT_E2EE 50
|
||||
#define DC_INFO_OUTGOING_CALL 60
|
||||
#define DC_INFO_INCOMING_CALL 65
|
||||
|
||||
|
||||
/**
|
||||
@@ -6636,6 +6737,63 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_CHANNEL_OVERFLOW 2400
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Incoming call.
|
||||
* 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 ID of the info-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 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()
|
||||
*/
|
||||
#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.
|
||||
*
|
||||
* @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
|
||||
|
||||
/**
|
||||
* An incoming or outgoing call was ended using dc_end_call().
|
||||
* Moreover, the event is sent when the call was not accepted within 1 minute timeout.
|
||||
*
|
||||
* 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
|
||||
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -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, .. }
|
||||
@@ -767,8 +779,23 @@ 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::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. } => 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()
|
||||
@@ -1167,6 +1194,61 @@ 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,
|
||||
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, place_call_info))
|
||||
.context("Failed to place call")
|
||||
.log_err(ctx)
|
||||
.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,
|
||||
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()");
|
||||
return 0;
|
||||
}
|
||||
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, accept_call_info))
|
||||
.context("Failed to accept call")
|
||||
.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))
|
||||
.context("Failed to end call")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_draft(
|
||||
context: *mut dc_context_t,
|
||||
|
||||
@@ -416,6 +416,37 @@ pub enum EventType {
|
||||
/// Number of events skipped.
|
||||
n: u64,
|
||||
},
|
||||
|
||||
/// Incoming call.
|
||||
IncomingCall {
|
||||
/// ID of the info message referring to the call.
|
||||
msg_id: u32,
|
||||
/// User-defined info as passed to place_outgoing_call()
|
||||
place_call_info: String,
|
||||
},
|
||||
|
||||
/// Incoming call accepted.
|
||||
/// This is esp. interesting to stop ringing on other devices.
|
||||
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.
|
||||
OutgoingCallAccepted {
|
||||
/// 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,
|
||||
},
|
||||
|
||||
/// Call ended.
|
||||
CallEnded {
|
||||
/// ID of the info message referring to the call.
|
||||
msg_id: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<CoreEventType> for EventType {
|
||||
@@ -566,6 +597,30 @@ impl From<CoreEventType> for EventType {
|
||||
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
||||
CoreEventType::AccountsChanged => AccountsChanged,
|
||||
CoreEventType::AccountsItemChanged => AccountsItemChanged,
|
||||
CoreEventType::IncomingCall {
|
||||
msg_id,
|
||||
place_call_info,
|
||||
} => IncomingCall {
|
||||
msg_id: msg_id.to_u32(),
|
||||
place_call_info,
|
||||
},
|
||||
CoreEventType::IncomingCallAccepted {
|
||||
msg_id,
|
||||
accept_call_info,
|
||||
} => IncomingCallAccepted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
accept_call_info,
|
||||
},
|
||||
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(),
|
||||
},
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
||||
|
||||
@@ -437,6 +437,11 @@ pub enum SystemMessageType {
|
||||
|
||||
/// This message contains a users iroh node address.
|
||||
IrohNodeAddr,
|
||||
|
||||
OutgoingCall,
|
||||
IncomingCall,
|
||||
CallAccepted,
|
||||
CallEnded,
|
||||
}
|
||||
|
||||
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
@@ -463,6 +468,10 @@ impl From<deltachat::mimeparser::SystemMessage> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,32 +160,6 @@ def test_html_message(acfactory, lp):
|
||||
assert html_text in msg2.html
|
||||
|
||||
|
||||
def test_videochat_invitation_message(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
text = "You are invited to a video chat, click https://meet.jit.si/WxEGad0gGzX to join."
|
||||
|
||||
lp.sec("ac1: prepare and send text message to ac2")
|
||||
msg1 = chat.send_text("message0")
|
||||
assert not msg1.is_videochat_invitation()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message0"
|
||||
assert not msg2.is_videochat_invitation()
|
||||
|
||||
lp.sec("ac1: prepare and send videochat invitation to ac2")
|
||||
msg1 = Message.new_empty(ac1, "videochat")
|
||||
msg1.set_text(text)
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.is_videochat_invitation()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == text
|
||||
assert msg2.is_videochat_invitation()
|
||||
|
||||
|
||||
def test_webxdc_message(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
307
src/calls.rs
Normal file
307
src/calls.rs
Normal file
@@ -0,0 +1,307 @@
|
||||
//! # 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.
|
||||
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, 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::{Result, ensure};
|
||||
use std::time::Duration;
|
||||
use tokio::task;
|
||||
use tokio::time::sleep;
|
||||
|
||||
/// 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, 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.
|
||||
pub msg: Message,
|
||||
}
|
||||
|
||||
impl CallInfo {
|
||||
fn is_stale_call(&self) -> bool {
|
||||
self.remaining_ring_seconds() <= 0
|
||||
}
|
||||
|
||||
fn remaining_ring_seconds(&self) -> i64 {
|
||||
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 {
|
||||
/// Start an outgoing call.
|
||||
pub async fn place_outgoing_call(
|
||||
&self,
|
||||
chat_id: ChatId,
|
||||
place_call_info: String,
|
||||
) -> Result<MsgId> {
|
||||
let chat = Chat::load_from_db(self, chat_id).await?;
|
||||
ensure!(chat.typ == Chattype::Single && !chat.is_self_talk());
|
||||
|
||||
let mut call = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: "Calling...".into(),
|
||||
..Default::default()
|
||||
};
|
||||
call.param.set_cmd(SystemMessage::OutgoingCall);
|
||||
call.param.set(Param::WebrtcRoom, &place_call_info);
|
||||
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.
|
||||
pub async fn accept_incoming_call(
|
||||
&self,
|
||||
call_id: MsgId,
|
||||
accept_call_info: String,
|
||||
) -> Result<()> {
|
||||
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
|
||||
ensure!(call.is_incoming);
|
||||
|
||||
let chat = Chat::load_from_db(self, call.msg.chat_id).await?;
|
||||
if chat.is_contact_request() {
|
||||
chat.id.accept(self).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(),
|
||||
..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(())
|
||||
}
|
||||
|
||||
/// Cancel, reject 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?;
|
||||
|
||||
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.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;
|
||||
}
|
||||
|
||||
self.emit_event(EventType::CallEnded {
|
||||
msg_id: call.msg.id,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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_id(call_id).await?;
|
||||
if !call.is_accepted {
|
||||
context.emit_event(EventType::CallEnded {
|
||||
msg_id: call.msg.id,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_call_msg(
|
||||
&self,
|
||||
mime_message: &MimeMessage,
|
||||
call_id: MsgId,
|
||||
) -> Result<()> {
|
||||
match mime_message.is_system_message {
|
||||
SystemMessage::IncomingCall => {
|
||||
let call = self.load_call_by_id(call_id).await?;
|
||||
if call.is_incoming {
|
||||
if call.is_stale_call() {
|
||||
call.update_text(self, "Missed call").await?;
|
||||
self.emit_incoming_msg(call.msg.chat_id, call_id);
|
||||
} else {
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_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(
|
||||
self.clone(),
|
||||
wait.try_into()?,
|
||||
call.msg.id,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
}
|
||||
}
|
||||
SystemMessage::CallAccepted => {
|
||||
let call = self.load_call_by_id(call_id).await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
SystemMessage::CallEnded => {
|
||||
let call = self.load_call_by_id(call_id).await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
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? {
|
||||
self.emit_event(EventType::CallEnded { 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)
|
||||
}
|
||||
|
||||
fn load_call_by_message(&self, call: Message) -> Result<CallInfo> {
|
||||
ensure!(
|
||||
call.get_info_type() == SystemMessage::IncomingCall
|
||||
|| call.get_info_type() == SystemMessage::OutgoingCall
|
||||
);
|
||||
|
||||
Ok(CallInfo {
|
||||
is_incoming: call.get_info_type() == SystemMessage::IncomingCall,
|
||||
is_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,
|
||||
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(())
|
||||
}
|
||||
|
||||
fn is_call_accepted(&self) -> Result<bool> {
|
||||
ensure!(
|
||||
self.get_info_type() == SystemMessage::IncomingCall
|
||||
|| self.get_info_type() == SystemMessage::OutgoingCall
|
||||
);
|
||||
Ok(self.param.get_int(Param::Arg) == Some(1))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod calls_tests;
|
||||
372
src/calls/calls_tests.rs
Normal file
372
src/calls/calls_tests.rs
Normal file
@@ -0,0 +1,372 @@
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::test_utils::{TestContext, TestContextManager, sync};
|
||||
|
||||
struct CallSetup {
|
||||
pub alice: TestContext,
|
||||
pub alice2: TestContext,
|
||||
pub alice_call: Message,
|
||||
pub bob: TestContext,
|
||||
pub bob2: TestContext,
|
||||
pub bob_call: Message,
|
||||
pub bob2_call: Message,
|
||||
}
|
||||
|
||||
async fn setup_call() -> Result<CallSetup> {
|
||||
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, "place_info".to_string())
|
||||
.await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
assert_eq!(sent1.sender_msg_id, test_msg_id);
|
||||
let alice_call = Message::load_from_db(&alice, sent1.sender_msg_id).await?;
|
||||
let alice2_call = alice2.recv_msg(&sent1).await;
|
||||
for (t, m) in [(&alice, &alice_call), (&alice2, &alice2_call)] {
|
||||
assert!(m.is_info());
|
||||
assert_eq!(m.get_info_type(), SystemMessage::OutgoingCall);
|
||||
let info = t.load_call_by_id(m.id).await?;
|
||||
assert!(!info.is_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
|
||||
let bob_call = bob.recv_msg(&sent1).await;
|
||||
let bob2_call = bob2.recv_msg(&sent1).await;
|
||||
for (t, m) in [(&bob, &bob_call), (&bob2, &bob2_call)] {
|
||||
assert!(m.is_info());
|
||||
assert_eq!(m.get_info_type(), SystemMessage::IncomingCall);
|
||||
t.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCall { .. }))
|
||||
.await;
|
||||
let info = t.load_call_by_id(m.id).await?;
|
||||
assert!(!info.is_accepted);
|
||||
assert_eq!(info.place_call_info, "place_info");
|
||||
}
|
||||
|
||||
Ok(CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
})
|
||||
}
|
||||
|
||||
async fn accept_call() -> Result<CallSetup> {
|
||||
let CallSetup {
|
||||
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, "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_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");
|
||||
|
||||
let bob_accept_msg = bob2.recv_msg(&sent2).await;
|
||||
assert!(bob_accept_msg.is_info());
|
||||
assert_eq!(bob_accept_msg.get_info_type(), SystemMessage::CallAccepted);
|
||||
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
|
||||
|
||||
// Alice receives the acceptance message
|
||||
let alice_accept_msg = alice.recv_msg(&sent2).await;
|
||||
assert!(alice_accept_msg.is_info());
|
||||
assert_eq!(
|
||||
alice_accept_msg.get_info_type(),
|
||||
SystemMessage::CallAccepted
|
||||
);
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
|
||||
.await;
|
||||
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");
|
||||
|
||||
let alice2_accept_msg = alice2.recv_msg(&sent2).await;
|
||||
assert!(alice2_accept_msg.is_info());
|
||||
assert_eq!(
|
||||
alice2_accept_msg.get_info_type(),
|
||||
SystemMessage::CallAccepted
|
||||
);
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
|
||||
.await;
|
||||
|
||||
Ok(CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
})
|
||||
}
|
||||
|
||||
fn assert_is_call_ended_info_msg(msg: Message) {
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::CallEnded);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_accept_call_callee_ends() -> Result<()> {
|
||||
// Alice calls Bob, Bob accepts
|
||||
let CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
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;
|
||||
|
||||
let bob2_end_call_msg = bob2.recv_msg(&sent3).await;
|
||||
assert_is_call_ended_info_msg(bob2_end_call_msg);
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
|
||||
// Alice receives the ending message
|
||||
let alice_end_call_msg = alice.recv_msg(&sent3).await;
|
||||
assert_is_call_ended_info_msg(alice_end_call_msg);
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
|
||||
let alice2_end_call_msg = alice2.recv_msg(&sent3).await;
|
||||
assert_is_call_ended_info_msg(alice2_end_call_msg);
|
||||
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 CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
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;
|
||||
|
||||
let alice2_end_call_msg = alice2.recv_msg(&sent3).await;
|
||||
assert_is_call_ended_info_msg(alice2_end_call_msg);
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
|
||||
// Bob receives the ending message
|
||||
let bob_end_call_msg = bob.recv_msg(&sent3).await;
|
||||
assert_is_call_ended_info_msg(bob_end_call_msg);
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
|
||||
let bob2_end_call_msg = bob2.recv_msg(&sent3).await;
|
||||
assert_is_call_ended_info_msg(bob2_end_call_msg);
|
||||
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 CallSetup {
|
||||
bob,
|
||||
bob2,
|
||||
bob_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 CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
bob,
|
||||
bob2,
|
||||
..
|
||||
} = 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;
|
||||
|
||||
let alice2_call_ended_msg = alice2.recv_msg(&sent3).await;
|
||||
assert_is_call_ended_info_msg(alice2_call_ended_msg);
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
|
||||
// Bob receives the ending message
|
||||
let bob_call_ended_msg = bob.recv_msg(&sent3).await;
|
||||
assert_is_call_ended_info_msg(bob_call_ended_msg);
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
|
||||
let bob2_call_ended_msg = bob2.recv_msg(&sent3).await;
|
||||
assert_is_call_ended_info_msg(bob2_call_ended_msg);
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
|
||||
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());
|
||||
let remaining_seconds = call_info.remaining_ring_seconds();
|
||||
assert!(remaining_seconds == RINGING_SECONDS || remaining_seconds == RINGING_SECONDS - 1);
|
||||
|
||||
// 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());
|
||||
let remaining_seconds = call_info.remaining_ring_seconds();
|
||||
assert!(remaining_seconds == RINGING_SECONDS - 5 || remaining_seconds == RINGING_SECONDS - 6);
|
||||
|
||||
// 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());
|
||||
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<()> {
|
||||
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 alice_call = Message::load_from_db(&alice, alice_call.id).await?;
|
||||
assert!(alice_call.is_call_accepted()?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_udpate_call_text() -> Result<()> {
|
||||
let CallSetup {
|
||||
alice, alice_call, ..
|
||||
} = setup_call().await?;
|
||||
|
||||
let call_info = alice.load_call_by_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(())
|
||||
}
|
||||
@@ -376,6 +376,36 @@ 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,
|
||||
/// 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.
|
||||
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,
|
||||
|
||||
@@ -86,6 +86,7 @@ pub enum HeaderDef {
|
||||
|
||||
ChatDispositionNotificationTo,
|
||||
ChatWebrtcRoom,
|
||||
ChatWebrtcAccepted,
|
||||
|
||||
/// This message deletes the messages listed in the value by rfc724_mid.
|
||||
ChatDelete,
|
||||
|
||||
@@ -53,6 +53,7 @@ pub use events::*;
|
||||
|
||||
mod aheader;
|
||||
pub mod blob;
|
||||
pub mod calls;
|
||||
pub mod chat;
|
||||
pub mod chatlist;
|
||||
pub mod config;
|
||||
|
||||
@@ -973,6 +973,10 @@ impl Message {
|
||||
| SystemMessage::WebxdcStatusUpdate
|
||||
| SystemMessage::WebxdcInfoMessage
|
||||
| SystemMessage::IrohNodeAddr
|
||||
| SystemMessage::OutgoingCall
|
||||
| SystemMessage::IncomingCall
|
||||
| SystemMessage::CallAccepted
|
||||
| SystemMessage::CallEnded
|
||||
| SystemMessage::Unknown => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use anyhow::{Context as _, Result, anyhow, bail, ensure};
|
||||
use base64::Engine as _;
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use deltachat_contact_tools::sanitize_bidi_characters;
|
||||
@@ -1533,6 +1533,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(),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -1557,6 +1578,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(
|
||||
@@ -1567,6 +1591,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
|
||||
|
||||
@@ -216,6 +216,22 @@ pub enum SystemMessage {
|
||||
|
||||
/// "Messages are end-to-end encrypted."
|
||||
ChatE2ee = 50,
|
||||
|
||||
/// This system message represents an outgoing call.
|
||||
/// This message is visible to the user as an "info" message.
|
||||
OutgoingCall = 60,
|
||||
|
||||
/// This system message represents an incoming call.
|
||||
/// This message is visible to the user as an "info" message.
|
||||
IncomingCall = 65,
|
||||
|
||||
/// 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 = 66,
|
||||
|
||||
/// Message indicating that a call was ended.
|
||||
CallEnded = 67,
|
||||
}
|
||||
|
||||
const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
|
||||
@@ -676,6 +692,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;
|
||||
@@ -698,16 +724,24 @@ 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 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(room) = room {
|
||||
if content == "videochat-invitation" {
|
||||
part.typ = Viewtype::VideochatInvitation;
|
||||
part.param
|
||||
.set(Param::WebrtcRoom, instance.unwrap_or_default());
|
||||
}
|
||||
part.param.set(Param::WebrtcRoom, room);
|
||||
} else if let Some(accepted) = accepted {
|
||||
part.param.set(Param::WebrtcAccepted, accepted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1000,12 +1000,16 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
}
|
||||
|
||||
if received_msg.hidden {
|
||||
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
|
||||
} else if let Some(replace_chat_id) = replace_chat_id {
|
||||
context.emit_msgs_changed_without_msg_id(replace_chat_id);
|
||||
} else if !chat_id.is_trash() {
|
||||
let fresh = received_msg.state == MessageState::InFresh;
|
||||
let fresh = received_msg.state == MessageState::InFresh
|
||||
&& mime_parser.is_system_message != SystemMessage::CallAccepted
|
||||
&& mime_parser.is_system_message != SystemMessage::CallEnded;
|
||||
for msg_id in &received_msg.msg_ids {
|
||||
chat_id.emit_msg_event(context, *msg_id, mime_parser.incoming && fresh);
|
||||
}
|
||||
@@ -1998,6 +2002,22 @@ 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
|
||||
{
|
||||
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 hidden = mime_parser.parts.iter().all(|part| part.is_reaction);
|
||||
let mut parts = mime_parser.parts.iter().peekable();
|
||||
while let Some(part) = parts.next() {
|
||||
|
||||
@@ -71,6 +71,9 @@ 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)]
|
||||
@@ -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}.");
|
||||
|
||||
Reference in New Issue
Block a user