diff --git a/Cargo.toml b/Cargo.toml index 8d362cb..99268e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,9 @@ 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"] } +urlencoding = "2.1.3" + # Optional features for specific OS functionality [target.'cfg(unix)'.dependencies] nix = "0.30.1" # Unix-specific functionality diff --git a/docs/docs/rhai/mycelium_tutorial.md b/docs/docs/rhai/mycelium_tutorial.md new file mode 100644 index 0000000..12a17e4 --- /dev/null +++ b/docs/docs/rhai/mycelium_tutorial.md @@ -0,0 +1,386 @@ +# Mycelium Tutorial for Rhai + +This tutorial explains how to use the Mycelium networking functionality in Rhai scripts. Mycelium is a peer-to-peer networking system that allows nodes to communicate with each other, and the Rhai bindings provide an easy way to interact with Mycelium from your scripts. + +## Introduction + +The Mycelium module for Rhai provides the following capabilities: + +- Getting node information +- Managing peers (listing, adding, removing) +- Viewing routing information +- Sending and receiving messages between nodes + +This tutorial will walk you through using these features with example scripts. + +## Prerequisites + +Before using the Mycelium functionality in Rhai, you need: + +1. A running Mycelium node accessible via HTTP + > See https://github.com/threefoldtech/mycelium +2. The Rhai runtime with Mycelium module enabled + +## Basic Mycelium Operations + +Let's start by exploring the basic operations available in Mycelium using the `mycelium_basic.rhai` example. + +### Getting Node Information + +To get information about your Mycelium node: + +```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.nodeSubnet}`); + print(`Node public key: ${node_info.nodePubkey}`); +} catch(err) { + print(`Error getting node info: ${err}`); +} +``` + +This code: +1. Sets the API URL for your Mycelium node +2. Calls `mycelium_get_node_info()` to retrieve information about the node +3. Prints the node's subnet and public key + +### Managing Peers + +#### Listing Peers + +To list all peers connected to your Mycelium node: + +```rhai +// 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 Endpoint: ${peer.endpoint.proto}://${peer.endpoint.socketAddr}`); + print(` Type: ${peer.type}`); + print(` Connection State: ${peer.connectionState}`); + print(` Bytes sent: ${peer.txBytes}`); + print(` Bytes received: ${peer.rxBytes}`); + } + } +} catch(err) { + print(`Error listing peers: ${err}`); +} +``` + +This code: +1. Calls `mycelium_list_peers()` to get all connected peers +2. Iterates through the peers and prints their details + +#### Adding a Peer + +To add a new peer to your Mycelium node: + +```rhai +// Add a new peer +print("\nAdding a new peer:"); +let new_peer_address = "tcp://65.21.231.58:9651"; +try { + let result = mycelium_add_peer(api_url, new_peer_address); + print(`Peer added: ${result.success}`); +} catch(err) { + print(`Error adding peer: ${err}`); +} +``` + +This code: +1. Specifies a peer address to add +2. Calls `mycelium_add_peer()` to add the peer to your node +3. Prints whether the operation was successful + +#### Removing a Peer + +To remove a peer from your Mycelium node: + +```rhai +// Remove a peer +print("\nRemoving a peer:"); +let peer_id = "tcp://65.21.231.58:9651"; // This is the peer we added earlier +try { + let result = mycelium_remove_peer(api_url, peer_id); + print(`Peer removed: ${result.success}`); +} catch(err) { + print(`Error removing peer: ${err}`); +} +``` + +This code: +1. Specifies the peer ID to remove +2. Calls `mycelium_remove_peer()` to remove the peer +3. Prints whether the operation was successful + +### Viewing Routing Information + +#### Listing Selected Routes + +To list the selected routes in your Mycelium node: + +```rhai +// 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}`); +} +``` + +This code: +1. Calls `mycelium_list_selected_routes()` to get all selected routes +2. Iterates through the routes and prints their details + +#### Listing Fallback Routes + +To list the fallback routes in your Mycelium node: + +```rhai +// 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}`); +} +``` + +This code: +1. Calls `mycelium_list_fallback_routes()` to get all fallback routes +2. Iterates through the routes and prints their details + +## Sending Messages + +Now let's look at how to send messages using the `mycelium_send_message.rhai` example. + +```rhai +// API URL for Mycelium +let api_url = "http://localhost:1111"; + +// Send a message +print("\nSending a message:"); +let destination = "5af:ae6b:dcd8:ffdb:b71:7dde:d3:1033"; // Replace with the actual destination IP address +let topic = "test_topic"; +let message = "Hello from Rhai sender!"; +let deadline_secs = -10; // Seconds we wait for a reply + +try { + print(`Attempting to send message to ${destination} on topic '${topic}'`); + let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs); + print(`result: ${result}`); + print(`Message sent: ${result.success}`); + if result.id != "" { + print(`Message ID: ${result.id}`); + } +} catch(err) { + print(`Error sending message: ${err}`); +} +``` + +This code: +1. Sets the API URL for your Mycelium node +2. Specifies the destination IP address, topic, message content, and deadline +3. Calls `mycelium_send_message()` to send the message +4. Prints the result, including the message ID if successful + +### Important Parameters for Sending Messages + +- `api_url`: The URL of your Mycelium node's API +- `destination`: The IP address of the destination node +- `topic`: The topic to send the message on (must match what the receiver is listening for) +- `message`: The content of the message +- `deadline_secs`: Time in seconds to wait for a reply. Use a negative value if you don't want to wait for a reply. + +## Receiving Messages + +Now let's look at how to receive messages using the `mycelium_receive_message.rhai` example. + +```rhai +// API URL for Mycelium +let api_url = "http://localhost:2222"; + +// Receive messages +print("\nReceiving messages:"); +let receive_topic = "test_topic"; +let wait_deadline_secs = 100; + +print(`Listening for messages on topic '${receive_topic}'...`); +try { + let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs); + + if messages.is_empty() { + // print("No new messages received in this poll."); + } else { + print("Received a message:"); + print(` Message id: ${messages.id}`); + print(` Message from: ${messages.srcIp}`); + print(` Topic: ${messages.topic}`); + print(` Payload: ${messages.payload}`); + } +} catch(err) { + print(`Error receiving messages: ${err}`); +} + +print("Finished attempting to receive messages."); +``` + +This code: +1. Sets the API URL for your Mycelium node +2. Specifies the topic to listen on and how long to wait for messages +3. Calls `mycelium_receive_messages()` to receive messages +4. Processes and prints any received messages + +### Important Parameters for Receiving Messages + +- `api_url`: The URL of your Mycelium node's API +- `receive_topic`: The topic to listen for messages on (must match what the sender is using) +- `wait_deadline_secs`: Time in seconds to wait for messages to arrive. The function will block for this duration if no messages are immediately available. + +## Complete Messaging Example + +To set up a complete messaging system, you would typically run two instances of Mycelium (node A sender, node B receiver). + +1. Run the `mycelium_receive_message.rhai` script to listen for messages. **Fill in the API address of node B**. +2. Run the `mycelium_send_message.rhai` script to send messages. **Fill in the API address of node A, and fill in the overlay address of node B as destination**. + +### Setting Up the Receiver + +First, start a Mycelium node and run the receiver script: + +```rhai +// API URL for Mycelium +let api_url = "http://localhost:2222"; // Your receiver node's API URL + +// Receive messages +let receive_topic = "test_topic"; +let wait_deadline_secs = 100; // Wait up to 100 seconds for messages + +print(`Listening for messages on topic '${receive_topic}'...`); +try { + let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs); + + if messages.is_empty() { + print("No new messages received in this poll."); + } else { + print("Received a message:"); + print(` Message id: ${messages.id}`); + print(` Message from: ${messages.srcIp}`); + print(` Topic: ${messages.topic}`); + print(` Payload: ${messages.payload}`); + } +} catch(err) { + print(`Error receiving messages: ${err}`); +} +``` + +### Setting Up the Sender + +Then, on another Mycelium node, run the sender script: + +```rhai +// API URL for Mycelium +let api_url = "http://localhost:1111"; // Your sender node's API URL + +// Send a message +let destination = "5af:ae6b:dcd8:ffdb:b71:7dde:d3:1033"; // The receiver node's IP address +let topic = "test_topic"; // Must match the receiver's topic +let message = "Hello from Rhai sender!"; +let deadline_secs = -10; // Don't wait for a reply + +try { + print(`Attempting to send message to ${destination} on topic '${topic}'`); + 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}`); +} +``` + +### Example: setting up 2 different Mycelium peers on same the host and sending/receiving a message + +#### Obtain Mycelium + +- Download the latest Mycelium binary from https://github.com/threefoldtech/mycelium/releases/ +- Or compile from source + +#### Setup +- Create two different private key files. Each key file should contain exactely 32 bytes. In this example we'll save these files as `sender.bin` and `receiver.bin`. Note: generate your own 32-byte key files, the values below are just used as examples. +> `echo '9f3d72c1a84be6f027bba94cde015ee839cedb2ac4f2822bfc94449e3e2a1c6a' > sender.bin` + +> `echo 'e81c5a76f42bd9a3c73fe0bb2196acdfb6348e99d0b01763a2e57ce3a4e8f5dd' > receiver.bin` + +#### Start the nodes +- **Sender**: this node will have the API server hosted on `127.0.0.1:1111` and the JSON-RPC server on `127.0.0.1:8991`. +> `sudo ./mycelium --key-file sender.bin --disable-peer-discovery --disable-quic --no-tun --api-addr 127.0.0.1:1111 --jsonrpc-addr 127.0.0.1:8991` + +- **Receiver**: this node will have the API server hosted on `127.0.0.1:2222` and the JSON-RPC server on `127.0.0.1:8992`. +> `sudo ./mycelium --key-file receiver.bin --disable-peer-discovery --disable-quic --no-tun --api-addr 127.0.0.1:2222 --jsonrpc-addr 127.0.0.1:8992 --peers tcp://:9651` +- Obtain the Mycelium overlay IP by running `./mycelium --key-file receiver.bin --api-addr 127.0.0.1:2222 inspect`. **Replace this IP as destination in the [mycelium_send_message.rhai](../../../examples/mycelium/mycelium_send_message.rhai) example**. + +#### Execute the examples +- First build by executing `./build_herdo.sh` from the SAL root directory +- `cd target/debug` + +- Run the sender script: `sudo ./herodo --path ../../examples/mycelium/mycelium_send_message.rhai` +``` +Executing: ../../examples/mycelium/mycelium_send_message.rhai + +Sending a message: +Attempting to send message to 50e:6d75:4568:366e:f75:2ac3:bbb1:3fdd on topic 'test_topic' +result: #{"id": "bfd47dc689a7b826"} +Message sent: +Message ID: bfd47dc689a7b826 +Script executed successfull +``` + +- Run the receiver script: `sudo ./herodo --path ../../examples/mycelium/mycelium_receive_message.rhai` +``` +Executing: ../../examples/mycelium/mycelium_receive_message.rhai + +Receiving messages: +Listening for messages on topic 'test_topic'... +Received a message: + Message id: bfd47dc689a7b826 + Message from: 45d:26e1:a413:9d08:80ce:71c6:a931:4315 + Topic: dGVzdF90b3BpYw== + Payload: SGVsbG8gZnJvbSBSaGFpIHNlbmRlciE= +Finished attempting to receive messages. +Script executed successfully +``` +> Decoding the payload `SGVsbG8gZnJvbSBSaGFpIHNlbmRlciE=` results in the expected `Hello from Rhai sender!` message. Mission succesful! + diff --git a/examples/mycelium/mycelium_basic.rhai b/examples/mycelium/mycelium_basic.rhai new file mode 100644 index 0000000..1297fa4 --- /dev/null +++ b/examples/mycelium/mycelium_basic.rhai @@ -0,0 +1,133 @@ +// 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.nodeSubnet}`); + print(`Node public key: ${node_info.nodePubkey}`); +} 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 Endpoint: ${peer.endpoint.proto}://${peer.endpoint.socketAddr}`); + print(` Type: ${peer.type}`); + print(` Connection State: ${peer.connectionState}`); + 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://65.21.231.58: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 +// TO SEND A MESSAGE FILL IN THE DESTINATION IP ADDRESS +// -----------------------------------------------------// +// print("\nSending a message:"); +// let destination = < FILL IN CORRECT DEST IP > +// 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 +// RECEIVING MESSAGES SHOULD BE DONE ON THE DESTINATION NODE FROM THE CALL ABOVE +// -----------------------------------------------------------------------------// +// 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 = "tcp://65.21.231.58:9651"; // This is the peer we added earlier +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/examples/mycelium/mycelium_receive_message.rhai b/examples/mycelium/mycelium_receive_message.rhai new file mode 100644 index 0000000..80aef19 --- /dev/null +++ b/examples/mycelium/mycelium_receive_message.rhai @@ -0,0 +1,31 @@ +// Script to receive Mycelium messages + +// API URL for Mycelium +let api_url = "http://localhost:2222"; + +// Receive messages +// This script will listen for messages on a specific topic. +// Ensure the sender script is using the same topic. +// -----------------------------------------------------------------------------// +print("\nReceiving messages:"); +let receive_topic = "test_topic"; +let wait_deadline_secs = 100; + +print(`Listening for messages on topic '${receive_topic}'...`); +try { + let messages = mycelium_receive_messages(api_url, receive_topic, wait_deadline_secs); + + if messages.is_empty() { + // print("No new messages received in this poll."); + } else { + print("Received a message:"); + print(` Message id: ${messages.id}`); + print(` Message from: ${messages.srcIp}`); + print(` Topic: ${messages.topic}`); + print(` Payload: ${messages.payload}`); + } +} catch(err) { + print(`Error receiving messages: ${err}`); +} + +print("Finished attempting to receive messages."); \ No newline at end of file diff --git a/examples/mycelium/mycelium_send_message.rhai b/examples/mycelium/mycelium_send_message.rhai new file mode 100644 index 0000000..628a21e --- /dev/null +++ b/examples/mycelium/mycelium_send_message.rhai @@ -0,0 +1,25 @@ +// Script to send a Mycelium message + +// API URL for Mycelium +let api_url = "http://localhost:1111"; + +// Send a message +// TO SEND A MESSAGE FILL IN THE DESTINATION IP ADDRESS +// -----------------------------------------------------// +print("\nSending a message:"); +let destination = "50e:6d75:4568:366e:f75:2ac3:bbb1:3fdd"; // IMPORTANT: Replace with the actual destination IP address +let topic = "test_topic"; +let message = "Hello from Rhai sender!"; +let deadline_secs = -10; // Seconds we wait for a reply + +try { + print(`Attempting to send message to ${destination} on topic '${topic}'`); + let result = mycelium_send_message(api_url, destination, topic, message, deadline_secs); + print(`result: ${result}`); + print(`Message sent: ${result.success}`); + if result.id != "" { + print(`Message ID: ${result.id}`); + } +} catch(err) { + print(`Error sending message: ${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..89f9b5d --- /dev/null +++ b/src/mycelium/mod.rs @@ -0,0 +1,313 @@ +use base64::{ + engine::general_purpose, + Engine as _, +}; +use reqwest::Client; +use serde_json::Value; +use std::time::Duration; + +/// 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/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` - 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/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` - 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 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` - 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/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` - 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/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` - 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, // This is passed in URL query +) -> Result { + 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` - The received messages as a JSON value, or an error message +pub async fn receive_messages( + api_url: &str, + topic: &str, + wait_deadline: Option, +) -> Result { + 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) +} 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..03cc68c --- /dev/null +++ b/src/rhai/mycelium.rs @@ -0,0 +1,224 @@ +//! Rhai wrappers for Mycelium client module functions +//! +//! This module provides Rhai wrappers for the functions in the Mycelium client module. + +use std::time::Duration; + +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, reply_deadline_secs: i64) -> Result> { + let rt = get_runtime()?; + + let deadline = if reply_deadline_secs < 0 { + None + } else { + Some(Duration::from_secs(reply_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, wait_deadline_secs: i64) -> Result> { + let rt = get_runtime()?; + + let deadline = if wait_deadline_secs < 0 { + None + } else { + Some(Duration::from_secs(wait_deadline_secs as u64)) + }; + + let result = rt.block_on(async { + client::receive_messages(api_url, topic, deadline).await + }); + + let messages = result.to_rhai_error()?; + + Ok(value_to_dynamic(messages)) +} \ No newline at end of file