sal-mycelium 0.1.0

SAL Mycelium - Client interface for interacting with Mycelium node's HTTP API
Documentation
//! SAL Mycelium - Client interface for interacting with Mycelium node's HTTP API
//!
//! This crate provides a client interface for interacting with a Mycelium node's HTTP API.
//! Mycelium is a decentralized networking project, and this SAL module allows Rust applications
//! and `herodo` Rhai scripts to manage and communicate over a Mycelium network.
//!
//! The module enables operations such as:
//! - Querying node status and information
//! - Managing peer connections (listing, adding, removing)
//! - Inspecting routing tables (selected and fallback routes)
//! - Sending messages to other Mycelium nodes
//! - Receiving messages from subscribed topics
//!
//! All interactions with the Mycelium API are performed asynchronously.

use base64::{engine::general_purpose, Engine as _};
use reqwest::Client;
use serde_json::Value;
use std::time::Duration;

pub mod rhai;

/// Get information about the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
///
/// # Returns
///
/// * `Result<Value, String>` - The node information as a JSON value, or an error message
pub async fn get_node_info(api_url: &str) -> Result<Value, String> {
    let client = Client::new();
    let url = format!("{}/api/v1/admin", api_url);

    let response = client
        .get(&url)
        .send()
        .await
        .map_err(|e| format!("Failed to send request: {}", e))?;

    let status = response.status();
    if !status.is_success() {
        return Err(format!("Request failed with status: {}", status));
    }

    let result: Value = response
        .json()
        .await
        .map_err(|e| format!("Failed to parse response: {}", e))?;

    Ok(result)
}

/// List all peers connected to the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
///
/// # Returns
///
/// * `Result<Value, String>` - The list of peers as a JSON value, or an error message
pub async fn list_peers(api_url: &str) -> Result<Value, String> {
    let client = Client::new();
    let url = format!("{}/api/v1/admin/peers", api_url);

    let response = client
        .get(&url)
        .send()
        .await
        .map_err(|e| format!("Failed to send request: {}", e))?;

    let status = response.status();
    if !status.is_success() {
        return Err(format!("Request failed with status: {}", status));
    }

    let result: Value = response
        .json()
        .await
        .map_err(|e| format!("Failed to parse response: {}", e))?;

    Ok(result)
}

/// Add a new peer to the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
/// * `peer_address` - The address of the peer to add
///
/// # Returns
///
/// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message
pub async fn add_peer(api_url: &str, peer_address: &str) -> Result<Value, String> {
    let client = Client::new();
    let url = format!("{}/api/v1/admin/peers", api_url);

    let response = client
        .post(&url)
        .json(&serde_json::json!({
            "endpoint": peer_address
        }))
        .send()
        .await
        .map_err(|e| format!("Failed to send request: {}", e))?;

    let status = response.status();
    if status == reqwest::StatusCode::NO_CONTENT {
        // Successfully added, but no content to parse
        return Ok(serde_json::json!({"success": true}));
    }
    if !status.is_success() {
        return Err(format!("Request failed with status: {}", status));
    }

    // For other success statuses that might have a body
    let result: Value = response
        .json()
        .await
        .map_err(|e| format!("Failed to parse response: {}", e))?;

    Ok(result)
}

/// Remove a peer from the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
/// * `peer_id` - The ID of the peer to remove
///
/// # Returns
///
/// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message
pub async fn remove_peer(api_url: &str, peer_id: &str) -> Result<Value, String> {
    let client = Client::new();
    let peer_id_url_encoded = urlencoding::encode(peer_id);
    let url = format!("{}/api/v1/admin/peers/{}", api_url, peer_id_url_encoded);

    let response = client
        .delete(&url)
        .send()
        .await
        .map_err(|e| format!("Failed to send request: {}", e))?;

    let status = response.status();
    if status == reqwest::StatusCode::NO_CONTENT {
        // Successfully removed, but no content to parse
        return Ok(serde_json::json!({"success": true}));
    }
    if !status.is_success() {
        return Err(format!("Request failed with status: {}", status));
    }

    let result: Value = response
        .json()
        .await
        .map_err(|e| format!("Failed to parse response: {}", e))?;

    Ok(result)
}

