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.
* 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().
*

View File

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

View File

@@ -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<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)]
mod calls_tests;

View File

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

View File

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