use crate::action::{self, AmiAction, LogoffAction, PingAction};
use crate::connection::{ConnectionCommand, ConnectionManager};
use crate::error::{AmiError, AmiTerminalError, Result};
use crate::event::AmiEvent;
use crate::response::{AmiResponse, RequestLifecycle};
use asterisk_rs_core::auth::Credentials;
use asterisk_rs_core::config::{ConnectionState, ReconnectPolicy};
use asterisk_rs_core::event::{EventBus, EventSubscription};
use std::sync::Arc;
use std::time::Duration;
use tokio::time::Instant;
const DEFAULT_PORT: u16 = 5038;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone)]
pub struct AmiClient {
connection: Arc<ConnectionManager>,
event_bus: EventBus<AmiEvent>,
timeout: Duration,
}
impl AmiClient {
pub fn builder() -> AmiClientBuilder {
AmiClientBuilder::default()
}
pub async fn send_action<A: AmiAction>(&self, action: &A) -> Result<AmiResponse> {
let (action_id, message) = action.to_message();
let (response_tx, response_rx) = tokio::sync::oneshot::channel();
let lifecycle = Arc::new(RequestLifecycle::default());
let deadline = self.action_deadline()?;
self.connection
.send(
ConnectionCommand::SendAction {
message,
action_id: action_id.clone(),
deadline,
timeout: self.timeout,
lifecycle: lifecycle.clone(),
response_tx,
},
deadline,
self.timeout,
)
.await?;
self.await_action_result(action_id, deadline, lifecycle, response_rx)
.await
}
pub async fn ping(&self) -> Result<AmiResponse> {
self.send_action(&PingAction).await
}
pub async fn originate(&self, action: action::OriginateAction) -> Result<AmiResponse> {
self.send_action(&action).await
}
pub async fn hangup(&self, action: action::HangupAction) -> Result<AmiResponse> {
self.send_action(&action).await
}
pub async fn command(&self, command: impl Into<String>) -> Result<AmiResponse> {
self.send_action(&action::CommandAction::new(command)).await
}
pub fn subscribe(&self) -> EventSubscription<AmiEvent> {
self.event_bus.subscribe()
}
pub fn call_tracker(
&self,
) -> (
crate::tracker::CallTracker,
tokio::sync::mpsc::Receiver<crate::tracker::CompletedCall>,
) {
crate::tracker::CallTracker::new(self.subscribe())
}
pub async fn send_collecting<A: AmiAction>(
&self,
action: &A,
) -> Result<crate::response::EventListResponse> {
let (action_id, message) = action.to_message();
let (response_tx, response_rx) = tokio::sync::oneshot::channel();
let lifecycle = Arc::new(RequestLifecycle::default());
let deadline = self.action_deadline()?;
self.connection
.send(
ConnectionCommand::SendEventGeneratingAction {
message,
action_id: action_id.clone(),
deadline,
timeout: self.timeout,
lifecycle: lifecycle.clone(),
response_tx,
},
deadline,
self.timeout,
)
.await?;
self.await_action_result(action_id, deadline, lifecycle, response_rx)
.await
}
pub fn subscribe_filtered(
&self,
predicate: impl Fn(&AmiEvent) -> bool + Send + 'static,
) -> asterisk_rs_core::event::FilteredSubscription<AmiEvent> {
self.event_bus.subscribe_filtered(predicate)
}
pub fn connection_state(&self) -> ConnectionState {
self.connection.state()
}
pub fn terminal_error(&self) -> Option<AmiTerminalError> {
self.connection.terminal_error()
}
pub async fn disconnect(&self) -> Result<()> {
let _ = self.send_action(&LogoffAction).await;
self.connection.shutdown().await;
Ok(())
}
fn action_deadline(&self) -> Result<Instant> {
Instant::now()
.checked_add(self.timeout)
.ok_or_else(|| AmiError::InvalidConfig {
details: "action timeout is too large".to_owned(),
})
}
async fn await_action_result<T>(
&self,
action_id: String,
deadline: Instant,
lifecycle: Arc<RequestLifecycle>,
mut response_rx: tokio::sync::oneshot::Receiver<Result<T>>,
) -> Result<T> {
match tokio::time::timeout_at(deadline, &mut response_rx).await {
Ok(Ok(result)) => result,
Ok(Err(_)) => Err(AmiError::ResponseChannelClosed),
Err(_) => {
if let Ok(result) = response_rx.try_recv() {
return result;
}
if lifecycle.cancel_queued() {
Err(self.action_timeout())
} else if lifecycle.may_have_executed() {
Err(AmiError::OutcomeUnknown { action_id })
} else {
Err(self.action_timeout())
}
}
}
}
fn action_timeout(&self) -> AmiError {
AmiError::Timeout(asterisk_rs_core::error::TimeoutError::Action {
elapsed: self.timeout,
})
}
}
impl std::fmt::Debug for AmiClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AmiClient")
.field("state", &self.connection.state())
.finish()
}
}
#[derive(Debug)]
#[must_use]
pub struct AmiClientBuilder {
host: String,
port: u16,
credentials: Option<Credentials>,
reconnect_policy: ReconnectPolicy,
timeout: Duration,
connect_timeout: Duration,
event_capacity: usize,
ping_interval: Option<Duration>,
require_challenge: bool,
}
impl Default for AmiClientBuilder {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: DEFAULT_PORT,
credentials: None,
reconnect_policy: ReconnectPolicy::default(),
timeout: DEFAULT_TIMEOUT,
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
event_capacity: 1024,
ping_interval: None,
require_challenge: true,
}
}
}
impl AmiClientBuilder {
pub fn host(mut self, host: impl Into<String>) -> Self {
self.host = host.into();
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
pub fn credentials(mut self, username: impl Into<String>, secret: impl Into<String>) -> Self {
self.credentials = Some(Credentials::new(username, secret));
self
}
pub fn reconnect(mut self, policy: ReconnectPolicy) -> Self {
self.reconnect_policy = policy;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = timeout;
self
}
pub fn event_capacity(mut self, capacity: usize) -> Self {
self.event_capacity = capacity;
self
}
pub fn require_challenge(mut self, require: bool) -> Self {
self.require_challenge = require;
self
}
pub fn ping_interval(mut self, interval: Duration) -> Self {
self.ping_interval = Some(interval);
self
}
pub async fn build(self) -> Result<AmiClient> {
let credentials = self.credentials.ok_or(AmiError::Auth(
asterisk_rs_core::error::AuthError::InvalidCredentials,
))?;
if self.event_capacity == 0 {
return Err(AmiError::InvalidConfig {
details: "event_capacity must be greater than zero".to_owned(),
});
}
if self.timeout.is_zero() {
return Err(AmiError::InvalidConfig {
details: "timeout must be greater than zero".to_owned(),
});
}
if self.connect_timeout.is_zero() {
return Err(AmiError::InvalidConfig {
details: "connect_timeout must be greater than zero".to_owned(),
});
}
if self.ping_interval == Some(Duration::ZERO) {
return Err(AmiError::InvalidConfig {
details: "ping_interval must be greater than zero".to_owned(),
});
}
if self
.ping_interval
.is_some_and(|interval| Instant::now().checked_add(interval).is_none())
{
return Err(AmiError::InvalidConfig {
details: "ping_interval is too large".to_owned(),
});
}
if let Err(details) = self.reconnect_policy.validate() {
return Err(AmiError::InvalidConfig {
details: details.to_owned(),
});
}
let event_bus = EventBus::new(self.event_capacity);
let address = format!("{}:{}", self.host, self.port);
let (connection, startup_rx) = ConnectionManager::spawn(
address,
credentials,
event_bus.clone(),
self.reconnect_policy,
self.ping_interval,
self.require_challenge,
);
match tokio::time::timeout(self.connect_timeout, startup_rx).await {
Ok(Ok(result)) => result?,
Ok(Err(_)) => return Err(AmiError::Disconnected),
Err(_) => {
return Err(AmiError::Timeout(
asterisk_rs_core::error::TimeoutError::Connection {
elapsed: self.connect_timeout,
},
));
}
}
Ok(AmiClient {
connection: Arc::new(connection),
event_bus,
timeout: self.timeout,
})
}
}