/// List all selected routes in the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
///
/// # Returns
///
/// * `Result<Value, String>` - The list of selected routes as a JSON value, or an error message
pub async fn list_selected_routes(api_url: &str) -> Result<Value, String> {
    let client = Client::new();
    let url = format!("{}/api/v1/admin/routes/selected", api_url);

    let response = client
        .get(&url)
        .send()
        .await
        .map_err(|e| format!("Failed to send request: {}", e))?;

    let status = response.status();
    if !status.is_success() {
        return Err(format!("Request failed with status: {}", status));
    }

    let result: Value = response
        .json()
        .await
        .map_err(|e| format!("Failed to parse response: {}", e))?;

    Ok(result)
}

/// List all fallback routes in the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
///
/// # Returns
///
/// * `Result<Value, String>` - The list of fallback routes as a JSON value, or an error message
pub async fn list_fallback_routes(api_url: &str) -> Result<Value, String> {
    let client = Client::new();
    let url = format!("{}/api/v1/admin/routes/fallback", api_url);

    let response = client
        .get(&url)
        .send()
        .await
        .map_err(|e| format!("Failed to send request: {}", e))?;

    let status = response.status();
    if !status.is_success() {
        return Err(format!("Request failed with status: {}", status));
    }

    let result: Value = response
        .json()
        .await
        .map_err(|e| format!("Failed to parse response: {}", e))?;

    Ok(result)
}

/// Send a message to a destination via the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
/// * `destination` - The destination address
/// * `topic` - The message topic
/// * `message` - The message content
/// * `reply_deadline` - The deadline in seconds; pass `-1` to indicate we do not want to wait on a reply
///
/// # Returns
///
/// * `Result<Value, String>` - The result of the operation as a JSON value, or an error message
pub async fn send_message(
    api_url: &str,
    destination: &str,
    topic: &str,
    message: &str,
    reply_deadline: Option<Duration>, // This is passed in URL query
) -> Result<Value, String> {
    let client = Client::new();
    let url = format!("{}/api/v1/messages", api_url);

    let mut request = client.post(&url);
    if let Some(deadline) = reply_deadline {
        request = request.query(&[("reply_timeout", deadline.as_secs())]);
    }

    let response = request
        .json(&serde_json::json!({
                "dst": { "ip": destination },
                "topic": general_purpose::STANDARD.encode(topic),
                "payload": general_purpose::STANDARD.encode(message)
        }))
        .send()
        .await
        .map_err(|e| format!("Failed to send request: {}", e))?;

    let status = response.status();
    if !status.is_success() {
        return Err(format!("Request failed with status: {}", status));
    }

    let result: Value = response
        .json()
        .await
        .map_err(|e| format!("Failed to parse response: {}", e))?;

    Ok(result)
}

/// Receive messages from a topic via the Mycelium node
///
/// # Arguments
///
/// * `api_url` - The URL of the Mycelium API
/// * `topic` - The message topic
/// * `wait_deadline` - Time we wait for receiving a message
///
/// # Returns
///
/// * `Result<Value, String>` - The received messages as a JSON value, or an error message
pub async fn receive_messages(
    api_url: &str,
    topic: &str,
    wait_deadline: Option<Duration>,
) -> Result<Value, String> {
    let client = Client::new();
    let url = format!("{}/api/v1/messages", api_url);

    let mut request = client.get(&url);

    if let Some(deadline) = wait_deadline {
        request = request.query(&[
            ("topic", general_purpose::STANDARD.encode(topic)),
            ("timeout", deadline.as_secs().to_string()),
        ])
    } else {
        request = request.query(&[("topic", general_purpose::STANDARD.encode(topic))])
    };

    let response = request
        .send()
        .await
        .map_err(|e| format!("Failed to send request: {}", e))?;

    let status = response.status();
    if !status.is_success() {
        return Err(format!("Request failed with status: {}", status));
    }

    let result: Value = response
        .json()
        .await
        .map_err(|e| format!("Failed to parse response: {}", e))?;

    Ok(result)
}