asterisk-rs 0.1.7

Async Rust client for Asterisk AMI, AGI, and ARI
Documentation

asterisk-rs

crates.io docs.rs CI MSRV License

Async Rust client for Asterisk PBX. Originate calls, handle events, control channels, bridges, queues, and recordings across all three Asterisk interfaces.

  • AMI -- monitor and control Asterisk over TCP. Typed events, actions, automatic reconnection, MD5 auth.
  • AGI -- run dialplan logic from your Rust service. FastAGI server with typed async commands.
  • ARI -- full call control via REST + WebSocket. Resource handles, typed events with metadata.

Quick Example

use asterisk_rs::ami::{AmiClient, AmiEvent};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let client = AmiClient::builder()
        .host("10.0.0.1")
        .credentials("admin", "secret")
        .build()
        .await?;

    // subscribe to hangup events only
    let mut hangups = client.subscribe_filtered(|e| {
        e.event_name() == "Hangup"
    });

    while let Some(event) = hangups.recv().await {
        if let AmiEvent::Hangup { channel, cause, cause_txt, .. } = event {
            tracing::info!(%channel, %cause, %cause_txt, "channel hung up");
        }
    }

    Ok(())
}

Install

Use the umbrella crate to pull in whichever protocols you need:

[dependencies]
asterisk-rs = "0.1"

Or add individual protocol crates directly:

[dependencies]
asterisk-rs-ami = "0.4"   # AMI only
asterisk-rs-agi = "0.2"   # AGI only
asterisk-rs-ari = "0.4"   # ARI only

Feature Selection

The umbrella crate enables all protocols by default. To select only what you need:

[dependencies]
asterisk-rs = { version = "0.1", default-features = false, features = ["ami"] }
# or: features = ["agi"]
# or: features = ["ari"]
# or: features = ["ami", "ari"]

Available features: ami, agi, ari. The pbx abstraction requires ami.

Protocols

Protocol Default Port Transport Use Case
AMI 5038 TCP Monitoring, call control, system management
AGI 4573 TCP Dialplan logic, IVR, call routing
ARI 8088 HTTP + WS Stasis applications, full media control

Capabilities

  • Typed actions, events, and commands for the full Asterisk protocol surface
  • Filtered event subscriptions -- receive only what you need
  • Event-collecting actions -- send_collecting() gathers multi-event responses (Status, QueueStatus, etc.)
  • Automatic reconnection with exponential backoff, jitter, and re-authentication
  • Call tracker -- correlates AMI events into CompletedCall records (channel, duration, cause, full event log)
  • PBX abstraction -- Pbx::dial() wraps originate + OriginateResponse correlation into one async call
  • Pending resources -- ARI PendingChannel/PendingBridge pre-subscribe before REST to eliminate event races
  • Transport modes -- ARI supports HTTP (request/response) or WebSocket (bidirectional streaming)
  • Outbound WebSocket server -- AriServer accepts Asterisk 22+ outbound WS connections
  • Media channel -- low-level audio I/O over WebSocket for external media applications
  • Resource handles for ARI (ChannelHandle, BridgeHandle, PlaybackHandle, RecordingHandle)
  • Domain types for hangup causes, channel states, device states, dial statuses, and more
  • ARI event metadata (application, timestamp, asterisk_id) on every event
  • AMI command output capture for Response: Follows
  • URL-safe query encoding, HTTP timeouts, WebSocket lifecycle management
  • #[non_exhaustive] enums -- new variants won't break your code
  • Structured logging via tracing

More Examples

AMI: call tracker

use asterisk_rs::ami::AmiClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let client = AmiClient::builder()
        .host("127.0.0.1")
        .credentials("admin", "secret")
        .build()
        .await?;

    let (tracker, mut rx) = client.call_tracker();

    while let Some(call) = rx.recv().await {
        tracing::info!(
            channel = %call.channel,
            duration = ?call.duration,
            cause = %call.cause_txt,
            "call completed"
        );
    }

    tracker.shutdown();
    Ok(())
}

AGI: IVR handler

use asterisk_rs::agi::{AgiChannel, AgiHandler, AgiRequest, AgiServer};

struct IvrHandler;

impl AgiHandler for IvrHandler {
    async fn handle(&self, _request: AgiRequest, mut channel: AgiChannel)
        -> asterisk_rs::agi::error::Result<()>
    {
        channel.answer().await?;
        channel.stream_file("welcome", "#").await?;
        let response = channel.get_data("press-ext", 5000, 4).await?;
        tracing::info!(digits = response.result, "caller input");
        channel.hangup(None).await?;
        Ok(())
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let (server, _shutdown) = AgiServer::builder()
        .bind("0.0.0.0:4573")
        .handler(IvrHandler)
        .max_connections(100)
        .build()
        .await?;

    server.run().await?;
    Ok(())
}

ARI: pending channel

use asterisk_rs::ari::config::AriConfigBuilder;
use asterisk_rs::ari::{AriClient, AriEvent, PendingChannel, TransportMode};
use asterisk_rs::ari::resources::channel::OriginateParams;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let config = AriConfigBuilder::new("my-app")
        .host("127.0.0.1")
        .port(8088)
        .username("asterisk")
        .password("asterisk")
        .build()?;

    let client = AriClient::connect(config).await?;

    // pre-subscribe before originate so no events are missed
    let pending = client.channel();
    let params = OriginateParams {
        endpoint: "PJSIP/100".into(),
        app: Some("my-app".into()),
        ..Default::default()
    };
    let (handle, mut events) = pending.originate(params).await?;

    while let Some(msg) = events.recv().await {
        match msg.event {
            AriEvent::StasisStart { .. } => {
                handle.answer().await?;
                handle.play("sound:hello-world").await?;
                handle.hangup(None).await?;
            }
            AriEvent::ChannelDestroyed { cause_txt, .. } => {
                tracing::info!(%cause_txt, "channel destroyed");
                break;
            }
            _ => {}
        }
    }

    Ok(())
}

PBX: dial and wait

use asterisk_rs::ami::AmiClient;
use asterisk_rs::pbx::{DialOptions, Pbx};
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let client = AmiClient::builder()
        .host("127.0.0.1")
        .credentials("admin", "secret")
        .build()
        .await?;

    let mut pbx = Pbx::new(client);

    let call = pbx.dial(
        "PJSIP/100",
        "200",
        Some(
            DialOptions::new()
                .caller_id("Rust PBX <100>")
                .timeout_ms(30000),
        ),
    ).await?;

    call.wait_for_answer(Duration::from_secs(30)).await?;
    tracing::info!("call answered");

    call.hangup().await?;

    if let Some(completed) = pbx.next_completed_call().await {
        tracing::info!(duration = ?completed.duration, cause = %completed.cause_txt, "call record");
    }

    Ok(())
}

Documentation

MSRV

1.83 -- required for async fn in traits (RPITIT).

License

Licensed under either of

at your option.