fix(proxy-engine): respect explicit inbound route targets and store voicemail in the configured mailbox

This commit is contained in:
2026-04-14 18:58:48 +00:00
parent 89ae12318e
commit 30d056f376
8 changed files with 94 additions and 15 deletions

View File

@@ -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

View File

@@ -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<String>,
greeting_wav: Option<String>,
) -> Option<String> {
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<Mutex<TtsEngine>>) -> Option<String> {
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(&params).await.is_ok() {
Some(output.to_string())
} else {
None
}
}

View File

@@ -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<String>,
pub ring_all_devices: bool,
pub ring_browsers: bool,
pub voicemail_box: Option<String>,
pub ivr_menu_id: Option<String>,
@@ -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(),

View File

@@ -19,6 +19,7 @@ pub async fn run_voicemail_session(
rtp_socket: Arc<UdpSocket>,
provider_media: SocketAddr,
codec_pt: u8,
voicebox_id: Option<String>,
greeting_wav: Option<String>,
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,

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: 'siprouter',
version: '1.25.0',
version: '1.25.1',
description: 'undefined'
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: 'siprouter',
version: '1.25.0',
version: '1.25.1',
description: 'undefined'
}