From 30d056f376832d7b8f77711e40f7b5b1f3040f77 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 14 Apr 2026 18:58:48 +0000 Subject: [PATCH] fix(proxy-engine): respect explicit inbound route targets and store voicemail in the configured mailbox --- changelog.md | 7 ++ rust/crates/proxy-engine/src/call_manager.rs | 77 ++++++++++++++++++-- rust/crates/proxy-engine/src/config.rs | 8 +- rust/crates/proxy-engine/src/voicemail.rs | 3 + ts/00_commitinfo_data.ts | 2 +- ts/runtime/proxy-events.ts | 7 +- ts/shared/proxy-events.ts | 3 + ts_web/00_commitinfo_data.ts | 2 +- 8 files changed, 94 insertions(+), 15 deletions(-) diff --git a/changelog.md b/changelog.md index 3ff52f3..70f5104 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-04-14 - 1.25.1 - fix(proxy-engine) +respect explicit inbound route targets and store voicemail in the configured mailbox + +- Prevent inbound routes with an explicit empty target list from ringing arbitrary registered devices by distinguishing omitted targets from empty targets. +- Route unrouted or no-target inbound calls to voicemail with a generated unrouted greeting instead of falling back to random devices. +- Pass voicemail box identifiers through proxy events and runtime handling so recordings are saved and indexed under the correct mailbox instead of always using default. + ## 2026-04-14 - 1.25.0 - feat(proxy-engine) add live TTS streaming interactions and incoming number range support diff --git a/rust/crates/proxy-engine/src/call_manager.rs b/rust/crates/proxy-engine/src/call_manager.rs index 00b0322..3348cd2 100644 --- a/rust/crates/proxy-engine/src/call_manager.rs +++ b/rust/crates/proxy-engine/src/call_manager.rs @@ -863,13 +863,48 @@ impl CallManager { } } - // Pick the first registered device from the matched targets, or fall - // back to any-registered-device if the route has no resolved targets. - let device_addr = route - .device_ids - .iter() - .find_map(|id| registrar.get_device_contact(id)) - .or_else(|| self.resolve_first_device(config, registrar)); + // Explicit no-target inbound routes do not fall back to random devices. + // They either go to the configured voicemail box or play the unrouted + // greeting via the default voicemail flow. + if !route.ring_all_devices && route.device_ids.is_empty() && !route.ring_browsers { + let greeting_wav = if route.voicemail_box.is_some() { + resolve_greeting_wav(config, route.voicemail_box.as_deref(), &tts_engine).await + } else { + resolve_unrouted_greeting_wav(&tts_engine).await + }; + let call_id = self + .route_to_voicemail( + &call_id, + invite, + from_addr, + &caller_number, + provider_id, + provider_config, + config, + rtp_pool, + socket, + public_ip, + route.voicemail_box.clone(), + greeting_wav, + ) + .await?; + return Some(InboundCallCreated { + call_id, + ring_browsers, + }); + } + + // Device targeting is explicit: omitted targets ring any registered + // device, an empty target list rings nobody, and a populated list rings + // only those device IDs. + let device_addr = if route.ring_all_devices { + self.resolve_first_device(config, registrar) + } else { + route + .device_ids + .iter() + .find_map(|id| registrar.get_device_contact(id)) + }; let device_addr = match device_addr { Some(addr) => addr, @@ -890,6 +925,7 @@ impl CallManager { rtp_pool, socket, public_ip, + route.voicemail_box.clone(), greeting_wav, ) .await?; @@ -1715,6 +1751,7 @@ impl CallManager { rtp_pool: &mut RtpPortPool, socket: &UdpSocket, public_ip: Option<&str>, + voicebox_id: Option, greeting_wav: Option, ) -> Option { let lan_ip = &config.proxy.lan_ip; @@ -1810,17 +1847,22 @@ impl CallManager { .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_millis(); - let recording_dir = "nogit/voicemail/default".to_string(); + let recording_dir = format!( + ".nogit/voicemail/{}", + voicebox_id.as_deref().unwrap_or("default") + ); let recording_path = format!("{recording_dir}/msg-{timestamp}.wav"); let out_tx = self.out_tx.clone(); let call_id_owned = call_id.to_string(); let caller_owned = caller_number.to_string(); + let voicebox_id_owned = voicebox_id.clone(); let rtp_socket = rtp_alloc.socket; tokio::spawn(async move { crate::voicemail::run_voicemail_session( rtp_socket, provider_media, codec_pt, + voicebox_id_owned, greeting_wav, recording_path, 120_000, @@ -2109,3 +2151,22 @@ async fn resolve_greeting_wav( } None } + +async fn resolve_unrouted_greeting_wav(tts_engine: &Arc>) -> Option { + let output = ".nogit/tts/unrouted-number.wav"; + let params = serde_json::json!({ + "model": crate::tts::DEFAULT_MODEL_PATH, + "voices": crate::tts::DEFAULT_VOICES_PATH, + "voice": "af_bella", + "text": "This number is currently not being routed by siprouter.", + "output": output, + "cacheable": true, + }); + + let mut tts = tts_engine.lock().await; + if tts.generate(¶ms).await.is_ok() { + Some(output.to_string()) + } else { + None + } +} diff --git a/rust/crates/proxy-engine/src/config.rs b/rust/crates/proxy-engine/src/config.rs index ee53b17..3f86ef9 100644 --- a/rust/crates/proxy-engine/src/config.rs +++ b/rust/crates/proxy-engine/src/config.rs @@ -373,12 +373,14 @@ pub struct OutboundRouteResult { /// Result of resolving an inbound route. // -// `device_ids` and `ring_browsers` are consumed by create_inbound_call. +// `device_ids`, `ring_all_devices`, and `ring_browsers` are consumed by +// create_inbound_call. // The remaining fields (voicemail_box, ivr_menu_id, no_answer_timeout) // are resolved but not yet acted upon — see the multi-target TODO. #[allow(dead_code)] pub struct InboundRouteResult { pub device_ids: Vec, + pub ring_all_devices: bool, pub ring_browsers: bool, pub voicemail_box: Option, pub ivr_menu_id: Option, @@ -485,8 +487,10 @@ impl AppConfig { continue; } + let explicit_targets = route.action.targets.clone(); return Some(InboundRouteResult { - device_ids: route.action.targets.clone().unwrap_or_default(), + device_ids: explicit_targets.clone().unwrap_or_default(), + ring_all_devices: explicit_targets.is_none(), ring_browsers: route.action.ring_browsers.unwrap_or(false), voicemail_box: route.action.voicemail_box.clone(), ivr_menu_id: route.action.ivr_menu_id.clone(), diff --git a/rust/crates/proxy-engine/src/voicemail.rs b/rust/crates/proxy-engine/src/voicemail.rs index 71e8329..c4b5243 100644 --- a/rust/crates/proxy-engine/src/voicemail.rs +++ b/rust/crates/proxy-engine/src/voicemail.rs @@ -19,6 +19,7 @@ pub async fn run_voicemail_session( rtp_socket: Arc, provider_media: SocketAddr, codec_pt: u8, + voicebox_id: Option, greeting_wav: Option, recording_path: String, max_recording_ms: u64, @@ -33,6 +34,7 @@ pub async fn run_voicemail_session( "voicemail_started", serde_json::json!({ "call_id": call_id, + "voicebox_id": voicebox_id, "caller_number": caller_number, }), ); @@ -102,6 +104,7 @@ pub async fn run_voicemail_session( "recording_done", serde_json::json!({ "call_id": call_id, + "voicebox_id": voicebox_id, "file_path": result.file_path, "duration_ms": result.duration_ms, "caller_number": caller_number, diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index fa01293..495ff8a 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: 'siprouter', - version: '1.25.0', + version: '1.25.1', description: 'undefined' } diff --git a/ts/runtime/proxy-events.ts b/ts/runtime/proxy-events.ts index c5c3cee..8c050d1 100644 --- a/ts/runtime/proxy-events.ts +++ b/ts/runtime/proxy-events.ts @@ -168,12 +168,13 @@ export function registerProxyEventHandlers(options: IRegisterProxyEventHandlersO }); onProxyEvent('voicemail_started', (data) => { - log(`[voicemail] started for call ${data.call_id} caller=${data.caller_number}`); + log(`[voicemail] started for call ${data.call_id} box=${data.voicebox_id || 'default'} caller=${data.caller_number}`); }); onProxyEvent('recording_done', (data) => { - log(`[voicemail] recording done: ${data.file_path} (${data.duration_ms}ms) caller=${data.caller_number}`); - voiceboxManager.addMessage('default', { + const boxId = data.voicebox_id || 'default'; + log(`[voicemail] recording done: ${data.file_path} (${data.duration_ms}ms) box=${boxId} caller=${data.caller_number}`); + voiceboxManager.addMessage(boxId, { callerNumber: data.caller_number || 'Unknown', callerName: null, fileName: data.file_path, diff --git a/ts/shared/proxy-events.ts b/ts/shared/proxy-events.ts index 5cc87e3..5578ec1 100644 --- a/ts/shared/proxy-events.ts +++ b/ts/shared/proxy-events.ts @@ -108,10 +108,13 @@ export interface IWebRtcAudioRxEvent { export interface IVoicemailStartedEvent { call_id: string; + voicebox_id?: string; caller_number?: string; } export interface IRecordingDoneEvent { + call_id?: string; + voicebox_id?: string; file_path: string; duration_ms: number; caller_number?: string; diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index fa01293..495ff8a 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: 'siprouter', - version: '1.25.0', + version: '1.25.1', description: 'undefined' }