From 0517beef61b5baff38aedd4775e91cfde4a846b7 Mon Sep 17 00:00:00 2001 From: WofWca Date: Fri, 28 Aug 2026 13:11:09 +0400 Subject: [PATCH] fix: don't notify of missed call from blocked user Closes https://github.com/chatmail/core/issues/8576. The diff might look big, but it's only two things: - move `can_call_me` one scope up - replace `emit_incoming_msg` with `emit_msg_event` with `important = can_call_me` I decided not to completely unify the `important` logic with the other occurrence of `emit_msg_event()` as I suggested in the issue yet. That IMO should still be considered, but let's start simple. Note that there is #7840 which may be closed by #7955, which will basically supersede this MR. I think, however, that it's OK to merge this one, and then that one can just revert this one, including tests, and rebase on top of the revert. --- src/calls.rs | 48 ++++++++++--------- src/calls/calls_tests.rs | 99 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 23 deletions(-) diff --git a/src/calls.rs b/src/calls.rs index 290683caa..4a779a7bc 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -360,36 +360,40 @@ impl Context { }; if call.is_incoming() { + 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 call.is_stale() { let missed_call_str = stock_str::missed_call(self); call.update_text(self, &missed_call_str).await?; - self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call + let important = can_call_me; + // notify missed call + call.msg + .chat_id + .emit_msg_event(self, call.msg.id, important); } else { let incoming_call_str = stock_str::incoming_call(self, call.has_video_initially()); call.update_text(self, &incoming_call_str).await?; self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified - 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, diff --git a/src/calls/calls_tests.rs b/src/calls/calls_tests.rs index 0a1e2a617..491561a26 100644 --- a/src/calls/calls_tests.rs +++ b/src/calls/calls_tests.rs @@ -1,10 +1,14 @@ use super::*; use crate::chat::forward_msgs; use crate::config::Config; +use crate::contact::Contact; use crate::message::MessageState; use crate::receive_imf::receive_imf; use crate::test_utils; -use crate::test_utils::{TestContext, TestContextManager}; +use crate::test_utils::{ + ExpectedEvents, TestContext, TestContextManager, TimeShiftFalsePositiveNote, +}; +use crate::tools::SystemTime; struct CallSetup { pub alice: TestContext, @@ -448,6 +452,99 @@ async fn test_callee_sees_contact_request_call() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_call_from_blocked_chat_normal() -> Result<()> { + let stale = false; + test_get_call_from_blocked_chat(stale).await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_call_from_blocked_chat_stale() -> Result<()> { + let stale = true; + test_get_call_from_blocked_chat(stale).await +} + +async fn test_get_call_from_blocked_chat(stale: bool) -> Result<()> { + let _n = TimeShiftFalsePositiveNote; + + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + let alice_chat = alice.create_chat(bob).await; + let bob_chat = bob.create_chat(alice).await; + + assert!(!bob_chat.is_contact_request()); + Contact::block(bob, bob.add_or_lookup_contact_id(alice).await).await?; + + let alice_msg_id = alice + .place_outgoing_call(alice_chat.id, PLACE_INFO.to_string(), true) + .await?; + let sent_start_call = alice.pop_sent_msg().await; + alice.end_call(alice_msg_id).await?; + let sent_end_call = alice.pop_sent_msg().await; + + if stale { + SystemTime::shift(Duration::from_secs(5 * 60)); + } + + let bob_call = bob.recv_msg(&sent_start_call).await; + assert_eq!( + call_state(bob, bob_call.id).await?, + if stale { + CallState::Missed + } else { + CallState::Alerting + } + ); + // Messages from blocked contacts are immediately marked as "seen". + assert_eq!(bob_call.id.get_state(bob).await?, MessageState::InSeen); + bob.evtracker + .get_matching_ext( + bob, + ExpectedEvents { + expected: |evt| matches!(evt, EventType::MsgsChanged { .. }), + unexpected: |evt| { + matches!( + evt, + EventType::IncomingMsg { .. } + | EventType::IncomingCall { .. } + | EventType::IncomingCallAccepted { .. } + | EventType::OutgoingCallAccepted { .. } + ) + }, + }, + ) + .await + .unwrap(); + + bob.recv_msg_trash(&sent_end_call).await; + assert_eq!(call_state(bob, bob_call.id).await?, CallState::Missed); + assert_eq!(bob_call.id.get_state(bob).await?, MessageState::InSeen); + + bob.evtracker + .get_matching_ext( + bob, + ExpectedEvents { + expected: |evt| matches!(evt, EventType::CallEnded { .. }), + // No "missed call" notification or any other stuff. + unexpected: |evt| { + matches!( + evt, + EventType::IncomingMsg { .. } + | EventType::IncomingCall { .. } + | EventType::IncomingCallAccepted { .. } + | EventType::OutgoingCallAccepted { .. } + ) + }, + }, + ) + .await + .unwrap(); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_caller_cancels_call() -> Result<()> { // Alice calls Bob