trying to get intercom working

This commit is contained in:
legop3 2026-06-14 02:27:47 -04:00
parent 6e416573e5
commit 46c6907f59
5 changed files with 48 additions and 20 deletions

View file

@ -7,7 +7,7 @@ RTP_PUBLIC_IP=192.168.0.100
# DISCORD_OUTBOUND_SIP_HOST=192.168.0.25 # DISCORD_OUTBOUND_SIP_HOST=192.168.0.25
# DISCORD_OUTBOUND_SIP_PORT=5060 # DISCORD_OUTBOUND_SIP_PORT=5060
# DISCORD_OUTBOUND_SIP_TRANSPORT=udp # DISCORD_OUTBOUND_SIP_TRANSPORT=udp
# DISCORD_OUTBOUND_EXTENSION_PREFIX=*80 # DISCORD_OUTBOUND_EXTENSION_PREFIX=
# DATA_DIR=/var/lib/sipcord # DATA_DIR=/var/lib/sipcord
# CONFIG_PATH=./config.toml # CONFIG_PATH=./config.toml
# SOUNDS_DIR=./wav # SOUNDS_DIR=./wav

View file

@ -73,7 +73,7 @@ RTP_PUBLIC_IP=192.168.0.100
DISCORD_OUTBOUND_SIP_HOST=192.168.0.25 DISCORD_OUTBOUND_SIP_HOST=192.168.0.25
DISCORD_OUTBOUND_SIP_PORT=5060 DISCORD_OUTBOUND_SIP_PORT=5060
DISCORD_OUTBOUND_SIP_TRANSPORT=udp DISCORD_OUTBOUND_SIP_TRANSPORT=udp
DISCORD_OUTBOUND_EXTENSION_PREFIX=*80 DISCORD_OUTBOUND_EXTENSION_PREFIX=
``` ```
Set both IPs to the address other SIP devices use to reach the bridge. For Set both IPs to the address other SIP devices use to reach the bridge. For
@ -86,10 +86,11 @@ 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 Discord-originated extension calls. For a FreePBX box at `192.168.0.25`, that
means `DISCORD_OUTBOUND_SIP_HOST=192.168.0.25`. means `DISCORD_OUTBOUND_SIP_HOST=192.168.0.25`.
By default, Discord-originated calls prepend `*80` to the requested extension so Discord-originated calls dial the requested extension directly by default. The
FreePBX intercom/auto-answer targets work out of the box. Set bridge also attaches common auto-answer headers (`Call-Info:
`DISCORD_OUTBOUND_EXTENSION_PREFIX=` to an empty string if you want plain <uri>;answer-after=0` and `Alert-Info: Ring Answer`) to Discord-originated
extension dialing instead. outbound calls so auto-answer phones can behave more like FreePBX intercom
targets.
Create a `docker-compose.yml`: Create a `docker-compose.yml`:
@ -212,10 +213,10 @@ Behavior:
- The user running `/call` must already be in a Discord voice channel. - The user running `/call` must already be in a Discord voice channel.
- The bot uses that voice channel as the bridge destination. - The bot uses that voice channel as the bridge destination.
- The bridge dials the requested extension through the configured PBX target. - The bridge dials the requested extension through the configured PBX target.
- By default it prepends `*80`, so `/call extension:1101` becomes something like - It dials the requested extension directly, for example
`sip:*801101@192.168.0.25:5060;transport=udp`. `sip:1101@192.168.0.25:5060;transport=udp`.
- Set `DISCORD_OUTBOUND_EXTENSION_PREFIX=` to empty if you want `/call - Discord-originated outbound calls also include common auto-answer headers so
extension:1101` to dial `1101` directly instead. phones configured for that behavior can answer immediately.
- When the SIP side answers, the phone call is connected to the Discord voice - When the SIP side answers, the phone call is connected to the Discord voice
channel where the command was run. channel where the command was run.
@ -261,7 +262,7 @@ 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_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_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_SIP_TRANSPORT` | `udp` | Transport for Discord-originated outbound SIP calls: `udp`, `tcp`, or `tls` |
| `DISCORD_OUTBOUND_EXTENSION_PREFIX` | `*80` | Prefix prepended before the requested extension for Discord-originated calls; set empty for direct dialing | | `DISCORD_OUTBOUND_EXTENSION_PREFIX` | `""` | Optional prefix prepended before the requested extension for Discord-originated calls |
| `CONFIG_PATH` | `./config.toml` | Path to config.toml | | `CONFIG_PATH` | `./config.toml` | Path to config.toml |
| `DIALPLAN_PATH` | `./dialplan.toml` | Path to dialplan.toml | | `DIALPLAN_PATH` | `./dialplan.toml` | Path to dialplan.toml |
| `SOUNDS_DIR` | `./wav` | Path to sound files directory | | `SOUNDS_DIR` | `./wav` | Path to sound files directory |

View file

@ -553,6 +553,7 @@ impl BridgeCoordinator {
tracking_id: req.call_id.clone(), tracking_id: req.call_id.clone(),
sip_uri, sip_uri,
caller_display_name: Some(req.caller_username.clone()), caller_display_name: Some(req.caller_username.clone()),
auto_answer: true,
fork_total, fork_total,
}); });
outbound_backend.report_call_status(&req.call_id, "ringing"); outbound_backend.report_call_status(&req.call_id, "ringing");
@ -585,6 +586,7 @@ impl BridgeCoordinator {
tracking_id: req.call_id.clone(), tracking_id: req.call_id.clone(),
sip_uri, sip_uri,
caller_display_name: Some(req.caller_username.clone()), caller_display_name: Some(req.caller_username.clone()),
auto_answer: false,
fork_total, fork_total,
}); });
} }

View file

@ -72,7 +72,7 @@ fn default_discord_outbound_sip_transport() -> String {
"udp".to_string() "udp".to_string()
} }
fn default_discord_outbound_extension_prefix() -> String { fn default_discord_outbound_extension_prefix() -> String {
"*80".to_string() String::new()
} }
/// All environment variables consumed by the bridge, deserialized once at startup. /// All environment variables consumed by the bridge, deserialized once at startup.
@ -546,7 +546,7 @@ mod tests {
discord_outbound_sip_host: None, discord_outbound_sip_host: None,
discord_outbound_sip_port: 5060, discord_outbound_sip_port: 5060,
discord_outbound_sip_transport: "udp".to_string(), discord_outbound_sip_transport: "udp".to_string(),
discord_outbound_extension_prefix: "*80".to_string(), discord_outbound_extension_prefix: String::new(),
}; };
assert_eq!(env.resolved_data_dir(), "."); assert_eq!(env.resolved_data_dir(), ".");
} }
@ -574,7 +574,7 @@ mod tests {
discord_outbound_sip_host: None, discord_outbound_sip_host: None,
discord_outbound_sip_port: 5060, discord_outbound_sip_port: 5060,
discord_outbound_sip_transport: "udp".to_string(), discord_outbound_sip_transport: "udp".to_string(),
discord_outbound_extension_prefix: "*80".to_string(), discord_outbound_extension_prefix: String::new(),
}; };
assert_eq!(env.resolved_data_dir(), "/tmp"); assert_eq!(env.resolved_data_dir(), "/tmp");
} }
@ -602,7 +602,7 @@ mod tests {
discord_outbound_sip_host: None, discord_outbound_sip_host: None,
discord_outbound_sip_port: 5060, discord_outbound_sip_port: 5060,
discord_outbound_sip_transport: "udp".to_string(), discord_outbound_sip_transport: "udp".to_string(),
discord_outbound_extension_prefix: "*80".to_string(), discord_outbound_extension_prefix: String::new(),
}; };
let tls = env.to_tls_config(); let tls = env.to_tls_config();
assert_eq!(tls.cert_dir, PathBuf::from("/data/certs")); assert_eq!(tls.cert_dir, PathBuf::from("/data/certs"));
@ -646,13 +646,13 @@ mod tests {
discord_outbound_sip_host: Some("192.168.0.25".to_string()), discord_outbound_sip_host: Some("192.168.0.25".to_string()),
discord_outbound_sip_port: 5060, discord_outbound_sip_port: 5060,
discord_outbound_sip_transport: "udp".to_string(), discord_outbound_sip_transport: "udp".to_string(),
discord_outbound_extension_prefix: "*80".to_string(), discord_outbound_extension_prefix: String::new(),
}; };
let outbound = env.discord_outbound_sip_config().unwrap(); let outbound = env.discord_outbound_sip_config().unwrap();
assert_eq!( assert_eq!(
outbound.build_sip_uri("1101"), outbound.build_sip_uri("1101"),
"sip:*801101@192.168.0.25:5060;transport=udp" "sip:1101@192.168.0.25:5060;transport=udp"
); );
} }

View file

@ -117,6 +117,7 @@ pub enum SipCommand {
tracking_id: String, tracking_id: String,
sip_uri: String, sip_uri: String,
caller_display_name: Option<String>, caller_display_name: Option<String>,
auto_answer: bool,
/// Total number of fork legs for this tracking_id (for multi-contact forking) /// Total number of fork legs for this tracking_id (for multi-contact forking)
fork_total: usize, fork_total: usize,
}, },
@ -407,13 +408,14 @@ fn process_sip_command(cmd: SipCommand, calls: &Arc<DashMap<CallId, CallState>>)
tracking_id, tracking_id,
sip_uri, sip_uri,
caller_display_name, caller_display_name,
auto_answer,
fork_total, fork_total,
} => { } => {
info!( info!(
"Making outbound call: tracking_id={}, uri={}, caller={:?}, fork={}/{}", "Making outbound call: tracking_id={}, uri={}, caller={:?}, auto_answer={}, fork={}/{}",
tracking_id, sip_uri, caller_display_name, fork_total, fork_total tracking_id, sip_uri, caller_display_name, auto_answer, fork_total, fork_total
); );
match make_outbound_call(&sip_uri, caller_display_name.as_deref()) { match make_outbound_call(&sip_uri, caller_display_name.as_deref(), auto_answer) {
Ok(call_id) => { Ok(call_id) => {
// Store tracking_id -> call_id mapping // Store tracking_id -> call_id mapping
let outbound_calls = OUTBOUND_CALL_TRACKING.get_or_init(DashMap::new); let outbound_calls = OUTBOUND_CALL_TRACKING.get_or_init(DashMap::new);
@ -524,8 +526,10 @@ pub fn remove_outbound_tracking(call_id: CallId) -> Option<String> {
fn make_outbound_call( fn make_outbound_call(
sip_uri: &str, sip_uri: &str,
caller_display_name: Option<&str>, caller_display_name: Option<&str>,
auto_answer: bool,
) -> Result<CallId, SipCallError> { ) -> Result<CallId, SipCallError> {
unsafe { unsafe {
use self::ffi::pj_str::make_string_hdr;
let uri = let uri =
std::ffi::CString::new(sip_uri).map_err(|source| SipCallError::InvalidString { std::ffi::CString::new(sip_uri).map_err(|source| SipCallError::InvalidString {
field: "sip_uri", field: "sip_uri",
@ -548,6 +552,27 @@ fn make_outbound_call(
::pjsua::pjsua_msg_data_init(msg_data.as_mut_ptr()); ::pjsua::pjsua_msg_data_init(msg_data.as_mut_ptr());
let msg_data_ptr = msg_data.assume_init_mut(); 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", "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> // Build the From URI with display name: "name" <sip:sipcord@host>
// The local_uri field overrides the From header in the outgoing INVITE // The local_uri field overrides the From header in the outgoing INVITE
let from_uri_cstring; let from_uri_cstring;