mirror of
https://github.com/chatmail/core.git
synced 2026-04-28 19:06:35 +03:00
Merge remote-tracking branch 'origin/main' into hoc/channels-encryption-only-qrcodes
This commit is contained in:
@@ -17,7 +17,6 @@ pub enum EncryptPreference {
|
||||
#[default]
|
||||
NoPreference = 0,
|
||||
Mutual = 1,
|
||||
Reset = 20,
|
||||
}
|
||||
|
||||
impl fmt::Display for EncryptPreference {
|
||||
@@ -25,7 +24,6 @@ impl fmt::Display for EncryptPreference {
|
||||
match *self {
|
||||
EncryptPreference::Mutual => write!(fmt, "mutual"),
|
||||
EncryptPreference::NoPreference => write!(fmt, "nopreference"),
|
||||
EncryptPreference::Reset => write!(fmt, "reset"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,21 +46,13 @@ pub struct Aheader {
|
||||
pub addr: String,
|
||||
pub public_key: SignedPublicKey,
|
||||
pub prefer_encrypt: EncryptPreference,
|
||||
}
|
||||
|
||||
impl Aheader {
|
||||
/// Creates new autocrypt header
|
||||
pub fn new(
|
||||
addr: String,
|
||||
public_key: SignedPublicKey,
|
||||
prefer_encrypt: EncryptPreference,
|
||||
) -> Self {
|
||||
Aheader {
|
||||
addr,
|
||||
public_key,
|
||||
prefer_encrypt,
|
||||
}
|
||||
}
|
||||
// Whether `_verified` attribute is present.
|
||||
//
|
||||
// `_verified` attribute is an extension to `Autocrypt-Gossip`
|
||||
// header that is used to tell that the sender
|
||||
// marked this key as verified.
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for Aheader {
|
||||
@@ -71,6 +61,9 @@ impl fmt::Display for Aheader {
|
||||
if self.prefer_encrypt == EncryptPreference::Mutual {
|
||||
write!(fmt, " prefer-encrypt=mutual;")?;
|
||||
}
|
||||
if self.verified {
|
||||
write!(fmt, " _verified=1;")?;
|
||||
}
|
||||
|
||||
// adds a whitespace every 78 characters, this allows
|
||||
// email crate to wrap the lines according to RFC 5322
|
||||
@@ -125,6 +118,8 @@ impl FromStr for Aheader {
|
||||
.and_then(|raw| raw.parse().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let verified = attributes.remove("_verified").is_some();
|
||||
|
||||
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
|
||||
// Autocrypt-Level0: unknown attribute, treat the header as invalid
|
||||
if attributes.keys().any(|k| !k.starts_with('_')) {
|
||||
@@ -135,6 +130,7 @@ impl FromStr for Aheader {
|
||||
addr,
|
||||
public_key,
|
||||
prefer_encrypt,
|
||||
verified,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -152,10 +148,11 @@ mod tests {
|
||||
|
||||
assert_eq!(h.addr, "me@mail.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
|
||||
assert_eq!(h.verified, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// EncryptPreference::Reset is an internal value, parser should never return it
|
||||
// Non-standard values of prefer-encrypt such as `reset` are treated as no preference.
|
||||
#[test]
|
||||
fn test_from_str_reset() -> Result<()> {
|
||||
let raw = format!("addr=reset@example.com; prefer-encrypt=reset; keydata={RAWKEY}");
|
||||
@@ -245,11 +242,12 @@ mod tests {
|
||||
assert!(
|
||||
format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"test@example.com".to_string(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::Mutual
|
||||
)
|
||||
Aheader {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.contains("prefer-encrypt=mutual;")
|
||||
);
|
||||
@@ -260,11 +258,12 @@ mod tests {
|
||||
assert!(
|
||||
!format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"test@example.com".to_string(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::NoPreference
|
||||
)
|
||||
Aheader {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.contains("prefer-encrypt")
|
||||
);
|
||||
@@ -273,13 +272,27 @@ mod tests {
|
||||
assert!(
|
||||
format!(
|
||||
"{}",
|
||||
Aheader::new(
|
||||
"TeSt@eXaMpLe.cOm".to_string(),
|
||||
SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
EncryptPreference::Mutual
|
||||
)
|
||||
Aheader {
|
||||
addr: "TeSt@eXaMpLe.cOm".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.contains("test@example.com")
|
||||
);
|
||||
|
||||
assert!(
|
||||
format!(
|
||||
"{}",
|
||||
Aheader {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
verified: true
|
||||
}
|
||||
)
|
||||
.contains("_verified")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
338
src/calls.rs
338
src/calls.rs
@@ -1,16 +1,17 @@
|
||||
//! # Handle calls.
|
||||
//!
|
||||
//! Internally, calls are bound to the user-visible info message initializing the call.
|
||||
//! This means, the "Call ID" is a "Message ID" currently - similar to webxdc.
|
||||
//! Internally, calls are bound a user-visible message initializing the call.
|
||||
//! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs.
|
||||
use crate::chat::{Chat, ChatId, send_msg};
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::message::{self, Message, MsgId, Viewtype, rfc724_mid_exists};
|
||||
use crate::log::info;
|
||||
use crate::message::{self, Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
use crate::sync::SyncData;
|
||||
use crate::tools::time;
|
||||
use anyhow::{Result, ensure};
|
||||
use std::time::Duration;
|
||||
@@ -28,29 +29,30 @@ use tokio::time::sleep;
|
||||
/// as the callee won't start the call afterwards.
|
||||
const RINGING_SECONDS: i64 = 60;
|
||||
|
||||
/// For persisting parameters in the call, we use Param::Arg*
|
||||
const CALL_ACCEPTED_TIMESTAMP: Param = Param::Arg;
|
||||
const CALL_ENDED_TIMESTAMP: Param = Param::Arg4;
|
||||
|
||||
/// Information about the status of a call.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CallInfo {
|
||||
/// Incoming or outgoing call?
|
||||
pub is_incoming: bool,
|
||||
|
||||
/// Was an incoming call accepted on this device?
|
||||
/// For privacy reasons, only for accepted incoming calls, callee sends a message to caller on `end_call()`.
|
||||
/// On other devices and for outgoing calls, `is_accepted` is never set.
|
||||
pub is_accepted: bool,
|
||||
|
||||
/// User-defined text as given to place_outgoing_call()
|
||||
pub place_call_info: String,
|
||||
|
||||
/// User-defined text as given to accept_incoming_call()
|
||||
pub accept_call_info: String,
|
||||
|
||||
/// Info message referring to the call.
|
||||
/// Message referring to the call.
|
||||
/// Data are persisted along with the message using Param::Arg*
|
||||
pub msg: Message,
|
||||
}
|
||||
|
||||
impl CallInfo {
|
||||
fn is_stale_call(&self) -> bool {
|
||||
fn is_incoming(&self) -> bool {
|
||||
self.msg.from_id != ContactId::SELF
|
||||
}
|
||||
|
||||
fn is_stale(&self) -> bool {
|
||||
self.remaining_ring_seconds() <= 0
|
||||
}
|
||||
|
||||
@@ -69,6 +71,60 @@ impl CallInfo {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_text_duration(&self, context: &Context) -> Result<()> {
|
||||
let minutes = self.get_duration_seconds() / 60;
|
||||
let duration = match minutes {
|
||||
0 => "<1 minute".to_string(),
|
||||
1 => "1 minute".to_string(),
|
||||
n => format!("{} minutes", n),
|
||||
};
|
||||
|
||||
if self.is_incoming() {
|
||||
self.update_text(context, &format!("Incoming call\n{duration}"))
|
||||
.await?;
|
||||
} else {
|
||||
self.update_text(context, &format!("Outgoing call\n{duration}"))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark calls as accepted.
|
||||
/// This is needed for all devices where a stale-timer runs, to prevent accepted calls being terminated as stale.
|
||||
async fn mark_as_accepted(&mut self, context: &Context) -> Result<()> {
|
||||
self.msg.param.set_i64(CALL_ACCEPTED_TIMESTAMP, time());
|
||||
self.msg.update_param(context).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_accepted(&self) -> bool {
|
||||
self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP)
|
||||
}
|
||||
|
||||
async fn mark_as_ended(&mut self, context: &Context) -> Result<()> {
|
||||
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, time());
|
||||
self.msg.update_param(context).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_ended(&self) -> bool {
|
||||
self.msg.param.exists(CALL_ENDED_TIMESTAMP)
|
||||
}
|
||||
|
||||
fn get_duration_seconds(&self) -> i64 {
|
||||
if let (Some(start), Some(end)) = (
|
||||
self.msg.param.get_i64(CALL_ACCEPTED_TIMESTAMP),
|
||||
self.msg.param.get_i64(CALL_ENDED_TIMESTAMP),
|
||||
) {
|
||||
let seconds = end - start;
|
||||
if seconds <= 0 {
|
||||
return 1;
|
||||
}
|
||||
return seconds;
|
||||
}
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -82,11 +138,10 @@ impl Context {
|
||||
ensure!(chat.typ == Chattype::Single && !chat.is_self_talk());
|
||||
|
||||
let mut call = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: "Calling...".into(),
|
||||
viewtype: Viewtype::Call,
|
||||
text: "Outgoing call".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?;
|
||||
|
||||
@@ -107,60 +162,70 @@ impl Context {
|
||||
accept_call_info: String,
|
||||
) -> Result<()> {
|
||||
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
|
||||
ensure!(call.is_incoming);
|
||||
ensure!(call.is_incoming());
|
||||
if call.is_accepted() || call.is_ended() {
|
||||
info!(self, "Call already accepted/ended");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
call.mark_as_accepted(self).await?;
|
||||
let chat = Chat::load_from_db(self, call.msg.chat_id).await?;
|
||||
if chat.is_contact_request() {
|
||||
chat.id.accept(self).await?;
|
||||
}
|
||||
|
||||
call.msg
|
||||
.mark_call_as_accepted(self, accept_call_info.to_string())
|
||||
.await?;
|
||||
|
||||
// send an acceptance message around: to the caller as well as to the other devices of the callee
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: "Call accepted".into(),
|
||||
text: "[Call accepted]".into(),
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(SystemMessage::CallAccepted);
|
||||
msg.hidden = true;
|
||||
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,
|
||||
});
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancel, reject or hangup an incoming or outgoing call.
|
||||
/// Cancel, decline or hangup an incoming or outgoing call.
|
||||
pub async fn end_call(&self, call_id: MsgId) -> Result<()> {
|
||||
let call: CallInfo = self.load_call_by_id(call_id).await?;
|
||||
|
||||
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;
|
||||
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
|
||||
if call.is_ended() {
|
||||
info!(self, "Call already ended");
|
||||
return Ok(());
|
||||
}
|
||||
call.mark_as_ended(self).await?;
|
||||
|
||||
if !call.is_accepted() {
|
||||
if call.is_incoming() {
|
||||
call.update_text(self, "Declined call").await?;
|
||||
} else {
|
||||
call.update_text(self, "Cancelled call").await?;
|
||||
}
|
||||
} else {
|
||||
call.update_text_duration(self).await?;
|
||||
}
|
||||
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: "[Call ended]".into(),
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(SystemMessage::CallEnded);
|
||||
msg.hidden = true;
|
||||
msg.set_quote(self, Some(&call.msg)).await?;
|
||||
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
|
||||
|
||||
self.emit_event(EventType::CallEnded {
|
||||
msg_id: call.msg.id,
|
||||
});
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -170,8 +235,15 @@ impl Context {
|
||||
call_id: MsgId,
|
||||
) -> Result<()> {
|
||||
sleep(Duration::from_secs(wait)).await;
|
||||
let call = context.load_call_by_id(call_id).await?;
|
||||
if !call.is_accepted {
|
||||
let mut call = context.load_call_by_id(call_id).await?;
|
||||
if !call.is_accepted() && !call.is_ended() {
|
||||
call.mark_as_ended(&context).await?;
|
||||
if call.is_incoming() {
|
||||
call.update_text(&context, "Missed call").await?;
|
||||
} else {
|
||||
call.update_text(&context, "Cancelled call").await?;
|
||||
}
|
||||
context.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
context.emit_event(EventType::CallEnded {
|
||||
msg_id: call.msg.id,
|
||||
});
|
||||
@@ -181,70 +253,94 @@ impl Context {
|
||||
|
||||
pub(crate) async fn handle_call_msg(
|
||||
&self,
|
||||
mime_message: &MimeMessage,
|
||||
call_id: MsgId,
|
||||
mime_message: &MimeMessage,
|
||||
from_id: ContactId,
|
||||
) -> 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,
|
||||
));
|
||||
if mime_message.is_call() {
|
||||
let call = self.load_call_by_id(call_id).await?;
|
||||
if call.is_incoming() {
|
||||
if call.is_stale() {
|
||||
call.update_text(self, "Missed call").await?;
|
||||
self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
|
||||
} else {
|
||||
call.update_text(self, "Incoming call").await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
|
||||
self.emit_event(EventType::IncomingCall {
|
||||
msg_id: call.msg.id,
|
||||
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 {
|
||||
call.update_text(self, "Outgoing call").await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
}
|
||||
} else {
|
||||
match mime_message.is_system_message {
|
||||
SystemMessage::CallAccepted => {
|
||||
let mut call = self.load_call_by_id(call_id).await?;
|
||||
if call.is_ended() || call.is_accepted() {
|
||||
info!(self, "CallAccepted received for accepted/ended call");
|
||||
return Ok(());
|
||||
}
|
||||
} 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 });
|
||||
call.mark_as_accepted(self).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,
|
||||
});
|
||||
} else {
|
||||
let accept_call_info = mime_message
|
||||
.get_header(HeaderDef::ChatWebrtcAccepted)
|
||||
.unwrap_or_default();
|
||||
self.emit_event(EventType::OutgoingCallAccepted {
|
||||
msg_id: call.msg.id,
|
||||
accept_call_info: accept_call_info.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
SystemMessage::CallEnded => {
|
||||
let mut call = self.load_call_by_id(call_id).await?;
|
||||
if call.is_ended() {
|
||||
// may happen eg. if a a message is missed
|
||||
info!(self, "CallEnded received for ended call");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
call.mark_as_ended(self).await?;
|
||||
if !call.is_accepted() {
|
||||
if call.is_incoming() {
|
||||
if from_id == ContactId::SELF {
|
||||
call.update_text(self, "Declined call").await?;
|
||||
} else {
|
||||
call.update_text(self, "Missed call").await?;
|
||||
}
|
||||
} else {
|
||||
// outgoing
|
||||
if from_id == ContactId::SELF {
|
||||
call.update_text(self, "Cancelled call").await?;
|
||||
} else {
|
||||
call.update_text(self, "Declined call").await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
call.update_text_duration(self).await?;
|
||||
}
|
||||
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
self.emit_event(EventType::CallEnded {
|
||||
msg_id: call.msg.id,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -255,14 +351,9 @@ impl Context {
|
||||
}
|
||||
|
||||
fn load_call_by_message(&self, call: Message) -> Result<CallInfo> {
|
||||
ensure!(
|
||||
call.get_info_type() == SystemMessage::IncomingCall
|
||||
|| call.get_info_type() == SystemMessage::OutgoingCall
|
||||
);
|
||||
ensure!(call.viewtype == Viewtype::Call);
|
||||
|
||||
Ok(CallInfo {
|
||||
is_incoming: call.get_info_type() == SystemMessage::IncomingCall,
|
||||
is_accepted: call.is_call_accepted()?,
|
||||
place_call_info: call
|
||||
.param
|
||||
.get(Param::WebrtcRoom)
|
||||
@@ -278,30 +369,5 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::test_utils::{TestContext, TestContextManager, sync};
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
struct CallSetup {
|
||||
pub alice: TestContext,
|
||||
pub alice2: TestContext,
|
||||
pub alice_call: Message,
|
||||
pub alice2_call: Message,
|
||||
pub bob: TestContext,
|
||||
pub bob2: TestContext,
|
||||
pub bob_call: Message,
|
||||
pub bob2_call: Message,
|
||||
}
|
||||
|
||||
async fn assert_text(t: &TestContext, call_id: MsgId, text: &str) -> Result<()> {
|
||||
assert_eq!(Message::load_from_db(t, call_id).await?.text, text);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn setup_call() -> Result<CallSetup> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
@@ -26,18 +32,20 @@ async fn setup_call() -> Result<CallSetup> {
|
||||
// Alice's other device sees the same message as an outgoing call.
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let test_msg_id = alice
|
||||
.place_outgoing_call(alice_chat.id, "place_info".to_string())
|
||||
.place_outgoing_call(alice_chat.id, "place-info-123".to_string())
|
||||
.await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
assert_eq!(sent1.sender_msg_id, test_msg_id);
|
||||
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);
|
||||
assert!(!m.is_info());
|
||||
assert_eq!(m.viewtype, Viewtype::Call);
|
||||
let info = t.load_call_by_id(m.id).await?;
|
||||
assert!(!info.is_accepted);
|
||||
assert_eq!(info.place_call_info, "place_info");
|
||||
assert!(!info.is_incoming());
|
||||
assert!(!info.is_accepted());
|
||||
assert_eq!(info.place_call_info, "place-info-123");
|
||||
assert_text(t, m.id, "Outgoing call").await?;
|
||||
}
|
||||
|
||||
// Bob receives the message referring to the call on two devices;
|
||||
@@ -45,20 +53,23 @@ async fn setup_call() -> Result<CallSetup> {
|
||||
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);
|
||||
assert!(!m.is_info());
|
||||
assert_eq!(m.viewtype, Viewtype::Call);
|
||||
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");
|
||||
assert!(info.is_incoming());
|
||||
assert!(!info.is_accepted());
|
||||
assert_eq!(info.place_call_info, "place-info-123");
|
||||
assert_text(t, m.id, "Incoming call").await?;
|
||||
}
|
||||
|
||||
Ok(CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
@@ -71,55 +82,53 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_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())
|
||||
// Bob accepts the incoming call
|
||||
bob.accept_incoming_call(bob_call.id, "accept-info-456".to_string())
|
||||
.await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.await;
|
||||
let sent2 = bob.pop_sent_msg().await;
|
||||
let info = bob.load_call_by_id(bob_call.id).await?;
|
||||
assert!(info.is_accepted);
|
||||
assert_eq!(info.place_call_info, "place_info");
|
||||
assert_eq!(info.accept_call_info, "accepted_info");
|
||||
assert!(info.is_accepted());
|
||||
assert_eq!(info.place_call_info, "place-info-123");
|
||||
|
||||
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.recv_msg_trash(&sent2).await;
|
||||
assert_text(&bob, bob_call.id, "Incoming call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
|
||||
.await;
|
||||
let info = bob2.load_call_by_id(bob2_call.id).await?;
|
||||
assert!(!info.is_accepted); // "accepted" is only true on the device that does the call
|
||||
assert!(info.is_accepted());
|
||||
|
||||
// Alice receives the acceptance message
|
||||
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
|
||||
alice.recv_msg_trash(&sent2).await;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call").await?;
|
||||
let ev = 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
|
||||
ev,
|
||||
EventType::OutgoingCallAccepted {
|
||||
msg_id: alice2_call.id,
|
||||
accept_call_info: "accept-info-456".to_string()
|
||||
}
|
||||
);
|
||||
let info = alice.load_call_by_id(alice_call.id).await?;
|
||||
assert!(info.is_accepted());
|
||||
assert_eq!(info.place_call_info, "place-info-123");
|
||||
|
||||
alice2.recv_msg_trash(&sent2).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
|
||||
@@ -129,6 +138,7 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
@@ -136,46 +146,45 @@ async fn accept_call() -> Result<CallSetup> {
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
alice_call,
|
||||
alice2,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
..
|
||||
} = accept_call().await?;
|
||||
|
||||
// Bob has accepted the call and also ends it
|
||||
bob.end_call(bob_call.id).await?;
|
||||
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
let sent3 = bob.pop_sent_msg().await;
|
||||
|
||||
let bob2_end_call_msg = bob2.recv_msg(&sent3).await;
|
||||
assert_is_call_ended_info_msg(bob2_end_call_msg);
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
|
||||
// Alice receives the ending message
|
||||
let alice_end_call_msg = alice.recv_msg(&sent3).await;
|
||||
assert_is_call_ended_info_msg(alice_end_call_msg);
|
||||
alice.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
|
||||
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.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
@@ -189,37 +198,41 @@ async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
// Alice calls Bob, Bob accepts
|
||||
let CallSetup {
|
||||
alice,
|
||||
alice_call,
|
||||
alice2,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
..
|
||||
} = accept_call().await?;
|
||||
|
||||
// Bob has accepted the call but Alice ends it
|
||||
alice.end_call(bob_call.id).await?;
|
||||
alice.end_call(alice_call.id).await?;
|
||||
assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.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.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
|
||||
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.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?;
|
||||
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.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -231,25 +244,47 @@ async fn test_accept_call_caller_ends() -> Result<()> {
|
||||
async fn test_callee_rejects_call() -> Result<()> {
|
||||
// Alice calls Bob
|
||||
let CallSetup {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
..
|
||||
} = setup_call().await?;
|
||||
|
||||
// Bob does not want to talk with Alice.
|
||||
// To protect Bob's privacy, no message is sent to Alice (who will time out).
|
||||
// To let Bob close the call window on all devices, a sync message is used instead.
|
||||
// Bob has accepted Alice before, but does not want to talk with Alice
|
||||
bob_call.chat_id.accept(&bob).await?;
|
||||
bob.end_call(bob_call.id).await?;
|
||||
assert_text(&bob, bob_call.id, "Declined call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
let sent3 = bob.pop_sent_msg().await;
|
||||
|
||||
sync(&bob, &bob2).await;
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Declined call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
|
||||
// Alice receives decline message
|
||||
alice.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice, alice_call.id, "Declined call").await?;
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
|
||||
alice2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Declined call").await?;
|
||||
alice2
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -260,35 +295,39 @@ async fn test_caller_cancels_call() -> Result<()> {
|
||||
alice,
|
||||
alice2,
|
||||
alice_call,
|
||||
alice2_call,
|
||||
bob,
|
||||
bob2,
|
||||
bob_call,
|
||||
bob2_call,
|
||||
..
|
||||
} = setup_call().await?;
|
||||
|
||||
// Alice changes their mind before Bob picks up
|
||||
alice.end_call(alice_call.id).await?;
|
||||
assert_text(&alice, alice_call.id, "Cancelled call").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.recv_msg_trash(&sent3).await;
|
||||
assert_text(&alice2, alice2_call.id, "Cancelled call").await?;
|
||||
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.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob, bob_call.id, "Missed call").await?;
|
||||
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.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Missed call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -306,7 +345,7 @@ async fn test_is_stale_call() -> Result<()> {
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!call_info.is_stale_call());
|
||||
assert!(!call_info.is_stale());
|
||||
let remaining_seconds = call_info.remaining_ring_seconds();
|
||||
assert!(remaining_seconds == RINGING_SECONDS || remaining_seconds == RINGING_SECONDS - 1);
|
||||
|
||||
@@ -318,7 +357,7 @@ async fn test_is_stale_call() -> Result<()> {
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!call_info.is_stale_call());
|
||||
assert!(!call_info.is_stale());
|
||||
let remaining_seconds = call_info.remaining_ring_seconds();
|
||||
assert!(remaining_seconds == RINGING_SECONDS - 5 || remaining_seconds == RINGING_SECONDS - 6);
|
||||
|
||||
@@ -330,28 +369,32 @@ async fn test_is_stale_call() -> Result<()> {
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
assert!(call_info.is_stale_call());
|
||||
assert!(call_info.is_stale());
|
||||
assert_eq!(call_info.remaining_ring_seconds(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mark_call_as_accepted() -> Result<()> {
|
||||
async fn test_mark_calls() -> Result<()> {
|
||||
let CallSetup {
|
||||
alice, alice_call, ..
|
||||
} = setup_call().await?;
|
||||
assert!(!alice_call.is_call_accepted()?);
|
||||
|
||||
let mut alice_call = Message::load_from_db(&alice, alice_call.id).await?;
|
||||
assert!(!alice_call.is_call_accepted()?);
|
||||
alice_call
|
||||
.mark_call_as_accepted(&alice, "accepted_info".to_string())
|
||||
.await?;
|
||||
assert!(alice_call.is_call_accepted()?);
|
||||
let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?;
|
||||
assert!(!call_info.is_accepted());
|
||||
assert!(!call_info.is_ended());
|
||||
call_info.mark_as_accepted(&alice).await?;
|
||||
assert!(call_info.is_accepted());
|
||||
assert!(!call_info.is_ended());
|
||||
|
||||
let alice_call = Message::load_from_db(&alice, alice_call.id).await?;
|
||||
assert!(alice_call.is_call_accepted()?);
|
||||
let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?;
|
||||
assert!(call_info.is_accepted());
|
||||
assert!(!call_info.is_ended());
|
||||
|
||||
call_info.mark_as_ended(&alice).await?;
|
||||
assert!(call_info.is_accepted());
|
||||
assert!(call_info.is_ended());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2717,7 +2717,10 @@ impl ChatIdBlocked {
|
||||
}
|
||||
|
||||
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
|
||||
if msg.viewtype == Viewtype::Text
|
||||
|| msg.viewtype == Viewtype::VideochatInvitation
|
||||
|| msg.viewtype == Viewtype::Call
|
||||
{
|
||||
// the caller should check if the message text is empty
|
||||
} else if msg.viewtype.has_file() {
|
||||
let viewtype_orig = msg.viewtype;
|
||||
@@ -3204,6 +3207,7 @@ pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: Strin
|
||||
original_msg.viewtype != Viewtype::VideochatInvitation,
|
||||
"Cannot edit videochat invitations"
|
||||
);
|
||||
ensure!(original_msg.viewtype != Viewtype::Call, "Cannot edit calls");
|
||||
ensure!(
|
||||
!original_msg.text.is_empty(), // avoid complexity in UI element changes. focus is typos and rewordings
|
||||
"Cannot add text"
|
||||
|
||||
@@ -151,10 +151,6 @@ pub enum Config {
|
||||
/// setting up a second device, or receiving a sync message.
|
||||
BccSelf,
|
||||
|
||||
/// True if encryption is preferred according to Autocrypt standard.
|
||||
#[strum(props(default = "1"))]
|
||||
E2eeEnabled,
|
||||
|
||||
/// True if Message Delivery Notifications (read receipts) should
|
||||
/// be sent and requested.
|
||||
#[strum(props(default = "1"))]
|
||||
@@ -705,7 +701,6 @@ impl Context {
|
||||
Config::Socks5Enabled
|
||||
| Config::ProxyEnabled
|
||||
| Config::BccSelf
|
||||
| Config::E2eeEnabled
|
||||
| Config::MdnsEnabled
|
||||
| Config::SentboxWatch
|
||||
| Config::MvboxMove
|
||||
|
||||
@@ -833,7 +833,6 @@ impl Context {
|
||||
.query_get_value("PRAGMA journal_mode;", ())
|
||||
.await?
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?;
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await?;
|
||||
let sync_msgs = self.get_config_int(Config::SyncMsgs).await?;
|
||||
@@ -967,7 +966,6 @@ impl Context {
|
||||
res.insert("configured_mvbox_folder", configured_mvbox_folder);
|
||||
res.insert("configured_trash_folder", configured_trash_folder);
|
||||
res.insert("mdns_enabled", mdns_enabled.to_string());
|
||||
res.insert("e2ee_enabled", e2ee_enabled.to_string());
|
||||
res.insert("bcc_self", bcc_self.to_string());
|
||||
res.insert("sync_msgs", sync_msgs.to_string());
|
||||
res.insert("disable_idle", disable_idle.to_string());
|
||||
|
||||
15
src/e2ee.rs
15
src/e2ee.rs
@@ -4,10 +4,8 @@ use std::io::Cursor;
|
||||
|
||||
use anyhow::Result;
|
||||
use mail_builder::mime::MimePart;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::key::{SignedPublicKey, load_self_public_key, load_self_secret_key};
|
||||
use crate::pgp;
|
||||
@@ -21,9 +19,7 @@ pub struct EncryptHelper {
|
||||
|
||||
impl EncryptHelper {
|
||||
pub async fn new(context: &Context) -> Result<EncryptHelper> {
|
||||
let prefer_encrypt =
|
||||
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
|
||||
.unwrap_or_default();
|
||||
let prefer_encrypt = EncryptPreference::Mutual;
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
let public_key = load_self_public_key(context).await?;
|
||||
|
||||
@@ -35,9 +31,12 @@ impl EncryptHelper {
|
||||
}
|
||||
|
||||
pub fn get_aheader(&self) -> Aheader {
|
||||
let pk = self.public_key.clone();
|
||||
let addr = self.addr.to_string();
|
||||
Aheader::new(addr, pk, self.prefer_encrypt)
|
||||
Aheader {
|
||||
addr: self.addr.clone(),
|
||||
public_key: self.public_key.clone(),
|
||||
prefer_encrypt: self.prefer_encrypt,
|
||||
verified: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to encrypt the passed in `mail`.
|
||||
|
||||
@@ -388,8 +388,6 @@ pub enum EventType {
|
||||
IncomingCallAccepted {
|
||||
/// ID of the message referring to the call.
|
||||
msg_id: MsgId,
|
||||
/// User-defined info as passed to accept_incoming_call()
|
||||
accept_call_info: String,
|
||||
},
|
||||
|
||||
/// Outgoing call accepted.
|
||||
|
||||
26
src/imex.rs
26
src/imex.rs
@@ -140,32 +140,8 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
|
||||
}
|
||||
|
||||
async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
|
||||
// try hard to only modify key-state
|
||||
let (private_key, header) = SignedSecretKey::from_asc(armored)?;
|
||||
let private_key = SignedSecretKey::from_asc(armored)?;
|
||||
let public_key = private_key.split_public_key()?;
|
||||
if let Some(preferencrypt) = header.get("Autocrypt-Prefer-Encrypt") {
|
||||
let e2ee_enabled = match preferencrypt.as_str() {
|
||||
"nopreference" => 0,
|
||||
"mutual" => 1,
|
||||
_ => {
|
||||
bail!("invalid Autocrypt-Prefer-Encrypt header: {:?}", header);
|
||||
}
|
||||
};
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int("e2ee_enabled", e2ee_enabled)
|
||||
.await?;
|
||||
} else {
|
||||
// `Autocrypt-Prefer-Encrypt` is not included
|
||||
// in keys exported to file.
|
||||
//
|
||||
// `Autocrypt-Prefer-Encrypt` also SHOULD be sent
|
||||
// in Autocrypt Setup Message according to Autocrypt specification,
|
||||
// but K-9 6.802 does not include this header.
|
||||
//
|
||||
// We keep current setting in this case.
|
||||
info!(context, "No Autocrypt-Prefer-Encrypt header.");
|
||||
};
|
||||
|
||||
let keypair = pgp::KeyPair {
|
||||
public: public_key,
|
||||
|
||||
@@ -93,10 +93,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
|
||||
bail!("Passphrase must be at least 2 chars long.");
|
||||
};
|
||||
let private_key = load_self_secret_key(context).await?;
|
||||
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await? {
|
||||
false => None,
|
||||
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
|
||||
};
|
||||
let ac_headers = Some(("Autocrypt-Prefer-Encrypt", "mutual"));
|
||||
let private_key_asc = private_key.to_asc(ac_headers);
|
||||
let encr = pgp::symm_encrypt_setup_file(passphrase, private_key_asc.into_bytes())
|
||||
.await?
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::context::Context;
|
||||
@@ -14,7 +13,7 @@ pub use crate::pgp;
|
||||
|
||||
use self::pgp::KeyPair;
|
||||
|
||||
pub fn key_from_asc(data: &str) -> Result<(key::SignedSecretKey, BTreeMap<String, String>)> {
|
||||
pub fn key_from_asc(data: &str) -> Result<key::SignedSecretKey> {
|
||||
key::SignedSecretKey::from_asc(data)
|
||||
}
|
||||
|
||||
|
||||
32
src/key.rs
32
src/key.rs
@@ -71,31 +71,17 @@ pub(crate) trait DcKey: Serialize + Deserializable + Clone {
|
||||
}
|
||||
|
||||
/// Create a key from an ASCII-armored string.
|
||||
///
|
||||
/// Returns the key and a map of any headers which might have been set in
|
||||
/// the ASCII-armored representation.
|
||||
fn from_asc(data: &str) -> Result<(Self, BTreeMap<String, String>)> {
|
||||
fn from_asc(data: &str) -> Result<Self> {
|
||||
let bytes = data.as_bytes();
|
||||
let res = Self::from_armor_single(Cursor::new(bytes));
|
||||
let (key, headers) = match res {
|
||||
let (key, _headers) = match res {
|
||||
Err(pgp::errors::Error::NoMatchingPacket { .. }) => match Self::is_private() {
|
||||
true => bail!("No private key packet found"),
|
||||
false => bail!("No public key packet found"),
|
||||
},
|
||||
_ => res.context("rPGP error")?,
|
||||
};
|
||||
let headers = headers
|
||||
.into_iter()
|
||||
.map(|(key, values)| {
|
||||
(
|
||||
key.trim().to_lowercase(),
|
||||
values
|
||||
.last()
|
||||
.map_or_else(String::new, |s| s.trim().to_string()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Ok((key, headers))
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// Serialise the key as bytes.
|
||||
@@ -446,7 +432,7 @@ pub(crate) async fn store_self_keypair(context: &Context, keypair: &KeyPair) ->
|
||||
/// to avoid generating the key in tests.
|
||||
/// Use import/export APIs instead.
|
||||
pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Result<()> {
|
||||
let secret = SignedSecretKey::from_asc(secret_data)?.0;
|
||||
let secret = SignedSecretKey::from_asc(secret_data)?;
|
||||
let public = secret.split_public_key()?;
|
||||
let keypair = KeyPair { public, secret };
|
||||
store_self_keypair(context, &keypair).await?;
|
||||
@@ -532,7 +518,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_from_armored_string() {
|
||||
let (private_key, _) = SignedSecretKey::from_asc(
|
||||
let private_key = SignedSecretKey::from_asc(
|
||||
"-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
xcLYBF0fgz4BCADnRUV52V4xhSsU56ZaAn3+3oG86MZhXy4X8w14WZZDf0VJGeTh
|
||||
@@ -600,17 +586,13 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
fn test_asc_roundtrip() {
|
||||
let key = KEYPAIR.public.clone();
|
||||
let asc = key.to_asc(Some(("spam", "ham")));
|
||||
let (key2, hdrs) = SignedPublicKey::from_asc(&asc).unwrap();
|
||||
let key2 = SignedPublicKey::from_asc(&asc).unwrap();
|
||||
assert_eq!(key, key2);
|
||||
assert_eq!(hdrs.len(), 1);
|
||||
assert_eq!(hdrs.get("spam"), Some(&String::from("ham")));
|
||||
|
||||
let key = KEYPAIR.secret.clone();
|
||||
let asc = key.to_asc(Some(("spam", "ham")));
|
||||
let (key2, hdrs) = SignedSecretKey::from_asc(&asc).unwrap();
|
||||
let key2 = SignedSecretKey::from_asc(&asc).unwrap();
|
||||
assert_eq!(key, key2);
|
||||
assert_eq!(hdrs.len(), 1);
|
||||
assert_eq!(hdrs.get("spam"), Some(&String::from("ham")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -973,8 +973,6 @@ impl Message {
|
||||
| SystemMessage::WebxdcStatusUpdate
|
||||
| SystemMessage::WebxdcInfoMessage
|
||||
| SystemMessage::IrohNodeAddr
|
||||
| SystemMessage::OutgoingCall
|
||||
| SystemMessage::IncomingCall
|
||||
| SystemMessage::CallAccepted
|
||||
| SystemMessage::CallEnded
|
||||
| SystemMessage::Unknown => Ok(None),
|
||||
@@ -2293,6 +2291,9 @@ pub enum Viewtype {
|
||||
/// Message is an invitation to a videochat.
|
||||
VideochatInvitation = 70,
|
||||
|
||||
/// Message is an incoming or outgoing call.
|
||||
Call = 71,
|
||||
|
||||
/// Message is an webxdc instance.
|
||||
Webxdc = 80,
|
||||
|
||||
@@ -2316,6 +2317,7 @@ impl Viewtype {
|
||||
Viewtype::Video => true,
|
||||
Viewtype::File => true,
|
||||
Viewtype::VideochatInvitation => false,
|
||||
Viewtype::Call => false,
|
||||
Viewtype::Webxdc => true,
|
||||
Viewtype::Vcard => true,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use base64::Engine as _;
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use deltachat_contact_tools::sanitize_bidi_characters;
|
||||
@@ -1132,13 +1132,14 @@ impl MimeFactory {
|
||||
continue;
|
||||
}
|
||||
|
||||
let header = Aheader::new(
|
||||
addr.clone(),
|
||||
key.clone(),
|
||||
let header = Aheader {
|
||||
addr: addr.clone(),
|
||||
public_key: key.clone(),
|
||||
// Autocrypt 1.1.0 specification says that
|
||||
// `prefer-encrypt` attribute SHOULD NOT be included.
|
||||
EncryptPreference::NoPreference,
|
||||
)
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
verified: false,
|
||||
}
|
||||
.to_string();
|
||||
|
||||
message = message.header(
|
||||
@@ -1614,15 +1615,6 @@ 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",
|
||||
@@ -1659,6 +1651,14 @@ impl MimeFactory {
|
||||
"Chat-Content",
|
||||
mail_builder::headers::raw::Raw::new("videochat-invitation").into(),
|
||||
));
|
||||
} else if msg.viewtype == Viewtype::Call {
|
||||
headers.push((
|
||||
"Chat-Content",
|
||||
mail_builder::headers::raw::Raw::new("call").into(),
|
||||
));
|
||||
placeholdertext = Some(
|
||||
"[This is a 'Call'. The sender uses an experiment not supported on your version yet]".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if msg.param.exists(Param::WebrtcRoom) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! # MIME message parsing module.
|
||||
|
||||
use std::cmp::min;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
use std::str;
|
||||
use std::str::FromStr;
|
||||
@@ -36,6 +36,17 @@ use crate::tools::{
|
||||
use crate::{chatlist_events, location, stock_str, tools};
|
||||
use crate::{constants, token};
|
||||
|
||||
/// Public key extracted from `Autocrypt-Gossip`
|
||||
/// header with associated information.
|
||||
#[derive(Debug)]
|
||||
pub struct GossipedKey {
|
||||
/// Public key extracted from `keydata` attribute.
|
||||
pub public_key: SignedPublicKey,
|
||||
|
||||
/// True if `Autocrypt-Gossip` has a `_verified` attribute.
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
/// A parsed MIME message.
|
||||
///
|
||||
/// This represents the relevant information of a parsed MIME message
|
||||
@@ -85,7 +96,7 @@ pub(crate) struct MimeMessage {
|
||||
|
||||
/// The addresses for which there was a gossip header
|
||||
/// and their respective gossiped keys.
|
||||
pub gossiped_keys: HashMap<String, SignedPublicKey>,
|
||||
pub gossiped_keys: BTreeMap<String, GossipedKey>,
|
||||
|
||||
/// Fingerprint of the key in the Autocrypt header.
|
||||
///
|
||||
@@ -217,17 +228,7 @@ 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.
|
||||
@@ -705,12 +706,6 @@ 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" {
|
||||
@@ -751,6 +746,8 @@ impl MimeMessage {
|
||||
if let Some(room) = room {
|
||||
if content == "videochat-invitation" {
|
||||
part.typ = Viewtype::VideochatInvitation;
|
||||
} else if content == "call" {
|
||||
part.typ = Viewtype::Call
|
||||
}
|
||||
part.param.set(Param::WebrtcRoom, room);
|
||||
} else if let Some(accepted) = accepted {
|
||||
@@ -780,7 +777,10 @@ impl MimeMessage {
|
||||
| Viewtype::Vcard
|
||||
| Viewtype::File
|
||||
| Viewtype::Webxdc => true,
|
||||
Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
|
||||
Viewtype::Unknown
|
||||
| Viewtype::Text
|
||||
| Viewtype::VideochatInvitation
|
||||
| Viewtype::Call => false,
|
||||
})
|
||||
{
|
||||
let mut parts = std::mem::take(&mut self.parts);
|
||||
@@ -1532,7 +1532,7 @@ impl MimeMessage {
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
Ok((key, _)) => key,
|
||||
Ok(key) => key,
|
||||
};
|
||||
if let Err(err) = key.verify() {
|
||||
warn!(context, "Attached PGP key verification failed: {err:#}.");
|
||||
@@ -1595,6 +1595,13 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a message is a call.
|
||||
pub(crate) fn is_call(&self) -> bool {
|
||||
self.parts
|
||||
.first()
|
||||
.is_some_and(|part| part.typ == Viewtype::Call)
|
||||
}
|
||||
|
||||
pub fn replace_msg_by_error(&mut self, error_msg: &str) {
|
||||
self.is_system_message = SystemMessage::Unknown;
|
||||
if let Some(part) = self.parts.first_mut() {
|
||||
@@ -1989,9 +1996,9 @@ async fn parse_gossip_headers(
|
||||
from: &str,
|
||||
recipients: &[SingleInfo],
|
||||
gossip_headers: Vec<String>,
|
||||
) -> Result<HashMap<String, SignedPublicKey>> {
|
||||
) -> Result<BTreeMap<String, GossipedKey>> {
|
||||
// XXX split the parsing from the modification part
|
||||
let mut gossiped_keys: HashMap<String, SignedPublicKey> = Default::default();
|
||||
let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default();
|
||||
|
||||
for value in &gossip_headers {
|
||||
let header = match value.parse::<Aheader>() {
|
||||
@@ -2033,7 +2040,12 @@ async fn parse_gossip_headers(
|
||||
)
|
||||
.await?;
|
||||
|
||||
gossiped_keys.insert(header.addr.to_lowercase(), header.public_key);
|
||||
let gossiped_key = GossipedKey {
|
||||
public_key: header.public_key,
|
||||
|
||||
verified: header.verified,
|
||||
};
|
||||
gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key);
|
||||
}
|
||||
|
||||
Ok(gossiped_keys)
|
||||
|
||||
@@ -13,8 +13,8 @@ use std::sync::LazyLock;
|
||||
// 163.md: 163.com
|
||||
static P_163: Provider = Provider {
|
||||
id: "163",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "Enable \"POP3/SMTP/IMAP\" on the website, add a third-party auth code and use that as the login password",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/163",
|
||||
server: &[
|
||||
@@ -98,7 +98,7 @@ static P_ALIYUN: Provider = Provider {
|
||||
static P_AOL: Provider = Provider {
|
||||
id: "aol",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "To log in to AOL with Delta Chat, you need to set up an app password in the AOL web interface.",
|
||||
before_login_hint: "To log in to AOL, you need to set up an app password in the AOL web interface.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/aol",
|
||||
server: &[
|
||||
@@ -432,7 +432,7 @@ static P_EXAMPLE_COM: Provider = Provider {
|
||||
id: "example.com",
|
||||
status: Status::Broken,
|
||||
before_login_hint: "Hush this provider doesn't exist!",
|
||||
after_login_hint: "This provider doesn't really exist, so you can't use it :/ If you need an email provider for Delta Chat, take a look at providers.delta.chat!",
|
||||
after_login_hint: "This provider doesn't really exist, so you can't use it :/ If you need an email provider, take a look at providers.delta.chat!",
|
||||
overview_page: "https://providers.delta.chat/example-com",
|
||||
server: &[
|
||||
Server {
|
||||
@@ -459,7 +459,7 @@ static P_EXAMPLE_COM: Provider = Provider {
|
||||
static P_FASTMAIL: Provider = Provider {
|
||||
id: "fastmail",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "You must create an app-specific password for Delta Chat before you can log in.",
|
||||
before_login_hint: "You must create an app-specific password before you can log in.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/fastmail",
|
||||
server: &[
|
||||
@@ -526,7 +526,7 @@ static P_FIVE_CHAT: Provider = Provider {
|
||||
static P_FREENET_DE: Provider = Provider {
|
||||
id: "freenet.de",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "Um deine freenet.de E-Mail-Adresse mit Delta Chat zu benutzen, musst du erst auf der freenet.de-Webseite \"POP3/IMAP/SMTP\" aktivieren.",
|
||||
before_login_hint: "Um deine freenet.de E-Mail-Adresse zu benutzen, musst du erst auf der freenet.de-Webseite \"POP3/IMAP/SMTP\" aktivieren.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/freenet-de",
|
||||
server: &[
|
||||
@@ -647,10 +647,6 @@ static P_HERMES_RADIO: Provider = Provider {
|
||||
key: Config::MdnsEnabled,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::E2eeEnabled,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::ShowEmails,
|
||||
value: "2",
|
||||
@@ -663,7 +659,7 @@ static P_HERMES_RADIO: Provider = Provider {
|
||||
static P_HEY_COM: Provider = Provider {
|
||||
id: "hey.com",
|
||||
status: Status::Broken,
|
||||
before_login_hint: "hey.com does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to hey.com.",
|
||||
before_login_hint: "hey.com does not offer the standard IMAP e-mail protocol, so you cannot log in to hey.com.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/hey-com",
|
||||
server: &[],
|
||||
@@ -702,7 +698,7 @@ static P_I3_NET: Provider = Provider {
|
||||
static P_ICLOUD: Provider = Provider {
|
||||
id: "icloud",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "You must create an app-specific password for Delta Chat before login.",
|
||||
before_login_hint: "You must create an app-specific password before login.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/icloud",
|
||||
server: &[
|
||||
@@ -787,7 +783,7 @@ static P_KONTENT_COM: Provider = Provider {
|
||||
static P_MAIL_COM: Provider = Provider {
|
||||
id: "mail.com",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "To log in with Delta Chat, you first need to activate POP3/IMAP in your mail.com settings. Note that this is a mail.com Premium feature only.",
|
||||
before_login_hint: "To log in, you first need to activate POP3/IMAP in your mail.com settings. Note that this is a mail.com Premium feature only.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/mail-com",
|
||||
server: &[],
|
||||
@@ -828,7 +824,7 @@ static P_MAIL_DE: Provider = Provider {
|
||||
static P_MAIL_RU: Provider = Provider {
|
||||
id: "mail.ru",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru, чтобы mail.ru работал с Delta Chat.",
|
||||
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru, чтобы mail.ru работал с chatmail.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/mail-ru",
|
||||
server: &[
|
||||
@@ -1222,8 +1218,8 @@ static P_NUBO_COOP: Provider = Provider {
|
||||
// outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com, outlook.de
|
||||
static P_OUTLOOK_COM: Provider = Provider {
|
||||
id: "outlook.com",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
status: Status::Broken,
|
||||
before_login_hint: "Unfortunately, Outlook does not allow using passwords anymore, per-app-passwords are currently not working.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/outlook-com",
|
||||
server: &[
|
||||
@@ -1321,8 +1317,8 @@ static P_POSTEO: Provider = Provider {
|
||||
static P_PROTONMAIL: Provider = Provider {
|
||||
id: "protonmail",
|
||||
status: Status::Broken,
|
||||
before_login_hint: "Protonmail does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to Protonmail.",
|
||||
after_login_hint: "To use Delta Chat with Protonmail, the IMAP bridge must be running in the background. If you have connectivity issues, double check whether it works as expected.",
|
||||
before_login_hint: "Protonmail does not offer the standard IMAP e-mail protocol, so you cannot log in with to Protonmail.",
|
||||
after_login_hint: "To use Protonmail, the IMAP bridge must be running in the background. If you have connectivity issues, double check whether it works as expected.",
|
||||
overview_page: "https://providers.delta.chat/protonmail",
|
||||
server: &[],
|
||||
opt: ProviderOptions::new(),
|
||||
@@ -1362,7 +1358,7 @@ static P_PURELYMAIL_COM: Provider = Provider {
|
||||
static P_QQ: Provider = Provider {
|
||||
id: "qq",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "Manually enabling IMAP/SMTP and creating an app-specific password for Delta Chat are required.",
|
||||
before_login_hint: "Manually enabling IMAP/SMTP and creating an app-specific password are required.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/qq",
|
||||
server: &[
|
||||
@@ -1390,7 +1386,7 @@ static P_QQ: Provider = Provider {
|
||||
static P_RAMBLER_RU: Provider = Provider {
|
||||
id: "rambler.ru",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "Чтобы войти в Рамблер/почта через Delta Chat, необходимо предварительно включить доступ с помощью почтовых клиентов на сайте mail.rambler.ru",
|
||||
before_login_hint: "Чтобы войти в Рамблер/почта, необходимо предварительно включить доступ с помощью почтовых клиентов на сайте mail.rambler.ru",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/rambler-ru",
|
||||
server: &[
|
||||
@@ -1566,7 +1562,7 @@ static P_SYSTEMLI_ORG: Provider = Provider {
|
||||
static P_T_ONLINE: Provider = Provider {
|
||||
id: "t-online",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "To use Delta Chat with a T-Online email address, you need to create an app password in the web interface.",
|
||||
before_login_hint: "To use a T-Online email address, you need to create an app password in the web interface.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/t-online",
|
||||
server: &[
|
||||
@@ -1677,7 +1673,7 @@ static P_TISCALI_IT: Provider = Provider {
|
||||
static P_TUTANOTA: Provider = Provider {
|
||||
id: "tutanota",
|
||||
status: Status::Broken,
|
||||
before_login_hint: "Tutanota does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to Tutanota.",
|
||||
before_login_hint: "Tutanota does not offer the standard IMAP e-mail protocol, so you cannot log in to Tutanota.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/tutanota",
|
||||
server: &[],
|
||||
@@ -1787,7 +1783,7 @@ static P_VIVALDI: Provider = Provider {
|
||||
static P_VK_COM: Provider = Provider {
|
||||
id: "vk.com",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru https://account.mail.ru/user/2-step-auth/passwords/ чтобы vk.com работал с Delta Chat.",
|
||||
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru https://account.mail.ru/user/2-step-auth/passwords/ чтобы vk.com работал с chatmail.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/vk-com",
|
||||
server: &[
|
||||
@@ -1906,7 +1902,7 @@ static P_WKPB_DE: Provider = Provider {
|
||||
static P_YAHOO: Provider = Provider {
|
||||
id: "yahoo",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "To use Delta Chat with your Yahoo email address you have to create an \"App-Password\" in the account security screen.",
|
||||
before_login_hint: "To use your Yahoo email address you have to create an \"App-Password\" in the account security screen.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/yahoo",
|
||||
server: &[
|
||||
@@ -2662,4 +2658,4 @@ pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider
|
||||
});
|
||||
|
||||
pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> =
|
||||
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2024, 9, 13).unwrap());
|
||||
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2025, 9, 4).unwrap());
|
||||
|
||||
@@ -74,7 +74,7 @@ fn pad_device_token(s: &str) -> String {
|
||||
///
|
||||
/// The result is base64-encoded and not ASCII armored to avoid dealing with newlines.
|
||||
pub(crate) fn encrypt_device_token(device_token: &str) -> Result<String> {
|
||||
let public_key = pgp::composed::SignedPublicKey::from_asc(NOTIFIERS_PUBLIC_KEY)?.0;
|
||||
let public_key = pgp::composed::SignedPublicKey::from_asc(NOTIFIERS_PUBLIC_KEY)?;
|
||||
let encryption_subkey = public_key
|
||||
.public_subkeys
|
||||
.first()
|
||||
|
||||
@@ -808,19 +808,18 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
authcode,
|
||||
..
|
||||
} => {
|
||||
token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?;
|
||||
token::delete(context, token::Namespace::Auth, &authcode).await?;
|
||||
token::delete(context, "").await?;
|
||||
context
|
||||
.sync_qr_code_token_deletion(invitenumber, authcode)
|
||||
.await?;
|
||||
}
|
||||
Qr::WithdrawVerifyGroup {
|
||||
grpid,
|
||||
invitenumber,
|
||||
authcode,
|
||||
..
|
||||
} => {
|
||||
token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?;
|
||||
token::delete(context, token::Namespace::Auth, &authcode).await?;
|
||||
token::delete(context, &grpid).await?;
|
||||
context
|
||||
.sync_qr_code_token_deletion(invitenumber, authcode)
|
||||
.await?;
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::*;
|
||||
use crate::chat::{ProtectionStatus, create_group_chat};
|
||||
use crate::config::Config;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::test_utils::{TestContext, TestContextManager, sync};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_http() -> Result<()> {
|
||||
@@ -509,6 +509,77 @@ async fn test_withdraw_verifygroup() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_withdraw_multidevice() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
|
||||
alice.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
alice2.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
|
||||
// Alice creates two QR codes on the first device:
|
||||
// group QR code and contact QR code.
|
||||
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
let chat2_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group 2").await?;
|
||||
let contact_qr = get_securejoin_qr(alice, None).await?;
|
||||
let group_qr = get_securejoin_qr(alice, Some(chat_id)).await?;
|
||||
let group2_qr = get_securejoin_qr(alice, Some(chat2_id)).await?;
|
||||
|
||||
assert!(matches!(
|
||||
check_qr(alice, &contact_qr).await?,
|
||||
Qr::WithdrawVerifyContact { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
check_qr(alice, &group_qr).await?,
|
||||
Qr::WithdrawVerifyGroup { .. }
|
||||
));
|
||||
|
||||
// Sync group QR codes.
|
||||
sync(alice, alice2).await;
|
||||
assert!(matches!(
|
||||
check_qr(alice2, &group_qr).await?,
|
||||
Qr::WithdrawVerifyGroup { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
check_qr(alice2, &group2_qr).await?,
|
||||
Qr::WithdrawVerifyGroup { .. }
|
||||
));
|
||||
|
||||
// Alice creates a contact QR code on second device
|
||||
// and withdraws it.
|
||||
let contact_qr2 = get_securejoin_qr(alice2, None).await?;
|
||||
set_config_from_qr(alice2, &contact_qr2).await?;
|
||||
assert!(matches!(
|
||||
check_qr(alice2, &contact_qr2).await?,
|
||||
Qr::ReviveVerifyContact { .. }
|
||||
));
|
||||
|
||||
// Alice also withdraws second group QR code on second device.
|
||||
set_config_from_qr(alice2, &group2_qr).await?;
|
||||
|
||||
// Sync messages are sent from Alice's second device to first device.
|
||||
sync(alice2, alice).await;
|
||||
|
||||
// Now first device has reset all contact QR codes
|
||||
// and second group QR code,
|
||||
// but first group QR code is still valid.
|
||||
assert!(matches!(
|
||||
check_qr(alice, &contact_qr2).await?,
|
||||
Qr::ReviveVerifyContact { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
check_qr(alice, &group_qr).await?,
|
||||
Qr::WithdrawVerifyGroup { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
check_qr(alice, &group2_qr).await?,
|
||||
Qr::ReviveVerifyGroup { .. }
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_and_apply_dclogin() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Internet Message Format reception pipeline.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::iter;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::ephemeral::{Timer as EphemeralTimer, stock_ephemeral_timer_changed};
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table};
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
use crate::key::{self_fingerprint, self_fingerprint_opt};
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{info, warn};
|
||||
@@ -35,7 +35,7 @@ use crate::logged_debug_assert;
|
||||
use crate::message::{
|
||||
self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists,
|
||||
};
|
||||
use crate::mimeparser::{AvatarAction, MimeMessage, SystemMessage, parse_message_ids};
|
||||
use crate::mimeparser::{AvatarAction, GossipedKey, MimeMessage, SystemMessage, parse_message_ids};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
|
||||
use crate::reaction::{Reaction, set_msg_reaction};
|
||||
@@ -838,7 +838,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
let fingerprint = gossiped_key.dc_fingerprint().hex();
|
||||
let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
|
||||
transaction.execute(
|
||||
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
|
||||
VALUES (?, ?, ?)
|
||||
@@ -1003,8 +1003,10 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
}
|
||||
|
||||
if mime_parser.is_system_message == SystemMessage::IncomingCall {
|
||||
context.handle_call_msg(&mime_parser, insert_msg_id).await?;
|
||||
if mime_parser.is_call() {
|
||||
context
|
||||
.handle_call_msg(insert_msg_id, &mime_parser, from_id)
|
||||
.await?;
|
||||
} else if received_msg.hidden {
|
||||
// No need to emit an event about the changed message
|
||||
} else if let Some(replace_chat_id) = replace_chat_id {
|
||||
@@ -1156,6 +1158,11 @@ async fn decide_chat_assignment(
|
||||
{
|
||||
info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
|
||||
true
|
||||
} else if mime_parser.is_system_message == SystemMessage::CallAccepted
|
||||
|| mime_parser.is_system_message == SystemMessage::CallEnded
|
||||
{
|
||||
info!(context, "Call state changed (TRASH).");
|
||||
true
|
||||
} else if mime_parser.decrypting_failed && !mime_parser.incoming {
|
||||
// Outgoing undecryptable message.
|
||||
let last_time = context
|
||||
@@ -1998,7 +2005,9 @@ async fn add_parts(
|
||||
if let Some(call) =
|
||||
message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
|
||||
{
|
||||
context.handle_call_msg(mime_parser, call.get_id()).await?;
|
||||
context
|
||||
.handle_call_msg(call.get_id(), mime_parser, from_id)
|
||||
.await?;
|
||||
} else {
|
||||
warn!(context, "Call: Cannot load parent.")
|
||||
}
|
||||
@@ -2931,7 +2940,7 @@ async fn apply_group_changes(
|
||||
// just like we have ChatGroupMemberRemovedFpr.
|
||||
// The result of the error is that info message
|
||||
// may contain display name of the wrong contact.
|
||||
let fingerprint = key.dc_fingerprint().hex();
|
||||
let fingerprint = key.public_key.dc_fingerprint().hex();
|
||||
if let Some(contact_id) =
|
||||
lookup_key_contact_by_fingerprint(context, &fingerprint).await?
|
||||
{
|
||||
@@ -3750,10 +3759,28 @@ async fn mark_recipients_as_verified(
|
||||
to_ids: &[Option<ContactId>],
|
||||
mimeparser: &MimeMessage,
|
||||
) -> Result<()> {
|
||||
let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
|
||||
for gossiped_key in mimeparser
|
||||
.gossiped_keys
|
||||
.values()
|
||||
.filter(|gossiped_key| gossiped_key.verified)
|
||||
{
|
||||
let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
|
||||
let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if to_id == ContactId::SELF || to_id == from_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
mark_contact_id_as_verified(context, to_id, verifier_id).await?;
|
||||
ChatId::set_protection_for_contact(context, to_id, mimeparser.timestamp_sent).await?;
|
||||
}
|
||||
|
||||
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
|
||||
for to_id in to_ids.iter().filter_map(|&x| x) {
|
||||
if to_id == ContactId::SELF || to_id == from_id {
|
||||
continue;
|
||||
@@ -3846,7 +3873,7 @@ async fn add_or_lookup_contacts_by_address_list(
|
||||
async fn add_or_lookup_key_contacts(
|
||||
context: &Context,
|
||||
address_list: &[SingleInfo],
|
||||
gossiped_keys: &HashMap<String, SignedPublicKey>,
|
||||
gossiped_keys: &BTreeMap<String, GossipedKey>,
|
||||
fingerprints: &[Fingerprint],
|
||||
origin: Origin,
|
||||
) -> Result<Vec<Option<ContactId>>> {
|
||||
@@ -3862,7 +3889,7 @@ async fn add_or_lookup_key_contacts(
|
||||
// Iterator has not ran out of fingerprints yet.
|
||||
fp.hex()
|
||||
} else if let Some(key) = gossiped_keys.get(addr) {
|
||||
key.dc_fingerprint().hex()
|
||||
key.public_key.dc_fingerprint().hex()
|
||||
} else if context.is_self_addr(addr).await? {
|
||||
contact_ids.push(Some(ContactId::SELF));
|
||||
continue;
|
||||
|
||||
@@ -308,7 +308,9 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
let mut self_found = false;
|
||||
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
|
||||
for (addr, key) in &mime_message.gossiped_keys {
|
||||
if key.dc_fingerprint() == self_fingerprint && context.is_self_addr(addr).await? {
|
||||
if key.public_key.dc_fingerprint() == self_fingerprint
|
||||
&& context.is_self_addr(addr).await?
|
||||
{
|
||||
self_found = true;
|
||||
break;
|
||||
}
|
||||
@@ -596,7 +598,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
|
||||
if key.dc_fingerprint() != contact_fingerprint {
|
||||
if key.public_key.dc_fingerprint() != contact_fingerprint {
|
||||
// Fingerprint does not match, ignore.
|
||||
warn!(context, "Fingerprint does not match.");
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::chat::{CantSendReason, remove_contact_from_chat};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::mimeparser::GossipedKey;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::{self, messages_e2e_encrypted};
|
||||
use crate::test_utils::{
|
||||
@@ -186,7 +187,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
);
|
||||
|
||||
if case == SetupContactCase::WrongAliceGossip {
|
||||
let wrong_pubkey = load_self_public_key(&bob).await.unwrap();
|
||||
let wrong_pubkey = GossipedKey {
|
||||
public_key: load_self_public_key(&bob).await.unwrap(),
|
||||
verified: false,
|
||||
};
|
||||
let alice_pubkey = msg
|
||||
.gossiped_keys
|
||||
.insert(alice_addr.to_string(), wrong_pubkey)
|
||||
|
||||
@@ -97,7 +97,7 @@ impl Summary {
|
||||
let prefix = if msg.state == MessageState::OutDraft {
|
||||
Some(SummaryPrefix::Draft(stock_str::draft(context).await))
|
||||
} else if msg.from_id == ContactId::SELF {
|
||||
if msg.is_info() {
|
||||
if msg.is_info() || msg.viewtype == Viewtype::Call {
|
||||
None
|
||||
} else {
|
||||
Some(SummaryPrefix::Me(stock_str::self_msg(context).await))
|
||||
@@ -233,6 +233,16 @@ impl Message {
|
||||
type_file = self.param.get(Param::Summary1).map(|s| s.to_string());
|
||||
append_text = true;
|
||||
}
|
||||
Viewtype::Call => {
|
||||
emoji = Some("📞");
|
||||
type_name = Some(if self.from_id == ContactId::SELF {
|
||||
"Outgoing call".to_string()
|
||||
} else {
|
||||
"Incoming call".to_string()
|
||||
});
|
||||
type_file = None;
|
||||
append_text = false
|
||||
}
|
||||
Viewtype::Text | Viewtype::Unknown => {
|
||||
emoji = None;
|
||||
if self.param.get_cmd() == SystemMessage::LocationOnly {
|
||||
|
||||
19
src/sync.rs
19
src/sync.rs
@@ -71,9 +71,6 @@ pub(crate) enum SyncData {
|
||||
DeleteMessages {
|
||||
msgs: Vec<String>, // RFC724 id (i.e. "Message-Id" header)
|
||||
},
|
||||
RejectIncomingCall {
|
||||
msg: String, // RFC724 id (i.e. "Message-Id" header)
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -267,7 +264,6 @@ impl Context {
|
||||
SyncData::Config { key, val } => self.sync_config(key, val).await,
|
||||
SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await,
|
||||
SyncData::DeleteMessages { msgs } => self.sync_message_deletion(msgs).await,
|
||||
SyncData::RejectIncomingCall { msg } => self.sync_call_rejection(msg).await,
|
||||
},
|
||||
SyncDataOrUnknown::Unknown(data) => {
|
||||
warn!(self, "Ignored unknown sync item: {data}.");
|
||||
@@ -297,8 +293,15 @@ impl Context {
|
||||
}
|
||||
|
||||
async fn delete_qr_token(&self, token: &QrTokenData) -> Result<()> {
|
||||
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
|
||||
token::delete(self, Namespace::Auth, &token.auth).await?;
|
||||
self.sql
|
||||
.execute(
|
||||
"DELETE FROM tokens
|
||||
WHERE foreign_key IN
|
||||
(SELECT foreign_key FROM tokens
|
||||
WHERE token=? OR token=?)",
|
||||
(&token.invitenumber, &token.auth),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -569,8 +572,8 @@ mod tests {
|
||||
.await?
|
||||
.is_none()
|
||||
);
|
||||
assert!(token::exists(&t, Namespace::InviteNumber, "yip-in").await?);
|
||||
assert!(token::exists(&t, Namespace::Auth, "yip-auth").await?);
|
||||
assert!(!token::exists(&t, Namespace::InviteNumber, "yip-in").await?);
|
||||
assert!(!token::exists(&t, Namespace::Auth, "yip-auth").await?);
|
||||
assert!(!token::exists(&t, Namespace::Auth, "non-existent").await?);
|
||||
assert!(!token::exists(&t, Namespace::Auth, "directly deleted").await?);
|
||||
|
||||
|
||||
@@ -1282,9 +1282,8 @@ impl SentMessage<'_> {
|
||||
///
|
||||
/// The keypair was created using the crate::key::tests::gen_key test.
|
||||
pub fn alice_keypair() -> KeyPair {
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/alice-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
let secret =
|
||||
key::SignedSecretKey::from_asc(include_str!("../test-data/key/alice-secret.asc")).unwrap();
|
||||
let public = secret.split_public_key().unwrap();
|
||||
KeyPair { public, secret }
|
||||
}
|
||||
@@ -1293,9 +1292,8 @@ pub fn alice_keypair() -> KeyPair {
|
||||
///
|
||||
/// Like [alice_keypair] but a different key and identity.
|
||||
pub fn bob_keypair() -> KeyPair {
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
let secret =
|
||||
key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap();
|
||||
let public = secret.split_public_key().unwrap();
|
||||
KeyPair { public, secret }
|
||||
}
|
||||
@@ -1306,8 +1304,7 @@ pub fn bob_keypair() -> KeyPair {
|
||||
pub fn charlie_keypair() -> KeyPair {
|
||||
let secret =
|
||||
key::SignedSecretKey::from_asc(include_str!("../test-data/key/charlie-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
.unwrap();
|
||||
let public = secret.split_public_key().unwrap();
|
||||
KeyPair { public, secret }
|
||||
}
|
||||
@@ -1316,9 +1313,8 @@ pub fn charlie_keypair() -> KeyPair {
|
||||
///
|
||||
/// Like [alice_keypair] but a different key and identity.
|
||||
pub fn dom_keypair() -> KeyPair {
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/dom-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
let secret =
|
||||
key::SignedSecretKey::from_asc(include_str!("../test-data/key/dom-secret.asc")).unwrap();
|
||||
let public = secret.split_public_key().unwrap();
|
||||
KeyPair { public, secret }
|
||||
}
|
||||
@@ -1327,9 +1323,8 @@ pub fn dom_keypair() -> KeyPair {
|
||||
///
|
||||
/// Like [alice_keypair] but a different key and identity.
|
||||
pub fn elena_keypair() -> KeyPair {
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/elena-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
let secret =
|
||||
key::SignedSecretKey::from_asc(include_str!("../test-data/key/elena-secret.asc")).unwrap();
|
||||
let public = secret.split_public_key().unwrap();
|
||||
KeyPair { public, secret }
|
||||
}
|
||||
@@ -1338,9 +1333,8 @@ pub fn elena_keypair() -> KeyPair {
|
||||
///
|
||||
/// Like [alice_keypair] but a different key and identity.
|
||||
pub fn fiona_keypair() -> KeyPair {
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
let secret =
|
||||
key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc")).unwrap();
|
||||
let public = secret.split_public_key().unwrap();
|
||||
KeyPair { public, secret }
|
||||
}
|
||||
@@ -1476,8 +1470,7 @@ pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) {
|
||||
pub(crate) async fn sync(alice0: &TestContext, alice1: &TestContext) {
|
||||
alice0.send_sync_msg().await.unwrap();
|
||||
let sync_msg = alice0.pop_sent_sync_msg().await;
|
||||
let no_msg = alice1.recv_msg_opt(&sync_msg).await;
|
||||
assert!(no_msg.is_none());
|
||||
alice1.recv_msg_trash(&sync_msg).await;
|
||||
}
|
||||
|
||||
/// Pretty-print an event to stdout
|
||||
|
||||
11
src/token.rs
11
src/token.rs
@@ -119,13 +119,14 @@ pub async fn auth_foreign_key(context: &Context, token: &str) -> Result<Option<S
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete(context: &Context, namespace: Namespace, token: &str) -> Result<()> {
|
||||
/// Resets all tokens corresponding to the `foreign_key`.
|
||||
///
|
||||
/// `foreign_key` is a group ID to reset all group tokens
|
||||
/// or empty string to reset all setup contact tokens.
|
||||
pub async fn delete(context: &Context, foreign_key: &str) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM tokens WHERE namespc=? AND token=?;",
|
||||
(namespace, token),
|
||||
)
|
||||
.execute("DELETE FROM tokens WHERE foreign_key=?", (foreign_key,))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user