diff --git a/.env.example b/.env.example index 96ab09c..0d57008 100644 --- a/.env.example +++ b/.env.example @@ -7,7 +7,6 @@ RTP_PUBLIC_IP=192.168.0.100 # DISCORD_OUTBOUND_SIP_HOST=192.168.0.25 # DISCORD_OUTBOUND_SIP_PORT=5060 # DISCORD_OUTBOUND_SIP_TRANSPORT=udp -# DISCORD_OUTBOUND_EXTENSION_PREFIX= # DATA_DIR=/var/lib/sipcord # CONFIG_PATH=./config.toml # SOUNDS_DIR=./wav diff --git a/README.md b/README.md index cb53be4..21f5f5f 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,6 @@ RTP_PUBLIC_IP=192.168.0.100 DISCORD_OUTBOUND_SIP_HOST=192.168.0.25 DISCORD_OUTBOUND_SIP_PORT=5060 DISCORD_OUTBOUND_SIP_TRANSPORT=udp -DISCORD_OUTBOUND_EXTENSION_PREFIX= ``` Set both IPs to the address other SIP devices use to reach the bridge. For @@ -86,12 +85,6 @@ Set `DISCORD_OUTBOUND_SIP_HOST` to the PBX or SIP server that should receive Discord-originated extension calls. For a FreePBX box at `192.168.0.25`, that means `DISCORD_OUTBOUND_SIP_HOST=192.168.0.25`. -Discord-originated calls dial the requested extension directly by default. The -bridge also attaches common auto-answer headers (`Call-Info: -;answer-after=0` and `Alert-Info: ;info=Ring Answer`) -to Discord-originated outbound calls, and appends `intercom=true` to the SIP -URI, so auto-answer phones can behave more like FreePBX intercom targets. - Create a `docker-compose.yml`: ```yaml @@ -200,13 +193,14 @@ to the bridge host address, not the FreePBX address and not `0.0.0.0`. ### 4c. Discord -> extension calling -If `DISCORD_OUTBOUND_SIP_HOST` is set, the bot registers a `/call` slash command -in each guild it is connected to. +If `DISCORD_OUTBOUND_SIP_HOST` is set, the bot registers `/call` and `/hangup` +slash commands in each guild it is connected to. Usage: ```text /call extension:1101 +/hangup ``` Behavior: @@ -215,17 +209,17 @@ Behavior: - The bridge dials the requested extension through the configured PBX target. - It dials the requested extension directly, for example `sip:1101@192.168.0.25:5060;transport=udp`. -- Discord-originated outbound calls also include auto-answer headers and append - `intercom=true` to the SIP URI so phones configured for that behavior can - answer immediately. - When the SIP side answers, the phone call is connected to the Discord voice channel where the command was run. +- `/hangup` ends active SIP calls connected to the voice channel where the + command was run. Current scope: - `/call` is implemented for the static self-host backend. - It dials a configured PBX/SIP host by extension. -- It does not yet include a Discord `/hangup` command or rich status updates - back into Discord after the initial slash command reply. +- `/hangup` ends calls already connected to the current Discord voice channel. +- Rich status updates back into Discord after the initial slash command reply + are not implemented yet. ### 4d. Build from source @@ -263,7 +257,6 @@ Dial `1000` (or whatever you put in `dialplan.toml`) and you should hear the bot | `DISCORD_OUTBOUND_SIP_HOST` | *(disabled if unset)* | PBX/SIP host used by Discord `/call` | | `DISCORD_OUTBOUND_SIP_PORT` | `5060` | Port for Discord-originated outbound SIP calls | | `DISCORD_OUTBOUND_SIP_TRANSPORT` | `udp` | Transport for Discord-originated outbound SIP calls: `udp`, `tcp`, or `tls` | -| `DISCORD_OUTBOUND_EXTENSION_PREFIX` | `""` | Optional prefix prepended before the requested extension for Discord-originated calls | | `CONFIG_PATH` | `./config.toml` | Path to config.toml | | `DIALPLAN_PATH` | `./dialplan.toml` | Path to dialplan.toml | | `SOUNDS_DIR` | `./wav` | Path to sound files directory | diff --git a/sipcord-bridge/src/call/mod.rs b/sipcord-bridge/src/call/mod.rs index 78536fb..4a81489 100644 --- a/sipcord-bridge/src/call/mod.rs +++ b/sipcord-bridge/src/call/mod.rs @@ -553,7 +553,6 @@ impl BridgeCoordinator { tracking_id: req.call_id.clone(), sip_uri, caller_display_name: Some(req.caller_username.clone()), - auto_answer: true, fork_total, }); outbound_backend.report_call_status(&req.call_id, "ringing"); @@ -586,7 +585,6 @@ impl BridgeCoordinator { tracking_id: req.call_id.clone(), sip_uri, caller_display_name: Some(req.caller_username.clone()), - auto_answer: false, fork_total, }); } @@ -595,6 +593,50 @@ impl BridgeCoordinator { } }); + // Handle hangup requests from the backend (Discord /hangup) + let hangup_backend = self.backend.clone(); + let hangup_bridges = self.bridges.clone(); + let hangup_sip_cmd_tx = self.sip_cmd_tx.clone(); + + let hangup_handle = tokio::spawn(async move { + while let Some(req) = hangup_backend.next_hangup_request().await { + let channel_id = match req.channel_id.parse::() { + Ok(channel_id) => channel_id, + Err(e) => { + warn!( + "Invalid /hangup channel id {} for request {}: {}", + req.channel_id, req.request_id, e + ); + continue; + } + }; + + let call_ids: Vec = hangup_bridges + .get(&channel_id) + .map(|bridge| bridge.sip_calls.iter().copied().collect()) + .unwrap_or_default(); + + if call_ids.is_empty() { + info!( + "No active SIP calls to hang up for Discord channel {} (requested by {})", + channel_id, req.requested_by + ); + continue; + } + + info!( + "Hanging up {} active SIP call(s) for Discord channel {} (requested by {})", + call_ids.len(), + channel_id, + req.requested_by + ); + + for call_id in call_ids { + let _ = hangup_sip_cmd_tx.send(SipCommand::Hangup { call_id }); + } + } + }); + // Handle Discord events let discord_event_rx = self.discord_event_rx.clone(); @@ -962,6 +1004,7 @@ impl BridgeCoordinator { _ = discord_handle => { info!("Discord event handler finished"); } _ = health_check_handle => { info!("Health check handler finished"); } _ = outbound_handle => { info!("Outbound call handler finished"); } + _ = hangup_handle => { info!("Hangup request handler finished"); } } Ok(()) diff --git a/sipcord-bridge/src/config.rs b/sipcord-bridge/src/config.rs index 273ab40..d178593 100644 --- a/sipcord-bridge/src/config.rs +++ b/sipcord-bridge/src/config.rs @@ -71,9 +71,6 @@ fn default_discord_outbound_sip_port() -> u16 { fn default_discord_outbound_sip_transport() -> String { "udp".to_string() } -fn default_discord_outbound_extension_prefix() -> String { - String::new() -} /// All environment variables consumed by the bridge, deserialized once at startup. #[derive(Debug, Clone, serde::Deserialize)] @@ -120,8 +117,6 @@ pub struct EnvConfig { pub discord_outbound_sip_port: u16, #[serde(default = "default_discord_outbound_sip_transport")] pub discord_outbound_sip_transport: String, - #[serde(default = "default_discord_outbound_extension_prefix")] - pub discord_outbound_extension_prefix: String, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -147,28 +142,18 @@ pub struct DiscordOutboundSipConfig { pub host: String, pub port: u16, pub transport: OutboundSipTransport, - pub extension_prefix: String, } impl DiscordOutboundSipConfig { pub fn build_sip_uri(&self, extension: &str) -> String { - let dialed_extension = format!("{}{}", self.extension_prefix, extension); match self.transport { OutboundSipTransport::Udp => { - format!( - "sip:{}@{}:{};transport=udp", - dialed_extension, self.host, self.port - ) + format!("sip:{}@{}:{};transport=udp", extension, self.host, self.port) } OutboundSipTransport::Tcp => { - format!( - "sip:{}@{}:{};transport=tcp", - dialed_extension, self.host, self.port - ) - } - OutboundSipTransport::Tls => { - format!("sips:{}@{}:{}", dialed_extension, self.host, self.port) + format!("sip:{}@{}:{};transport=tcp", extension, self.host, self.port) } + OutboundSipTransport::Tls => format!("sips:{}@{}:{}", extension, self.host, self.port), } } } @@ -245,7 +230,6 @@ impl EnvConfig { host, port: self.discord_outbound_sip_port, transport, - extension_prefix: self.discord_outbound_extension_prefix.clone(), }) } @@ -546,7 +530,6 @@ mod tests { discord_outbound_sip_host: None, discord_outbound_sip_port: 5060, discord_outbound_sip_transport: "udp".to_string(), - discord_outbound_extension_prefix: String::new(), }; assert_eq!(env.resolved_data_dir(), "."); } @@ -574,7 +557,6 @@ mod tests { discord_outbound_sip_host: None, discord_outbound_sip_port: 5060, discord_outbound_sip_transport: "udp".to_string(), - discord_outbound_extension_prefix: String::new(), }; assert_eq!(env.resolved_data_dir(), "/tmp"); } @@ -602,7 +584,6 @@ mod tests { discord_outbound_sip_host: None, discord_outbound_sip_port: 5060, discord_outbound_sip_transport: "udp".to_string(), - discord_outbound_extension_prefix: String::new(), }; let tls = env.to_tls_config(); assert_eq!(tls.cert_dir, PathBuf::from("/data/certs")); @@ -646,7 +627,6 @@ mod tests { discord_outbound_sip_host: Some("192.168.0.25".to_string()), discord_outbound_sip_port: 5060, discord_outbound_sip_transport: "udp".to_string(), - discord_outbound_extension_prefix: String::new(), }; let outbound = env.discord_outbound_sip_config().unwrap(); diff --git a/sipcord-bridge/src/main.rs b/sipcord-bridge/src/main.rs index 06e0b83..79e2018 100644 --- a/sipcord-bridge/src/main.rs +++ b/sipcord-bridge/src/main.rs @@ -59,10 +59,12 @@ async fn run_static_router() -> Result<(), BridgeError> { // Load dialplan let dialplan_path = PathBuf::from(&EnvConfig::global().dialplan_path); let (outbound_request_tx, outbound_request_rx) = tokio::sync::mpsc::unbounded_channel(); + let (hangup_request_tx, hangup_request_rx) = tokio::sync::mpsc::unbounded_channel(); let backend = Arc::new(StaticBackend::load( &dialplan_path, bot_token.clone(), outbound_request_rx, + hangup_request_rx, )?); // Create SIP transport (no TLS for static router) @@ -87,6 +89,7 @@ async fn run_static_router() -> Result<(), BridgeError> { .map(|sip| DiscordOutboundCallConfig { sip, request_tx: outbound_request_tx, + hangup_tx: hangup_request_tx, bot_token: bot_token.clone(), }); let shared_discord = SharedDiscordClient::new(&bot_token, outbound_call_config).await?; diff --git a/sipcord-bridge/src/routing/mod.rs b/sipcord-bridge/src/routing/mod.rs index 0ddbaf6..4bda564 100644 --- a/sipcord-bridge/src/routing/mod.rs +++ b/sipcord-bridge/src/routing/mod.rs @@ -17,6 +17,16 @@ pub struct OutboundCallRequest { pub created_at: std::time::Instant, } +/// Hangup request from the backend (e.g., Discord /hangup command) +#[derive(Debug, Clone)] +pub struct HangupCallRequest { + pub request_id: String, + pub guild_id: String, + pub channel_id: String, + pub requested_by: String, + pub created_at: std::time::Instant, +} + /// Result of routing an incoming SIP call pub enum RouteDecision { /// Connect to this Discord voice channel @@ -104,4 +114,7 @@ pub trait Backend: Send + Sync { /// Get the next outbound call request (None if backend doesn't support outbound) async fn next_outbound_request(&self) -> Option; + + /// Get the next hangup request (None if backend doesn't support hangup control) + async fn next_hangup_request(&self) -> Option; } diff --git a/sipcord-bridge/src/routing/static_router.rs b/sipcord-bridge/src/routing/static_router.rs index 5b8a4dc..70a7065 100644 --- a/sipcord-bridge/src/routing/static_router.rs +++ b/sipcord-bridge/src/routing/static_router.rs @@ -22,7 +22,9 @@ use tokio::sync::Mutex; use tracing::info; use crate::config::ConfigError; -use crate::routing::{Backend, CallError, CallStartedInfo, OutboundCallRequest, RouteDecision}; +use crate::routing::{ + Backend, CallError, CallStartedInfo, HangupCallRequest, OutboundCallRequest, RouteDecision, +}; use crate::services::snowflake::Snowflake; use crate::transport::sip::DigestAuthParams; @@ -46,6 +48,7 @@ pub struct StaticBackend { bot_token: String, extensions: HashMap, outbound_rx: Arc>>, + hangup_rx: Arc>>, } impl StaticBackend { @@ -54,6 +57,7 @@ impl StaticBackend { path: &Path, bot_token: String, outbound_rx: tokio::sync::mpsc::UnboundedReceiver, + hangup_rx: tokio::sync::mpsc::UnboundedReceiver, ) -> Result { let content = std::fs::read_to_string(path).map_err(|source| ConfigError::Read { path: path.to_path_buf(), @@ -80,6 +84,7 @@ impl StaticBackend { bot_token, extensions: dialplan.extensions, outbound_rx: Arc::new(Mutex::new(outbound_rx)), + hangup_rx: Arc::new(Mutex::new(hangup_rx)), }) } } @@ -125,6 +130,10 @@ impl Backend for StaticBackend { async fn next_outbound_request(&self) -> Option { self.outbound_rx.lock().await.recv().await } + + async fn next_hangup_request(&self) -> Option { + self.hangup_rx.lock().await.recv().await + } } #[cfg(test)] @@ -144,7 +153,9 @@ mod tests { std::fs::write(&path, toml_content).unwrap(); let (_tx, rx) = tokio::sync::mpsc::unbounded_channel(); - let backend = StaticBackend::load(&path, "test_token".to_string(), rx).unwrap(); + let (_hangup_tx, hangup_rx) = tokio::sync::mpsc::unbounded_channel(); + let backend = + StaticBackend::load(&path, "test_token".to_string(), rx, hangup_rx).unwrap(); assert_eq!(backend.extensions.len(), 2); assert!(backend.extensions.contains_key("1000")); assert!(backend.extensions.contains_key("2000")); @@ -162,7 +173,8 @@ mod tests { std::fs::write(&path, toml_content).unwrap(); let (_tx, rx) = tokio::sync::mpsc::unbounded_channel(); - let backend = StaticBackend::load(&path, "tok".to_string(), rx).unwrap(); + let (_hangup_tx, hangup_rx) = tokio::sync::mpsc::unbounded_channel(); + let backend = StaticBackend::load(&path, "tok".to_string(), rx, hangup_rx).unwrap(); let rt = tokio::runtime::Builder::new_current_thread() .build() @@ -192,7 +204,8 @@ mod tests { std::fs::write(&path, toml_content).unwrap(); let (_tx, rx) = tokio::sync::mpsc::unbounded_channel(); - let backend = StaticBackend::load(&path, "tok".to_string(), rx).unwrap(); + let (_hangup_tx, hangup_rx) = tokio::sync::mpsc::unbounded_channel(); + let backend = StaticBackend::load(&path, "tok".to_string(), rx, hangup_rx).unwrap(); let rt = tokio::runtime::Builder::new_current_thread() .build() @@ -218,7 +231,8 @@ mod tests { std::fs::write(&path, "this is not valid toml [[[").unwrap(); let (_tx, rx) = tokio::sync::mpsc::unbounded_channel(); - let result = StaticBackend::load(&path, "tok".to_string(), rx); + let (_hangup_tx, hangup_rx) = tokio::sync::mpsc::unbounded_channel(); + let result = StaticBackend::load(&path, "tok".to_string(), rx, hangup_rx); assert!(result.is_err()); } } diff --git a/sipcord-bridge/src/transport/discord/mod.rs b/sipcord-bridge/src/transport/discord/mod.rs index 1017eac..8e3dff6 100644 --- a/sipcord-bridge/src/transport/discord/mod.rs +++ b/sipcord-bridge/src/transport/discord/mod.rs @@ -2,7 +2,7 @@ mod voice; use crate::audio::simd; use crate::config::DiscordOutboundSipConfig; -use crate::routing::OutboundCallRequest; +use crate::routing::{HangupCallRequest, OutboundCallRequest}; use crate::services::snowflake::Snowflake; use audioadapter::Adapter; use audioadapter_buffers::direct::SequentialSliceOfVecs; @@ -506,6 +506,7 @@ pub struct SharedDiscordClient { pub struct DiscordOutboundCallConfig { pub sip: DiscordOutboundSipConfig, pub request_tx: tokio::sync::mpsc::UnboundedSender, + pub hangup_tx: tokio::sync::mpsc::UnboundedSender, pub bot_token: String, } @@ -605,9 +606,9 @@ impl EventHandler for SharedClientEventHandler { if self.outbound_call_config.is_some() { for guild_status in &data_about_bot.guilds { - if let Err(e) = register_call_command(ctx, guild_status.id).await { + if let Err(e) = register_call_commands(ctx, guild_status.id).await { error!( - "Failed to register /call command for guild {}: {}", + "Failed to register call commands for guild {}: {}", guild_status.id, e ); } @@ -617,9 +618,12 @@ impl EventHandler for SharedClientEventHandler { FullEvent::InteractionCreate { interaction, .. } => { if let Some(ref cfg) = self.outbound_call_config && let Interaction::Command(command) = interaction - && command.data.name == "call" { - handle_call_command(ctx, command, cfg).await; + match command.data.name.as_str() { + "call" => handle_call_command(ctx, command, cfg).await, + "hangup" => handle_hangup_command(ctx, command, cfg).await, + _ => {} + } } } _ => {} @@ -627,8 +631,8 @@ impl EventHandler for SharedClientEventHandler { } } -async fn register_call_command(ctx: &Context, guild_id: GuildId) -> Result<(), serenity::Error> { - let command = CreateCommand::new("call") +async fn register_call_commands(ctx: &Context, guild_id: GuildId) -> Result<(), serenity::Error> { + let call_command = CreateCommand::new("call") .description("Call a SIP/PBX extension from your current voice channel") .add_option( CreateCommandOption::new( @@ -639,8 +643,12 @@ async fn register_call_command(ctx: &Context, guild_id: GuildId) -> Result<(), s .required(true), ); - guild_id.create_command(&ctx.http, command).await?; - info!("Registered /call command for guild {}", guild_id); + let hangup_command = CreateCommand::new("hangup") + .description("Hang up active SIP calls in your current voice channel"); + + guild_id.create_command(&ctx.http, call_command).await?; + guild_id.create_command(&ctx.http, hangup_command).await?; + info!("Registered /call and /hangup commands for guild {}", guild_id); Ok(()) } @@ -653,7 +661,10 @@ async fn handle_call_command( Ok(req) => { let extension = req.discord_username.clone(); match cfg.request_tx.send(req) { - Ok(()) => format!("Dialing extension `{}` from your current voice channel.", extension), + Ok(()) => format!( + "Dialing extension `{}` from your current voice channel.", + extension + ), Err(_) => "Outbound call queue is unavailable right now.".to_string(), } } @@ -675,6 +686,34 @@ async fn handle_call_command( } } +async fn handle_hangup_command( + ctx: &Context, + command: &CommandInteraction, + cfg: &DiscordOutboundCallConfig, +) { + let response = match build_hangup_request(ctx, command) { + Ok(req) => match cfg.hangup_tx.send(req) { + Ok(()) => "Hanging up active calls in your current voice channel.".to_string(), + Err(_) => "Hangup queue is unavailable right now.".to_string(), + }, + Err(msg) => msg, + }; + + if let Err(e) = command + .create_response( + &ctx.http, + CreateInteractionResponse::Message( + CreateInteractionResponseMessage::new() + .content(response) + .ephemeral(true), + ), + ) + .await + { + error!("Failed to respond to /hangup interaction: {}", e); + } +} + fn build_outbound_request( ctx: &Context, command: &CommandInteraction, @@ -697,18 +736,7 @@ fn build_outbound_request( ); } - let guild_id = command - .guild_id - .ok_or_else(|| "This command only works inside a server.".to_string())?; - let guild = ctx - .cache - .guild(guild_id) - .ok_or_else(|| "Guild is not available in cache yet. Try again in a moment.".to_string())?; - let voice_channel_id = guild - .voice_states - .get(&command.user.id) - .and_then(|state| state.channel_id) - .ok_or_else(|| "Join a voice channel first, then run `/call` there.".to_string())?; + let (guild_id, voice_channel_id) = current_voice_channel(ctx, command, "/call")?; let caller_username = command .member @@ -729,6 +757,49 @@ fn build_outbound_request( }) } +fn build_hangup_request( + ctx: &Context, + command: &CommandInteraction, +) -> Result { + let (guild_id, voice_channel_id) = current_voice_channel(ctx, command, "/hangup")?; + let requested_by = command + .member + .as_ref() + .and_then(|member| member.nick.clone()) + .or_else(|| command.user.global_name.clone()) + .unwrap_or_else(|| command.user.name.clone()) + .to_string(); + + Ok(HangupCallRequest { + request_id: format!("hangup-{}", command.id), + guild_id: guild_id.get().to_string(), + channel_id: voice_channel_id.get().to_string(), + requested_by, + created_at: std::time::Instant::now(), + }) +} + +fn current_voice_channel( + ctx: &Context, + command: &CommandInteraction, + command_name: &str, +) -> Result<(GuildId, ChannelId), String> { + let guild_id = command + .guild_id + .ok_or_else(|| "This command only works inside a server.".to_string())?; + let guild = ctx + .cache + .guild(guild_id) + .ok_or_else(|| "Guild is not available in cache yet. Try again in a moment.".to_string())?; + let voice_channel_id = guild + .voice_states + .get(&command.user.id) + .and_then(|state| state.channel_id) + .ok_or_else(|| format!("Join a voice channel first, then run `{command_name}` there."))?; + + Ok((guild_id, voice_channel_id)) +} + fn is_safe_extension(extension: &str) -> bool { !extension.is_empty() && extension.len() <= 64 diff --git a/sipcord-bridge/src/transport/sip/mod.rs b/sipcord-bridge/src/transport/sip/mod.rs index 4e48c0c..77bbc17 100644 --- a/sipcord-bridge/src/transport/sip/mod.rs +++ b/sipcord-bridge/src/transport/sip/mod.rs @@ -117,7 +117,6 @@ pub enum SipCommand { tracking_id: String, sip_uri: String, caller_display_name: Option, - auto_answer: bool, /// Total number of fork legs for this tracking_id (for multi-contact forking) fork_total: usize, }, @@ -408,14 +407,13 @@ fn process_sip_command(cmd: SipCommand, calls: &Arc>) tracking_id, sip_uri, caller_display_name, - auto_answer, fork_total, } => { info!( - "Making outbound call: tracking_id={}, uri={}, caller={:?}, auto_answer={}, fork={}/{}", - tracking_id, sip_uri, caller_display_name, auto_answer, fork_total, fork_total + "Making outbound call: tracking_id={}, uri={}, caller={:?}, fork={}/{}", + tracking_id, sip_uri, caller_display_name, fork_total, fork_total ); - match make_outbound_call(&sip_uri, caller_display_name.as_deref(), auto_answer) { + match make_outbound_call(&sip_uri, caller_display_name.as_deref()) { Ok(call_id) => { // Store tracking_id -> call_id mapping let outbound_calls = OUTBOUND_CALL_TRACKING.get_or_init(DashMap::new); @@ -526,24 +524,10 @@ pub fn remove_outbound_tracking(call_id: CallId) -> Option { fn make_outbound_call( sip_uri: &str, caller_display_name: Option<&str>, - auto_answer: bool, ) -> Result { unsafe { - use self::ffi::pj_str::make_string_hdr; - let outbound_uri = if auto_answer { - if sip_uri.contains("intercom=true") { - sip_uri.to_string() - } else if let Some((base, params)) = sip_uri.split_once(';') { - format!("{base};intercom=true;{params}") - } else { - format!("{sip_uri};intercom=true") - } - } else { - sip_uri.to_string() - }; - let uri = - std::ffi::CString::new(outbound_uri).map_err(|source| SipCallError::InvalidString { + std::ffi::CString::new(sip_uri).map_err(|source| SipCallError::InvalidString { field: "sip_uri", source, })?; @@ -564,28 +548,6 @@ fn make_outbound_call( ::pjsua::pjsua_msg_data_init(msg_data.as_mut_ptr()); let msg_data_ptr = msg_data.assume_init_mut(); - if auto_answer { - let pool = ::pjsua::pjsua_pool_create(c"outbound_call".as_ptr(), 512, 512); - if pool.is_null() { - return Err(SipCallError::MakeCall(-1)); - } - - let call_info = make_string_hdr(pool, c"Call-Info", ";answer-after=0") - .map_err(|_| SipCallError::MakeCall(-1))?; - ::pjsua::pj_list_insert_before( - &mut msg_data_ptr.hdr_list as *mut _ as *mut ::pjsua::pj_list_type, - call_info as *mut ::pjsua::pj_list_type, - ); - - let alert_info = - make_string_hdr(pool, c"Alert-Info", ";info=Ring Answer") - .map_err(|_| SipCallError::MakeCall(-1))?; - ::pjsua::pj_list_insert_before( - &mut msg_data_ptr.hdr_list as *mut _ as *mut ::pjsua::pj_list_type, - alert_info as *mut ::pjsua::pj_list_type, - ); - } - // Build the From URI with display name: "name" // The local_uri field overrides the From header in the outgoing INVITE let from_uri_cstring;