From 1bd31f6b8ea1fd635ac61d03bac0b3fe6e71d0d2 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 25 Sep 2025 18:26:11 +0000 Subject: [PATCH] api: add CallState --- src/calls.rs | 130 +++++++++++++++++++++++++++++++++++++-- src/calls/calls_tests.rs | 61 ++++++++++++++++++ 2 files changed, 187 insertions(+), 4 deletions(-) diff --git a/src/calls.rs b/src/calls.rs index 890aa6d18..d10464ad2 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -33,10 +33,20 @@ 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* +// For persisting parameters in the call, we use Param::Arg* + const CALL_ACCEPTED_TIMESTAMP: Param = Param::Arg; const CALL_ENDED_TIMESTAMP: Param = Param::Arg4; +/// Set if incoming call was ended explicitly +/// by the other side before we accepted it. +/// +/// It is used to distinguish "ended" calls +/// that are rejected by us from the calls +/// cancelled by the other side +/// immediately after ringing started. +const CALL_CANCELLED_TIMESTAMP: Param = Param::Arg2; + /// Information about the status of a call. #[derive(Debug, Default)] pub struct CallInfo { @@ -109,12 +119,38 @@ impl CallInfo { self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP) } + /// Returns true if the call is missed + /// because the caller cancelled it + /// explicitly before ringing stopped. + /// + /// For outgoing calls this means + /// the receiver has rejected the call + /// explicitly. + pub fn is_cancelled(&self) -> bool { + self.msg.param.exists(CALL_CANCELLED_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(()) } + /// Explicitly mark the call as cancelled. + /// + /// For incoming calls this should be called + /// when "call ended" message is received + /// from the caller before we picked up the call. + /// In this case the call becomes "missed" early + /// before the ringing timeout. + async fn mark_as_cancelled(&mut self, context: &Context) -> Result<()> { + let now = time(); + self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, now); + self.msg.param.set_i64(CALL_CANCELLED_TIMESTAMP, now); + self.msg.update_param(context).await?; + Ok(()) + } + /// Returns true if the call is ended. pub fn is_ended(&self) -> bool { self.msg.param.exists(CALL_ENDED_TIMESTAMP) @@ -209,15 +245,17 @@ impl Context { info!(self, "Call already ended"); return Ok(()); } - call.mark_as_ended(self).await?; if !call.is_accepted() { if call.is_incoming() { + call.mark_as_ended(self).await?; call.update_text(self, "Declined call").await?; } else { + call.mark_as_cancelled(self).await?; call.update_text(self, "Cancelled call").await?; } } else { + call.mark_as_ended(self).await?; call.update_text_duration(self).await?; } @@ -246,10 +284,11 @@ impl Context { sleep(Duration::from_secs(wait)).await; 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.mark_as_cancelled(&context).await?; call.update_text(&context, "Missed call").await?; } else { + call.mark_as_ended(&context).await?; call.update_text(&context, "Cancelled call").await?; } context.emit_msgs_changed(call.msg.chat_id, call_id); @@ -332,23 +371,27 @@ impl Context { return Ok(()); } - call.mark_as_ended(self).await?; if !call.is_accepted() { if call.is_incoming() { if from_id == ContactId::SELF { + call.mark_as_ended(self).await?; call.update_text(self, "Declined call").await?; } else { + call.mark_as_cancelled(self).await?; call.update_text(self, "Missed call").await?; } } else { // outgoing if from_id == ContactId::SELF { + call.mark_as_cancelled(self).await?; call.update_text(self, "Cancelled call").await?; } else { + call.mark_as_ended(self).await?; call.update_text(self, "Declined call").await?; } } } else { + call.mark_as_ended(self).await?; call.update_text_duration(self).await?; } @@ -401,6 +444,85 @@ fn sdp_has_video(sdp: &str) -> Result { Ok(false) } +/// State of the call for display in the message bubble. +#[derive(Debug, PartialEq, Eq)] +pub enum CallState { + /// Fresh incoming or outgoing call that is still ringing. + /// + /// There is no separate state for outgoing call + /// that has been dialled but not ringing on the other side yet + /// as we don't know whether the other side received our call. + Alerting, + + /// Active call. + Active, + + /// Completed call that was once active + /// and then was terminated for any reason. + Completed { + /// Call duration in seconds. + duration: i64, + }, + + /// Incoming call that was not picked up within a timeout + /// or was explicitly ended by the caller before we picked up. + Missed, + + /// Incoming call that was explicitly ended on our side + /// before picking up or outgoing call + /// that was declined before the timeout. + Declined, + + /// Outgoing call that has been cancelled on our side + /// before receiving a response. + /// + /// Incoming calls cannot be cancelled, + /// on the receiver side cancelled calls + /// usually result in missed calls. + Cancelled, +} + +/// Returns call state given the message ID. +pub async fn call_state(context: &Context, msg_id: MsgId) -> Result { + let call = context.load_call_by_id(msg_id).await?; + let state = if call.is_incoming() { + if call.is_accepted() { + if call.is_ended() { + CallState::Completed { + duration: call.duration_seconds(), + } + } else { + CallState::Active + } + } else if call.is_cancelled() { + // Call was explicitly cancelled + // by the caller before we picked it up. + CallState::Missed + } else if call.is_ended() { + CallState::Declined + } else if call.is_stale() { + CallState::Missed + } else { + CallState::Alerting + } + } else if call.is_accepted() { + if call.is_ended() { + CallState::Completed { + duration: call.duration_seconds(), + } + } else { + CallState::Active + } + } else if call.is_cancelled() { + CallState::Cancelled + } else if call.is_ended() || call.is_stale() { + CallState::Declined + } else { + CallState::Alerting + }; + Ok(state) +} + /// ICE server for JSON serialization. #[derive(Serialize, Debug, Clone, PartialEq)] struct IceServer { diff --git a/src/calls/calls_tests.rs b/src/calls/calls_tests.rs index 254b06d08..c87c58629 100644 --- a/src/calls/calls_tests.rs +++ b/src/calls/calls_tests.rs @@ -57,6 +57,7 @@ async fn setup_call() -> Result { assert!(!info.is_accepted()); assert_eq!(info.place_call_info, PLACE_INFO); assert_text(t, m.id, "Outgoing call").await?; + assert_eq!(call_state(t, m.id).await?, CallState::Alerting); } // Bob receives the message referring to the call on two devices; @@ -74,6 +75,7 @@ async fn setup_call() -> Result { assert!(!info.is_accepted()); assert_eq!(info.place_call_info, PLACE_INFO); assert_text(t, m.id, "Incoming call").await?; + assert_eq!(call_state(t, m.id).await?, CallState::Alerting); } Ok(CallSetup { @@ -111,6 +113,7 @@ async fn accept_call() -> Result { 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!(call_state(&bob, bob_call.id).await?, CallState::Active); bob2.recv_msg_trash(&sent2).await; assert_text(&bob, bob_call.id, "Incoming call").await?; @@ -119,6 +122,7 @@ async fn accept_call() -> Result { .await; let info = bob2.load_call_by_id(bob2_call.id).await?; assert!(info.is_accepted()); + assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Active); // Alice receives the acceptance message alice.recv_msg_trash(&sent2).await; @@ -137,6 +141,7 @@ async fn accept_call() -> Result { 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!(call_state(&alice, alice_call.id).await?, CallState::Active); alice2.recv_msg_trash(&sent2).await; assert_text(&alice2, alice2_call.id, "Outgoing call").await?; @@ -144,6 +149,10 @@ async fn accept_call() -> Result { .evtracker .get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. })) .await; + assert_eq!( + call_state(&alice2, alice2_call.id).await?, + CallState::Active + ); Ok(CallSetup { alice, @@ -179,12 +188,20 @@ async fn test_accept_call_callee_ends() -> Result<()> { .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; let sent3 = bob.pop_sent_msg().await; + assert!(matches!( + call_state(&bob, bob_call.id).await?, + CallState::Completed { .. } + )); 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; + assert!(matches!( + call_state(&bob2, bob2_call.id).await?, + CallState::Completed { .. } + )); // Alice receives the ending message alice.recv_msg_trash(&sent3).await; @@ -193,6 +210,10 @@ async fn test_accept_call_callee_ends() -> Result<()> { .evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; + assert!(matches!( + call_state(&alice, alice_call.id).await?, + CallState::Completed { .. } + )); alice2.recv_msg_trash(&sent3).await; assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?; @@ -200,6 +221,10 @@ async fn test_accept_call_callee_ends() -> Result<()> { .evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; + assert!(matches!( + call_state(&alice2, alice2_call.id).await?, + CallState::Completed { .. } + )); Ok(()) } @@ -227,6 +252,10 @@ async fn test_accept_call_caller_ends() -> Result<()> { .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; let sent3 = alice.pop_sent_msg().await; + assert!(matches!( + call_state(&alice, alice_call.id).await?, + CallState::Completed { .. } + )); alice2.recv_msg_trash(&sent3).await; assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?; @@ -234,6 +263,10 @@ async fn test_accept_call_caller_ends() -> Result<()> { .evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; + assert!(matches!( + call_state(&alice2, alice2_call.id).await?, + CallState::Completed { .. } + )); // Bob receives the ending message bob.recv_msg_trash(&sent3).await; @@ -241,12 +274,20 @@ async fn test_accept_call_caller_ends() -> Result<()> { bob.evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; + assert!(matches!( + call_state(&bob, bob_call.id).await?, + CallState::Completed { .. } + )); 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; + assert!(matches!( + call_state(&bob2, bob2_call.id).await?, + CallState::Completed { .. } + )); Ok(()) } @@ -274,12 +315,14 @@ async fn test_callee_rejects_call() -> Result<()> { .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; let sent3 = bob.pop_sent_msg().await; + assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Declined); 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; + assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Declined); // Alice receives decline message alice.recv_msg_trash(&sent3).await; @@ -288,6 +331,10 @@ async fn test_callee_rejects_call() -> Result<()> { .evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; + assert_eq!( + call_state(&alice, alice_call.id).await?, + CallState::Declined + ); alice2.recv_msg_trash(&sent3).await; assert_text(&alice2, alice2_call.id, "Declined call").await?; @@ -295,6 +342,10 @@ async fn test_callee_rejects_call() -> Result<()> { .evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; + assert_eq!( + call_state(&alice2, alice2_call.id).await?, + CallState::Declined + ); Ok(()) } @@ -322,6 +373,10 @@ async fn test_caller_cancels_call() -> Result<()> { .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; let sent3 = alice.pop_sent_msg().await; + assert_eq!( + call_state(&alice, alice_call.id).await?, + CallState::Cancelled + ); alice2.recv_msg_trash(&sent3).await; assert_text(&alice2, alice2_call.id, "Cancelled call").await?; @@ -329,6 +384,10 @@ async fn test_caller_cancels_call() -> Result<()> { .evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; + assert_eq!( + call_state(&alice2, alice2_call.id).await?, + CallState::Cancelled + ); // Bob receives the ending message bob.recv_msg_trash(&sent3).await; @@ -336,12 +395,14 @@ async fn test_caller_cancels_call() -> Result<()> { bob.evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; + assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Missed); 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; + assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Missed); Ok(()) }