asterisk-manager 1.0.0

An asynchronous Rust library for interacting with the Asterisk Manager Interface (AMI), featuring a strongly-typed, stream-based API.
Documentation

Asterisk Manager (asterisk-manager)

Crates.io Docs.rs MIT License CI

A modern, asynchronous, strongly-typed, and stream-based library for interacting with the Asterisk Manager Interface (AMI) in Rust.

This crate simplifies communication with AMI by handling connection, authentication, sending actions, and consuming events in an idiomatic Rust way, using Tokio and a type system that helps prevent compile-time errors.

Table of Contents

✨ Features

  • Strongly-Typed AMI Messages: Actions, Events, and Responses are modeled as Rust enums and structs. This reduces runtime errors, improves safety, and enables powerful autocompletion in your editor.
  • Stream-Based API: Consume AMI events reactively and efficiently using the Stream abstraction from tokio_stream, integrating seamlessly with the Tokio ecosystem.
  • Fully Asynchronous: Built on Tokio for non-blocking, high-performance operations, ideal for concurrent applications.
  • Action-Response Correlation: Send an action and receive a Future that resolves to the corresponding response, making request/response logic straightforward.
  • Detailed Error Handling: A comprehensive AmiError enum allows robust handling of different failure scenarios (I/O, authentication, parsing, timeouts, etc.).

🚀 Getting Started

1. Installation

Add asterisk-manager to your Cargo.toml. The library requires Tokio as the async runtime.

[dependencies]
asterisk-manager = "1.0.0" # Replace with the latest version
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1"
log = "0.4"

The dependencies serde, serde_json, and uuid are managed by asterisk-manager.

2. Usage Example

This example connects to AMI, listens for events in a separate task, sends a Ping action, and awaits the response.

use asterisk_manager::{Manager, ManagerOptions, AmiAction, AmiEvent};
use tokio_stream::StreamExt;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
        // 1. Define connection options
        let options = ManagerOptions {
                port: 5038,
                host: "127.0.0.1".to_string(),
                username: "admin".to_string(),
                password: "password".to_string(),
                events: true, // Enable receiving all events
        };

        // 2. Create a new Manager instance
        let manager = Manager::new(options);

        // 3. Connect and login. The library manages an internal I/O task.
        if let Err(e) = manager.connect_and_login().await {
                eprintln!("Failed to connect and login: {}", e);
                return Err(e.into());
        }
        println!("Connected and logged in to AMI!");

        // 4. Obtain a stream for all AMI events
        let mut event_stream = manager.all_events_stream().await;

        // 5. Start a task to continuously consume events
        tokio::spawn(async move {
                println!("Event task started. Waiting for events...");
                while let Some(event_result) = event_stream.next().await {
                        match event_result {
                                Ok(event) => {
                                        // Handle the event
                                        match event {
                                                AmiEvent::PeerStatus(status) => {
                                                        println!("[Event] Peer Status: {} -> {}", status.peer, status.peer_status);
                                                }
                                                AmiEvent::Newchannel(new_channel) => {
                                                         println!("[Event] New channel created: {}", new_channel.channel);
                                                }
                                                _ => {
                                                        // Print other events
                                                        // println!("[Event] Received: {:?}", event);
                                                }
                                        }
                                }
                                Err(e) => {
                                        eprintln!("Event stream error: {}", e);
                                }
                        }
                }
                println!("Event task ended.");
        });

        // 6. Send an action and await the response
        println!("Sending Ping...");
        let ping_action = AmiAction::Ping { action_id: None };
        match manager.send_action(ping_action).await {
                Ok(response) => {
                        println!("Ping response received: {:?}", response);
                }
                Err(e) => {
                        eprintln!("Failed to send Ping action: {}", e);
                }
        }

        // Give some time for the event task to receive something (for this example only)
        tokio::time::sleep(Duration::from_secs(10)).await;

        // 7. Disconnect
        manager.disconnect().await?;
        println!("Disconnected.");

        Ok(())
}

📖 Core Concepts

The Manager

The Manager struct is the main entry point of the library. It acts as a handle for the AMI connection. Internally, it manages connection state, authentication, and a background I/O task that reads and processes all messages from the Asterisk server.

Manager is Clone, Send, and Sync, allowing it to be safely shared between multiple tasks, such as in a web application using Actix Web or Axum.

Sending Actions

To send an action to Asterisk, use the manager.send_action() method. It accepts an instance of the AmiAction enum and returns a Future.

let action = AmiAction::Command {
        command: "sip show peers".to_string(),
        action_id: None, // The library generates an ActionID if None
};

let response_result = manager.send_action(action).await;

The Future resolves to a Result<AmiResponse, AmiError>. This allows you to asynchronously await the direct response to your action (e.g., Response: Success or Response: Error).

Consuming Events

The library uses a "fan-out" approach for events. A single I/O task reads all events from Asterisk and broadcasts them to all interested consumers via a broadcast::channel.

To consume events, obtain a stream with manager.all_events_stream().await.

let mut stream = manager.all_events_stream().await;

while let Some(result) = stream.next().await {
        if let Ok(event) = result {
                println!("Event received: {:?}", event);
        }
}

This allows multiple parts of your application to independently and concurrently listen to the same AMI events.

Error Handling

All fallible operations return Result<T, AmiError>. The AmiError enum describes the error source, allowing granular failure handling.

match manager.connect_and_login().await {
        Ok(_) => println!("Success!"),
        Err(AmiError::Io(e)) => eprintln!("I/O error: {}", e),
        Err(AmiError::AuthenticationFailed(reason)) => eprintln!("Authentication failed: {}", reason),
        Err(AmiError::Timeout) => eprintln!("Operation timed out"),
        Err(e) => eprintln!("Other error: {}", e),
}

🔌 Reconnection Strategy

This library does not include built-in automatic reconnection logic by design. The philosophy is to provide robust building blocks so you can implement the reconnection strategy that best fits your application (e.g., exponential backoff, fixed number of attempts, etc.).

The Manager exposes the necessary methods and error types (AmiError::ConnectionClosed, AmiError::Io) to detect a disconnection and trigger your reconnection logic.

For a complete and robust example of a web application with resilient reconnection logic, see the examples/actix_web_example.rs file in this repository.

🤝 Contributing

Contributions are very welcome! If you find a bug, have a suggestion for improvement, or want to add support for more actions and events, feel free to open an Issue or a Pull Request.

📜 License

This project is licensed under the MIT License.

⭐ Acknowledgements

This work was inspired by the simplicity and effectiveness of the following libraries:

- [NodeJS-AsteriskManager](https://github.com/pipobscure/NodeJS-AsteriskManager)
- [node-asterisk](https://github.com/mscdex/node-asterisk)
- Thanks to all contributors and the Rust community.