mirror of
https://github.com/chatmail/core.git
synced 2026-04-25 17:36:30 +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:
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