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:
bjoern
2025-08-30 23:48:38 +02:00
committed by GitHub
parent 4258088fb4
commit 0bbd910883
16 changed files with 1129 additions and 40 deletions

307
src/calls.rs Normal file
View 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
View 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(())
}

View File

@@ -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,

View File

@@ -86,6 +86,7 @@ pub enum HeaderDef {
ChatDispositionNotificationTo,
ChatWebrtcRoom,
ChatWebrtcAccepted,
/// This message deletes the messages listed in the value by rfc724_mid.
ChatDelete,

View File

@@ -53,6 +53,7 @@ pub use events::*;
mod aheader;
pub mod blob;
pub mod calls;
pub mod chat;
pub mod chatlist;
pub mod config;

View File

@@ -973,6 +973,10 @@ impl Message {
| SystemMessage::WebxdcStatusUpdate
| SystemMessage::WebxdcInfoMessage
| SystemMessage::IrohNodeAddr
| SystemMessage::OutgoingCall
| SystemMessage::IncomingCall
| SystemMessage::CallAccepted
| SystemMessage::CallEnded
| SystemMessage::Unknown => Ok(None),
}
}

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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() {

View File

@@ -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}.");