api: add who_can_call_me config option

This commit is contained in:
link2xt
2026-01-05 16:15:34 +00:00
committed by l
parent cdacad235e
commit 7e811469b3
5 changed files with 117 additions and 20 deletions

View File

@@ -516,6 +516,10 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled. * - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
* 0 = WebXDC realtime API is disabled and behaves as noop. * 0 = WebXDC realtime API is disabled and behaves as noop.
* 1 = WebXDC realtime API is enabled (default). * 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(). * If you want to retrieve a value, use dc_get_config().
* *

View File

@@ -107,3 +107,48 @@ def test_no_contact_request_call(acfactory) -> None:
msg = bob.get_message_by_id(event.msg_id) msg = bob.get_message_by_id(event.msg_id)
if msg.get_snapshot().text == "Hello!": if msg.get_snapshot().text == "Hello!":
break 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

View File

@@ -4,6 +4,7 @@
//! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs. //! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs.
use crate::chat::ChatIdBlocked; use crate::chat::ChatIdBlocked;
use crate::chat::{Chat, ChatId, send_msg}; use crate::chat::{Chat, ChatId, send_msg};
use crate::config::Config;
use crate::constants::{Blocked, Chattype}; use crate::constants::{Blocked, Chattype};
use crate::contact::ContactId; use crate::contact::ContactId;
use crate::context::{Context, WeakContext}; use crate::context::{Context, WeakContext};
@@ -16,6 +17,8 @@ use crate::net::dns::lookup_host_with_cache;
use crate::param::Param; use crate::param::Param;
use crate::tools::{normalize_text, time}; use crate::tools::{normalize_text, time};
use anyhow::{Context as _, Result, ensure}; use anyhow::{Context as _, Result, ensure};
use deltachat_derive::{FromSql, ToSql};
use num_traits::FromPrimitive;
use sdp::SessionDescription; use sdp::SessionDescription;
use serde::Serialize; use serde::Serialize;
use std::io::Cursor; use std::io::Cursor;
@@ -348,26 +351,34 @@ impl Context {
false false
} }
}; };
if let Some(chat_id_blocked) = let can_call_me = match who_can_call_me(self).await? {
ChatIdBlocked::lookup_by_contact(self, from_id).await? WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id)
{ .await?
match chat_id_blocked.blocked { .is_some_and(|chat_id_blocked| {
Blocked::Not => { match chat_id_blocked.blocked {
self.emit_event(EventType::IncomingCall { Blocked::Not => true,
msg_id: call.msg.id, Blocked::Yes | Blocked::Request => {
chat_id: call.msg.chat_id, // Do not notify about incoming calls
place_call_info: call.place_call_info.to_string(), // from contact requests and blocked contacts.
has_video, //
}); // User can still access the call and accept it
} // via the chat in case of contact requests.
Blocked::Yes | Blocked::Request => { false
// Do not notify about incoming calls }
// from contact requests and blocked contacts. }
// }),
// User can still access the call and accept it WhoCanCallMe::Everybody => ChatIdBlocked::lookup_by_contact(self, from_id)
// via the chat in case of contact requests. .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 wait = call.remaining_ring_seconds();
let context = self.get_weak_context(); let context = self.get_weak_context();
@@ -712,5 +723,32 @@ pub async fn ice_servers(context: &Context) -> Result<String> {
} }
} }
/// "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<WhoCanCallMe> {
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)] #[cfg(test)]
mod calls_tests; mod calls_tests;

View File

@@ -446,6 +446,12 @@ pub enum Config {
/// Protected Email". /// Protected Email".
#[strum(props(default = "1"))] #[strum(props(default = "1"))]
StdHeaderProtectionComposing, StdHeaderProtectionComposing,
/// Who can call me.
///
/// The options are from the `WhoCanCallMe` enum.
#[strum(props(default = "1"))]
WhoCanCallMe,
} }
impl Config { impl Config {

View File

@@ -954,6 +954,10 @@ impl Context {
"show_emails", "show_emails",
self.get_config_int(Config::ShowEmails).await?.to_string(), 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( res.insert(
"download_limit", "download_limit",
self.get_config_int(Config::DownloadLimit) self.get_config_int(Config::DownloadLimit)