slop hangup

This commit is contained in:
legop3 2026-06-14 04:20:43 -04:00
parent 14fecbb3e1
commit e4576231ba
9 changed files with 188 additions and 110 deletions

View file

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

View file

@ -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:
<uri>;answer-after=0` and `Alert-Info: <http://127.0.0.1>;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 |

View file

@ -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::<Snowflake>() {
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<CallId> = 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(())

View file

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

View file

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

View file

@ -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<OutboundCallRequest>;
/// Get the next hangup request (None if backend doesn't support hangup control)
async fn next_hangup_request(&self) -> Option<HangupCallRequest>;
}

View file

@ -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<String, ExtensionTarget>,
outbound_rx: Arc<Mutex<tokio::sync::mpsc::UnboundedReceiver<OutboundCallRequest>>>,
hangup_rx: Arc<Mutex<tokio::sync::mpsc::UnboundedReceiver<HangupCallRequest>>>,
}
impl StaticBackend {
@ -54,6 +57,7 @@ impl StaticBackend {
path: &Path,
bot_token: String,
outbound_rx: tokio::sync::mpsc::UnboundedReceiver<OutboundCallRequest>,
hangup_rx: tokio::sync::mpsc::UnboundedReceiver<HangupCallRequest>,
) -> Result<Self, ConfigError> {
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<OutboundCallRequest> {
self.outbound_rx.lock().await.recv().await
}
async fn next_hangup_request(&self) -> Option<HangupCallRequest> {
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());
}
}

View file

@ -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<OutboundCallRequest>,
pub hangup_tx: tokio::sync::mpsc::UnboundedSender<HangupCallRequest>,
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<HangupCallRequest, String> {
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

View file

@ -117,7 +117,6 @@ pub enum SipCommand {
tracking_id: String,
sip_uri: String,
caller_display_name: Option<String>,
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<DashMap<CallId, CallState>>)
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<String> {
fn make_outbound_call(
sip_uri: &str,
caller_display_name: Option<&str>,
auto_answer: bool,
) -> Result<CallId, SipCallError> {
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", "<uri>;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", "<http://127.0.0.1>;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" <sip:sipcord@host>
// The local_uri field overrides the From header in the outgoing INVITE
let from_uri_cstring;