diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 2b297d6e64..6d47d4191a 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -516,6 +516,10 @@ char* dc_get_blobdir (const dc_context_t* context); * - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled. * 0 = WebXDC realtime API is disabled and behaves as noop. * 1 = WebXDC realtime API is enabled (default). + * - `who_can_call_me` = Who can cause call notifications. + * 0 = Everybody (except explicitly blocked contacts), + * 1 = Contacts (default, does not include contact requests), + * 2 = Nobody (calls never result in a notification). * * If you want to retrieve a value, use dc_get_config(). * diff --git a/deltachat-rpc-client/tests/test_calls.py b/deltachat-rpc-client/tests/test_calls.py index cd10c4beed..e3ac28d500 100644 --- a/deltachat-rpc-client/tests/test_calls.py +++ b/deltachat-rpc-client/tests/test_calls.py @@ -107,3 +107,48 @@ def test_no_contact_request_call(acfactory) -> None: msg = bob.get_message_by_id(event.msg_id) if msg.get_snapshot().text == "Hello!": break + + +def test_who_can_call_me_nobody(acfactory) -> None: + alice, bob = acfactory.get_online_accounts(2) + + # Bob sets "who can call me" to "nobody" (2) + bob.set_config("who_can_call_me", "2") + + # Bob even accepts Alice in advance so the chat does not appear as contact request. + bob.create_chat(alice) + + alice_chat_bob = alice.create_chat(bob) + alice_chat_bob.place_outgoing_call("offer") + alice_chat_bob.send_text("Hello!") + + # Notification for "Hello!" message should arrive + # without the call ringing. + while True: + event = bob.wait_for_event() + + # There should be no incoming call notification. + assert event.kind != EventType.INCOMING_CALL + + if event.kind == EventType.INCOMING_MSG: + msg = bob.get_message_by_id(event.msg_id) + if msg.get_snapshot().text == "Hello!": + break + + +def test_who_can_call_me_everybody(acfactory) -> None: + """Test that if "who can call me" setting is set to "everybody", calls arrive even in contact request chats.""" + alice, bob = acfactory.get_online_accounts(2) + + # Bob sets "who can call me" to "nobody" (0) + bob.set_config("who_can_call_me", "0") + + alice_chat_bob = alice.create_chat(bob) + alice_chat_bob.place_outgoing_call("offer") + incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL) + + incoming_call_message = Message(bob, incoming_call_event.msg_id) + + # Even with the call arriving, the chat is still in the contact request mode. + incoming_chat = incoming_call_message.get_snapshot().chat + assert incoming_chat.get_basic_snapshot().is_contact_request diff --git a/src/calls.rs b/src/calls.rs index 38f196a94e..2154354ac8 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -4,6 +4,7 @@ //! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs. use crate::chat::ChatIdBlocked; use crate::chat::{Chat, ChatId, send_msg}; +use crate::config::Config; use crate::constants::{Blocked, Chattype}; use crate::contact::ContactId; use crate::context::{Context, WeakContext}; @@ -16,6 +17,8 @@ use crate::net::dns::lookup_host_with_cache; use crate::param::Param; use crate::tools::{normalize_text, time}; use anyhow::{Context as _, Result, ensure}; +use deltachat_derive::{FromSql, ToSql}; +use num_traits::FromPrimitive; use sdp::SessionDescription; use serde::Serialize; use std::io::Cursor; @@ -348,26 +351,34 @@ impl Context { false } }; - if let Some(chat_id_blocked) = - ChatIdBlocked::lookup_by_contact(self, from_id).await? - { - match chat_id_blocked.blocked { - Blocked::Not => { - self.emit_event(EventType::IncomingCall { - msg_id: call.msg.id, - chat_id: call.msg.chat_id, - place_call_info: call.place_call_info.to_string(), - has_video, - }); - } - Blocked::Yes | Blocked::Request => { - // Do not notify about incoming calls - // from contact requests and blocked contacts. - // - // User can still access the call and accept it - // via the chat in case of contact requests. - } - } + let can_call_me = match who_can_call_me(self).await? { + WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id) + .await? + .is_some_and(|chat_id_blocked| { + match chat_id_blocked.blocked { + Blocked::Not => true, + Blocked::Yes | Blocked::Request => { + // Do not notify about incoming calls + // from contact requests and blocked contacts. + // + // User can still access the call and accept it + // via the chat in case of contact requests. + false + } + } + }), + WhoCanCallMe::Everybody => ChatIdBlocked::lookup_by_contact(self, from_id) + .await? + .is_none_or(|chat_id_blocked| chat_id_blocked.blocked != Blocked::Yes), + WhoCanCallMe::Nobody => false, + }; + if can_call_me { + self.emit_event(EventType::IncomingCall { + msg_id: call.msg.id, + chat_id: call.msg.chat_id, + place_call_info: call.place_call_info.to_string(), + has_video, + }); } let wait = call.remaining_ring_seconds(); let context = self.get_weak_context(); @@ -712,5 +723,32 @@ pub async fn ice_servers(context: &Context) -> Result { } } +/// "Who can call me" config options. +#[derive( + Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, +)] +#[repr(u8)] +pub enum WhoCanCallMe { + /// Everybody can call me if they are not blocked. + /// + /// This includes contact requests. + Everybody = 0, + + /// Every contact who is not blocked and not a contact request, can call. + #[default] + Contacts = 1, + + /// Nobody can call me. + Nobody = 2, +} + +/// Returns currently configuration of the "who can call me" option. +async fn who_can_call_me(context: &Context) -> Result { + let who_can_call_me = + WhoCanCallMe::from_i32(context.get_config_int(Config::WhoCanCallMe).await?) + .unwrap_or_default(); + Ok(who_can_call_me) +} + #[cfg(test)] mod calls_tests; diff --git a/src/config.rs b/src/config.rs index 5760426ea3..e719ac3892 100644 --- a/src/config.rs +++ b/src/config.rs @@ -446,6 +446,12 @@ pub enum Config { /// Protected Email". #[strum(props(default = "1"))] StdHeaderProtectionComposing, + + /// Who can call me. + /// + /// The options are from the `WhoCanCallMe` enum. + #[strum(props(default = "1"))] + WhoCanCallMe, } impl Config { diff --git a/src/context.rs b/src/context.rs index 4f079f9417..12c3f645d1 100644 --- a/src/context.rs +++ b/src/context.rs @@ -954,6 +954,10 @@ impl Context { "show_emails", self.get_config_int(Config::ShowEmails).await?.to_string(), ); + res.insert( + "who_can_call_me", + self.get_config_int(Config::WhoCanCallMe).await?.to_string(), + ); res.insert( "download_limit", self.get_config_int(Config::DownloadLimit)