rvoip_dialog_core/api/client.rs
1//! Dialog Client API
2//!
3//! This module provides a high-level client interface for SIP dialog management,
4//! abstracting the complexity of the underlying DialogManager for client use cases.
5//!
6//! ## Overview
7//!
8//! The DialogClient is the primary interface for client-side SIP operations, providing
9//! a clean, intuitive API that handles the complexities of SIP dialog management while
10//! offering powerful features for call control, media negotiation, and session coordination.
11//!
12//! ### Key Features
13//!
14//! - **Call Management**: Make outgoing calls, handle responses, manage call lifecycle
15//! - **Dialog Operations**: Create, manage, and terminate SIP dialogs
16//! - **Session Integration**: Built-in coordination with session-core for media management
17//! - **Request/Response Handling**: Send arbitrary SIP methods and handle responses
18//! - **Statistics & Monitoring**: Track call metrics and dialog states
19//! - **Dependency Injection**: Clean architecture with proper separation of concerns
20//!
21//! ## Quick Start
22//!
23//! ### Basic Client Setup
24//!
25//! ```rust,no_run
26//! use rvoip_dialog_core::api::{DialogClient, DialogApi, ClientConfig};
27//! use rvoip_transaction_core::TransactionManager;
28//! use std::sync::Arc;
29//!
30//! #[tokio::main]
31//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
32//! // Set up dependencies (transport setup omitted for brevity)
33//! # let transport = unimplemented!(); // Mock transport
34//! let tx_mgr = Arc::new(TransactionManager::new_sync(transport));
35//! let config = ClientConfig::new("127.0.0.1:0".parse()?)
36//! .with_from_uri("sip:alice@example.com");
37//!
38//! // Create and start the client
39//! let client = DialogClient::with_dependencies(tx_mgr, config).await?;
40//! client.start().await?;
41//!
42//! // Make a call
43//! let call = client.make_call(
44//! "sip:alice@example.com",
45//! "sip:bob@example.com",
46//! Some("v=0\r\no=- 123 456 IN IP4 192.168.1.100\r\n...".to_string())
47//! ).await?;
48//!
49//! println!("Call created: {}", call.call_id());
50//!
51//! // Clean up
52//! call.hangup().await?;
53//! client.stop().await?;
54//!
55//! Ok(())
56//! }
57//! ```
58//!
59//! ## Architecture & Design Patterns
60//!
61//! ### Dependency Injection Pattern (Recommended)
62//!
63//! The DialogClient follows a clean dependency injection pattern to maintain proper
64//! separation of concerns. The client does not manage transport concerns directly:
65//!
66//! ```rust,no_run
67//! use rvoip_dialog_core::api::{DialogClient, ClientConfig};
68//! use rvoip_transaction_core::TransactionManager;
69//! use std::sync::Arc;
70//!
71//! # async fn setup_example() -> Result<(), Box<dyn std::error::Error>> {
72//! // 1. Set up transport layer (your responsibility)
73//! # let transport = unimplemented!(); // Your transport implementation
74//!
75//! // 2. Create transaction manager with transport
76//! let tx_mgr = Arc::new(TransactionManager::new_sync(transport));
77//!
78//! // 3. Configure client behavior
79//! let config = ClientConfig::new("192.168.1.100:5060".parse()?)
80//! .with_from_uri("sip:user@domain.com")
81//! .with_auth("username", "password");
82//!
83//! // 4. Create client with dependencies
84//! let client = DialogClient::with_dependencies(tx_mgr, config).await?;
85//! # Ok(())
86//! # }
87//! ```
88//!
89//! ### Session Coordination Pattern
90//!
91//! For applications that need media management, use the session coordination pattern:
92//!
93//! ```rust,no_run
94//! use rvoip_dialog_core::api::{DialogClient, DialogApi};
95//! use rvoip_dialog_core::events::SessionCoordinationEvent;
96//! use tokio::sync::mpsc;
97//!
98//! # async fn session_example() -> Result<(), Box<dyn std::error::Error>> {
99//! # let (tx_mgr, config) = setup_dependencies().await?;
100//! let client = DialogClient::with_dependencies(tx_mgr, config).await?;
101//!
102//! // Set up session coordination
103//! let (session_tx, mut session_rx) = mpsc::channel(100);
104//! client.set_session_coordinator(session_tx).await?;
105//! client.start().await?;
106//!
107//! // Handle session events
108//! tokio::spawn(async move {
109//! while let Some(event) = session_rx.recv().await {
110//! match event {
111//! SessionCoordinationEvent::IncomingCall { dialog_id, .. } => {
112//! println!("Incoming call: {}", dialog_id);
113//! // Coordinate with media layer
114//! },
115//! SessionCoordinationEvent::CallAnswered { dialog_id, session_answer } => {
116//! println!("Call answered: {}", dialog_id);
117//! // Set up media based on SDP answer
118//! },
119//! _ => {}
120//! }
121//! }
122//! });
123//! # Ok(())
124//! # }
125//! # async fn setup_dependencies() -> Result<(std::sync::Arc<rvoip_transaction_core::TransactionManager>, rvoip_dialog_core::api::ClientConfig), Box<dyn std::error::Error>> { unimplemented!() }
126//! ```
127//!
128//! ## Usage Patterns
129//!
130//! ### Pattern 1: Simple Call Management
131//!
132//! For basic voice calls with minimal complexity:
133//!
134//! ```rust,no_run
135//! use rvoip_dialog_core::api::DialogClient;
136//!
137//! # async fn simple_calls(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
138//! // Make a call
139//! let call = client.make_call(
140//! "sip:caller@example.com",
141//! "sip:callee@example.com",
142//! Some("SDP offer".to_string())
143//! ).await?;
144//!
145//! // Basic call operations
146//! call.hold(Some("SDP with hold".to_string())).await?;
147//! call.resume(Some("SDP with active media".to_string())).await?;
148//! call.transfer("sip:transfer@example.com".to_string()).await?;
149//! call.hangup().await?;
150//! # Ok(())
151//! # }
152//! ```
153//!
154//! ### Pattern 2: Advanced Dialog Management
155//!
156//! For applications requiring fine-grained control over SIP dialogs:
157//!
158//! ```rust,no_run
159//! use rvoip_dialog_core::api::DialogClient;
160//! use rvoip_sip_core::Method;
161//!
162//! # async fn advanced_dialogs(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
163//! // Create dialog without initial request
164//! let dialog = client.create_dialog("sip:me@here.com", "sip:you@there.com").await?;
165//!
166//! // Send custom SIP methods
167//! client.send_notify(&dialog.id(), "presence".to_string(), Some("online".to_string())).await?;
168//! client.send_update(&dialog.id(), Some("Updated SDP".to_string())).await?;
169//! client.send_info(&dialog.id(), "Application data".to_string()).await?;
170//!
171//! // Monitor dialog state
172//! let state = client.get_dialog_state(&dialog.id()).await?;
173//! println!("Dialog state: {:?}", state);
174//!
175//! // Clean up
176//! client.terminate_dialog(&dialog.id()).await?;
177//! # Ok(())
178//! # }
179//! ```
180//!
181//! ### Pattern 3: Response Handling & Server-like Behavior
182//!
183//! For applications that need to handle incoming requests and send responses:
184//!
185//! ```rust,no_run
186//! use rvoip_dialog_core::api::DialogClient;
187//! use rvoip_sip_core::StatusCode;
188//! use rvoip_transaction_core::TransactionKey;
189//!
190//! # async fn response_handling(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
191//! # let transaction_id: TransactionKey = unimplemented!();
192//! # let dialog_id = unimplemented!();
193//!
194//! // Build and send responses
195//! let response = client.build_response(&transaction_id, StatusCode::Ok, Some("Response body".to_string())).await?;
196//! client.send_response(&transaction_id, response).await?;
197//!
198//! // Or use convenience method
199//! client.send_status_response(&transaction_id, StatusCode::Accepted, None).await?;
200//!
201//! // Dialog-aware responses
202//! let dialog_response = client.build_dialog_response(
203//! &transaction_id,
204//! &dialog_id,
205//! StatusCode::Ok,
206//! Some("Dialog response".to_string())
207//! ).await?;
208//! client.send_response(&transaction_id, dialog_response).await?;
209//! # Ok(())
210//! # }
211//! ```
212//!
213//! ## Error Handling
214//!
215//! The DialogClient provides comprehensive error handling with specific error types:
216//!
217//! ```rust,no_run
218//! use rvoip_dialog_core::api::{DialogClient, ApiError};
219//!
220//! # async fn error_handling(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
221//! match client.make_call("invalid-uri", "another-invalid", None).await {
222//! Ok(call) => {
223//! println!("Call successful: {}", call.call_id());
224//! },
225//! Err(ApiError::Configuration { message }) => {
226//! eprintln!("Configuration issue: {}", message);
227//! // Fix configuration and retry
228//! },
229//! Err(ApiError::Network { message }) => {
230//! eprintln!("Network problem: {}", message);
231//! // Check connectivity, retry with backoff
232//! },
233//! Err(ApiError::Protocol { message }) => {
234//! eprintln!("SIP protocol error: {}", message);
235//! // Log for debugging, potentially report upstream
236//! },
237//! Err(ApiError::Dialog { message }) => {
238//! eprintln!("Dialog state error: {}", message);
239//! // Handle dialog lifecycle issues
240//! },
241//! Err(ApiError::Internal { message }) => {
242//! eprintln!("Internal error: {}", message);
243//! // Log for debugging, potentially restart client
244//! },
245//! }
246//! # Ok(())
247//! # }
248//! ```
249//!
250//! ## Configuration Examples
251//!
252//! ### Production Client Configuration
253//!
254//! ```rust
255//! use rvoip_dialog_core::api::ClientConfig;
256//! use std::time::Duration;
257//!
258//! let config = ClientConfig::new("192.168.1.100:5060".parse().unwrap())
259//! .with_from_uri("sip:service@company.com")
260//! .with_auth("service_user", "secure_password");
261//!
262//! // Customize for production load
263//! let mut prod_config = config;
264//! prod_config.dialog = prod_config.dialog
265//! .with_timeout(Duration::from_secs(300))
266//! .with_max_dialogs(10000)
267//! .with_user_agent("MyApp/2.0 (Production)");
268//! ```
269//!
270//! ### Development Configuration
271//!
272//! ```rust
273//! use rvoip_dialog_core::api::ClientConfig;
274//! use std::time::Duration;
275//!
276//! let dev_config = ClientConfig::new("127.0.0.1:0".parse().unwrap())
277//! .with_from_uri("sip:dev@localhost");
278//!
279//! // Fast timeouts for testing
280//! let mut test_config = dev_config;
281//! test_config.dialog = test_config.dialog
282//! .with_timeout(Duration::from_secs(30))
283//! .with_user_agent("MyApp-Dev/1.0");
284//! ```
285//!
286//! ## Integration with Other Components
287//!
288//! ### Media Layer Integration
289//!
290//! ```rust,no_run
291//! use rvoip_dialog_core::api::{DialogClient, DialogApi};
292//! use rvoip_dialog_core::events::SessionCoordinationEvent;
293//!
294//! # async fn media_integration(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
295//! // Set up session coordination for media management
296//! let (session_tx, mut session_rx) = tokio::sync::mpsc::channel(100);
297//! client.set_session_coordinator(session_tx).await?;
298//!
299//! // Handle media-related events
300//! tokio::spawn(async move {
301//! while let Some(event) = session_rx.recv().await {
302//! match event {
303//! SessionCoordinationEvent::IncomingCall { dialog_id, request, .. } => {
304//! // Extract SDP from request, set up media
305//! println!("Setting up media for call: {}", dialog_id);
306//! },
307//! SessionCoordinationEvent::CallAnswered { dialog_id, session_answer } => {
308//! // Use SDP answer to configure media streams
309//! println!("Configuring media with answer: {}", session_answer);
310//! },
311//! _ => {}
312//! }
313//! }
314//! });
315//! # Ok(())
316//! # }
317//! ```
318//!
319//! ### Monitoring & Statistics
320//!
321//! ```rust,no_run
322//! use rvoip_dialog_core::api::{DialogClient, DialogApi};
323//!
324//! # async fn monitoring(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
325//! // Get client statistics
326//! let stats = client.get_stats().await;
327//! println!("=== Client Statistics ===");
328//! println!("Active dialogs: {}", stats.active_dialogs);
329//! println!("Total dialogs: {}", stats.total_dialogs);
330//! println!("Success rate: {:.1}%",
331//! 100.0 * stats.successful_calls as f64 / (stats.successful_calls + stats.failed_calls) as f64);
332//! println!("Average call duration: {:.1}s", stats.avg_call_duration);
333//!
334//! // List active dialogs
335//! let active = client.active_dialogs().await;
336//! for dialog in active {
337//! let info = dialog.info().await?;
338//! println!("Dialog {}: {} -> {} ({})",
339//! dialog.id(), info.local_uri, info.remote_uri, info.state);
340//! }
341//! # Ok(())
342//! # }
343//! ```
344//!
345//! ## Best Practices
346//!
347//! 1. **Always use dependency injection**: Use `with_dependencies()` or `with_global_events()` in production
348//! 2. **Handle session coordination**: Set up proper event handling for media integration
349//! 3. **Monitor client health**: Regularly check statistics and active dialog counts
350//! 4. **Implement proper error handling**: Match on specific error types for appropriate responses
351//! 5. **Clean shutdown**: Always call `stop()` before terminating the application
352//! 6. **Use configuration validation**: Call `validate()` on configurations before use
353//! 7. **Leverage handles**: Use DialogHandle and CallHandle for dialog-specific operations
354//!
355//! ## Thread Safety
356//!
357//! DialogClient is designed to be thread-safe and can be safely shared across async tasks:
358//!
359//! ```rust,no_run
360//! use rvoip_dialog_core::api::DialogClient;
361//! use std::sync::Arc;
362//!
363//! # async fn thread_safety(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
364//! let client = Arc::new(client);
365//!
366//! // Spawn multiple tasks using the same client
367//! let client1 = client.clone();
368//! let task1 = tokio::spawn(async move {
369//! client1.make_call("sip:a@test.com", "sip:b@test.com", None).await
370//! });
371//!
372//! let client2 = client.clone();
373//! let task2 = tokio::spawn(async move {
374//! client2.make_call("sip:c@test.com", "sip:d@test.com", None).await
375//! });
376//!
377//! // Both tasks can safely use the client concurrently
378//! let (call1, call2) = tokio::try_join!(task1, task2)?;
379//! # Ok(())
380//! # }
381//! ```
382
383use std::sync::Arc;
384use std::net::SocketAddr;
385use tokio::sync::mpsc;
386use tracing::{info, debug, warn};
387
388use rvoip_transaction_core::{TransactionManager, TransactionKey, TransactionEvent};
389use rvoip_transaction_core::builders::dialog_quick;
390use rvoip_sip_core::{Uri, Method, Response, StatusCode};
391
392use crate::manager::DialogManager;
393use crate::events::SessionCoordinationEvent;
394use crate::dialog::{DialogId, Dialog, DialogState};
395use super::{
396 ApiResult, ApiError, DialogApi, DialogStats,
397 config::ClientConfig,
398 common::{DialogHandle, CallHandle},
399};
400
401/// High-level client interface for SIP dialog management
402///
403/// The DialogClient provides a comprehensive, easy-to-use interface for client-side
404/// SIP operations. It handles the complexities of SIP dialog management while offering
405/// powerful features for call control, media negotiation, and session coordination.
406///
407/// ## Key Capabilities
408///
409/// - **Call Management**: Create, control, and terminate voice/video calls
410/// - **Dialog Operations**: Full SIP dialog lifecycle management
411/// - **Media Coordination**: Integration with media layers through session events
412/// - **Request/Response Handling**: Send arbitrary SIP methods and build responses
413/// - **Authentication**: Built-in support for SIP authentication challenges
414/// - **Statistics & Monitoring**: Real-time metrics and dialog state tracking
415///
416/// ## Constructor Patterns
417///
418/// The DialogClient supports multiple construction patterns to fit different use cases:
419///
420/// ### Pattern 1: Dependency Injection (Recommended)
421///
422/// ```rust,no_run
423/// use rvoip_dialog_core::api::{DialogClient, ClientConfig};
424/// use rvoip_transaction_core::TransactionManager;
425/// use std::sync::Arc;
426///
427/// # async fn dependency_injection() -> Result<(), Box<dyn std::error::Error>> {
428/// // Your application manages transport
429/// # let transport = unimplemented!(); // Your transport implementation
430/// let tx_mgr = Arc::new(TransactionManager::new_sync(transport));
431/// let config = ClientConfig::new("127.0.0.1:0".parse()?);
432///
433/// let client = DialogClient::with_dependencies(tx_mgr, config).await?;
434/// # Ok(())
435/// # }
436/// ```
437///
438/// ### Pattern 2: Global Events (Best for Event Processing)
439///
440/// ```rust,no_run
441/// use rvoip_dialog_core::api::{DialogClient, ClientConfig};
442/// use rvoip_transaction_core::{TransactionManager, TransactionEvent};
443/// use tokio::sync::mpsc;
444/// use std::sync::Arc;
445///
446/// # async fn global_events() -> Result<(), Box<dyn std::error::Error>> {
447/// # let transport = unimplemented!(); // Your transport
448/// let tx_mgr = Arc::new(TransactionManager::new_sync(transport));
449/// let (events_tx, events_rx) = mpsc::channel(1000);
450/// let config = ClientConfig::new("127.0.0.1:0".parse()?);
451///
452/// let client = DialogClient::with_global_events(tx_mgr, events_rx, config).await?;
453/// # Ok(())
454/// # }
455/// ```
456///
457/// ## Complete Usage Example
458///
459/// ```rust,no_run
460/// use rvoip_dialog_core::api::{DialogClient, DialogApi, ClientConfig};
461/// use rvoip_transaction_core::TransactionManager;
462/// use std::sync::Arc;
463///
464/// #[tokio::main]
465/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
466/// // Set up dependencies
467/// # let transport = unimplemented!(); // Your transport setup
468/// let tx_mgr = Arc::new(TransactionManager::new_sync(transport));
469/// let config = ClientConfig::new("192.168.1.100:5060".parse()?)
470/// .with_from_uri("sip:alice@company.com")
471/// .with_auth("alice", "secret123");
472///
473/// // Create and start client
474/// let client = DialogClient::with_dependencies(tx_mgr, config).await?;
475/// client.start().await?;
476///
477/// // Make an outgoing call
478/// let call = client.make_call(
479/// "sip:alice@company.com",
480/// "sip:bob@partner.com",
481/// Some("v=0\r\no=alice 123 456 IN IP4 192.168.1.100\r\n...".to_string())
482/// ).await?;
483///
484/// println!("Call initiated: {}", call.call_id());
485///
486/// // Call operations
487/// tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
488/// call.hold(Some("SDP with hold attributes".to_string())).await?;
489///
490/// tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
491/// call.resume(Some("SDP with active media".to_string())).await?;
492///
493/// // Transfer the call
494/// call.transfer("sip:voicemail@company.com".to_string()).await?;
495///
496/// // Get statistics
497/// let stats = client.get_stats().await;
498/// println!("Client stats: {} active dialogs, {} total",
499/// stats.active_dialogs, stats.total_dialogs);
500///
501/// // Clean shutdown
502/// client.stop().await?;
503///
504/// Ok(())
505/// }
506/// ```
507///
508/// ## Advanced Dialog Operations
509///
510/// ```rust,no_run
511/// use rvoip_dialog_core::api::DialogClient;
512/// use rvoip_sip_core::{Method, StatusCode};
513///
514/// # async fn advanced_operations(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
515/// // Create a dialog without sending initial request
516/// let dialog = client.create_dialog("sip:me@here.com", "sip:you@there.com").await?;
517///
518/// // Send custom SIP methods within the dialog
519/// let info_tx = client.send_info(&dialog.id(), "Application-specific data".to_string()).await?;
520/// let notify_tx = client.send_notify(&dialog.id(), "presence".to_string(), Some("online".to_string())).await?;
521/// let update_tx = client.send_update(&dialog.id(), Some("Updated media SDP".to_string())).await?;
522///
523/// // Monitor dialog state
524/// let state = client.get_dialog_state(&dialog.id()).await?;
525/// println!("Dialog state: {:?}", state);
526///
527/// // Build and send a response (if acting as UAS)
528/// # let transaction_id = unimplemented!();
529/// let response = client.build_response(&transaction_id, StatusCode::Ok, Some("Success".to_string())).await?;
530/// client.send_response(&transaction_id, response).await?;
531///
532/// // Terminate the dialog
533/// client.terminate_dialog(&dialog.id()).await?;
534/// # Ok(())
535/// # }
536/// ```
537///
538/// ## Session Coordination Integration
539///
540/// ```rust,no_run
541/// use rvoip_dialog_core::api::{DialogClient, DialogApi};
542/// use rvoip_dialog_core::events::SessionCoordinationEvent;
543/// use tokio::sync::mpsc;
544///
545/// # async fn session_integration(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
546/// // Set up session coordination for media management
547/// let (session_tx, mut session_rx) = mpsc::channel(100);
548/// client.set_session_coordinator(session_tx).await?;
549/// client.start().await?;
550///
551/// // Handle session events from the client
552/// let session_handler = tokio::spawn(async move {
553/// while let Some(event) = session_rx.recv().await {
554/// match event {
555/// SessionCoordinationEvent::IncomingCall { dialog_id, request, .. } => {
556/// println!("Incoming call on dialog {}", dialog_id);
557/// // Extract SDP, set up media, send response
558/// },
559/// SessionCoordinationEvent::CallAnswered { dialog_id, session_answer } => {
560/// println!("Call {} answered", dialog_id);
561/// // Configure media streams based on SDP answer
562/// },
563/// SessionCoordinationEvent::CallTerminated { dialog_id, reason } => {
564/// println!("Call {} terminated: {}", dialog_id, reason);
565/// // Clean up media resources
566/// },
567/// _ => {}
568/// }
569/// }
570/// });
571///
572/// // Make calls while session handler processes events
573/// let call = client.make_call("sip:me@here.com", "sip:you@there.com", None).await?;
574///
575/// // Session events will be automatically generated and handled
576/// tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
577/// call.hangup().await?;
578///
579/// # Ok(())
580/// # }
581/// ```
582///
583/// ## Error Handling Strategies
584///
585/// ```rust,no_run
586/// use rvoip_dialog_core::api::{DialogClient, ApiError};
587/// use tokio::time::{sleep, Duration};
588///
589/// # async fn error_handling_strategies(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
590/// // Retry logic for network errors
591/// let mut attempts = 0;
592/// let max_attempts = 3;
593///
594/// loop {
595/// match client.make_call("sip:me@here.com", "sip:you@there.com", None).await {
596/// Ok(call) => {
597/// println!("Call successful: {}", call.call_id());
598/// break;
599/// },
600/// Err(ApiError::Network { message }) if attempts < max_attempts => {
601/// attempts += 1;
602/// println!("Network error (attempt {}): {}", attempts, message);
603/// sleep(Duration::from_secs(2u64.pow(attempts))).await; // Exponential backoff
604/// continue;
605/// },
606/// Err(ApiError::Configuration { message }) => {
607/// eprintln!("Configuration error: {}", message);
608/// // Fix configuration and restart
609/// break;
610/// },
611/// Err(ApiError::Protocol { message }) => {
612/// eprintln!("SIP protocol error: {}", message);
613/// // Log for debugging, continue with other operations
614/// break;
615/// },
616/// Err(e) => {
617/// eprintln!("Unrecoverable error: {}", e);
618/// break;
619/// }
620/// }
621/// }
622/// # Ok(())
623/// # }
624/// ```
625pub struct DialogClient {
626 /// Underlying dialog manager
627 dialog_manager: Arc<DialogManager>,
628
629 /// Client configuration
630 config: ClientConfig,
631
632 /// Statistics tracking
633 stats: Arc<tokio::sync::RwLock<ClientStats>>,
634}
635
636/// Internal statistics tracking
637#[derive(Debug, Default)]
638struct ClientStats {
639 active_dialogs: usize,
640 total_dialogs: u64,
641 successful_calls: u64,
642 failed_calls: u64,
643 total_call_duration: f64,
644}
645
646impl DialogClient {
647 /// Create a new dialog client with simple configuration
648 ///
649 /// This is the easiest way to create a client - just provide a local address
650 /// and the client will be configured with sensible defaults.
651 ///
652 /// # Arguments
653 /// * `local_address` - Local address to use (e.g., "127.0.0.1:0")
654 ///
655 /// # Returns
656 /// A configured DialogClient ready to start
657 pub async fn new(local_address: &str) -> ApiResult<Self> {
658 let addr: SocketAddr = local_address.parse()
659 .map_err(|e| ApiError::Configuration {
660 message: format!("Invalid local address '{}': {}", local_address, e)
661 })?;
662
663 let config = ClientConfig::new(addr);
664 Self::with_config(config).await
665 }
666
667 /// Create a dialog client with custom configuration
668 ///
669 /// **ARCHITECTURAL NOTE**: This method requires dependency injection to maintain
670 /// proper separation of concerns. dialog-core should not directly manage transport
671 /// concerns - that's the responsibility of transaction-core.
672 ///
673 /// Use `with_global_events()` or `with_dependencies()` instead, where you provide
674 /// a pre-configured TransactionManager that handles all transport setup.
675 ///
676 /// # Arguments
677 /// * `config` - Client configuration (for validation and future use)
678 ///
679 /// # Returns
680 /// An error directing users to the proper dependency injection constructors
681 pub async fn with_config(config: ClientConfig) -> ApiResult<Self> {
682 // Validate configuration for future use
683 config.validate()
684 .map_err(|e| ApiError::Configuration { message: e })?;
685
686 // Return architectural guidance error
687 Err(ApiError::Configuration {
688 message: format!(
689 "Simple construction violates architectural separation of concerns. \
690 dialog-core should not manage transport directly. \
691 \nUse dependency injection instead:\
692 \n\n1. with_global_events(transaction_manager, events, config) - RECOMMENDED\
693 \n2. with_dependencies(transaction_manager, config)\
694 \n\nExample setup in your application:\
695 \n // Set up transport and transaction manager in your app\
696 \n let (tx_mgr, events) = TransactionManager::with_transport(transport).await?;\
697 \n let client = DialogClient::with_global_events(tx_mgr, events, config).await?;\
698 \n\nSee examples/ directory for complete setup patterns.",
699 )
700 })
701 }
702
703 /// Create a dialog client with dependency injection and global events (RECOMMENDED)
704 ///
705 /// This is the **recommended** constructor for production applications. It uses global
706 /// transaction event subscription which provides better event handling reliability and
707 /// prevents event loss that can occur with per-transaction subscriptions.
708 ///
709 /// ## Why Global Events?
710 ///
711 /// - **Reliable Event Processing**: All transaction events flow through a single channel
712 /// - **No Event Loss**: Events are queued and processed in order
713 /// - **Better Resource Management**: Single event loop handles all transactions
714 /// - **Cleaner Architecture**: Separation between event generation and consumption
715 ///
716 /// ## Usage Pattern
717 ///
718 /// ```rust,no_run
719 /// use rvoip_dialog_core::api::{DialogClient, DialogApi, ClientConfig};
720 /// use rvoip_transaction_core::{TransactionManager, TransactionEvent};
721 /// use tokio::sync::mpsc;
722 /// use std::sync::Arc;
723 ///
724 /// #[tokio::main]
725 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
726 /// // Set up transport layer (your responsibility)
727 /// # let transport = unimplemented!(); // Your transport implementation
728 ///
729 /// // Create transaction manager
730 /// let tx_mgr = Arc::new(TransactionManager::new_sync(transport));
731 ///
732 /// // Set up global event channel
733 /// let (event_tx, event_rx) = mpsc::channel(1000);
734 ///
735 /// // Configure client
736 /// let config = ClientConfig::new("192.168.1.100:5060".parse()?)
737 /// .with_from_uri("sip:client@company.com")
738 /// .with_auth("client_user", "password123");
739 ///
740 /// // Create client with global events
741 /// let client = DialogClient::with_global_events(tx_mgr, event_rx, config).await?;
742 ///
743 /// // Start client and begin processing
744 /// client.start().await?;
745 ///
746 /// // Client is now ready for operations
747 /// let call = client.make_call(
748 /// "sip:client@company.com",
749 /// "sip:target@partner.com",
750 /// None
751 /// ).await?;
752 ///
753 /// // Clean shutdown
754 /// call.hangup().await?;
755 /// client.stop().await?;
756 ///
757 /// Ok(())
758 /// }
759 /// ```
760 ///
761 /// ## Advanced Event Processing
762 ///
763 /// ```rust,no_run
764 /// use rvoip_dialog_core::api::{DialogClient, ClientConfig, DialogApi};
765 /// use rvoip_transaction_core::{TransactionManager, TransactionEvent};
766 /// use tokio::sync::mpsc;
767 /// use std::sync::Arc;
768 ///
769 /// # async fn advanced_event_processing() -> Result<(), Box<dyn std::error::Error>> {
770 /// # let transport = unimplemented!();
771 /// let tx_mgr = Arc::new(TransactionManager::new_sync(transport));
772 /// let (event_tx, event_rx) = mpsc::channel(1000);
773 /// let config = ClientConfig::new("127.0.0.1:0".parse()?);
774 ///
775 /// // Create client
776 /// let client = DialogClient::with_global_events(tx_mgr.clone(), event_rx, config).await?;
777 ///
778 /// // You can also handle transaction events directly if needed
779 /// tokio::spawn(async move {
780 /// // This would be your custom transaction event processor
781 /// // The client gets its own copy of events through the channel
782 /// });
783 ///
784 /// client.start().await?;
785 /// # Ok(())
786 /// # }
787 /// ```
788 ///
789 /// # Arguments
790 /// * `transaction_manager` - Pre-configured transaction manager with transport
791 /// * `transaction_events` - Global transaction event receiver channel
792 /// * `config` - Client configuration with network and authentication settings
793 ///
794 /// # Returns
795 /// A configured DialogClient ready to start processing
796 ///
797 /// # Errors
798 /// - `ApiError::Configuration` - Invalid configuration parameters
799 /// - `ApiError::Internal` - Failed to create dialog manager with global events
800 pub async fn with_global_events(
801 transaction_manager: Arc<TransactionManager>,
802 transaction_events: mpsc::Receiver<TransactionEvent>,
803 config: ClientConfig,
804 ) -> ApiResult<Self> {
805 // Validate configuration
806 config.validate()
807 .map_err(|e| ApiError::Configuration { message: e })?;
808
809 info!("Creating DialogClient with global transaction events (RECOMMENDED PATTERN)");
810
811 // Create dialog manager with global event subscription (ROOT CAUSE FIX)
812 let dialog_manager = Arc::new(
813 DialogManager::with_global_events(transaction_manager, transaction_events, config.dialog.local_address).await
814 .map_err(|e| ApiError::Internal {
815 message: format!("Failed to create dialog manager with global events: {}", e)
816 })?
817 );
818
819 Ok(Self {
820 dialog_manager,
821 config,
822 stats: Arc::new(tokio::sync::RwLock::new(ClientStats::default())),
823 })
824 }
825
826 /// Create a dialog client with dependency injection
827 ///
828 /// This constructor provides direct dependency injection for scenarios where you need
829 /// full control over the dependencies. It's particularly useful for testing, legacy
830 /// integration, or when you have existing infrastructure to integrate with.
831 ///
832 /// ## When to Use This Pattern
833 ///
834 /// - **Testing**: When you need to inject mock dependencies
835 /// - **Legacy Integration**: When working with existing transaction managers
836 /// - **Simple Use Cases**: When you don't need complex event processing
837 /// - **Custom Event Handling**: When you want to handle transaction events differently
838 ///
839 /// ## Production Considerations
840 ///
841 /// **⚠️ Warning**: This method uses individual transaction event subscriptions which
842 /// may not be as reliable as global event subscriptions. For production applications,
843 /// consider using `with_global_events()` instead.
844 ///
845 /// ## Usage Examples
846 ///
847 /// ### Basic Usage
848 ///
849 /// ```rust,no_run
850 /// use rvoip_dialog_core::api::{DialogClient, DialogApi, ClientConfig};
851 /// use rvoip_transaction_core::TransactionManager;
852 /// use std::sync::Arc;
853 ///
854 /// # async fn basic_usage() -> Result<(), Box<dyn std::error::Error>> {
855 /// // Set up transport (your responsibility)
856 /// # let transport = unimplemented!(); // Your transport implementation
857 /// let tx_mgr = Arc::new(TransactionManager::new_sync(transport));
858 /// let config = ClientConfig::new("127.0.0.1:0".parse()?);
859 ///
860 /// // Create client with dependency injection
861 /// let client = DialogClient::with_dependencies(tx_mgr, config).await?;
862 /// client.start().await?;
863 ///
864 /// // Client is ready for use
865 /// let stats = client.get_stats().await;
866 /// println!("Client initialized: {} active dialogs", stats.active_dialogs);
867 /// # Ok(())
868 /// # }
869 /// ```
870 ///
871 /// ### Testing Pattern
872 ///
873 /// ```rust,no_run
874 /// use rvoip_dialog_core::api::{DialogClient, ClientConfig};
875 /// use rvoip_transaction_core::TransactionManager;
876 /// use std::sync::Arc;
877 ///
878 /// # async fn testing_pattern() -> Result<(), Box<dyn std::error::Error>> {
879 /// // Create mock transport for testing
880 /// # let mock_transport = unimplemented!(); // Your mock transport
881 /// let tx_mgr = Arc::new(TransactionManager::new_sync(mock_transport));
882 /// let config = ClientConfig::new("127.0.0.1:0".parse()?)
883 /// .with_from_uri("sip:test@localhost");
884 ///
885 /// let client = DialogClient::with_dependencies(tx_mgr, config).await?;
886 ///
887 /// // Test client operations
888 /// assert_eq!(client.config().from_uri.as_ref().unwrap(), "sip:test@localhost");
889 /// # Ok(())
890 /// # }
891 /// ```
892 ///
893 /// ### Integration with Existing Infrastructure
894 ///
895 /// ```rust,no_run
896 /// use rvoip_dialog_core::api::{DialogClient, ClientConfig, DialogApi};
897 /// use rvoip_transaction_core::TransactionManager;
898 /// use std::sync::Arc;
899 ///
900 /// # async fn infrastructure_integration(existing_tx_mgr: Arc<TransactionManager>) -> Result<(), Box<dyn std::error::Error>> {
901 /// // Use existing transaction manager from your infrastructure
902 /// let config = ClientConfig::new("10.0.1.50:5060".parse()?)
903 /// .with_from_uri("sip:service@internal.company.com")
904 /// .with_auth("service", "internal_password");
905 ///
906 /// let client = DialogClient::with_dependencies(existing_tx_mgr, config).await?;
907 ///
908 /// // Integrate with existing systems
909 /// client.start().await?;
910 /// # Ok(())
911 /// # }
912 /// ```
913 ///
914 /// # Arguments
915 /// * `transaction_manager` - Pre-configured transaction manager with transport
916 /// * `config` - Client configuration with network and authentication settings
917 ///
918 /// # Returns
919 /// A configured DialogClient ready to start processing
920 ///
921 /// # Errors
922 /// - `ApiError::Configuration` - Invalid configuration parameters
923 /// - `ApiError::Internal` - Failed to create dialog manager
924 pub async fn with_dependencies(
925 transaction_manager: Arc<TransactionManager>,
926 config: ClientConfig,
927 ) -> ApiResult<Self> {
928 // Validate configuration
929 config.validate()
930 .map_err(|e| ApiError::Configuration { message: e })?;
931
932 info!("Creating DialogClient with injected dependencies");
933 warn!("WARNING: Using old DialogManager::new() pattern - consider upgrading to with_global_events() for better reliability");
934
935 // Create dialog manager with injected dependencies (OLD PATTERN - may have event issues)
936 let dialog_manager = Arc::new(
937 DialogManager::new(transaction_manager, config.dialog.local_address).await
938 .map_err(|e| ApiError::Internal {
939 message: format!("Failed to create dialog manager: {}", e)
940 })?
941 );
942
943 Ok(Self {
944 dialog_manager,
945 config,
946 stats: Arc::new(tokio::sync::RwLock::new(ClientStats::default())),
947 })
948 }
949
950 /// Make an outgoing call
951 ///
952 /// Creates a new SIP dialog and initiates an outgoing call by sending an INVITE request.
953 /// This is the primary method for establishing voice/video calls and handles all the
954 /// complexity of SIP dialog creation, request generation, and initial negotiation.
955 ///
956 /// ## Call Flow
957 ///
958 /// 1. **URI Validation**: Validates and parses the provided SIP URIs
959 /// 2. **Dialog Creation**: Creates a new outgoing dialog with unique identifiers
960 /// 3. **INVITE Generation**: Builds and sends an INVITE request with optional SDP
961 /// 4. **Handle Creation**: Returns a CallHandle for subsequent call operations
962 /// 5. **Statistics Update**: Updates internal call tracking metrics
963 ///
964 /// ## Usage Examples
965 ///
966 /// ### Basic Call
967 ///
968 /// ```rust,no_run
969 /// use rvoip_dialog_core::api::DialogClient;
970 ///
971 /// # async fn basic_call(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
972 /// let call = client.make_call(
973 /// "sip:alice@company.com", // Who the call is from
974 /// "sip:bob@partner.com", // Who to call
975 /// None // No SDP offer (late negotiation)
976 /// ).await?;
977 ///
978 /// println!("Call initiated: {}", call.call_id());
979 /// // Call is now ringing, wait for response...
980 /// # Ok(())
981 /// # }
982 /// ```
983 ///
984 /// ### Call with SDP Offer (Early Media Negotiation)
985 ///
986 /// ```rust,no_run
987 /// use rvoip_dialog_core::api::DialogClient;
988 ///
989 /// # async fn call_with_sdp(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
990 /// let sdp_offer = r#"v=0
991 /// o=alice 2890844526 2890844527 IN IP4 192.168.1.100
992 /// s=Call
993 /// c=IN IP4 192.168.1.100
994 /// t=0 0
995 /// m=audio 5004 RTP/AVP 0
996 /// a=rtpmap:0 PCMU/8000"#;
997 ///
998 /// let call = client.make_call(
999 /// "sip:alice@company.com",
1000 /// "sip:bob@partner.com",
1001 /// Some(sdp_offer.to_string())
1002 /// ).await?;
1003 ///
1004 /// println!("Call with media offer sent: {}", call.call_id());
1005 /// # Ok(())
1006 /// # }
1007 /// ```
1008 ///
1009 /// ### Production Call with Error Handling
1010 ///
1011 /// ```rust,no_run
1012 /// use rvoip_dialog_core::api::{DialogClient, ApiError};
1013 /// use tokio::time::{timeout, Duration};
1014 ///
1015 /// # async fn production_call(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
1016 /// // Set a timeout for the call setup
1017 /// let call_result = timeout(Duration::from_secs(30), async {
1018 /// client.make_call(
1019 /// "sip:service@company.com",
1020 /// "sip:customer@external.com",
1021 /// None
1022 /// ).await
1023 /// }).await;
1024 ///
1025 /// match call_result {
1026 /// Ok(Ok(call)) => {
1027 /// println!("Call successful: {}", call.call_id());
1028 ///
1029 /// // Set up call monitoring
1030 /// tokio::spawn(async move {
1031 /// if call.is_active().await {
1032 /// println!("Call is active and can be managed");
1033 /// }
1034 /// });
1035 /// },
1036 /// Ok(Err(ApiError::Configuration { message })) => {
1037 /// eprintln!("Configuration error: {}", message);
1038 /// // Fix URIs and retry
1039 /// },
1040 /// Ok(Err(ApiError::Network { message })) => {
1041 /// eprintln!("Network error: {}", message);
1042 /// // Check connectivity and retry
1043 /// },
1044 /// Err(_) => {
1045 /// eprintln!("Call setup timed out");
1046 /// // Handle timeout scenario
1047 /// },
1048 /// Ok(Err(e)) => {
1049 /// eprintln!("Call failed: {}", e);
1050 /// }
1051 /// }
1052 /// # Ok(())
1053 /// # }
1054 /// ```
1055 ///
1056 /// ### Batch Call Operations
1057 ///
1058 /// ```rust,no_run
1059 /// use rvoip_dialog_core::api::DialogClient;
1060 ///
1061 /// # async fn batch_calls(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
1062 /// let call_targets = vec![
1063 /// ("sip:alice@company.com", "sip:customer1@external.com"),
1064 /// ("sip:alice@company.com", "sip:customer2@external.com"),
1065 /// ("sip:alice@company.com", "sip:customer3@external.com"),
1066 /// ];
1067 ///
1068 /// // Make multiple calls sequentially for simplicity
1069 /// let mut calls = Vec::new();
1070 /// for (from, to) in call_targets {
1071 /// let call = client.make_call(from, to, None).await?;
1072 /// calls.push(call);
1073 /// }
1074 /// println!("Successfully initiated {} calls", calls.len());
1075 ///
1076 /// // Monitor all calls
1077 /// for call in calls {
1078 /// let info = call.info().await?;
1079 /// println!("Call {}: {} -> {} ({})",
1080 /// call.call_id(), info.local_uri, info.remote_uri, info.state);
1081 /// }
1082 /// # Ok(())
1083 /// # }
1084 /// ```
1085 ///
1086 /// # Arguments
1087 /// * `from_uri` - Local SIP URI (From header) - who the call is from
1088 /// * `to_uri` - Remote SIP URI (To header) - who to call
1089 /// * `sdp_offer` - Optional SDP offer for media negotiation
1090 ///
1091 /// # Returns
1092 /// A CallHandle for managing the call lifecycle and operations
1093 ///
1094 /// # Errors
1095 /// - `ApiError::Configuration` - Invalid SIP URIs provided
1096 /// - `ApiError::Dialog` - Failed to create dialog or send INVITE
1097 /// - `ApiError::Internal` - Internal dialog manager error
1098 pub async fn make_call(
1099 &self,
1100 from_uri: &str,
1101 to_uri: &str,
1102 sdp_offer: Option<String>,
1103 ) -> ApiResult<CallHandle> {
1104 info!("Making call from {} to {}", from_uri, to_uri);
1105
1106 // Parse URIs
1107 let local_uri: Uri = from_uri.parse()
1108 .map_err(|e| ApiError::Configuration {
1109 message: format!("Invalid from URI '{}': {}", from_uri, e)
1110 })?;
1111
1112 let remote_uri: Uri = to_uri.parse()
1113 .map_err(|e| ApiError::Configuration {
1114 message: format!("Invalid to URI '{}': {}", to_uri, e)
1115 })?;
1116
1117 // Create outgoing dialog
1118 let dialog_id = self.dialog_manager.create_outgoing_dialog(
1119 local_uri,
1120 remote_uri,
1121 None, // Let dialog manager generate call-id
1122 ).await.map_err(ApiError::from)?;
1123
1124 // Send INVITE request
1125 let body_bytes = sdp_offer.map(|s| bytes::Bytes::from(s));
1126 let _transaction_key = self.dialog_manager.send_request(&dialog_id, Method::Invite, body_bytes).await
1127 .map_err(ApiError::from)?;
1128
1129 // Update statistics
1130 {
1131 let mut stats = self.stats.write().await;
1132 stats.active_dialogs += 1;
1133 stats.total_dialogs += 1;
1134 }
1135
1136 debug!("Call created with dialog ID: {}", dialog_id);
1137 Ok(CallHandle::new(dialog_id, self.dialog_manager.clone()))
1138 }
1139
1140 /// Create a new dialog without sending a request
1141 ///
1142 /// Creates a SIP dialog without immediately sending an INVITE or other initial request.
1143 /// This is useful for advanced scenarios where you need fine-grained control over the
1144 /// dialog lifecycle, want to send custom requests, or need to prepare dialogs for
1145 /// specific protocol sequences.
1146 ///
1147 /// ## When to Use This Method
1148 ///
1149 /// - **Custom Protocol Sequences**: When you need to send non-INVITE initial requests
1150 /// - **Conditional Call Setup**: When call initiation depends on external factors
1151 /// - **Advanced Dialog Management**: When you need dialog state before sending requests
1152 /// - **Testing Scenarios**: When you want to test dialog creation independently
1153 /// - **Batch Operations**: When preparing multiple dialogs for coordinated operations
1154 ///
1155 /// ## Usage Examples
1156 ///
1157 /// ### Basic Dialog Creation
1158 ///
1159 /// ```rust,no_run
1160 /// use rvoip_dialog_core::api::DialogClient;
1161 /// use rvoip_sip_core::Method;
1162 ///
1163 /// # async fn basic_dialog(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
1164 /// // Create dialog without initial request
1165 /// let dialog = client.create_dialog(
1166 /// "sip:alice@company.com",
1167 /// "sip:bob@partner.com"
1168 /// ).await?;
1169 ///
1170 /// println!("Dialog created: {}", dialog.id());
1171 ///
1172 /// // Now you can send custom requests
1173 /// dialog.send_request(Method::Options, None).await?;
1174 /// dialog.send_request(Method::Info, Some("Custom info".to_string())).await?;
1175 ///
1176 /// // Check dialog state
1177 /// let state = dialog.state().await?;
1178 /// println!("Dialog state: {:?}", state);
1179 /// # Ok(())
1180 /// # }
1181 /// ```
1182 ///
1183 /// ### Conditional Call Setup
1184 ///
1185 /// ```rust,no_run
1186 /// use rvoip_dialog_core::api::DialogClient;
1187 /// use rvoip_sip_core::Method;
1188 ///
1189 /// # async fn conditional_setup(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
1190 /// // Create dialog first
1191 /// let dialog = client.create_dialog(
1192 /// "sip:service@company.com",
1193 /// "sip:customer@external.com"
1194 /// ).await?;
1195 ///
1196 /// // Check external conditions before proceeding
1197 /// if check_customer_availability().await? {
1198 /// // Send INVITE if customer is available
1199 /// dialog.send_request(Method::Invite, Some("SDP offer".to_string())).await?;
1200 /// println!("Call initiated for available customer");
1201 /// } else {
1202 /// // Send MESSAGE instead
1203 /// dialog.send_request(Method::Message, Some("Customer callback requested".to_string())).await?;
1204 /// println!("Message sent to unavailable customer");
1205 /// }
1206 /// # Ok(())
1207 /// # }
1208 /// # async fn check_customer_availability() -> Result<bool, Box<dyn std::error::Error>> { Ok(true) }
1209 /// ```
1210 ///
1211 /// ### Advanced Protocol Sequences
1212 ///
1213 /// ```rust,no_run
1214 /// use rvoip_dialog_core::api::DialogClient;
1215 /// use rvoip_sip_core::Method;
1216 ///
1217 /// # async fn advanced_protocol(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
1218 /// let dialog = client.create_dialog(
1219 /// "sip:alice@company.com",
1220 /// "sip:conference@partner.com"
1221 /// ).await?;
1222 ///
1223 /// // Custom protocol: Send OPTIONS first to check capabilities
1224 /// let options_tx = dialog.send_request(Method::Options, None).await?;
1225 /// println!("Sent OPTIONS: {}", options_tx);
1226 ///
1227 /// // Wait for response processing (in real code, you'd handle this via events)
1228 /// tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
1229 ///
1230 /// // Send SUBSCRIBE for presence information
1231 /// let subscribe_tx = dialog.send_request(
1232 /// Method::Subscribe,
1233 /// Some("Event: presence\r\nExpires: 3600".to_string())
1234 /// ).await?;
1235 /// println!("Sent SUBSCRIBE: {}", subscribe_tx);
1236 ///
1237 /// // Finally, send INVITE for the actual call
1238 /// let invite_tx = dialog.send_request(Method::Invite, Some("SDP offer".to_string())).await?;
1239 /// println!("Sent INVITE: {}", invite_tx);
1240 /// # Ok(())
1241 /// # }
1242 /// ```
1243 ///
1244 /// ### Batch Dialog Preparation
1245 ///
1246 /// ```rust,no_run
1247 /// use rvoip_dialog_core::api::DialogClient;
1248 /// use std::future::Future;
1249 ///
1250 /// # async fn batch_dialogs(client: DialogClient) -> Result<(), Box<dyn std::error::Error>> {
1251 /// let targets = vec![
1252 /// "sip:customer1@external.com",
1253 /// "sip:customer2@external.com",
1254 /// "sip:customer3@external.com",
1255 /// ];
1256 ///
1257 /// // Create multiple dialogs concurrently using join_all from std
1258 /// let mut dialogs = Vec::new();
1259 /// for target in targets {
1260 /// let dialog = client.create_dialog("sip:service@company.com", target).await?;
1261 /// dialogs.push(dialog);
1262 /// }
1263 /// println!("Created {} dialogs", dialogs.len());
1264 ///
1265 /// // Now you can coordinate operations across all dialogs
1266 /// for (i, dialog) in dialogs.iter().enumerate() {
1267 /// // Stagger call initiation
1268 /// tokio::time::sleep(tokio::time::Duration::from_millis(i as u64 * 100)).await;
1269 /// dialog.send_request(rvoip_sip_core::Method::Invite, None).await?;
1270 /// }
1271 /// # Ok(())
1272 /// # }
1273 /// ```
1274 ///
1275 /// # Arguments
1276 /// * `from_uri` - Local SIP URI (From header)
1277 /// * `to_uri` - Remote SIP URI (To header)
1278 ///
1279 /// # Returns
1280 /// A DialogHandle for the new dialog
1281 ///
1282 /// # Errors
1283 /// - `ApiError::Configuration` - Invalid SIP URIs provided
1284 /// - `ApiError::Dialog` - Failed to create dialog
1285 /// - `ApiError::Internal` - Internal dialog manager error
1286 pub async fn create_dialog(&self, from_uri: &str, to_uri: &str) -> ApiResult<DialogHandle> {
1287 debug!("Creating dialog from {} to {}", from_uri, to_uri);
1288
1289 // Parse URIs
1290 let local_uri: Uri = from_uri.parse()
1291 .map_err(|e| ApiError::Configuration {
1292 message: format!("Invalid from URI '{}': {}", from_uri, e)
1293 })?;
1294
1295 let remote_uri: Uri = to_uri.parse()
1296 .map_err(|e| ApiError::Configuration {
1297 message: format!("Invalid to URI '{}': {}", to_uri, e)
1298 })?;
1299
1300 // Create outgoing dialog
1301 let dialog_id = self.dialog_manager.create_outgoing_dialog(
1302 local_uri,
1303 remote_uri,
1304 None,
1305 ).await.map_err(ApiError::from)?;
1306
1307 // Update statistics
1308 {
1309 let mut stats = self.stats.write().await;
1310 stats.active_dialogs += 1;
1311 stats.total_dialogs += 1;
1312 }
1313
1314 Ok(DialogHandle::new(dialog_id, self.dialog_manager.clone()))
1315 }
1316
1317 // **NEW**: Dialog-level coordination methods for session-core integration
1318
1319 /// Send a SIP request within an existing dialog
1320 ///
1321 /// Sends arbitrary SIP methods within an established dialog. This method provides
1322 /// direct access to SIP protocol operations and is essential for session-core
1323 /// coordination, custom protocol implementations, and advanced call control.
1324 ///
1325 /// ## Supported SIP Methods
1326 ///
1327 /// This method can send any SIP method within a dialog context:
1328 ///
1329 /// - **INVITE**: Re-INVITE for media changes, call transfers
1330 /// - **BYE**: Call termination (prefer using `send_bye()` convenience method)
1331 /// - **UPDATE**: Media parameter updates (RFC 3311)
1332 /// - **INFO**: Application-specific information (RFC 6086)
1333 /// - **REFER**: Call transfers and redirections (RFC 3515)
1334 /// - **NOTIFY**: Event notifications (RFC 3265)
1335 /// - **MESSAGE**: Instant messaging within dialogs
1336 /// - **OPTIONS**: Capability queries
1337 /// - **SUBSCRIBE**: Event subscriptions
1338 ///
1339 /// ## Usage Examples
1340 ///
1341 /// ### Media Management with UPDATE
1342 ///
1343 /// ```rust,no_run
1344 /// use rvoip_dialog_core::api::DialogClient;
1345 /// use rvoip_sip_core::Method;
1346 /// use rvoip_dialog_core::dialog::DialogId;
1347 ///
1348 /// # async fn media_update(client: DialogClient, dialog_id: DialogId) -> Result<(), Box<dyn std::error::Error>> {
1349 /// // Send UPDATE to modify media parameters
1350 /// let updated_sdp = r#"v=0
1351 /// o=alice 2890844526 2890844528 IN IP4 192.168.1.100
1352 /// s=Updated Call
1353 /// c=IN IP4 192.168.1.100
1354 /// t=0 0
1355 /// m=audio 5004 RTP/AVP 0 8
1356 /// a=rtpmap:0 PCMU/8000
1357 /// a=rtpmap:8 PCMA/8000"#;
1358 ///
1359 /// let tx_key = client.send_request_in_dialog(
1360 /// &dialog_id,
1361 /// Method::Update,
1362 /// Some(bytes::Bytes::from(updated_sdp))
1363 /// ).await?;
1364 ///
1365 /// println!("Sent UPDATE request: {}", tx_key);
1366 /// # Ok(())
1367 /// # }
1368 /// ```
1369 ///
1370 /// ### Application Information Exchange
1371 ///
1372 /// ```rust,no_run
1373 /// use rvoip_dialog_core::api::DialogClient;
1374 /// use rvoip_sip_core::Method;
1375 /// use rvoip_dialog_core::dialog::DialogId;
1376 ///
1377 /// # async fn info_exchange(client: DialogClient, dialog_id: DialogId) -> Result<(), Box<dyn std::error::Error>> {
1378 /// // Send application-specific information
1379 /// let app_data = r#"Content-Type: application/json
1380 ///
1381 /// {
1382 /// "action": "screen_share_request",
1383 /// "timestamp": "2024-01-15T10:30:00Z",
1384 /// "session_id": "abc123"
1385 /// }"#;
1386 ///
1387 /// let tx_key = client.send_request_in_dialog(
1388 /// &dialog_id,
1389 /// Method::Info,
1390 /// Some(bytes::Bytes::from(app_data))
1391 /// ).await?;
1392 ///
1393 /// println!("Sent INFO with application data: {}", tx_key);
1394 /// # Ok(())
1395 /// # }
1396 /// ```
1397 ///
1398 /// ### Event Notifications
1399 ///
1400 /// ```rust,no_run
1401 /// use rvoip_dialog_core::api::DialogClient;
1402 /// use rvoip_sip_core::Method;
1403 /// use rvoip_dialog_core::dialog::DialogId;
1404 ///
1405 /// # async fn event_notification(client: DialogClient, dialog_id: DialogId) -> Result<(), Box<dyn std::error::Error>> {
1406 /// // Send NOTIFY for presence update
1407 /// let notify_body = r#"Event: presence
1408 /// Subscription-State: active;expires=3600
1409 /// Content-Type: application/pidf+xml
1410 ///
1411 /// <?xml version="1.0" encoding="UTF-8"?>
1412 /// <presence xmlns="urn:ietf:params:xml:ns:pidf" entity="sip:alice@company.com">
1413 /// <tuple id="tuple1">
1414 /// <status><basic>open</basic></status>
1415 /// <contact>sip:alice@company.com</contact>
1416 /// </tuple>
1417 /// </presence>"#;
1418 ///
1419 /// let tx_key = client.send_request_in_dialog(
1420 /// &dialog_id,
1421 /// Method::Notify,
1422 /// Some(bytes::Bytes::from(notify_body))
1423 /// ).await?;
1424 ///
1425 /// println!("Sent NOTIFY for presence: {}", tx_key);
1426 /// # Ok(())
1427 /// # }
1428 /// ```
1429 ///
1430 /// ### Call Transfer with REFER
1431 ///
1432 /// ```rust,no_run
1433 /// use rvoip_dialog_core::api::DialogClient;
1434 /// use rvoip_sip_core::Method;
1435 /// use rvoip_dialog_core::dialog::DialogId;
1436 ///
1437 /// # async fn call_transfer(client: DialogClient, dialog_id: DialogId) -> Result<(), Box<dyn std::error::Error>> {
1438 /// // Send REFER for call transfer
1439 /// let refer_body = r#"Refer-To: sip:bob@partner.com
1440 /// Referred-By: sip:alice@company.com
1441 /// Contact: sip:alice@company.com
1442 /// Content-Length: 0"#;
1443 ///
1444 /// let tx_key = client.send_request_in_dialog(
1445 /// &dialog_id,
1446 /// Method::Refer,
1447 /// Some(bytes::Bytes::from(refer_body))
1448 /// ).await?;
1449 ///
1450 /// println!("Sent REFER for transfer: {}", tx_key);
1451 /// # Ok(())
1452 /// # }
1453 /// ```
1454 ///
1455 /// ### Session Coordination Pattern
1456 ///
1457 /// ```rust,no_run
1458 /// use rvoip_dialog_core::api::DialogClient;
1459 /// use rvoip_sip_core::Method;
1460 /// use rvoip_dialog_core::dialog::DialogId;
1461 ///
1462 /// # async fn session_coordination(client: DialogClient, dialog_id: DialogId) -> Result<(), Box<dyn std::error::Error>> {
1463 /// // Coordinate with session layer for media changes
1464 /// let media_session = setup_media_session().await?;
1465 /// let new_sdp = media_session.generate_offer().await?;
1466 ///
1467 /// // Send re-INVITE with new media parameters
1468 /// let tx_key = client.send_request_in_dialog(
1469 /// &dialog_id,
1470 /// Method::Invite,
1471 /// Some(bytes::Bytes::from(new_sdp))
1472 /// ).await?;
1473 ///
1474 /// println!("Sent re-INVITE for media change: {}", tx_key);
1475 ///
1476 /// // The response will be handled by session coordination events
1477 /// # Ok(())
1478 /// # }
1479 /// # async fn setup_media_session() -> Result<MediaSession, Box<dyn std::error::Error>> { Ok(MediaSession) }
1480 /// # struct MediaSession;
1481 /// # impl MediaSession {
1482 /// # async fn generate_offer(&self) -> Result<String, Box<dyn std::error::Error>> { Ok("SDP".to_string()) }
1483 /// # }
1484 /// ```
1485 ///
1486 /// # Arguments
1487 /// * `dialog_id` - The dialog ID to send the request within
1488 /// * `method` - SIP method to send (INVITE, BYE, UPDATE, INFO, etc.)
1489 /// * `body` - Optional message body (SDP, application data, etc.)
1490 ///
1491 /// # Returns
1492 /// Transaction key for tracking the request and its response
1493 ///
1494 /// # Errors
1495 /// - `ApiError::Dialog` - Dialog not found or invalid state
1496 /// - `ApiError::Protocol` - Invalid method for current dialog state
1497 /// - `ApiError::Internal` - Failed to send request
1498 pub async fn send_request_in_dialog(
1499 &self,
1500 dialog_id: &DialogId,
1501 method: Method,
1502 body: Option<bytes::Bytes>
1503 ) -> ApiResult<TransactionKey> {
1504 debug!("Sending {} request in dialog {}", method, dialog_id);
1505
1506 self.dialog_manager.send_request(dialog_id, method, body).await
1507 .map_err(ApiError::from)
1508 }
1509
1510 /// Get detailed information about a dialog
1511 ///
1512 /// Provides access to the complete dialog state for session coordination
1513 /// and monitoring purposes.
1514 ///
1515 /// # Arguments
1516 /// * `dialog_id` - The dialog ID to query
1517 ///
1518 /// # Returns
1519 /// Complete dialog information
1520 pub async fn get_dialog_info(&self, dialog_id: &DialogId) -> ApiResult<Dialog> {
1521 self.dialog_manager.get_dialog(dialog_id)
1522 .map_err(ApiError::from)
1523 }
1524
1525 /// Get the current state of a dialog
1526 ///
1527 /// Provides quick access to dialog state without retrieving the full
1528 /// dialog information.
1529 ///
1530 /// # Arguments
1531 /// * `dialog_id` - The dialog ID to query
1532 ///
1533 /// # Returns
1534 /// Current dialog state
1535 pub async fn get_dialog_state(&self, dialog_id: &DialogId) -> ApiResult<DialogState> {
1536 self.dialog_manager.get_dialog_state(dialog_id)
1537 .map_err(ApiError::from)
1538 }
1539
1540 /// Terminate a dialog and clean up resources
1541 ///
1542 /// This method provides direct control over dialog termination,
1543 /// which is essential for session lifecycle management.
1544 ///
1545 /// # Arguments
1546 /// * `dialog_id` - The dialog ID to terminate
1547 ///
1548 /// # Returns
1549 /// Success or error
1550 pub async fn terminate_dialog(&self, dialog_id: &DialogId) -> ApiResult<()> {
1551 debug!("Terminating dialog {}", dialog_id);
1552
1553 self.dialog_manager.terminate_dialog(dialog_id).await
1554 .map_err(ApiError::from)
1555 }
1556
1557 /// List all active dialog IDs
1558 ///
1559 /// Provides access to all active dialogs for monitoring and
1560 /// management purposes.
1561 ///
1562 /// # Returns
1563 /// Vector of active dialog IDs
1564 pub async fn list_active_dialogs(&self) -> Vec<DialogId> {
1565 self.dialog_manager.list_dialogs()
1566 }
1567
1568 /// Send a SIP response for a transaction
1569 ///
1570 /// Provides direct control over response generation, which is essential
1571 /// for custom response handling in session coordination.
1572 ///
1573 /// # Arguments
1574 /// * `transaction_id` - Transaction to respond to
1575 /// * `response` - Complete SIP response
1576 ///
1577 /// # Returns
1578 /// Success or error
1579 pub async fn send_response(
1580 &self,
1581 transaction_id: &TransactionKey,
1582 response: Response
1583 ) -> ApiResult<()> {
1584 debug!("Sending response for transaction {}", transaction_id);
1585
1586 self.dialog_manager.send_response(transaction_id, response).await
1587 .map_err(ApiError::from)
1588 }
1589
1590 /// Build a SIP response with automatic header generation
1591 ///
1592 /// Convenience method for creating properly formatted SIP responses
1593 /// with correct headers and routing information using Phase 3 dialog functions.
1594 ///
1595 /// # Arguments
1596 /// * `transaction_id` - Transaction to respond to
1597 /// * `status_code` - SIP status code
1598 /// * `body` - Optional response body
1599 ///
1600 /// # Returns
1601 /// Built SIP response ready for sending
1602 pub async fn build_response(
1603 &self,
1604 transaction_id: &TransactionKey,
1605 status_code: StatusCode,
1606 body: Option<String>
1607 ) -> ApiResult<Response> {
1608 debug!("Building response with status {} for transaction {} using Phase 3 functions", status_code, transaction_id);
1609
1610 // Get original request from transaction manager
1611 let original_request = self.dialog_manager()
1612 .transaction_manager()
1613 .original_request(transaction_id)
1614 .await
1615 .map_err(|e| ApiError::Internal {
1616 message: format!("Failed to get original request: {}", e)
1617 })?
1618 .ok_or_else(|| ApiError::Internal {
1619 message: "No original request found for transaction".to_string()
1620 })?;
1621
1622 // Use Phase 3 dialog quick function for instant response creation - ONE LINER!
1623 let response = dialog_quick::response_for_dialog_transaction(
1624 transaction_id.to_string(),
1625 original_request,
1626 None, // No specific dialog ID
1627 status_code,
1628 self.dialog_manager.local_address,
1629 body,
1630 None // No custom reason
1631 ).map_err(|e| ApiError::Internal {
1632 message: format!("Failed to build response using Phase 3 functions: {}", e)
1633 })?;
1634
1635 debug!("Successfully built response with status {} for transaction {} using Phase 3 functions", status_code, transaction_id);
1636 Ok(response)
1637 }
1638
1639 /// Build a dialog-aware response with enhanced context
1640 ///
1641 /// This method provides dialog-aware response building using Phase 3 dialog utilities
1642 /// to ensure proper response construction for dialog transactions.
1643 ///
1644 /// # Arguments
1645 /// * `transaction_id` - Transaction to respond to
1646 /// * `dialog_id` - Dialog ID for context
1647 /// * `status_code` - SIP status code
1648 /// * `body` - Optional response body
1649 ///
1650 /// # Returns
1651 /// Built SIP response with dialog awareness
1652 pub async fn build_dialog_response(
1653 &self,
1654 transaction_id: &TransactionKey,
1655 dialog_id: &DialogId,
1656 status_code: StatusCode,
1657 body: Option<String>
1658 ) -> ApiResult<Response> {
1659 debug!("Building dialog-aware response with status {} for transaction {} in dialog {} using Phase 3 functions",
1660 status_code, transaction_id, dialog_id);
1661
1662 // Get original request from transaction manager
1663 let original_request = self.dialog_manager()
1664 .transaction_manager()
1665 .original_request(transaction_id)
1666 .await
1667 .map_err(|e| ApiError::Internal {
1668 message: format!("Failed to get original request: {}", e)
1669 })?
1670 .ok_or_else(|| ApiError::Internal {
1671 message: "No original request found for transaction".to_string()
1672 })?;
1673
1674 // Use Phase 3 dialog quick function with dialog context - ONE LINER!
1675 let response = dialog_quick::response_for_dialog_transaction(
1676 transaction_id.to_string(),
1677 original_request,
1678 Some(dialog_id.to_string()),
1679 status_code,
1680 self.dialog_manager.local_address,
1681 body,
1682 None // No custom reason
1683 ).map_err(|e| ApiError::Internal {
1684 message: format!("Failed to build dialog response using Phase 3 functions: {}", e)
1685 })?;
1686
1687 debug!("Successfully built dialog-aware response with status {} for transaction {} in dialog {} using Phase 3 functions",
1688 status_code, transaction_id, dialog_id);
1689 Ok(response)
1690 }
1691
1692 /// Send a status response with automatic response building
1693 ///
1694 /// Convenience method for sending simple status responses without
1695 /// manual response construction.
1696 ///
1697 /// # Arguments
1698 /// * `transaction_id` - Transaction to respond to
1699 /// * `status_code` - SIP status code
1700 /// * `reason` - Optional reason phrase
1701 ///
1702 /// # Returns
1703 /// Success or error
1704 pub async fn send_status_response(
1705 &self,
1706 transaction_id: &TransactionKey,
1707 status_code: StatusCode,
1708 reason: Option<String>
1709 ) -> ApiResult<()> {
1710 debug!("Sending status response {} for transaction {}", status_code, transaction_id);
1711
1712 // Build the response using our build_response method
1713 let response = self.build_response(transaction_id, status_code, reason).await?;
1714
1715 // Send the response using the dialog manager
1716 self.send_response(transaction_id, response).await?;
1717
1718 debug!("Successfully sent status response {} for transaction {}", status_code, transaction_id);
1719 Ok(())
1720 }
1721
1722 // **NEW**: SIP method-specific convenience methods
1723
1724 /// Send a BYE request to terminate a dialog
1725 ///
1726 /// Convenience method for the common operation of ending a call
1727 /// by sending a BYE request.
1728 ///
1729 /// # Arguments
1730 /// * `dialog_id` - Dialog to terminate
1731 ///
1732 /// # Returns
1733 /// Transaction key for the BYE request
1734 pub async fn send_bye(&self, dialog_id: &DialogId) -> ApiResult<TransactionKey> {
1735 info!("Sending BYE for dialog {}", dialog_id);
1736 self.send_request_in_dialog(dialog_id, Method::Bye, None).await
1737 }
1738
1739 /// Send a REFER request for call transfer
1740 ///
1741 /// Convenience method for initiating call transfers using the
1742 /// REFER method as defined in RFC 3515.
1743 ///
1744 /// # Arguments
1745 /// * `dialog_id` - Dialog to send REFER within
1746 /// * `target_uri` - URI to transfer the call to
1747 /// * `refer_body` - Optional REFER body with additional headers
1748 ///
1749 /// # Returns
1750 /// Transaction key for the REFER request
1751 pub async fn send_refer(
1752 &self,
1753 dialog_id: &DialogId,
1754 target_uri: String,
1755 refer_body: Option<String>
1756 ) -> ApiResult<TransactionKey> {
1757 info!("Sending REFER for dialog {} to {}", dialog_id, target_uri);
1758
1759 let body = if let Some(custom_body) = refer_body {
1760 custom_body
1761 } else {
1762 format!("Refer-To: {}\r\n", target_uri)
1763 };
1764
1765 self.send_request_in_dialog(dialog_id, Method::Refer, Some(bytes::Bytes::from(body))).await
1766 }
1767
1768 /// Send a NOTIFY request for event notifications
1769 ///
1770 /// Convenience method for sending event notifications using the
1771 /// NOTIFY method as defined in RFC 3265.
1772 ///
1773 /// # Arguments
1774 /// * `dialog_id` - Dialog to send NOTIFY within
1775 /// * `event` - Event type being notified
1776 /// * `body` - Optional notification body
1777 ///
1778 /// # Returns
1779 /// Transaction key for the NOTIFY request
1780 pub async fn send_notify(
1781 &self,
1782 dialog_id: &DialogId,
1783 event: String,
1784 body: Option<String>
1785 ) -> ApiResult<TransactionKey> {
1786 info!("Sending NOTIFY for dialog {} event {}", dialog_id, event);
1787
1788 let notify_body = body.map(|b| bytes::Bytes::from(b));
1789 self.send_request_in_dialog(dialog_id, Method::Notify, notify_body).await
1790 }
1791
1792 /// Send an UPDATE request for media modifications
1793 ///
1794 /// Convenience method for updating media parameters using the
1795 /// UPDATE method as defined in RFC 3311.
1796 ///
1797 /// # Arguments
1798 /// * `dialog_id` - Dialog to send UPDATE within
1799 /// * `sdp` - Optional SDP body with new media parameters
1800 ///
1801 /// # Returns
1802 /// Transaction key for the UPDATE request
1803 pub async fn send_update(
1804 &self,
1805 dialog_id: &DialogId,
1806 sdp: Option<String>
1807 ) -> ApiResult<TransactionKey> {
1808 info!("Sending UPDATE for dialog {}", dialog_id);
1809
1810 let update_body = sdp.map(|s| bytes::Bytes::from(s));
1811 self.send_request_in_dialog(dialog_id, Method::Update, update_body).await
1812 }
1813
1814 /// Send an INFO request for application-specific information
1815 ///
1816 /// Convenience method for sending application-specific information
1817 /// using the INFO method as defined in RFC 6086.
1818 ///
1819 /// # Arguments
1820 /// * `dialog_id` - Dialog to send INFO within
1821 /// * `info_body` - Information to send
1822 ///
1823 /// # Returns
1824 /// Transaction key for the INFO request
1825 pub async fn send_info(
1826 &self,
1827 dialog_id: &DialogId,
1828 info_body: String
1829 ) -> ApiResult<TransactionKey> {
1830 info!("Sending INFO for dialog {}", dialog_id);
1831
1832 self.send_request_in_dialog(dialog_id, Method::Info, Some(bytes::Bytes::from(info_body))).await
1833 }
1834
1835 /// Get client configuration
1836 pub fn config(&self) -> &ClientConfig {
1837 &self.config
1838 }
1839
1840 /// Get a list of all active dialog handles
1841 pub async fn active_dialogs(&self) -> Vec<DialogHandle> {
1842 let dialog_ids = self.dialog_manager.list_dialogs();
1843 dialog_ids.into_iter()
1844 .map(|id| DialogHandle::new(id, self.dialog_manager.clone()))
1845 .collect()
1846 }
1847}
1848
1849impl DialogApi for DialogClient {
1850 fn dialog_manager(&self) -> &Arc<DialogManager> {
1851 &self.dialog_manager
1852 }
1853
1854 async fn set_session_coordinator(&self, sender: mpsc::Sender<SessionCoordinationEvent>) -> ApiResult<()> {
1855 info!("Setting session coordinator for dialog client");
1856 self.dialog_manager.set_session_coordinator(sender).await;
1857 Ok(())
1858 }
1859
1860 async fn start(&self) -> ApiResult<()> {
1861 info!("Starting dialog client");
1862
1863 self.dialog_manager.start().await
1864 .map_err(ApiError::from)?;
1865
1866 info!("✅ Dialog client started successfully");
1867 Ok(())
1868 }
1869
1870 async fn stop(&self) -> ApiResult<()> {
1871 info!("Stopping dialog client");
1872
1873 self.dialog_manager.stop().await
1874 .map_err(ApiError::from)?;
1875
1876 info!("✅ Dialog client stopped successfully");
1877 Ok(())
1878 }
1879
1880 async fn get_stats(&self) -> DialogStats {
1881 let stats = self.stats.read().await;
1882 DialogStats {
1883 active_dialogs: stats.active_dialogs,
1884 total_dialogs: stats.total_dialogs,
1885 successful_calls: stats.successful_calls,
1886 failed_calls: stats.failed_calls,
1887 avg_call_duration: if stats.successful_calls > 0 {
1888 stats.total_call_duration / stats.successful_calls as f64
1889 } else {
1890 0.0
1891 },
1892 }
1893 }
1894}