Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 46 additions & 8 deletions src/call/app/app_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ pub struct CallInfo {
pub route_name: Option<String>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AppRouteContext {
pub callee: String,
pub sip_headers: HashMap<String, String>,
pub variables: HashMap<String, String>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AppInvocationContext {
pub app_execution_id: u64,
pub callee: String,
pub sip_headers: HashMap<String, String>,
pub variables: HashMap<String, String>,
}

pub struct AppSharedState {
/// Arbitrary typed data, keyed by string.
///
Expand Down Expand Up @@ -100,6 +115,9 @@ pub struct ApplicationContext {
/// Call metadata.
pub call_info: CallInfo,

/// Immutable metadata owned by the current application generation.
pub invocation: Option<AppInvocationContext>,

/// System configuration.
pub config: Arc<Config>,

Expand Down Expand Up @@ -144,6 +162,7 @@ impl ApplicationContext {
http_client: crate::http_util::build_keepalive_client(None, None)
.unwrap_or_else(|_| reqwest::Client::new()),
call_info,
invocation: None,
config,
rwi_gateway: None,
ivr_trace: None,
Expand Down Expand Up @@ -198,6 +217,21 @@ pub fn extract_sip_headers(request: &rsipstack::sip::Request) -> HashMap<String,
headers
}

/// Merge route-produced headers into an existing snapshot using SIP's
/// case-insensitive header-name semantics.
pub fn merge_sip_headers(
base: &HashMap<String, String>,
routed: &[rsipstack::sip::Header],
) -> HashMap<String, String> {
let mut merged = base.clone();
for header in routed {
let name = header.name().to_string();
merged.retain(|key, _| !key.eq_ignore_ascii_case(&name));
merged.insert(name, header.value().to_string());
}
merged
}

impl std::fmt::Debug for ApplicationContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ApplicationContext")
Expand Down Expand Up @@ -266,6 +300,7 @@ mod tests {
version: rsipstack::sip::Version::V2,
headers: vec![
Header::Other("X-Custom".to_string(), "original-value".to_string()),
Header::Other("x-custom".to_string(), "duplicate-value".to_string()),
Header::Other("X-Forwarded-For".to_string(), "192.168.1.1".to_string()),
]
.into(),
Expand All @@ -283,6 +318,7 @@ mod tests {

let original = extract_sip_headers(&req);
assert_eq!(original.get("X-Custom").unwrap(), "original-value");
assert_eq!(original.get("x-custom").unwrap(), "duplicate-value");
assert_eq!(original.get("X-Forwarded-For").unwrap(), "192.168.1.1");
assert!(
original.get("From").is_none(),
Expand All @@ -291,27 +327,29 @@ mod tests {

// Simulate routing-modified headers (overriding X-Custom, adding P-Asserted-Identity)
let routed_headers: Option<Vec<Header>> = Some(vec![
Header::Other("X-Custom".to_string(), "routing-value".to_string()),
Header::Other("x-custom".to_string(), "routing-value".to_string()),
Header::Other(
"P-Asserted-Identity".to_string(),
"<sip:routing@pbx.com>".to_string(),
),
]);

// Apply the same merge logic as in sip_session.rs
let mut merged = original;
if let Some(ref routed) = routed_headers {
for h in routed {
merged.insert(h.name().to_string(), h.value().to_string());
}
}
let merged = merge_sip_headers(&original, routed_headers.as_deref().unwrap_or_default());

// Verify routing headers override originals
assert_eq!(
merged.get("X-Custom").unwrap(),
merged.get("x-custom").unwrap(),
"routing-value",
"routed headers should override original"
);
assert_eq!(
merged
.keys()
.filter(|key| key.eq_ignore_ascii_case("X-Custom"))
.count(),
1
);
// Verify unmodified original headers are preserved
assert_eq!(
merged.get("X-Forwarded-For").unwrap(),
Expand Down
9 changes: 9 additions & 0 deletions src/call/app/ivr/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use super::config::{ActionNode, EntryAction};
pub enum ActionResult {
Terminal(TerminalAction),
ChainedTo(ActionNode),
/// Complete an explicitly empty prompt without waiting for a media event.
ImmediateAudioComplete,
WaitFor(WaitEvent),
/// Chain to a registered domain [`CallApp`] (voicemail, csat_survey, …).
StartSubApp(Box<dyn CallApp>),
Expand Down Expand Up @@ -341,6 +343,11 @@ pub async fn execute_action(
tts_api_url,
..
} => {
// Some("") is a successful no-media prompt; None still means missing audio input.
let explicitly_empty = tts_api_url.is_none()
&& tts_text.as_deref() == Some("")
&& file.as_deref().unwrap_or_default().is_empty()
&& record_name_list.is_none();
let resolved_text = if tts_api_url.is_some() {
match fetch_tts_text_from_api(tts_api_url.as_deref().unwrap(), sess, ctx).await {
Some(text) => Some(text),
Expand All @@ -366,6 +373,8 @@ pub async fn execute_action(
Ok(ActionResult::WaitFor(WaitEvent::AudioComplete {
interrupted: false,
}))
} else if explicitly_empty {
Ok(ActionResult::ImmediateAudioComplete)
} else {
Ok(ActionResult::WaitFor(WaitEvent::NoAudio))
}
Expand Down
Loading
Loading