# Rust Urbit HTTP API This library wraps the Urbit ship http interface exposing it as an easy-to-use Rust crate. [![awesome urbit badge](https://img.shields.io/badge/~-awesome%20urbit-lightgrey)](https://github.com/urbit/awesome-urbit) All implementation details such as auth cookies, EventSource connections, tracking message ids, and other such matters are automatically handled for you, and as enables a greatly improved experience in writing Rust apps that interact with Urbit ships. This crate currently enables devs to: 1. Authorize oneself and open a channel with the ship. 2. Subscribe to any app/path so that one can read the events currently taking place inside of the ship. 3. Issue pokes/scries/threads. 4. Graph-store support with native Rust `Graph` interface for working with graphs. 5. Simple rust-based interface for Urbit chats. 6. Simple rust-based interface for Urbit notebooks. ## Basic Design There are 3 main structs that this library exposes for interacting with an Urbit ship: 1. `ShipInterface` 2. `Channel` 3. `Subscription` A `Subscription` is created by a `Channel` which is created by a `ShipInterface`. In other words, first you need to connect to an Urbit ship (using `ShipInterface`) before you can initiate a messaging `Channel`, before you can create a `Subscription` to an app/path. ### ShipInterface The `ShipInterface` exposes a few useful methods that will be useful when creating apps. The more commonly used methods below these allow you to create a new `ShipInterface` (thereby authorizing yourself with the ship), and create a new `Channel`. ```rust /// Logs into the given ship and creates a new `ShipInterface`. /// `ship_url` should be `http://ip:port` of the given ship. Example: /// `http://0.0.0.0:8080`. `ship_code` is the code acquire from your ship /// by typing `+code` in dojo. pub fn new(ship_url: &str, ship_code: &str) -> Result; /// Create a `Channel` using this `ShipInterface` pub fn create_channel(&mut self) -> Result; ``` You also have the ability to scry and run threads via spider. ```rust /// Send a scry using the `ShipInterface` pub fn scry(&self, app: &str, path: &str) -> Result; /// Run a thread via spider using the `ShipInterface` pub fn spider(&self, input_mark: &str, output_mark: &str, thread_name: &str, body: &JsonValue) -> Result; ``` ### Channel `Channel` is the most useful struct, because it holds methods related to interacting with both pokes and subscriptions. It is instructive to look at the definition of the `Channel` struct to understand how it works: ```rust // A Channel which is used to interact with a ship pub struct Channel<'a> { /// `ShipInterface` this channel is created from pub ship_interface: &'a ShipInterface, /// The uid of the channel pub uid: String, /// The url of the channel pub url: String, // The list of `Subscription`s for this channel pub subscription_list: Vec, // / The `EventSource` for this channel which reads all of // / the SSE events. event_receiver: ReceiverSource, /// The current number of messages that have been sent out (which are /// also defined as message ids) via this `Channel` pub message_id_count: u64, } ``` Once a `Channel` is created, an `EventSource` connection is created with the ship on a separate thread. This thread accepts all of the incoming events, and queues them on a (Rust) unbounded channel which is accessible internally via the `event_receiver`. This field itself isn't public, but processing events in this crate is handled with a much higher-level interface for the app developer. Take note that a `Channel` has a `subscription_list`. As you will see below, each `Channel` exposes methods for creating subscriptions, which automatically get added to the `subscription_list`. Once `Subscription`s are created/added to the list, the `Channel` will evidently start to receive event messages via SSE (which will be queued for reading in the `event_receiver`). From the app developer's perspective, all one has to do is call the `parse_event_messages()` method on your `Channel`, and all of the queued events will be processed and passed on to the correct `Subscription`'s `message_list`. This is useful once multiple `Subscriptions` are created on a single channel, as the messages will be pre-sorted automatically for you. Once the event messages are parsed, then one can simply call the `find_subscription` method in order to interact with the `Subscription` and read its messages. The following are the useful methods exposed by a `Channel`: ```rust /// Sends a poke over the channel pub fn poke(&mut self, app: &str, mark: &str, json: &JsonValue) -> Result; /// Create a new `Subscription` and thus subscribes to events on the ship with the provided app/path. pub fn create_new_subscription(&mut self, app: &str, path: &str) -> Result; /// Parses SSE messages for this channel and moves them into /// the proper corresponding `Subscription`'s `message_list`. pub fn parse_event_messages(&mut self); /// Finds the first `Subscription` in the list which has a matching /// `app` and `path`; pub fn find_subscription(&self, app: &str, path: &str) -> Option<&Subscription>; /// Finds the first `Subscription` in the list which has a matching /// `app` and `path`, removes it from the list, and tells the ship /// that you are unsubscribing. pub fn unsubscribe(&mut self, app: &str, path: &str) -> Option; /// Deletes the channel pub fn delete_channel(self); /// Exposes an interface for interacting with a ship's Graph Store directly. pub fn graph_store(&mut self) -> GraphStore; /// Exposes an interface for interacting with Urbit chats. pub fn chat(&mut self) -> Chat; /// Exposes an interface for interacting with Urbit notebooks. pub fn notebook(&mute self) -> Notebook; ``` ### Subscription As mentioned in the previous section, a `Subscription` contains it's own `message_list` field where messages are stored after a `Channel` processes them. From an app developer's perspective, this is the only useful feature of the `Subscription` struct. Once acquired, it is used simply to read the messages. To improve the message reading experience, the `Subscription` struct exposes a useful method: ```rust /// Pops a message from the front of `Subscription`'s `message_list`. /// If no messages are left, returns `None`. pub fn pop_message(&mut self) -> Option; ``` ## Code Examples ### Poke Example This example displays how to connect to a ship using a `ShipInterface`, opening a `Channel`, issuing a `poke` over said channel, and then deleting the `Channel` to finish. ```rust // Import the `ShipInterface` struct use urbit_http_api::ShipInterface; fn main() { // Create a new `ShipInterface` for a local ~zod ship let mut ship_interface = ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap(); // Create a `Channel` let mut channel = ship_interface.create_channel().unwrap(); // Issue a poke over the channel let poke_res = channel.poke("hood", "helm-hi", &"This is a poke".into()); // Cleanup/delete the `Channel` once finished channel.delete_channel(); } ``` ### Graph Store Subscription Example This example shows how to create, interact with, and delete a `Subscription`. In this scenario we desire to read all new updates from Graph Store via our `Subscription` for 10 seconds, and then perform cleanup. ```rust use std::thread; use std::time::Duration; use urbit_http_api::ShipInterface; fn main() { // Create a new `ShipInterface` for a local ~zod ship let mut ship_interface = ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap(); // Create a `Channel` let mut channel = ship_interface.create_channel().unwrap(); // Create a `Subscription` for the `graph-store` app with the `/updates` path. This `Subscription` // is automatically added to the `Channel`'s `subscription_list`. channel .create_new_subscription("graph-store", "/updates") .unwrap(); // Create a loop that iterates 10 times for _ in 0..10 { // Parse all of the event messages to move them into the correct // `Subscription`s in the `Channel`'s `subscription_list`. channel.parse_event_messages(); // Find our graph-store `Subscription` let gs_sub = channel.find_subscription("graph-store", "/updates").unwrap(); // Pop all of the messages from our `gs_sub` and print them loop { let pop_res = gs_sub.pop_message(); if let Some(mess) = &pop_res { println!("Message: {:?}", mess); } // If no messages left, stop if let None = &pop_res { break; } } // Wait for 1 second before trying to parse the event messages again thread::sleep(Duration::new(1, 0)); } // Once finished, unsubscribe/destroy our `Subscription` channel.unsubscribe("graph-store", "/updates"); // Delete the channel channel.delete_channel(); } ``` ### Urbit Chat Messaging Example This example displays how to connect to a ship and send a message to an Urbit chat using the `Chat` struct interface. ```rust // Import the `ShipInterface` struct use urbit_http_api::{ShipInterface, chat::Message}; fn main() { // Create a new `ShipInterface` for a local ~zod ship let mut ship_interface = ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap(); // Create a `Channel` let mut channel = ship_interface.create_channel().unwrap(); // Create a `Message` which is formatted properly for an Urbit chat let message = Message::new() // Add text to your message .add_text("Checkout this cool article by ~wicdev-wisryt:") // Add a URL link to your message after the previous text (which gets automatically added on a new line) .add_url("https://urbit.org/blog/io-in-hoon/") // Add an image URL to your message after the previous url (which gets automatically added on a new line as a rendered image) .add_url("https://media.urbit.org/site/posts/essays/zion-canyon-1.jpg"); // Send the message to a chat hosted by ~zod named "test-93". // Note the connected ship must already have joined the chat in order to send a message to the chat. let _mess_res = channel .chat() .send_message("~zod", "test-93", &message); // Cleanup/delete the `Channel` once finished channel.delete_channel(); } ``` ### Urbit Chat Subscription Example This example shows how to utilize the higher-level `Chat` interface to subscribe to a chat and read all of the messages being posted in said chat. ```rust use std::thread; use std::time::Duration; use urbit_http_api::ShipInterface; fn main() { // Create a new `ShipInterface` for a local ~zod ship let mut ship_interface = ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap(); // Create a `Channel` let mut channel = ship_interface.create_channel().unwrap(); // Subscribe to a specific chat, and obtain a `Receiver` back which contains a stream of messages from the chat let chat_receiver = channel .chat() .subscribe_to_chat("~mocrux-nomdep", "test-93") .unwrap(); // Create a loop that iterates 10 times for _ in 0..10 { // If a message has been posted to the chat, unwrap it and acquire the `AuthoredMessage` if let Ok(authored_message) = chat_receiver.try_recv() { // Pretty print the author ship @p and the message contents println!( "~{}:{}", authored_message.author, authored_message.message.to_formatted_string() ); } // Wait for 1 second before checking again thread::sleep(Duration::new(1, 0)); } // Delete the channel channel.delete_channel(); } ``` --- This library was created by ~mocrux-nomdep([Robert Kornacki](https://github.com/robkorn)).