rvoip_sip/server/b2bua.rs
1//! Optional B2BUA convenience: wires the canonical incoming-INVITE →
2//! originate-outbound → bridge pattern entirely through
3//! [`UnifiedCoordinator`].
4//!
5//! Per CARVE_PLAN §5: validates that `server::*` stands on its own — a
6//! SIP-only consumer can use rvoip-sip without `rvoip-core` involvement by
7//! composing api/ calls.
8//!
9//! ```rust,no_run
10//! use rvoip_sip::server::b2bua::SipB2bua;
11//! use rvoip_sip::SessionId;
12//!
13//! # async fn example(
14//! # coordinator: std::sync::Arc<rvoip_sip::UnifiedCoordinator>,
15//! # incoming_session_id: SessionId,
16//! # ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
17//! let b2bua = SipB2bua::new(coordinator);
18//! let _bridge = b2bua
19//! .handle_inbound("sip:gw@example.com", &incoming_session_id, "sip:bob@upstream.example.net")
20//! .await?;
21//! # Ok(())
22//! # }
23//! ```
24
25use crate::api::unified::{BridgeError, BridgeHandle, UnifiedCoordinator};
26use crate::server::bridge::sip_bridge;
27use crate::SessionId;
28use std::sync::Arc;
29use std::time::Duration;
30
31const OUTBOUND_ANSWER_TIMEOUT: Duration = Duration::from_secs(30);
32
33/// Error returned by [`SipB2bua`] operations.
34#[derive(Debug, thiserror::Error)]
35pub enum B2buaError {
36 /// A session/signalling operation (accept or originate) failed.
37 #[error("session error: {0}")]
38 Session(#[from] crate::errors::SessionError),
39 /// Bridging the two legs failed.
40 #[error("bridge error: {0}")]
41 Bridge(BridgeError),
42}
43
44impl From<BridgeError> for B2buaError {
45 fn from(err: BridgeError) -> Self {
46 B2buaError::Bridge(err)
47 }
48}
49
50/// Convenience B2BUA that wires incoming-INVITE → originate-outbound →
51/// bridge entirely through [`UnifiedCoordinator`].
52#[derive(Clone)]
53pub struct SipB2bua {
54 coordinator: Arc<UnifiedCoordinator>,
55}
56
57impl SipB2bua {
58 /// Create a B2BUA over the given [`UnifiedCoordinator`].
59 pub fn new(coordinator: Arc<UnifiedCoordinator>) -> Self {
60 Self { coordinator }
61 }
62
63 /// Accept the inbound INVITE on `incoming`, originate an outbound leg to
64 /// `target_uri` from `from_uri`, then bridge the two. Returns the
65 /// resulting [`BridgeHandle`] (drop to tear down).
66 pub async fn handle_inbound(
67 &self,
68 from_uri: &str,
69 incoming: &SessionId,
70 target_uri: &str,
71 ) -> Result<BridgeHandle, B2buaError> {
72 self.coordinator.accept_call(incoming).await?;
73 let outbound = self
74 .coordinator
75 .invite(Some(from_uri.to_string()), target_uri.to_string())
76 .send()
77 .await?;
78 self.coordinator
79 .session(&outbound)
80 .wait_for_answered(Some(OUTBOUND_ANSWER_TIMEOUT))
81 .await?;
82 let handle = sip_bridge(&self.coordinator, incoming, &outbound).await?;
83 Ok(handle)
84 }
85}