rvoip_client_core/client/controls.rs
1//! Call control operations for the client-core library
2//!
3//! This module provides comprehensive call control functionality for managing active VoIP calls.
4//! It includes operations for call hold/resume, DTMF transmission, call transfer (both blind
5//! and attended), and capability management.
6//!
7//! # Call Control Features
8//!
9//! ## Hold and Resume Operations
10//! - **Hold Call**: Put a call on hold (mute audio, send hold indication)
11//! - **Resume Call**: Resume a call from hold state
12//! - **Hold Status**: Check if a call is currently on hold
13//!
14//! ## DTMF (Dual-Tone Multi-Frequency) Support
15//! - **Send DTMF**: Transmit dial tones during active calls
16//! - **Validation**: Ensure DTMF digits are valid (0-9, A-D, *, #)
17//! - **History Tracking**: Maintain DTMF transmission history
18//!
19//! ## Call Transfer Operations
20//! - **Blind Transfer**: Transfer call directly to destination without consultation
21//! - **Attended Transfer**: Consultation-based transfer with hold and release
22//! - **URI Validation**: Ensure transfer targets are valid SIP or TEL URIs
23//!
24//! ## Capability Management
25//! - **Dynamic Capabilities**: Determine available operations based on call state
26//! - **State-Aware**: Operations adapt to current call conditions
27//! - **Permission Checking**: Validate operations before execution
28//!
29//! # Architecture
30//!
31//! ```text
32//! ┌─────────────────────────┐
33//! │ Application Layer │
34//! └───────────┬─────────────┘
35//! │
36//! ┌───────────▼─────────────┐
37//! │ Call Controls │ ◄── This Module
38//! │ ┌─────────────────────┐ │
39//! │ │ Hold/Resume │ │ • State management
40//! │ │ DTMF Transmission │ │ • Session coordination
41//! │ │ Call Transfer │ │ • Event notification
42//! │ │ Capabilities │ │ • Error handling
43//! │ └─────────────────────┘ │
44//! └───────────┬─────────────┘
45//! │
46//! ┌───────────▼─────────────┐
47//! │ session-core │
48//! │ SessionControl API │
49//! └─────────────────────────┘
50//! ```
51//!
52//! # Usage Examples
53//!
54//! ## Basic Call Hold and Resume
55//!
56//! ```rust
57//! use rvoip_client_core::{ClientManager, ClientConfig, CallId};
58//!
59//! async fn hold_resume_example() -> Result<(), Box<dyn std::error::Error>> {
60//! let config = ClientConfig::new()
61//! .with_sip_addr("127.0.0.1:5060".parse()?);
62//! let client = ClientManager::new(config).await?;
63//! client.start().await?;
64//!
65//! // Assume we have an active call
66//! let call_id = CallId::new_v4();
67//!
68//! // Check capabilities first
69//! if let Ok(caps) = client.get_call_capabilities(&call_id).await {
70//! if caps.can_hold {
71//! // Put call on hold
72//! if let Err(e) = client.hold_call(&call_id).await {
73//! println!("Hold failed: {}", e);
74//! }
75//!
76//! // Check hold status
77//! if let Ok(on_hold) = client.is_call_on_hold(&call_id).await {
78//! println!("Call on hold: {}", on_hold);
79//! }
80//!
81//! // Resume call
82//! if let Err(e) = client.resume_call(&call_id).await {
83//! println!("Resume failed: {}", e);
84//! }
85//! }
86//! }
87//!
88//! Ok(())
89//! }
90//! ```
91//!
92//! ## DTMF Transmission
93//!
94//! ```rust
95//! use rvoip_client_core::{ClientManager, ClientConfig, CallId};
96//!
97//! async fn dtmf_example() -> Result<(), Box<dyn std::error::Error>> {
98//! let config = ClientConfig::new()
99//! .with_sip_addr("127.0.0.1:5061".parse()?);
100//! let client = ClientManager::new(config).await?;
101//! client.start().await?;
102//!
103//! let call_id = CallId::new_v4();
104//!
105//! // Send individual DTMF digits
106//! if let Err(e) = client.send_dtmf(&call_id, "1").await {
107//! println!("DTMF failed: {}", e);
108//! }
109//!
110//! // Send multiple digits
111//! if let Err(e) = client.send_dtmf(&call_id, "123*456#").await {
112//! println!("DTMF sequence failed: {}", e);
113//! }
114//!
115//! // Send extended DTMF (including A-D)
116//! if let Err(e) = client.send_dtmf(&call_id, "123A456B").await {
117//! println!("Extended DTMF failed: {}", e);
118//! }
119//!
120//! Ok(())
121//! }
122//! ```
123//!
124//! ## Call Transfer Operations
125//!
126//! ```rust
127//! use rvoip_client_core::{ClientManager, ClientConfig, CallId};
128//!
129//! async fn transfer_example() -> Result<(), Box<dyn std::error::Error>> {
130//! let config = ClientConfig::new()
131//! .with_sip_addr("127.0.0.1:5062".parse()?);
132//! let client = ClientManager::new(config).await?;
133//! client.start().await?;
134//!
135//! let call_id1 = CallId::new_v4();
136//! let call_id2 = CallId::new_v4();
137//!
138//! // Blind transfer to SIP URI
139//! if let Err(e) = client.transfer_call(&call_id1, "sip:transfer@example.com").await {
140//! println!("Blind transfer failed: {}", e);
141//! }
142//!
143//! // Attended transfer between two calls
144//! if let Err(e) = client.attended_transfer(&call_id1, &call_id2).await {
145//! println!("Attended transfer failed: {}", e);
146//! }
147//!
148//! Ok(())
149//! }
150//! ```
151
152use chrono::Utc;
153
154// Import session-core APIs
155use rvoip_session_core::api::{
156 SessionControl,
157};
158
159// Import client-core types
160use crate::{
161 ClientResult, ClientError,
162 call::CallId,
163};
164
165use crate::client::types::*;
166
167/// Call control operations implementation for ClientManager
168impl super::manager::ClientManager {
169 // ===== PRIORITY 3.2: CALL CONTROL OPERATIONS =====
170
171 /// Put an active call on hold
172 ///
173 /// This method places a call in hold state, which typically mutes the audio stream
174 /// and may play hold music to the remote party. The call remains connected but
175 /// media transmission is suspended until the call is resumed.
176 ///
177 /// # Arguments
178 ///
179 /// * `call_id` - The unique identifier of the call to put on hold
180 ///
181 /// # Returns
182 ///
183 /// Returns `Ok(())` if the call was successfully placed on hold.
184 ///
185 /// # Errors
186 ///
187 /// * `ClientError::CallNotFound` - If no call exists with the given ID
188 /// * `ClientError::InvalidCallState` - If the call is not in a holdable state
189 /// * `ClientError::CallSetupFailed` - If the hold operation fails
190 ///
191 /// # State Requirements
192 ///
193 /// The call must be in the `Connected` state to be placed on hold. Calls in
194 /// other states (such as `Ringing`, `Terminated`, etc.) cannot be held.
195 ///
196 /// # Examples
197 ///
198 /// ## Basic Hold Operation
199 ///
200 /// ```rust
201 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
202 ///
203 /// async fn hold_active_call() -> Result<(), Box<dyn std::error::Error>> {
204 /// let config = ClientConfig::new()
205 /// .with_sip_addr("127.0.0.1:5060".parse()?);
206 /// let client = ClientManager::new(config).await?;
207 /// client.start().await?;
208 ///
209 /// let call_id = CallId::new_v4();
210 ///
211 /// // Put call on hold
212 /// match client.hold_call(&call_id).await {
213 /// Ok(()) => {
214 /// println!("Call {} successfully placed on hold", call_id);
215 ///
216 /// // Verify hold status
217 /// if let Ok(on_hold) = client.is_call_on_hold(&call_id).await {
218 /// assert!(on_hold);
219 /// }
220 /// }
221 /// Err(e) => {
222 /// eprintln!("Failed to hold call: {}", e);
223 /// }
224 /// }
225 ///
226 /// Ok(())
227 /// }
228 /// ```
229 ///
230 /// ## Hold with Capability Check
231 ///
232 /// ```rust
233 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
234 ///
235 /// async fn safe_hold_call() -> Result<(), Box<dyn std::error::Error>> {
236 /// let config = ClientConfig::new()
237 /// .with_sip_addr("127.0.0.1:5061".parse()?);
238 /// let client = ClientManager::new(config).await?;
239 /// client.start().await?;
240 ///
241 /// let call_id = CallId::new_v4();
242 ///
243 /// // Check if call can be held before attempting
244 /// if let Ok(capabilities) = client.get_call_capabilities(&call_id).await {
245 /// if capabilities.can_hold {
246 /// client.hold_call(&call_id).await?;
247 /// println!("Call successfully held");
248 /// } else {
249 /// println!("Call cannot be held in current state");
250 /// }
251 /// }
252 ///
253 /// Ok(())
254 /// }
255 /// ```
256 ///
257 /// ## Error Handling
258 ///
259 /// ```rust
260 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId, ClientError};
261 ///
262 /// async fn hold_with_error_handling() -> Result<(), Box<dyn std::error::Error>> {
263 /// let config = ClientConfig::new()
264 /// .with_sip_addr("127.0.0.1:5062".parse()?);
265 /// let client = ClientManager::new(config).await?;
266 /// client.start().await?;
267 ///
268 /// let call_id = CallId::new_v4();
269 ///
270 /// match client.hold_call(&call_id).await {
271 /// Ok(()) => {
272 /// println!("✅ Call placed on hold successfully");
273 /// }
274 /// Err(ClientError::CallNotFound { .. }) => {
275 /// println!("❌ Call not found - may have been terminated");
276 /// }
277 /// Err(ClientError::InvalidCallState { current_state, .. }) => {
278 /// println!("❌ Cannot hold call in state: {:?}", current_state);
279 /// }
280 /// Err(e) => {
281 /// println!("❌ Hold operation failed: {}", e);
282 /// }
283 /// }
284 ///
285 /// Ok(())
286 /// }
287 /// ```
288 ///
289 /// # Side Effects
290 ///
291 /// - Updates call metadata with hold status and timestamp
292 /// - Triggers media events for hold state change
293 /// - May play hold music to the remote party (depending on server configuration)
294 /// - Audio transmission is suspended for the local party
295 pub async fn hold_call(&self, call_id: &CallId) -> ClientResult<()> {
296 let session_id = self.session_mapping.get(call_id)
297 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
298 .clone();
299
300 // Validate call state
301 if let Some(call_info) = self.call_info.get(call_id) {
302 match call_info.state {
303 crate::call::CallState::Connected => {
304 // OK to hold
305 }
306 crate::call::CallState::Terminated |
307 crate::call::CallState::Failed |
308 crate::call::CallState::Cancelled => {
309 return Err(ClientError::InvalidCallState {
310 call_id: *call_id,
311 current_state: call_info.state.clone()
312 });
313 }
314 _ => {
315 return Err(ClientError::InvalidCallStateGeneric {
316 expected: "Connected".to_string(),
317 actual: format!("{:?}", call_info.state)
318 });
319 }
320 }
321 }
322
323 // Use session-core hold functionality
324 SessionControl::hold_session(&self.coordinator, &session_id)
325 .await
326 .map_err(|e| ClientError::CallSetupFailed {
327 reason: format!("Failed to hold call: {}", e)
328 })?;
329
330 // Update call metadata
331 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
332 call_info.metadata.insert("on_hold".to_string(), "true".to_string());
333 call_info.metadata.insert("hold_initiated_at".to_string(), Utc::now().to_rfc3339());
334 }
335
336 // Emit MediaEvent for hold state change
337 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
338 let media_event = crate::events::MediaEventInfo {
339 call_id: *call_id,
340 event_type: crate::events::MediaEventType::HoldStateChanged { on_hold: true },
341 timestamp: Utc::now(),
342 metadata: {
343 let mut metadata = std::collections::HashMap::new();
344 metadata.insert("session_id".to_string(), session_id.0.clone());
345 metadata
346 },
347 };
348 handler.on_media_event(media_event).await;
349 }
350
351 tracing::info!("Put call {} on hold", call_id);
352 Ok(())
353 }
354
355 /// Resume a call from hold state
356 ///
357 /// This method resumes a previously held call, restoring audio transmission
358 /// and returning the call to its active connected state. The call must have
359 /// been previously placed on hold using `hold_call()`.
360 ///
361 /// # Arguments
362 ///
363 /// * `call_id` - The unique identifier of the call to resume
364 ///
365 /// # Returns
366 ///
367 /// Returns `Ok(())` if the call was successfully resumed from hold.
368 ///
369 /// # Errors
370 ///
371 /// * `ClientError::CallNotFound` - If no call exists with the given ID
372 /// * `ClientError::CallSetupFailed` - If the resume operation fails
373 ///
374 /// # State Requirements
375 ///
376 /// The call should be in a held state to be resumed. However, this method
377 /// will attempt to resume any call that exists, as the session-core layer
378 /// handles the actual state validation.
379 ///
380 /// # Examples
381 ///
382 /// ## Basic Resume Operation
383 ///
384 /// ```rust
385 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
386 ///
387 /// async fn resume_held_call() -> Result<(), Box<dyn std::error::Error>> {
388 /// let config = ClientConfig::new()
389 /// .with_sip_addr("127.0.0.1:5063".parse()?);
390 /// let client = ClientManager::new(config).await?;
391 /// client.start().await?;
392 ///
393 /// let call_id = CallId::new_v4();
394 ///
395 /// // First put call on hold (would normally be done earlier)
396 /// if let Err(e) = client.hold_call(&call_id).await {
397 /// println!("Hold failed: {}", e);
398 /// return Ok(());
399 /// }
400 ///
401 /// // Verify call is on hold
402 /// if let Ok(on_hold) = client.is_call_on_hold(&call_id).await {
403 /// println!("Call on hold: {}", on_hold);
404 /// }
405 ///
406 /// // Resume the call
407 /// match client.resume_call(&call_id).await {
408 /// Ok(()) => {
409 /// println!("Call {} successfully resumed", call_id);
410 ///
411 /// // Verify call is no longer on hold
412 /// if let Ok(on_hold) = client.is_call_on_hold(&call_id).await {
413 /// assert!(!on_hold);
414 /// }
415 /// }
416 /// Err(e) => {
417 /// eprintln!("Failed to resume call: {}", e);
418 /// }
419 /// }
420 ///
421 /// Ok(())
422 /// }
423 /// ```
424 ///
425 /// ## Resume with Capability Check
426 ///
427 /// ```rust
428 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
429 ///
430 /// async fn safe_resume_call() -> Result<(), Box<dyn std::error::Error>> {
431 /// let config = ClientConfig::new()
432 /// .with_sip_addr("127.0.0.1:5064".parse()?);
433 /// let client = ClientManager::new(config).await?;
434 /// client.start().await?;
435 ///
436 /// let call_id = CallId::new_v4();
437 ///
438 /// // Check if call can be resumed before attempting
439 /// if let Ok(capabilities) = client.get_call_capabilities(&call_id).await {
440 /// if capabilities.can_resume {
441 /// client.resume_call(&call_id).await?;
442 /// println!("Call successfully resumed");
443 /// } else {
444 /// println!("Call cannot be resumed (not on hold or wrong state)");
445 /// }
446 /// }
447 ///
448 /// Ok(())
449 /// }
450 /// ```
451 ///
452 /// ## Hold/Resume Cycle
453 ///
454 /// ```rust
455 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
456 /// use tokio::time::{sleep, Duration};
457 ///
458 /// async fn hold_resume_cycle() -> Result<(), Box<dyn std::error::Error>> {
459 /// let config = ClientConfig::new()
460 /// .with_sip_addr("127.0.0.1:5065".parse()?);
461 /// let client = ClientManager::new(config).await?;
462 /// client.start().await?;
463 ///
464 /// let call_id = CallId::new_v4();
465 ///
466 /// // Put call on hold
467 /// if client.hold_call(&call_id).await.is_ok() {
468 /// println!("Call placed on hold");
469 ///
470 /// // Wait briefly (in real app, this might be much longer)
471 /// sleep(Duration::from_millis(100)).await;
472 ///
473 /// // Resume the call
474 /// if client.resume_call(&call_id).await.is_ok() {
475 /// println!("Call resumed from hold");
476 ///
477 /// // Verify final state
478 /// if let Ok(on_hold) = client.is_call_on_hold(&call_id).await {
479 /// println!("Final hold state: {}", on_hold);
480 /// }
481 /// }
482 /// }
483 ///
484 /// Ok(())
485 /// }
486 /// ```
487 ///
488 /// # Side Effects
489 ///
490 /// - Updates call metadata to remove hold status and add resume timestamp
491 /// - Triggers media events for hold state change (on_hold: false)
492 /// - Resumes audio transmission between parties
493 /// - May stop hold music playback (if configured)
494 pub async fn resume_call(&self, call_id: &CallId) -> ClientResult<()> {
495 let session_id = self.session_mapping.get(call_id)
496 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
497 .clone();
498
499 // Use session-core resume functionality
500 SessionControl::resume_session(&self.coordinator, &session_id)
501 .await
502 .map_err(|e| ClientError::CallSetupFailed {
503 reason: format!("Failed to resume call: {}", e)
504 })?;
505
506 // Update call metadata
507 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
508 call_info.metadata.insert("on_hold".to_string(), "false".to_string());
509 call_info.metadata.insert("resumed_at".to_string(), Utc::now().to_rfc3339());
510 }
511
512 // Emit MediaEvent for hold state change
513 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
514 let media_event = crate::events::MediaEventInfo {
515 call_id: *call_id,
516 event_type: crate::events::MediaEventType::HoldStateChanged { on_hold: false },
517 timestamp: Utc::now(),
518 metadata: {
519 let mut metadata = std::collections::HashMap::new();
520 metadata.insert("session_id".to_string(), session_id.0.clone());
521 metadata
522 },
523 };
524 handler.on_media_event(media_event).await;
525 }
526
527 tracing::info!("Resumed call {} from hold", call_id);
528 Ok(())
529 }
530
531 /// Check if a call is currently on hold
532 ///
533 /// This method queries the hold status of a call by examining its metadata.
534 /// It returns `true` if the call is currently on hold, `false` if active,
535 /// or an error if the call doesn't exist.
536 ///
537 /// # Arguments
538 ///
539 /// * `call_id` - The unique identifier of the call to check
540 ///
541 /// # Returns
542 ///
543 /// Returns `Ok(true)` if the call is on hold, `Ok(false)` if active.
544 ///
545 /// # Errors
546 ///
547 /// * `ClientError::CallNotFound` - If no call exists with the given ID
548 ///
549 /// # Examples
550 ///
551 /// ## Basic Hold Status Check
552 ///
553 /// ```rust
554 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
555 ///
556 /// async fn check_hold_status() -> Result<(), Box<dyn std::error::Error>> {
557 /// let config = ClientConfig::new()
558 /// .with_sip_addr("127.0.0.1:5066".parse()?);
559 /// let client = ClientManager::new(config).await?;
560 /// client.start().await?;
561 ///
562 /// let call_id = CallId::new_v4();
563 ///
564 /// // Check initial status
565 /// match client.is_call_on_hold(&call_id).await {
566 /// Ok(on_hold) => {
567 /// println!("Call on hold: {}", on_hold);
568 /// assert!(!on_hold); // Should be false initially
569 /// }
570 /// Err(e) => {
571 /// println!("Error checking hold status: {}", e);
572 /// }
573 /// }
574 ///
575 /// Ok(())
576 /// }
577 /// ```
578 ///
579 /// ## Hold Status Monitoring
580 ///
581 /// ```rust
582 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
583 ///
584 /// async fn monitor_hold_status() -> Result<(), Box<dyn std::error::Error>> {
585 /// let config = ClientConfig::new()
586 /// .with_sip_addr("127.0.0.1:5067".parse()?);
587 /// let client = ClientManager::new(config).await?;
588 /// client.start().await?;
589 ///
590 /// let call_id = CallId::new_v4();
591 ///
592 /// // Check status before hold
593 /// if let Ok(status) = client.is_call_on_hold(&call_id).await {
594 /// println!("Before hold: {}", status);
595 /// }
596 ///
597 /// // Put call on hold (ignore errors for doc test)
598 /// let _ = client.hold_call(&call_id).await;
599 ///
600 /// // Check status after hold
601 /// if let Ok(status) = client.is_call_on_hold(&call_id).await {
602 /// println!("After hold: {}", status);
603 /// if status {
604 /// println!("✅ Call is now on hold");
605 /// }
606 /// }
607 ///
608 /// // Resume call (ignore errors for doc test)
609 /// let _ = client.resume_call(&call_id).await;
610 ///
611 /// // Check status after resume
612 /// if let Ok(status) = client.is_call_on_hold(&call_id).await {
613 /// println!("After resume: {}", status);
614 /// if !status {
615 /// println!("✅ Call is now active");
616 /// }
617 /// }
618 ///
619 /// Ok(())
620 /// }
621 /// ```
622 ///
623 /// ## Conditional Operations Based on Hold Status
624 ///
625 /// ```rust
626 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
627 ///
628 /// async fn conditional_operations() -> Result<(), Box<dyn std::error::Error>> {
629 /// let config = ClientConfig::new()
630 /// .with_sip_addr("127.0.0.1:5068".parse()?);
631 /// let client = ClientManager::new(config).await?;
632 /// client.start().await?;
633 ///
634 /// let call_id = CallId::new_v4();
635 ///
636 /// // Perform different actions based on hold status
637 /// match client.is_call_on_hold(&call_id).await {
638 /// Ok(true) => {
639 /// println!("Call is on hold - offering to resume");
640 /// // Could resume the call
641 /// // client.resume_call(&call_id).await?;
642 /// }
643 /// Ok(false) => {
644 /// println!("Call is active - offering to hold");
645 /// // Could put call on hold
646 /// // client.hold_call(&call_id).await?;
647 /// }
648 /// Err(e) => {
649 /// println!("Cannot check hold status: {}", e);
650 /// }
651 /// }
652 ///
653 /// Ok(())
654 /// }
655 /// ```
656 ///
657 /// # Implementation Notes
658 ///
659 /// This method checks the call's metadata for an "on_hold" field that is
660 /// set to "true" when a call is placed on hold and "false" when resumed.
661 /// If the metadata field doesn't exist, the call is considered not on hold.
662 pub async fn is_call_on_hold(&self, call_id: &CallId) -> ClientResult<bool> {
663 if let Some(call_info) = self.call_info.get(call_id) {
664 // Check metadata for hold status
665 let on_hold = call_info.metadata.get("on_hold")
666 .map(|s| s == "true")
667 .unwrap_or(false);
668 Ok(on_hold)
669 } else {
670 Err(ClientError::CallNotFound { call_id: *call_id })
671 }
672 }
673
674 /// Send DTMF (Dual-Tone Multi-Frequency) digits during an active call
675 ///
676 /// This method transmits DTMF tones to the remote party during a connected call.
677 /// DTMF is commonly used for navigating phone menus, entering PINs, or other
678 /// interactive voice response (IVR) interactions.
679 ///
680 /// # Arguments
681 ///
682 /// * `call_id` - The unique identifier of the call to send DTMF to
683 /// * `digits` - A string containing valid DTMF characters to transmit
684 ///
685 /// # Valid DTMF Characters
686 ///
687 /// - **Digits**: `0-9` (standard numeric keypad)
688 /// - **Letters**: `A-D` (extended DTMF for special applications)
689 /// - **Symbols**: `*` (star) and `#` (pound/hash)
690 ///
691 /// # Returns
692 ///
693 /// Returns `Ok(())` if the DTMF digits were successfully transmitted.
694 ///
695 /// # Errors
696 ///
697 /// * `ClientError::CallNotFound` - If no call exists with the given ID
698 /// * `ClientError::InvalidCallState` - If the call is not in a connected state
699 /// * `ClientError::InvalidConfiguration` - If digits are empty or contain invalid characters
700 /// * `ClientError::CallSetupFailed` - If the DTMF transmission fails
701 ///
702 /// # State Requirements
703 ///
704 /// The call must be in the `Connected` state to send DTMF. Calls that are
705 /// ringing, terminated, or in other states cannot transmit DTMF tones.
706 ///
707 /// # Examples
708 ///
709 /// ## Basic DTMF Transmission
710 ///
711 /// ```rust
712 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
713 ///
714 /// async fn send_basic_dtmf() -> Result<(), Box<dyn std::error::Error>> {
715 /// let config = ClientConfig::new()
716 /// .with_sip_addr("127.0.0.1:5069".parse()?);
717 /// let client = ClientManager::new(config).await?;
718 /// client.start().await?;
719 ///
720 /// let call_id = CallId::new_v4();
721 ///
722 /// // Send individual digit
723 /// match client.send_dtmf(&call_id, "1").await {
724 /// Ok(()) => println!("✅ Sent DTMF digit '1'"),
725 /// Err(e) => println!("❌ DTMF failed: {}", e),
726 /// }
727 ///
728 /// // Send multiple digits
729 /// if client.send_dtmf(&call_id, "123").await.is_ok() {
730 /// println!("✅ Sent DTMF sequence '123'");
731 /// }
732 ///
733 /// Ok(())
734 /// }
735 /// ```
736 ///
737 /// ## Interactive Menu Navigation
738 ///
739 /// ```rust
740 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
741 /// use tokio::time::{sleep, Duration};
742 ///
743 /// async fn navigate_menu() -> Result<(), Box<dyn std::error::Error>> {
744 /// let config = ClientConfig::new()
745 /// .with_sip_addr("127.0.0.1:5070".parse()?);
746 /// let client = ClientManager::new(config).await?;
747 /// client.start().await?;
748 ///
749 /// let call_id = CallId::new_v4();
750 ///
751 /// // Navigate through a typical phone menu
752 /// let menu_sequence = [
753 /// ("1", "Select English"),
754 /// ("2", "Customer Service"),
755 /// ("3", "Account Information"),
756 /// ("*", "Return to previous menu"),
757 /// ("#", "End menu navigation"),
758 /// ];
759 ///
760 /// for (digit, description) in menu_sequence {
761 /// if client.send_dtmf(&call_id, digit).await.is_ok() {
762 /// println!("📞 Sent '{}' - {}", digit, description);
763 ///
764 /// // Wait between menu selections
765 /// sleep(Duration::from_millis(50)).await;
766 /// }
767 /// }
768 ///
769 /// Ok(())
770 /// }
771 /// ```
772 ///
773 /// ## PIN Entry with Validation
774 ///
775 /// ```rust
776 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
777 ///
778 /// async fn enter_pin() -> Result<(), Box<dyn std::error::Error>> {
779 /// let config = ClientConfig::new()
780 /// .with_sip_addr("127.0.0.1:5071".parse()?);
781 /// let client = ClientManager::new(config).await?;
782 /// client.start().await?;
783 ///
784 /// let call_id = CallId::new_v4();
785 ///
786 /// // Example PIN entry
787 /// let pin = "1234";
788 ///
789 /// // Validate PIN before sending
790 /// for ch in pin.chars() {
791 /// if !ch.is_ascii_digit() {
792 /// println!("❌ Invalid PIN character: {}", ch);
793 /// return Ok(());
794 /// }
795 /// }
796 ///
797 /// // Send PIN digits
798 /// match client.send_dtmf(&call_id, pin).await {
799 /// Ok(()) => {
800 /// println!("✅ PIN entered successfully");
801 ///
802 /// // Send confirmation tone
803 /// if client.send_dtmf(&call_id, "#").await.is_ok() {
804 /// println!("✅ PIN confirmed with #");
805 /// }
806 /// }
807 /// Err(e) => {
808 /// println!("❌ PIN entry failed: {}", e);
809 /// }
810 /// }
811 ///
812 /// Ok(())
813 /// }
814 /// ```
815 ///
816 /// ## Extended DTMF Usage
817 ///
818 /// ```rust
819 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
820 ///
821 /// async fn extended_dtmf() -> Result<(), Box<dyn std::error::Error>> {
822 /// let config = ClientConfig::new()
823 /// .with_sip_addr("127.0.0.1:5072".parse()?);
824 /// let client = ClientManager::new(config).await?;
825 /// client.start().await?;
826 ///
827 /// let call_id = CallId::new_v4();
828 ///
829 /// // Use extended DTMF characters (A-D)
830 /// let extended_sequence = "123A456B789C0*#D";
831 ///
832 /// match client.send_dtmf(&call_id, extended_sequence).await {
833 /// Ok(()) => {
834 /// println!("✅ Extended DTMF sequence sent");
835 /// println!("Sequence: {}", extended_sequence);
836 /// }
837 /// Err(e) => {
838 /// println!("❌ Extended DTMF failed: {}", e);
839 /// }
840 /// }
841 ///
842 /// // Test individual extended characters
843 /// for digit in ['A', 'B', 'C', 'D'] {
844 /// let digit_str = digit.to_string();
845 /// if client.send_dtmf(&call_id, &digit_str).await.is_ok() {
846 /// println!("✅ Sent extended DTMF: {}", digit);
847 /// }
848 /// }
849 ///
850 /// Ok(())
851 /// }
852 /// ```
853 ///
854 /// # Side Effects
855 ///
856 /// - Updates call metadata with DTMF history and timestamps
857 /// - Triggers media events for DTMF transmission
858 /// - Transmits actual audio tones to the remote party
859 /// - Maintains a history of all DTMF transmissions for the call
860 ///
861 /// # Implementation Notes
862 ///
863 /// The method validates DTMF characters before transmission and maintains
864 /// a history of all DTMF sequences sent during the call. Both uppercase
865 /// and lowercase letters (A-D, a-d) are accepted and normalized.
866 pub async fn send_dtmf(&self, call_id: &CallId, digits: &str) -> ClientResult<()> {
867 let session_id = self.session_mapping.get(call_id)
868 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
869 .clone();
870
871 // Validate call state
872 if let Some(call_info) = self.call_info.get(call_id) {
873 match call_info.state {
874 crate::call::CallState::Connected => {
875 // OK to send DTMF
876 }
877 crate::call::CallState::Terminated |
878 crate::call::CallState::Failed |
879 crate::call::CallState::Cancelled => {
880 return Err(ClientError::InvalidCallState {
881 call_id: *call_id,
882 current_state: call_info.state.clone()
883 });
884 }
885 _ => {
886 return Err(ClientError::InvalidCallStateGeneric {
887 expected: "Connected".to_string(),
888 actual: format!("{:?}", call_info.state)
889 });
890 }
891 }
892 }
893
894 // Validate DTMF digits
895 if digits.is_empty() {
896 return Err(ClientError::InvalidConfiguration {
897 field: "dtmf_digits".to_string(),
898 reason: "DTMF digits cannot be empty".to_string()
899 });
900 }
901
902 // Check for valid DTMF characters (0-9, A-D, *, #)
903 for ch in digits.chars() {
904 if !matches!(ch, '0'..='9' | 'A'..='D' | 'a'..='d' | '*' | '#') {
905 return Err(ClientError::InvalidConfiguration {
906 field: "dtmf_digits".to_string(),
907 reason: format!("Invalid DTMF character: {}", ch)
908 });
909 }
910 }
911
912 // Use session-core DTMF functionality
913 SessionControl::send_dtmf(&self.coordinator, &session_id, digits)
914 .await
915 .map_err(|e| ClientError::CallSetupFailed {
916 reason: format!("Failed to send DTMF: {}", e)
917 })?;
918
919 // Update call metadata
920 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
921 let dtmf_history = call_info.metadata.entry("dtmf_history".to_string())
922 .or_insert_with(String::new);
923 if !dtmf_history.is_empty() {
924 dtmf_history.push(',');
925 }
926 dtmf_history.push_str(&format!("{}@{}", digits, Utc::now().to_rfc3339()));
927
928 call_info.metadata.insert("last_dtmf_sent".to_string(), digits.to_string());
929 call_info.metadata.insert("last_dtmf_at".to_string(), Utc::now().to_rfc3339());
930 }
931
932 // Emit MediaEvent for DTMF
933 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
934 let media_event = crate::events::MediaEventInfo {
935 call_id: *call_id,
936 event_type: crate::events::MediaEventType::DtmfSent { digits: digits.to_string() },
937 timestamp: Utc::now(),
938 metadata: {
939 let mut metadata = std::collections::HashMap::new();
940 metadata.insert("session_id".to_string(), session_id.0.clone());
941 metadata
942 },
943 };
944 handler.on_media_event(media_event).await;
945 }
946
947 tracing::info!("Sent DTMF '{}' to call {}", digits, call_id);
948 Ok(())
949 }
950
951 /// Transfer a call to another destination (blind transfer)
952 ///
953 /// This method performs a blind transfer, which immediately transfers the call
954 /// to the specified destination without consultation. The original caller is
955 /// connected directly to the transfer target, and the transferring party is
956 /// removed from the call.
957 ///
958 /// # Arguments
959 ///
960 /// * `call_id` - The unique identifier of the call to transfer
961 /// * `target` - The SIP or TEL URI of the transfer destination
962 ///
963 /// # Valid Target Formats
964 ///
965 /// - **SIP URI**: `sip:user@domain.com` or `sip:user@192.168.1.100:5060`
966 /// - **TEL URI**: `tel:+15551234567` (for PSTN numbers)
967 ///
968 /// # Returns
969 ///
970 /// Returns `Ok(())` if the transfer was successfully initiated.
971 ///
972 /// # Errors
973 ///
974 /// * `ClientError::CallNotFound` - If no call exists with the given ID
975 /// * `ClientError::InvalidCallState` - If the call is not in a transferable state
976 /// * `ClientError::InvalidConfiguration` - If the target URI is empty or invalid
977 /// * `ClientError::CallSetupFailed` - If the transfer operation fails
978 ///
979 /// # State Requirements
980 ///
981 /// The call must be in the `Connected` state to be transferred. Calls in
982 /// other states (ringing, terminated, etc.) cannot be transferred.
983 ///
984 /// # Examples
985 ///
986 /// ## Basic Blind Transfer
987 ///
988 /// ```rust
989 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
990 ///
991 /// async fn perform_blind_transfer() -> Result<(), Box<dyn std::error::Error>> {
992 /// let config = ClientConfig::new()
993 /// .with_sip_addr("127.0.0.1:5073".parse()?);
994 /// let client = ClientManager::new(config).await?;
995 /// client.start().await?;
996 ///
997 /// let call_id = CallId::new_v4();
998 ///
999 /// // Transfer to another SIP user
1000 /// let transfer_target = "sip:support@example.com";
1001 ///
1002 /// match client.transfer_call(&call_id, transfer_target).await {
1003 /// Ok(()) => {
1004 /// println!("✅ Call {} transferred to {}", call_id, transfer_target);
1005 /// }
1006 /// Err(e) => {
1007 /// println!("❌ Transfer failed: {}", e);
1008 /// }
1009 /// }
1010 ///
1011 /// Ok(())
1012 /// }
1013 /// ```
1014 ///
1015 /// ## Transfer to PSTN Number
1016 ///
1017 /// ```rust
1018 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
1019 ///
1020 /// async fn transfer_to_pstn() -> Result<(), Box<dyn std::error::Error>> {
1021 /// let config = ClientConfig::new()
1022 /// .with_sip_addr("127.0.0.1:5074".parse()?);
1023 /// let client = ClientManager::new(config).await?;
1024 /// client.start().await?;
1025 ///
1026 /// let call_id = CallId::new_v4();
1027 ///
1028 /// // Transfer to external phone number
1029 /// let phone_number = "tel:+15551234567";
1030 ///
1031 /// if client.transfer_call(&call_id, phone_number).await.is_ok() {
1032 /// println!("✅ Call transferred to phone: {}", phone_number);
1033 /// } else {
1034 /// println!("❌ PSTN transfer failed");
1035 /// }
1036 ///
1037 /// Ok(())
1038 /// }
1039 /// ```
1040 ///
1041 /// ## Transfer with Validation
1042 ///
1043 /// ```rust
1044 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
1045 ///
1046 /// async fn validated_transfer() -> Result<(), Box<dyn std::error::Error>> {
1047 /// let config = ClientConfig::new()
1048 /// .with_sip_addr("127.0.0.1:5075".parse()?);
1049 /// let client = ClientManager::new(config).await?;
1050 /// client.start().await?;
1051 ///
1052 /// let call_id = CallId::new_v4();
1053 /// let target = "sip:manager@company.com";
1054 ///
1055 /// // Check if call can be transferred before attempting
1056 /// if let Ok(capabilities) = client.get_call_capabilities(&call_id).await {
1057 /// if capabilities.can_transfer {
1058 /// // Validate target format
1059 /// if target.starts_with("sip:") || target.starts_with("tel:") {
1060 /// match client.transfer_call(&call_id, target).await {
1061 /// Ok(()) => {
1062 /// println!("✅ Transfer completed successfully");
1063 /// }
1064 /// Err(e) => {
1065 /// println!("❌ Transfer failed: {}", e);
1066 /// }
1067 /// }
1068 /// } else {
1069 /// println!("❌ Invalid target URI format");
1070 /// }
1071 /// } else {
1072 /// println!("❌ Call cannot be transferred in current state");
1073 /// }
1074 /// }
1075 ///
1076 /// Ok(())
1077 /// }
1078 /// ```
1079 ///
1080 /// ## Multiple Transfer Destinations
1081 ///
1082 /// ```rust
1083 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
1084 ///
1085 /// async fn try_multiple_transfers() -> Result<(), Box<dyn std::error::Error>> {
1086 /// let config = ClientConfig::new()
1087 /// .with_sip_addr("127.0.0.1:5076".parse()?);
1088 /// let client = ClientManager::new(config).await?;
1089 /// client.start().await?;
1090 ///
1091 /// let call_id = CallId::new_v4();
1092 ///
1093 /// // Try multiple transfer destinations in order
1094 /// let transfer_options = [
1095 /// ("sip:primary@support.com", "Primary Support"),
1096 /// ("sip:backup@support.com", "Backup Support"),
1097 /// ("tel:+15551234567", "Emergency Line"),
1098 /// ];
1099 ///
1100 /// for (target, description) in transfer_options {
1101 /// match client.transfer_call(&call_id, target).await {
1102 /// Ok(()) => {
1103 /// println!("✅ Successfully transferred to {} ({})", target, description);
1104 /// break; // Stop after first successful transfer
1105 /// }
1106 /// Err(e) => {
1107 /// println!("❌ Failed to transfer to {}: {}", description, e);
1108 /// // Continue to next option
1109 /// }
1110 /// }
1111 /// }
1112 ///
1113 /// Ok(())
1114 /// }
1115 /// ```
1116 ///
1117 /// # Transfer Types
1118 ///
1119 /// This method performs a **blind transfer** (also called unattended transfer):
1120 /// - The call is immediately transferred without consultation
1121 /// - The transferring party does not speak to the transfer target first
1122 /// - The original caller is connected directly to the transfer destination
1123 /// - The transferring party is removed from the call immediately
1124 ///
1125 /// For **attended transfers** (with consultation), use the `attended_transfer()` method.
1126 ///
1127 /// # Side Effects
1128 ///
1129 /// - Updates call metadata with transfer information and timestamp
1130 /// - Triggers media events for transfer initiation
1131 /// - The local party is immediately disconnected from the call
1132 /// - The remote party is connected to the transfer target
1133 ///
1134 /// # SIP Protocol Notes
1135 ///
1136 /// This method uses SIP REFER requests to perform the transfer, which is
1137 /// the standard mechanism defined in RFC 3515. The transfer target must
1138 /// be reachable and accept the incoming call for the transfer to succeed.
1139 pub async fn transfer_call(&self, call_id: &CallId, target: &str) -> ClientResult<()> {
1140 let session_id = self.session_mapping.get(call_id)
1141 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
1142 .clone();
1143
1144 // Validate call state
1145 if let Some(call_info) = self.call_info.get(call_id) {
1146 match call_info.state {
1147 crate::call::CallState::Connected => {
1148 // OK to transfer
1149 }
1150 crate::call::CallState::Terminated |
1151 crate::call::CallState::Failed |
1152 crate::call::CallState::Cancelled => {
1153 return Err(ClientError::InvalidCallState {
1154 call_id: *call_id,
1155 current_state: call_info.state.clone()
1156 });
1157 }
1158 _ => {
1159 return Err(ClientError::InvalidCallStateGeneric {
1160 expected: "Connected".to_string(),
1161 actual: format!("{:?}", call_info.state)
1162 });
1163 }
1164 }
1165 }
1166
1167 // Validate target URI
1168 if target.is_empty() {
1169 return Err(ClientError::InvalidConfiguration {
1170 field: "transfer_target".to_string(),
1171 reason: "Transfer target cannot be empty".to_string()
1172 });
1173 }
1174
1175 if !target.starts_with("sip:") && !target.starts_with("tel:") {
1176 return Err(ClientError::InvalidConfiguration {
1177 field: "transfer_target".to_string(),
1178 reason: "Transfer target must be a valid SIP or TEL URI".to_string()
1179 });
1180 }
1181
1182 // Use session-core transfer functionality
1183 SessionControl::transfer_session(&self.coordinator, &session_id, target)
1184 .await
1185 .map_err(|e| ClientError::CallSetupFailed {
1186 reason: format!("Failed to transfer call: {}", e)
1187 })?;
1188
1189 // Update call metadata
1190 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
1191 call_info.metadata.insert("transfer_target".to_string(), target.to_string());
1192 call_info.metadata.insert("transfer_initiated_at".to_string(), Utc::now().to_rfc3339());
1193 call_info.metadata.insert("transfer_type".to_string(), "blind".to_string());
1194 }
1195
1196 // Emit MediaEvent for transfer initiation
1197 if let Some(handler) = self.call_handler.client_event_handler.read().await.as_ref() {
1198 let media_event = crate::events::MediaEventInfo {
1199 call_id: *call_id,
1200 event_type: crate::events::MediaEventType::TransferInitiated {
1201 target: target.to_string(),
1202 transfer_type: "blind".to_string()
1203 },
1204 timestamp: Utc::now(),
1205 metadata: {
1206 let mut metadata = std::collections::HashMap::new();
1207 metadata.insert("session_id".to_string(), session_id.0.clone());
1208 metadata
1209 },
1210 };
1211 handler.on_media_event(media_event).await;
1212 }
1213
1214 tracing::info!("Initiated blind transfer of call {} to {}", call_id, target);
1215 Ok(())
1216 }
1217
1218 /// Perform an attended transfer (consultative transfer)
1219 ///
1220 /// This method performs an attended transfer, which connects two existing calls
1221 /// together. The typical scenario is having one call on hold while establishing
1222 /// a consultation call with the transfer target, then connecting the original
1223 /// caller directly to the transfer target.
1224 ///
1225 /// # Arguments
1226 ///
1227 /// * `call_id1` - The primary call to be transferred (usually the original call)
1228 /// * `call_id2` - The consultation call (the transfer target)
1229 ///
1230 /// # Transfer Process
1231 ///
1232 /// 1. **Hold**: The primary call (`call_id1`) is placed on hold
1233 /// 2. **Consultation**: The agent speaks with the transfer target (`call_id2`)
1234 /// 3. **Transfer**: The primary caller is connected to the transfer target
1235 /// 4. **Cleanup**: The consultation call is terminated
1236 ///
1237 /// # Returns
1238 ///
1239 /// Returns `Ok(())` if the attended transfer was successfully completed.
1240 ///
1241 /// # Errors
1242 ///
1243 /// * `ClientError::CallNotFound` - If either call ID doesn't exist
1244 /// * `ClientError::InvalidCallState` - If either call is not in a transferable state
1245 /// * `ClientError::CallSetupFailed` - If any step of the transfer process fails
1246 ///
1247 /// # State Requirements
1248 ///
1249 /// Both calls must be in the `Connected` state to perform an attended transfer.
1250 ///
1251 /// # Examples
1252 ///
1253 /// ## Basic Attended Transfer
1254 ///
1255 /// ```rust
1256 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
1257 ///
1258 /// async fn perform_attended_transfer() -> Result<(), Box<dyn std::error::Error>> {
1259 /// let config = ClientConfig::new()
1260 /// .with_sip_addr("127.0.0.1:5077".parse()?);
1261 /// let client = ClientManager::new(config).await?;
1262 /// client.start().await?;
1263 ///
1264 /// // Assume we have two active calls
1265 /// let primary_call = CallId::new_v4(); // Original caller
1266 /// let consultation_call = CallId::new_v4(); // Transfer target
1267 ///
1268 /// match client.attended_transfer(&primary_call, &consultation_call).await {
1269 /// Ok(()) => {
1270 /// println!("✅ Attended transfer completed successfully");
1271 /// println!("Primary caller connected to transfer target");
1272 /// }
1273 /// Err(e) => {
1274 /// println!("❌ Attended transfer failed: {}", e);
1275 /// }
1276 /// }
1277 ///
1278 /// Ok(())
1279 /// }
1280 /// ```
1281 ///
1282 /// ## Step-by-Step Transfer Workflow
1283 ///
1284 /// ```rust
1285 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
1286 /// use tokio::time::{sleep, Duration};
1287 ///
1288 /// async fn transfer_workflow() -> Result<(), Box<dyn std::error::Error>> {
1289 /// let config = ClientConfig::new()
1290 /// .with_sip_addr("127.0.0.1:5078".parse()?);
1291 /// let client = ClientManager::new(config).await?;
1292 /// client.start().await?;
1293 ///
1294 /// let customer_call = CallId::new_v4();
1295 /// let manager_call = CallId::new_v4();
1296 ///
1297 /// // Step 1: Answer customer call (would be done earlier)
1298 /// println!("📞 Customer call in progress...");
1299 ///
1300 /// // Step 2: Put customer on hold to make consultation call
1301 /// println!("⏸️ Putting customer on hold...");
1302 /// if client.hold_call(&customer_call).await.is_ok() {
1303 /// println!("✅ Customer on hold");
1304 /// }
1305 ///
1306 /// // Step 3: Make consultation call to manager (would be done earlier)
1307 /// println!("📞 Calling manager for consultation...");
1308 /// // client.make_call("sip:manager@company.com").await?;
1309 ///
1310 /// // Step 4: Brief consultation (simulated)
1311 /// sleep(Duration::from_millis(100)).await;
1312 /// println!("💬 Consultation complete - transferring call");
1313 ///
1314 /// // Step 5: Perform the attended transfer
1315 /// match client.attended_transfer(&customer_call, &manager_call).await {
1316 /// Ok(()) => {
1317 /// println!("✅ Transfer complete - customer now speaking with manager");
1318 /// }
1319 /// Err(e) => {
1320 /// println!("❌ Transfer failed: {}", e);
1321 /// // Would typically resume customer call here
1322 /// let _ = client.resume_call(&customer_call).await;
1323 /// }
1324 /// }
1325 ///
1326 /// Ok(())
1327 /// }
1328 /// ```
1329 ///
1330 /// ## Error Recovery Attended Transfer
1331 ///
1332 /// ```rust
1333 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId, ClientError};
1334 ///
1335 /// async fn robust_attended_transfer() -> Result<(), Box<dyn std::error::Error>> {
1336 /// let config = ClientConfig::new()
1337 /// .with_sip_addr("127.0.0.1:5079".parse()?);
1338 /// let client = ClientManager::new(config).await?;
1339 /// client.start().await?;
1340 ///
1341 /// let caller_id = CallId::new_v4();
1342 /// let target_id = CallId::new_v4();
1343 ///
1344 /// // Check capabilities before attempting transfer
1345 /// let can_transfer_caller = client.get_call_capabilities(&caller_id).await
1346 /// .map(|caps| caps.can_transfer)
1347 /// .unwrap_or(false);
1348 ///
1349 /// let can_transfer_target = client.get_call_capabilities(&target_id).await
1350 /// .map(|caps| caps.can_transfer)
1351 /// .unwrap_or(false);
1352 ///
1353 /// if !can_transfer_caller || !can_transfer_target {
1354 /// println!("❌ One or both calls cannot be transferred");
1355 /// return Ok(());
1356 /// }
1357 ///
1358 /// match client.attended_transfer(&caller_id, &target_id).await {
1359 /// Ok(()) => {
1360 /// println!("✅ Attended transfer successful");
1361 /// }
1362 /// Err(ClientError::CallNotFound { call_id }) => {
1363 /// println!("❌ Call {} no longer exists", call_id);
1364 /// }
1365 /// Err(ClientError::InvalidCallState { call_id, current_state }) => {
1366 /// println!("❌ Call {} in invalid state: {:?}", call_id, current_state);
1367 ///
1368 /// // Try to recover by resuming the original call
1369 /// if let Err(e) = client.resume_call(&caller_id).await {
1370 /// println!("❌ Failed to resume original call: {}", e);
1371 /// }
1372 /// }
1373 /// Err(e) => {
1374 /// println!("❌ Transfer failed: {}", e);
1375 ///
1376 /// // General recovery - try to resume original call
1377 /// let _ = client.resume_call(&caller_id).await;
1378 /// }
1379 /// }
1380 ///
1381 /// Ok(())
1382 /// }
1383 /// ```
1384 ///
1385 /// # Comparison with Blind Transfer
1386 ///
1387 /// | Feature | Blind Transfer | Attended Transfer |
1388 /// |---------|----------------|-------------------|
1389 /// | **Consultation** | No | Yes |
1390 /// | **Agent Control** | Immediate | Full control |
1391 /// | **Success Rate** | Lower | Higher |
1392 /// | **User Experience** | Basic | Professional |
1393 /// | **Call Setup** | Single call | Two calls |
1394 ///
1395 /// # Side Effects
1396 ///
1397 /// - The primary call is placed on hold during the process
1398 /// - Call metadata is updated with transfer type "attended"
1399 /// - Media events are triggered for transfer completion
1400 /// - The consultation call is automatically terminated
1401 /// - The transferring agent is removed from both calls
1402 ///
1403 /// # Best Practices
1404 ///
1405 /// 1. **Always verify both calls exist** before attempting transfer
1406 /// 2. **Check call capabilities** to ensure transfer is possible
1407 /// 3. **Implement error recovery** to handle failed transfers gracefully
1408 /// 4. **Inform the customer** when placing them on hold for consultation
1409 /// 5. **Have a fallback plan** if the transfer target is unavailable
1410 pub async fn attended_transfer(&self, call_id1: &CallId, call_id2: &CallId) -> ClientResult<()> {
1411 // Get session IDs for both calls (for validation, though not directly used below)
1412 let _session_id1 = self.session_mapping.get(call_id1)
1413 .ok_or(ClientError::CallNotFound { call_id: *call_id1 })?
1414 .clone();
1415 let _session_id2 = self.session_mapping.get(call_id2)
1416 .ok_or(ClientError::CallNotFound { call_id: *call_id2 })?
1417 .clone();
1418
1419 // Validate both calls are in connected state
1420 for call_id in [call_id1, call_id2] {
1421 if let Some(call_info) = self.call_info.get(call_id) {
1422 match call_info.state {
1423 crate::call::CallState::Connected => {
1424 // OK to transfer
1425 }
1426 crate::call::CallState::Terminated |
1427 crate::call::CallState::Failed |
1428 crate::call::CallState::Cancelled => {
1429 return Err(ClientError::InvalidCallState {
1430 call_id: *call_id,
1431 current_state: call_info.state.clone()
1432 });
1433 }
1434 _ => {
1435 return Err(ClientError::InvalidCallStateGeneric {
1436 expected: "Connected".to_string(),
1437 actual: format!("{:?}", call_info.state)
1438 });
1439 }
1440 }
1441 }
1442 }
1443
1444 // For attended transfer, we typically would:
1445 // 1. Put the first call on hold
1446 // 2. Establish a consultation call with the transfer target
1447 // 3. Complete the transfer connecting the original caller to the transfer target
1448 //
1449 // Since session-core doesn't have a specific attended transfer API,
1450 // we'll simulate it with available operations
1451
1452 // Put first call on hold
1453 self.hold_call(call_id1).await?;
1454
1455 // Get remote URI from second call to use as transfer target
1456 let target_uri = if let Some(call_info2) = self.call_info.get(call_id2) {
1457 call_info2.remote_uri.clone()
1458 } else {
1459 return Err(ClientError::CallNotFound { call_id: *call_id2 });
1460 };
1461
1462 // Transfer the first call to the target of the second call
1463 self.transfer_call(call_id1, &target_uri).await?;
1464
1465 // Hang up the consultation call since transfer is completing
1466 self.hangup_call(call_id2).await?;
1467
1468 // Update metadata for attended transfer
1469 if let Some(mut call_info) = self.call_info.get_mut(call_id1) {
1470 call_info.metadata.insert("transfer_type".to_string(), "attended".to_string());
1471 call_info.metadata.insert("consultation_call_id".to_string(), call_id2.to_string());
1472 call_info.metadata.insert("attended_transfer_completed_at".to_string(), Utc::now().to_rfc3339());
1473 }
1474
1475 tracing::info!("Completed attended transfer: call {} transferred to target of call {}", call_id1, call_id2);
1476 Ok(())
1477 }
1478
1479 /// Get call control capabilities for a specific call
1480 ///
1481 /// This method returns the available call control operations for a call based on
1482 /// its current state. Different call states support different operations, and this
1483 /// method helps applications determine what actions are available before attempting them.
1484 ///
1485 /// # Arguments
1486 ///
1487 /// * `call_id` - The unique identifier of the call to query
1488 ///
1489 /// # Returns
1490 ///
1491 /// Returns a `CallCapabilities` struct indicating which operations are available:
1492 ///
1493 /// - `can_hold` - Whether the call can be placed on hold
1494 /// - `can_resume` - Whether the call can be resumed from hold
1495 /// - `can_transfer` - Whether the call can be transferred
1496 /// - `can_send_dtmf` - Whether DTMF digits can be sent
1497 /// - `can_mute` - Whether the call can be muted
1498 /// - `can_hangup` - Whether the call can be terminated
1499 ///
1500 /// # Errors
1501 ///
1502 /// * `ClientError::CallNotFound` - If no call exists with the given ID
1503 ///
1504 /// # Capability Matrix by Call State
1505 ///
1506 /// | State | Hold | Resume | Transfer | DTMF | Mute | Hangup |
1507 /// |-------|------|--------|----------|------|------|--------|
1508 /// | **Connected** | ✅ | ⚡ | ✅ | ✅ | ✅ | ✅ |
1509 /// | **Ringing** | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
1510 /// | **Initiating** | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
1511 /// | **Proceeding** | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
1512 /// | **Terminated** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
1513 ///
1514 /// ⚡ = Available only if call is currently on hold
1515 ///
1516 /// # Examples
1517 ///
1518 /// ## Basic Capability Check
1519 ///
1520 /// ```rust
1521 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
1522 ///
1523 /// async fn check_capabilities() -> Result<(), Box<dyn std::error::Error>> {
1524 /// let config = ClientConfig::new()
1525 /// .with_sip_addr("127.0.0.1:5080".parse()?);
1526 /// let client = ClientManager::new(config).await?;
1527 /// client.start().await?;
1528 ///
1529 /// let call_id = CallId::new_v4();
1530 ///
1531 /// match client.get_call_capabilities(&call_id).await {
1532 /// Ok(capabilities) => {
1533 /// println!("Call capabilities:");
1534 /// println!(" Hold: {}", capabilities.can_hold);
1535 /// println!(" Resume: {}", capabilities.can_resume);
1536 /// println!(" Transfer: {}", capabilities.can_transfer);
1537 /// println!(" DTMF: {}", capabilities.can_send_dtmf);
1538 /// println!(" Mute: {}", capabilities.can_mute);
1539 /// println!(" Hangup: {}", capabilities.can_hangup);
1540 /// }
1541 /// Err(e) => {
1542 /// println!("Failed to get capabilities: {}", e);
1543 /// }
1544 /// }
1545 ///
1546 /// Ok(())
1547 /// }
1548 /// ```
1549 ///
1550 /// ## Conditional Operation Execution
1551 ///
1552 /// ```rust
1553 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
1554 ///
1555 /// async fn conditional_operations() -> Result<(), Box<dyn std::error::Error>> {
1556 /// let config = ClientConfig::new()
1557 /// .with_sip_addr("127.0.0.1:5081".parse()?);
1558 /// let client = ClientManager::new(config).await?;
1559 /// client.start().await?;
1560 ///
1561 /// let call_id = CallId::new_v4();
1562 ///
1563 /// if let Ok(caps) = client.get_call_capabilities(&call_id).await {
1564 /// // Only attempt operations that are available
1565 /// if caps.can_hold {
1566 /// println!("✅ Hold operation available");
1567 /// // client.hold_call(&call_id).await?;
1568 /// } else {
1569 /// println!("❌ Cannot hold call in current state");
1570 /// }
1571 ///
1572 /// if caps.can_send_dtmf {
1573 /// println!("✅ DTMF available - can send digits");
1574 /// // client.send_dtmf(&call_id, "123").await?;
1575 /// }
1576 ///
1577 /// if caps.can_transfer {
1578 /// println!("✅ Transfer available");
1579 /// // client.transfer_call(&call_id, "sip:target@example.com").await?;
1580 /// }
1581 /// }
1582 ///
1583 /// Ok(())
1584 /// }
1585 /// ```
1586 ///
1587 /// ## Dynamic UI Updates
1588 ///
1589 /// ```rust
1590 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
1591 ///
1592 /// async fn update_ui_based_on_capabilities() -> Result<(), Box<dyn std::error::Error>> {
1593 /// let config = ClientConfig::new()
1594 /// .with_sip_addr("127.0.0.1:5082".parse()?);
1595 /// let client = ClientManager::new(config).await?;
1596 /// client.start().await?;
1597 ///
1598 /// let call_id = CallId::new_v4();
1599 ///
1600 /// if let Ok(capabilities) = client.get_call_capabilities(&call_id).await {
1601 /// // Simulate UI button states
1602 /// let buttons = vec![
1603 /// ("Hold", capabilities.can_hold),
1604 /// ("Resume", capabilities.can_resume),
1605 /// ("Transfer", capabilities.can_transfer),
1606 /// ("DTMF", capabilities.can_send_dtmf),
1607 /// ("Mute", capabilities.can_mute),
1608 /// ("Hangup", capabilities.can_hangup),
1609 /// ];
1610 ///
1611 /// println!("UI Button States:");
1612 /// for (button_name, enabled) in buttons {
1613 /// let status = if enabled { "ENABLED" } else { "DISABLED" };
1614 /// println!(" [{}] {}", status, button_name);
1615 /// }
1616 ///
1617 /// // Special logic for hold/resume button
1618 /// if capabilities.can_hold && !capabilities.can_resume {
1619 /// println!("💡 Show 'Hold' button");
1620 /// } else if !capabilities.can_hold && capabilities.can_resume {
1621 /// println!("💡 Show 'Resume' button");
1622 /// }
1623 /// }
1624 ///
1625 /// Ok(())
1626 /// }
1627 /// ```
1628 ///
1629 /// ## Capability Monitoring
1630 ///
1631 /// ```rust
1632 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
1633 /// use tokio::time::{sleep, Duration};
1634 ///
1635 /// async fn monitor_capability_changes() -> Result<(), Box<dyn std::error::Error>> {
1636 /// let config = ClientConfig::new()
1637 /// .with_sip_addr("127.0.0.1:5083".parse()?);
1638 /// let client = ClientManager::new(config).await?;
1639 /// client.start().await?;
1640 ///
1641 /// let call_id = CallId::new_v4();
1642 ///
1643 /// // Monitor capabilities over time (e.g., during state changes)
1644 /// for i in 0..3 {
1645 /// if let Ok(caps) = client.get_call_capabilities(&call_id).await {
1646 /// println!("Check {}: Hold={}, Resume={}, Transfer={}",
1647 /// i + 1, caps.can_hold, caps.can_resume, caps.can_transfer);
1648 /// }
1649 ///
1650 /// sleep(Duration::from_millis(50)).await;
1651 /// }
1652 ///
1653 /// Ok(())
1654 /// }
1655 /// ```
1656 ///
1657 /// # Implementation Notes
1658 ///
1659 /// The capabilities are determined based on the call's current state:
1660 ///
1661 /// - **Connected calls** have the most capabilities available
1662 /// - **Ringing calls** can only be answered or rejected (hangup)
1663 /// - **Initiating calls** can only be cancelled (hangup)
1664 /// - **Terminated calls** have no available operations
1665 ///
1666 /// The `can_resume` capability is dynamically determined by checking if
1667 /// the call is currently on hold using the call's metadata.
1668 ///
1669 /// # Best Practices
1670 ///
1671 /// 1. **Always check capabilities** before attempting operations
1672 /// 2. **Update UI dynamically** based on capability changes
1673 /// 3. **Handle capability changes** during call state transitions
1674 /// 4. **Provide user feedback** when operations are not available
1675 /// 5. **Cache capabilities briefly** to avoid excessive queries
1676 pub async fn get_call_capabilities(&self, call_id: &CallId) -> ClientResult<CallCapabilities> {
1677 let call_info = self.get_call(call_id).await?;
1678
1679 let capabilities = match call_info.state {
1680 crate::call::CallState::Connected => CallCapabilities {
1681 can_hold: true,
1682 can_resume: self.is_call_on_hold(call_id).await.unwrap_or(false),
1683 can_transfer: true,
1684 can_send_dtmf: true,
1685 can_mute: true,
1686 can_hangup: true,
1687 },
1688 crate::call::CallState::Ringing | crate::call::CallState::IncomingPending => CallCapabilities {
1689 can_hold: false,
1690 can_resume: false,
1691 can_transfer: false,
1692 can_send_dtmf: false,
1693 can_mute: false,
1694 can_hangup: true, // Can reject
1695 },
1696 crate::call::CallState::Initiating | crate::call::CallState::Proceeding => CallCapabilities {
1697 can_hold: false,
1698 can_resume: false,
1699 can_transfer: false,
1700 can_send_dtmf: false,
1701 can_mute: false,
1702 can_hangup: true, // Can cancel
1703 },
1704 _ => CallCapabilities::default(), // Terminated states have no capabilities
1705 };
1706
1707 Ok(capabilities)
1708 }
1709}