#http-api #ship #http #urbit #library #airlock

urbit-http-api

Wraps the Urbit ship http api exposing it as an easy-to-use Rust crate

24 releases (5 breaking)

0.7.3 Oct 24, 2021
0.7.2 Jun 21, 2021
0.7.1 Apr 9, 2021
0.6.0 Mar 13, 2021

#1311 in Web programming

22 downloads per month
Used in urbit-chatbot-framework

MIT license

120KB
2.5K SLoC

Rust Urbit HTTP API

This library wraps the Urbit ship http interface exposing it as an easy-to-use Rust crate.

awesome urbit badge

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.

/// 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<ShipInterface>;

/// Create a `Channel` using this `ShipInterface`
pub fn create_channel(&mut self) -> Result<Channel>;

You also have the ability to scry and run threads via spider.

/// Send a scry using the `ShipInterface`
pub fn scry(&self, app: &str, path: &str) -> Result<Response>;

/// Run a thread via spider using the `ShipInterface`
pub fn spider(&self, input_mark: &str, output_mark: &str, thread_name: &str, body: &JsonValue) -> Result<Response>;

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:

// 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<Subscription>,
    // / 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 Subscriptions 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:

/// Sends a poke over the channel
pub fn poke(&mut self, app: &str, mark: &str, json: &JsonValue) -> Result<Response>;

/// 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<CreationID>;

/// 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<bool>;

/// 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:

/// 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<String>;

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.

// 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.

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.

// 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.

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).

Dependencies

~10–25MB
~399K SLoC