fix(proxy-engine): respect explicit inbound route targets and store voicemail in the configured mailbox
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# 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)
|
## 2026-04-14 - 1.25.0 - feat(proxy-engine)
|
||||||
add live TTS streaming interactions and incoming number range support
|
add live TTS streaming interactions and incoming number range support
|
||||||
|
|
||||||
|
|||||||
@@ -863,13 +863,48 @@ impl CallManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick the first registered device from the matched targets, or fall
|
// Explicit no-target inbound routes do not fall back to random devices.
|
||||||
// back to any-registered-device if the route has no resolved targets.
|
// They either go to the configured voicemail box or play the unrouted
|
||||||
let device_addr = route
|
// greeting via the default voicemail flow.
|
||||||
.device_ids
|
if !route.ring_all_devices && route.device_ids.is_empty() && !route.ring_browsers {
|
||||||
.iter()
|
let greeting_wav = if route.voicemail_box.is_some() {
|
||||||
.find_map(|id| registrar.get_device_contact(id))
|
resolve_greeting_wav(config, route.voicemail_box.as_deref(), &tts_engine).await
|
||||||
.or_else(|| self.resolve_first_device(config, registrar));
|
} 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 {
|
let device_addr = match device_addr {
|
||||||
Some(addr) => addr,
|
Some(addr) => addr,
|
||||||
@@ -890,6 +925,7 @@ impl CallManager {
|
|||||||
rtp_pool,
|
rtp_pool,
|
||||||
socket,
|
socket,
|
||||||
public_ip,
|
public_ip,
|
||||||
|
route.voicemail_box.clone(),
|
||||||
greeting_wav,
|
greeting_wav,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -1715,6 +1751,7 @@ impl CallManager {
|
|||||||
rtp_pool: &mut RtpPortPool,
|
rtp_pool: &mut RtpPortPool,
|
||||||
socket: &UdpSocket,
|
socket: &UdpSocket,
|
||||||
public_ip: Option<&str>,
|
public_ip: Option<&str>,
|
||||||
|
voicebox_id: Option<String>,
|
||||||
greeting_wav: Option<String>,
|
greeting_wav: Option<String>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let lan_ip = &config.proxy.lan_ip;
|
let lan_ip = &config.proxy.lan_ip;
|
||||||
@@ -1810,17 +1847,22 @@ impl CallManager {
|
|||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.as_millis();
|
.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 recording_path = format!("{recording_dir}/msg-{timestamp}.wav");
|
||||||
let out_tx = self.out_tx.clone();
|
let out_tx = self.out_tx.clone();
|
||||||
let call_id_owned = call_id.to_string();
|
let call_id_owned = call_id.to_string();
|
||||||
let caller_owned = caller_number.to_string();
|
let caller_owned = caller_number.to_string();
|
||||||
|
let voicebox_id_owned = voicebox_id.clone();
|
||||||
let rtp_socket = rtp_alloc.socket;
|
let rtp_socket = rtp_alloc.socket;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
crate::voicemail::run_voicemail_session(
|
crate::voicemail::run_voicemail_session(
|
||||||
rtp_socket,
|
rtp_socket,
|
||||||
provider_media,
|
provider_media,
|
||||||
codec_pt,
|
codec_pt,
|
||||||
|
voicebox_id_owned,
|
||||||
greeting_wav,
|
greeting_wav,
|
||||||
recording_path,
|
recording_path,
|
||||||
120_000,
|
120_000,
|
||||||
@@ -2109,3 +2151,22 @@ async fn resolve_greeting_wav(
|
|||||||
}
|
}
|
||||||
None
|
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(¶ms).await.is_ok() {
|
||||||
|
Some(output.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -373,12 +373,14 @@ pub struct OutboundRouteResult {
|
|||||||
|
|
||||||
/// Result of resolving an inbound route.
|
/// 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)
|
// The remaining fields (voicemail_box, ivr_menu_id, no_answer_timeout)
|
||||||
// are resolved but not yet acted upon — see the multi-target TODO.
|
// are resolved but not yet acted upon — see the multi-target TODO.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct InboundRouteResult {
|
pub struct InboundRouteResult {
|
||||||
pub device_ids: Vec<String>,
|
pub device_ids: Vec<String>,
|
||||||
|
pub ring_all_devices: bool,
|
||||||
pub ring_browsers: bool,
|
pub ring_browsers: bool,
|
||||||
pub voicemail_box: Option<String>,
|
pub voicemail_box: Option<String>,
|
||||||
pub ivr_menu_id: Option<String>,
|
pub ivr_menu_id: Option<String>,
|
||||||
@@ -485,8 +487,10 @@ impl AppConfig {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let explicit_targets = route.action.targets.clone();
|
||||||
return Some(InboundRouteResult {
|
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),
|
ring_browsers: route.action.ring_browsers.unwrap_or(false),
|
||||||
voicemail_box: route.action.voicemail_box.clone(),
|
voicemail_box: route.action.voicemail_box.clone(),
|
||||||
ivr_menu_id: route.action.ivr_menu_id.clone(),
|
ivr_menu_id: route.action.ivr_menu_id.clone(),
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub async fn run_voicemail_session(
|
|||||||
rtp_socket: Arc<UdpSocket>,
|
rtp_socket: Arc<UdpSocket>,
|
||||||
provider_media: SocketAddr,
|
provider_media: SocketAddr,
|
||||||
codec_pt: u8,
|
codec_pt: u8,
|
||||||
|
voicebox_id: Option<String>,
|
||||||
greeting_wav: Option<String>,
|
greeting_wav: Option<String>,
|
||||||
recording_path: String,
|
recording_path: String,
|
||||||
max_recording_ms: u64,
|
max_recording_ms: u64,
|
||||||
@@ -33,6 +34,7 @@ pub async fn run_voicemail_session(
|
|||||||
"voicemail_started",
|
"voicemail_started",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"call_id": call_id,
|
"call_id": call_id,
|
||||||
|
"voicebox_id": voicebox_id,
|
||||||
"caller_number": caller_number,
|
"caller_number": caller_number,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -102,6 +104,7 @@ pub async fn run_voicemail_session(
|
|||||||
"recording_done",
|
"recording_done",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"call_id": call_id,
|
"call_id": call_id,
|
||||||
|
"voicebox_id": voicebox_id,
|
||||||
"file_path": result.file_path,
|
"file_path": result.file_path,
|
||||||
"duration_ms": result.duration_ms,
|
"duration_ms": result.duration_ms,
|
||||||
"caller_number": caller_number,
|
"caller_number": caller_number,
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: 'siprouter',
|
name: 'siprouter',
|
||||||
version: '1.25.0',
|
version: '1.25.1',
|
||||||
description: 'undefined'
|
description: 'undefined'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,12 +168,13 @@ export function registerProxyEventHandlers(options: IRegisterProxyEventHandlersO
|
|||||||
});
|
});
|
||||||
|
|
||||||
onProxyEvent('voicemail_started', (data) => {
|
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) => {
|
onProxyEvent('recording_done', (data) => {
|
||||||
log(`[voicemail] recording done: ${data.file_path} (${data.duration_ms}ms) caller=${data.caller_number}`);
|
const boxId = data.voicebox_id || 'default';
|
||||||
voiceboxManager.addMessage('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',
|
callerNumber: data.caller_number || 'Unknown',
|
||||||
callerName: null,
|
callerName: null,
|
||||||
fileName: data.file_path,
|
fileName: data.file_path,
|
||||||
|
|||||||
@@ -108,10 +108,13 @@ export interface IWebRtcAudioRxEvent {
|
|||||||
|
|
||||||
export interface IVoicemailStartedEvent {
|
export interface IVoicemailStartedEvent {
|
||||||
call_id: string;
|
call_id: string;
|
||||||
|
voicebox_id?: string;
|
||||||
caller_number?: string;
|
caller_number?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRecordingDoneEvent {
|
export interface IRecordingDoneEvent {
|
||||||
|
call_id?: string;
|
||||||
|
voicebox_id?: string;
|
||||||
file_path: string;
|
file_path: string;
|
||||||
duration_ms: number;
|
duration_ms: number;
|
||||||
caller_number?: string;
|
caller_number?: string;
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: 'siprouter',
|
name: 'siprouter',
|
||||||
version: '1.25.0',
|
version: '1.25.1',
|
||||||
description: 'undefined'
|
description: 'undefined'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user