diff --git a/Cargo.toml b/Cargo.toml index 8d362cb..31bba39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,8 @@ tokio-postgres = "0.7.8" # Async PostgreSQL client tokio-test = "0.4.4" uuid = { version = "1.16.0", features = ["v4"] } zinit-client = { git = "https://github.com/threefoldtech/zinit", branch = "json_rpc", package = "zinit-client" } +reqwest = { version = "0.12.15", features = ["json"] } + # Optional features for specific OS functionality [target.'cfg(unix)'.dependencies] nix = "0.30.1" # Unix-specific functionality diff --git a/examples/mycelium/mycelium_basic.rhai b/examples/mycelium/mycelium_basic.rhai new file mode 100644 index 0000000..a6b79fe --- /dev/null +++ b/examples/mycelium/mycelium_basic.rhai @@ -0,0 +1,129 @@ +// Basic example of using the Mycelium client in Rhai + +// API URL for Mycelium +let api_url = "http://localhost:8989"; + +// Get node information +print("Getting node information:"); +try { + let node_info = mycelium_get_node_info(api_url); + print(`Node subnet: ${node_info.subnet}`); + print(`Node public key: ${node_info.publicKey}`); +} catch(err) { + print(`Error getting node info: ${err}`); +} + +// List all peers +print("\nListing all peers:"); +try { + let peers = mycelium_list_peers(api_url); + + if peers.is_empty() { + print("No peers connected."); + } else { + for peer in peers { + print(`Peer ID: ${peer.id}`); + print(` Address: ${peer.address}`); + print(` Connected: ${peer.connected}`); + print(` Bytes sent: ${peer.txBytes}`); + print(` Bytes received: ${peer.rxBytes}`); + } + } +} catch(err) { + print(`Error listing peers: ${err}`); +} + +// Add a new peer +print("\nAdding a new peer:"); +let new_peer_address = "tcp://185.69.166.8:9651"; +try { + let result = mycelium_add_peer(api_url, new_peer_address); + print(`Peer added: ${result.success}`); +} catch(err) { + print(`Error adding peer: ${err}`); +} + +// List selected routes +print("\nListing selected routes:"); +try { + let routes = mycelium_list_selected_routes(api_url); + + if routes.is_empty() { + print("No selected routes."); + } else { + for route in routes { + print(`Subnet: ${route.subnet}`); + print(` Next hop: ${route.nextHop}`); + print(` Metric: ${route.metric}`); + } + } +} catch(err) { + print(`Error listing routes: ${err}`); +} + +// List fallback routes +print("\nListing fallback routes:"); +try { + let routes = mycelium_list_fallback_routes(api_url); + + if routes.is_empty() { + print("No fallback routes."); + } else { + for route in routes { + print(`Subnet: ${route.subnet}`); + print(` Next hop: ${route.nextHop}`); + print(` Metric: ${route.metric}`); + } + } +} catch(err) { + print(`Error listing fallback routes: ${err}`); +} + +// Send a message +print("\nSending a message:"); +let destination = "400:1234:5678:9abc:def0:1234:5678:9abc"; +let topic = "test"; +let message = "Hello from Rhai!"; +let deadline_secs = 60; + +try { + let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs); + print(`Message sent: ${result.success}`); + if result.id { + print(`Message ID: ${result.id}`); + } +} catch(err) { + print(`Error sending message: ${err}`); +} + +// Receive messages +print("\nReceiving messages:"); +let receive_topic = "test"; +let count = 5; + +try { + let messages = mycelium_receive_messages(api_url, receive_topic, count); + + if messages.is_empty() { + print("No messages received."); + } else { + for msg in messages { + print(`Message from: ${msg.source}`); + print(` Topic: ${msg.topic}`); + print(` Content: ${msg.content}`); + print(` Timestamp: ${msg.timestamp}`); + } + } +} catch(err) { + print(`Error receiving messages: ${err}`); +} + +// Remove a peer +print("\nRemoving a peer:"); +let peer_id = "some-peer-id"; // Replace with an actual peer ID +try { + let result = mycelium_remove_peer(api_url, peer_id); + print(`Peer removed: ${result.success}`); +} catch(err) { + print(`Error removing peer: ${err}`); +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index bc8cbdf..91162b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,7 @@ pub mod text; pub mod virt; pub mod vault; pub mod zinit_client; +pub mod mycelium; // Version information /// Returns the version of the SAL library diff --git a/src/mycelium/mod.rs b/src/mycelium/mod.rs new file mode 100644 index 0000000..adbbc98 --- /dev/null +++ b/src/mycelium/mod.rs @@ -0,0 +1,280 @@ +use std::time::Duration; +use serde_json::Value; +use reqwest::Client; + +/// Get information about the Mycelium node +/// +/// # Arguments +/// +/// * `api_url` - The URL of the Mycelium API +/// +/// # Returns +/// +/// * `Result` - The node information as a JSON value, or an error message +pub async fn get_node_info(api_url: &str) -> Result { + 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` - The list of peers as a JSON value, or an error message +pub async fn list_peers(api_url: &str) -> Result { + let client = Client::new(); + let url = format!("{}/api/v1/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` - 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 { + let client = Client::new(); + let url = format!("{}/api/v1/peers", api_url); + + let response = client + .post(&url) + .json(&serde_json::json!({ + "address": peer_address + })) + .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) +} + +/// 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` - 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 { + let client = Client::new(); + let url = format!("{}/api/v1/peers/{}", api_url, peer_id); + + let response = client + .delete(&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 selected routes in the Mycelium node +/// +/// # Arguments +/// +/// * `api_url` - The URL of the Mycelium API +/// +/// # Returns +/// +/// * `Result` - The list of selected routes as a JSON value, or an error message +pub async fn list_selected_routes(api_url: &str) -> Result { + let client = Client::new(); + let url = format!("{}/api/v1/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` - The list of fallback routes as a JSON value, or an error message +pub async fn list_fallback_routes(api_url: &str) -> Result { + let client = Client::new(); + let url = format!("{}/api/v1/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 +/// * `deadline_secs` - The deadline in seconds +/// +/// # Returns +/// +/// * `Result` - 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, deadline_secs: u64) -> Result { + let client = Client::new(); + let url = format!("{}/api/v1/messages", api_url); + + // Convert deadline to seconds + let deadline = Duration::from_secs(deadline_secs).as_secs(); + + let response = client + .post(&url) + .json(&serde_json::json!({ + "destination": destination, + "topic": topic, + "content": message, + "deadline": deadline + })) + .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 +/// * `count` - The maximum number of messages to receive +/// +/// # Returns +/// +/// * `Result` - The received messages as a JSON value, or an error message +pub async fn receive_messages(api_url: &str, topic: &str, count: u32) -> Result { + let client = Client::new(); + let url = format!("{}/api/v1/messages/{}", api_url, topic); + + let response = client + .get(&url) + .query(&[("count", count)]) + .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) +} \ No newline at end of file diff --git a/src/rhai/mod.rs b/src/rhai/mod.rs index f3e380f..801864c 100644 --- a/src/rhai/mod.rs +++ b/src/rhai/mod.rs @@ -15,6 +15,7 @@ mod rfs; mod vault; mod text; mod zinit; +mod mycelium; #[cfg(test)] mod tests; @@ -95,6 +96,9 @@ pub use git::register_git_module; // Re-export zinit module pub use zinit::register_zinit_module; +// Re-export mycelium module +pub use mycelium::register_mycelium_module; + // Re-export text module pub use text::register_text_module; // Re-export text functions directly from text module @@ -155,6 +159,9 @@ pub fn register(engine: &mut Engine) -> Result<(), Box> { // Register Zinit module functions zinit::register_zinit_module(engine)?; + // Register Mycelium module functions + mycelium::register_mycelium_module(engine)?; + // Register Text module functions text::register_text_module(engine)?; diff --git a/src/rhai/mycelium.rs b/src/rhai/mycelium.rs new file mode 100644 index 0000000..4a9b342 --- /dev/null +++ b/src/rhai/mycelium.rs @@ -0,0 +1,235 @@ +//! Rhai wrappers for Mycelium client module functions +//! +//! This module provides Rhai wrappers for the functions in the Mycelium client module. + +use rhai::{Engine, EvalAltResult, Array, Dynamic, Map}; +use crate::mycelium as client; +use tokio::runtime::Runtime; +use serde_json::Value; +use crate::rhai::error::ToRhaiError; + +/// Register Mycelium module functions with the Rhai engine +/// +/// # Arguments +/// +/// * `engine` - The Rhai engine to register the functions with +/// +/// # Returns +/// +/// * `Result<(), Box>` - Ok if registration was successful, Err otherwise +pub fn register_mycelium_module(engine: &mut Engine) -> Result<(), Box> { + // Register Mycelium client functions + engine.register_fn("mycelium_get_node_info", mycelium_get_node_info); + engine.register_fn("mycelium_list_peers", mycelium_list_peers); + engine.register_fn("mycelium_add_peer", mycelium_add_peer); + engine.register_fn("mycelium_remove_peer", mycelium_remove_peer); + engine.register_fn("mycelium_list_selected_routes", mycelium_list_selected_routes); + engine.register_fn("mycelium_list_fallback_routes", mycelium_list_fallback_routes); + engine.register_fn("mycelium_send_message", mycelium_send_message); + engine.register_fn("mycelium_receive_messages", mycelium_receive_messages); + + Ok(()) +} + +// Helper function to get a runtime +fn get_runtime() -> Result> { + tokio::runtime::Runtime::new().map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to create Tokio runtime: {}", e).into(), + rhai::Position::NONE + )) + }) +} + +// Helper function to convert serde_json::Value to rhai::Dynamic +fn value_to_dynamic(value: Value) -> Dynamic { + match value { + Value::Null => Dynamic::UNIT, + Value::Bool(b) => Dynamic::from(b), + Value::Number(n) => { + if let Some(i) = n.as_i64() { + Dynamic::from(i) + } else if let Some(f) = n.as_f64() { + Dynamic::from(f) + } else { + Dynamic::from(n.to_string()) + } + }, + Value::String(s) => Dynamic::from(s), + Value::Array(arr) => { + let mut rhai_arr = Array::new(); + for item in arr { + rhai_arr.push(value_to_dynamic(item)); + } + Dynamic::from(rhai_arr) + }, + Value::Object(map) => { + let mut rhai_map = Map::new(); + for (k, v) in map { + rhai_map.insert(k.into(), value_to_dynamic(v)); + } + Dynamic::from_map(rhai_map) + } + } +} + +// Helper trait to convert String errors to Rhai errors +impl ToRhaiError for Result { + fn to_rhai_error(self) -> Result> { + self.map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Mycelium error: {}", e).into(), + rhai::Position::NONE + )) + }) + } +} + +// +// Mycelium Client Function Wrappers +// + +/// Wrapper for mycelium::get_node_info +/// +/// Gets information about the Mycelium node. +pub fn mycelium_get_node_info(api_url: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::get_node_info(api_url).await + }); + + let node_info = result.to_rhai_error()?; + + Ok(value_to_dynamic(node_info)) +} + +/// Wrapper for mycelium::list_peers +/// +/// Lists all peers connected to the Mycelium node. +pub fn mycelium_list_peers(api_url: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::list_peers(api_url).await + }); + + let peers = result.to_rhai_error()?; + + Ok(value_to_dynamic(peers)) +} + +/// Wrapper for mycelium::add_peer +/// +/// Adds a new peer to the Mycelium node. +pub fn mycelium_add_peer(api_url: &str, peer_address: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::add_peer(api_url, peer_address).await + }); + + let response = result.to_rhai_error()?; + + Ok(value_to_dynamic(response)) +} + +/// Wrapper for mycelium::remove_peer +/// +/// Removes a peer from the Mycelium node. +pub fn mycelium_remove_peer(api_url: &str, peer_id: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::remove_peer(api_url, peer_id).await + }); + + let response = result.to_rhai_error()?; + + Ok(value_to_dynamic(response)) +} + +/// Wrapper for mycelium::list_selected_routes +/// +/// Lists all selected routes in the Mycelium node. +pub fn mycelium_list_selected_routes(api_url: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::list_selected_routes(api_url).await + }); + + let routes = result.to_rhai_error()?; + + Ok(value_to_dynamic(routes)) +} + +/// Wrapper for mycelium::list_fallback_routes +/// +/// Lists all fallback routes in the Mycelium node. +pub fn mycelium_list_fallback_routes(api_url: &str) -> Result> { + let rt = get_runtime()?; + + let result = rt.block_on(async { + client::list_fallback_routes(api_url).await + }); + + let routes = result.to_rhai_error()?; + + Ok(value_to_dynamic(routes)) +} + +/// Wrapper for mycelium::send_message +/// +/// Sends a message to a destination via the Mycelium node. +pub fn mycelium_send_message(api_url: &str, destination: &str, topic: &str, message: &str, deadline_secs: i64) -> Result> { + let rt = get_runtime()?; + + // Convert deadline to u64 + let deadline = if deadline_secs < 0 { + return Err(Box::new(EvalAltResult::ErrorRuntime( + "Deadline cannot be negative".into(), + rhai::Position::NONE + ))); + } else { + deadline_secs as u64 + }; + + let result = rt.block_on(async { + client::send_message(api_url, destination, topic, message, deadline).await + }); + + let response = result.to_rhai_error()?; + + Ok(value_to_dynamic(response)) +} + +/// Wrapper for mycelium::receive_messages +/// +/// Receives messages from a topic via the Mycelium node. +pub fn mycelium_receive_messages(api_url: &str, topic: &str, count: i64) -> Result> { + let rt = get_runtime()?; + + // Convert count to u32 + let count = if count < 0 { + return Err(Box::new(EvalAltResult::ErrorRuntime( + "Count cannot be negative".into(), + rhai::Position::NONE + ))); + } else if count > u32::MAX as i64 { + return Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Count too large, maximum is {}", u32::MAX).into(), + rhai::Position::NONE + ))); + } else { + count as u32 + }; + + let result = rt.block_on(async { + client::receive_messages(api_url, topic, count).await + }); + + let messages = result.to_rhai_error()?; + + Ok(value_to_dynamic(messages)) +} \ No newline at end of file