use std::time::Duration;
use asterisk_rs_ami::AmiClient;
use asterisk_rs_ami::action::{HangupAction, OriginateAction};
use asterisk_rs_ami::event::AmiEvent;
use asterisk_rs_core::event::{EventReceive, EventSubscription};
#[derive(Debug)]
pub struct Call {
pub channel: String,
pub unique_id: String,
client: AmiClient,
answer_sub: EventSubscription<AmiEvent>,
}
impl Call {
pub async fn hangup(
&self,
) -> asterisk_rs_ami::error::Result<asterisk_rs_ami::response::AmiResponse> {
self.client.hangup(HangupAction::new(&self.channel)).await
}
pub async fn wait_for_answer(&mut self, timeout: Duration) -> Result<(), PbxError> {
let uid = self.unique_id.clone();
let result = tokio::time::timeout(timeout, async {
loop {
let event = match self.answer_sub.recv_outcome().await {
EventReceive::Event(event) => event,
EventReceive::Lagged(missed) => return Err(PbxError::EventLoss { missed }),
EventReceive::Closed => return Err(PbxError::Disconnected),
};
match event {
AmiEvent::Newstate {
unique_id,
channel_state_desc,
..
} if unique_id == uid && channel_state_desc == "Up" => {
return Ok(());
}
AmiEvent::Hangup {
unique_id,
cause,
cause_txt,
..
} if unique_id == uid => {
return Err(PbxError::CallFailed { cause, cause_txt });
}
_ => {}
}
}
})
.await;
match result {
Ok(inner) => inner,
Err(_) => Err(PbxError::Timeout),
}
}
}
#[derive(Debug, Clone, Default)]
#[must_use]
pub struct DialOptions {
pub caller_id: Option<String>,
pub timeout_ms: Option<u64>,
pub variables: Option<std::collections::HashMap<String, String>>,
}
impl DialOptions {
pub fn new() -> Self {
Self::default()
}
pub fn caller_id(mut self, cid: impl Into<String>) -> Self {
self.caller_id = Some(cid.into());
self
}
pub fn timeout_ms(mut self, ms: u64) -> Self {
self.timeout_ms = Some(ms);
self
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PbxError {
#[error("AMI error: {0}")]
Ami(#[from] asterisk_rs_ami::AmiError),
#[error("call failed: {cause} ({cause_txt})")]
CallFailed { cause: u32, cause_txt: String },
#[error("operation timed out")]
Timeout,
#[error("client disconnected")]
Disconnected,
#[error("call state invalidated after {missed} lost events")]
EventLoss { missed: u64 },
}
#[derive(Debug)]
pub struct Pbx {
client: AmiClient,
}
impl Pbx {
pub fn new(client: AmiClient) -> Self {
Self { client }
}
pub async fn dial(
&self,
from: impl Into<String>,
to: impl Into<String>,
options: Option<DialOptions>,
) -> Result<Call, PbxError> {
let from = from.into();
let to = to.into();
let opts = options.unwrap_or_default();
let mut action = OriginateAction::new(&from)
.extension(&to)
.context("default")
.priority(1)
.async_originate(true);
if let Some(ref cid) = opts.caller_id {
action = action.caller_id(cid);
}
if let Some(ms) = opts.timeout_ms {
action = action.timeout_ms(ms);
}
if let Some(ref vars) = opts.variables {
for (k, v) in vars {
action = action.variable(k, v);
}
}
let answer_sub = self.client.subscribe();
let mut orig_sub = self
.client
.subscribe_filtered(move |e| matches!(e, AmiEvent::OriginateResponse { .. }));
let orig_response = self.client.originate(action).await?;
if !orig_response.success {
return Err(PbxError::CallFailed {
cause: 0,
cause_txt: orig_response
.message
.unwrap_or_else(|| "originate rejected".to_owned()),
});
}
let expected_action_id = orig_response.action_id;
let originate_timeout = opts
.timeout_ms
.map(Duration::from_millis)
.unwrap_or(Duration::from_secs(30))
.checked_add(Duration::from_secs(5))
.unwrap_or(Duration::MAX);
let event = tokio::time::timeout(originate_timeout, async {
loop {
let event = match orig_sub.recv_outcome().await {
EventReceive::Event(event) => event,
EventReceive::Lagged(missed) => return Err(PbxError::EventLoss { missed }),
EventReceive::Closed => return Err(PbxError::Disconnected),
};
if event.action_id() == Some(expected_action_id.as_str()) {
if let AmiEvent::OriginateResponse {
channel,
unique_id,
response,
..
} = event
{
return Ok((channel, unique_id, response));
}
}
}
})
.await
.map_err(|_| PbxError::Timeout)??;
let (channel, unique_id, response) = event;
if response.eq_ignore_ascii_case("failure") {
return Err(PbxError::CallFailed {
cause: 0,
cause_txt: "originate failed".to_owned(),
});
}
Ok(Call {
channel,
unique_id,
client: self.client.clone(),
answer_sub,
})
}
pub fn client(&self) -> &AmiClient {
&self.client
}
}