├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── apps ├── chat.rs ├── collections.rs ├── dm.rs ├── harkstore.rs ├── invitestore.rs ├── mod.rs └── notebook.rs ├── channel.rs ├── error.rs ├── graph.rs ├── graphstore.rs ├── helper.rs ├── interface.rs ├── lib.rs ├── local_config.rs ├── subscription.rs └── traits ├── messaging.rs └── mod.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "urbit-http-api" 3 | version = "0.7.3" 4 | authors = ["Robert Kornacki <11645932+robkorn@users.noreply.github.com>"] 5 | edition = "2018" 6 | description = "Wraps the Urbit ship http api exposing it as an easy-to-use Rust crate." 7 | repository = "https://github.com/robkorn/rust-urbit-http-api" 8 | license = "MIT" 9 | keywords = ["urbit", "http", "ship", "airlock", "library"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | eventsource-threaded = "0.1.0" 15 | thiserror = "1.0.23" 16 | json = "0.12.4" 17 | rand = "0.8.3" 18 | reqwest = {version = "0.11.0", features= ["blocking", "json"]} 19 | yaml-rust = "0.4.5" 20 | regex = "1.4.3" 21 | chrono = "0.4.19" 22 | crossbeam = "0.8.0" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Robert Kornacki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust Urbit HTTP API 2 | 3 | This library wraps the Urbit ship http interface exposing it as an easy-to-use Rust crate. 4 | 5 | [![awesome urbit badge](https://img.shields.io/badge/~-awesome%20urbit-lightgrey)](https://github.com/urbit/awesome-urbit) 6 | 7 | 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. 8 | 9 | This crate currently enables devs to: 10 | 11 | 1. Authorize oneself and open a channel with the ship. 12 | 2. Subscribe to any app/path so that one can read the events currently taking place inside of the ship. 13 | 3. Issue pokes/scries/threads. 14 | 4. Graph-store support with native Rust `Graph` interface for working with graphs. 15 | 5. Simple rust-based interface for Urbit chats. 16 | 6. Simple rust-based interface for Urbit notebooks. 17 | 18 | ## Basic Design 19 | 20 | There are 3 main structs that this library exposes for interacting with an Urbit ship: 21 | 22 | 1. `ShipInterface` 23 | 2. `Channel` 24 | 3. `Subscription` 25 | 26 | 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. 27 | 28 | ### ShipInterface 29 | 30 | The `ShipInterface` exposes a few useful methods that will be useful when creating apps. 31 | 32 | 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`. 33 | 34 | ```rust 35 | /// Logs into the given ship and creates a new `ShipInterface`. 36 | /// `ship_url` should be `http://ip:port` of the given ship. Example: 37 | /// `http://0.0.0.0:8080`. `ship_code` is the code acquire from your ship 38 | /// by typing `+code` in dojo. 39 | pub fn new(ship_url: &str, ship_code: &str) -> Result; 40 | 41 | /// Create a `Channel` using this `ShipInterface` 42 | pub fn create_channel(&mut self) -> Result; 43 | ``` 44 | 45 | You also have the ability to scry and run threads via spider. 46 | 47 | ```rust 48 | /// Send a scry using the `ShipInterface` 49 | pub fn scry(&self, app: &str, path: &str) -> Result; 50 | 51 | /// Run a thread via spider using the `ShipInterface` 52 | pub fn spider(&self, input_mark: &str, output_mark: &str, thread_name: &str, body: &JsonValue) -> Result; 53 | ``` 54 | 55 | ### Channel 56 | 57 | `Channel` is the most useful struct, because it holds methods related to interacting with both pokes and subscriptions. 58 | 59 | It is instructive to look at the definition of the `Channel` struct to understand how it works: 60 | 61 | ```rust 62 | // A Channel which is used to interact with a ship 63 | pub struct Channel<'a> { 64 | /// `ShipInterface` this channel is created from 65 | pub ship_interface: &'a ShipInterface, 66 | /// The uid of the channel 67 | pub uid: String, 68 | /// The url of the channel 69 | pub url: String, 70 | // The list of `Subscription`s for this channel 71 | pub subscription_list: Vec, 72 | // / The `EventSource` for this channel which reads all of 73 | // / the SSE events. 74 | event_receiver: ReceiverSource, 75 | /// The current number of messages that have been sent out (which are 76 | /// also defined as message ids) via this `Channel` 77 | pub message_id_count: u64, 78 | } 79 | ``` 80 | 81 | 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. 82 | 83 | 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`. 84 | 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`). 85 | 86 | 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. 87 | 88 | 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. 89 | 90 | The following are the useful methods exposed by a `Channel`: 91 | 92 | ```rust 93 | /// Sends a poke over the channel 94 | pub fn poke(&mut self, app: &str, mark: &str, json: &JsonValue) -> Result; 95 | 96 | /// Create a new `Subscription` and thus subscribes to events on the ship with the provided app/path. 97 | pub fn create_new_subscription(&mut self, app: &str, path: &str) -> Result; 98 | 99 | /// Parses SSE messages for this channel and moves them into 100 | /// the proper corresponding `Subscription`'s `message_list`. 101 | pub fn parse_event_messages(&mut self); 102 | 103 | /// Finds the first `Subscription` in the list which has a matching 104 | /// `app` and `path`; 105 | pub fn find_subscription(&self, app: &str, path: &str) -> Option<&Subscription>; 106 | 107 | /// Finds the first `Subscription` in the list which has a matching 108 | /// `app` and `path`, removes it from the list, and tells the ship 109 | /// that you are unsubscribing. 110 | pub fn unsubscribe(&mut self, app: &str, path: &str) -> Option; 111 | 112 | /// Deletes the channel 113 | pub fn delete_channel(self); 114 | 115 | /// Exposes an interface for interacting with a ship's Graph Store directly. 116 | pub fn graph_store(&mut self) -> GraphStore; 117 | 118 | /// Exposes an interface for interacting with Urbit chats. 119 | pub fn chat(&mut self) -> Chat; 120 | 121 | /// Exposes an interface for interacting with Urbit notebooks. 122 | pub fn notebook(&mute self) -> Notebook; 123 | 124 | ``` 125 | 126 | ### Subscription 127 | 128 | As mentioned in the previous section, a `Subscription` contains it's own `message_list` field where messages are stored after a `Channel` processes them. 129 | 130 | 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. 131 | 132 | To improve the message reading experience, the `Subscription` struct exposes a useful method: 133 | 134 | ```rust 135 | /// Pops a message from the front of `Subscription`'s `message_list`. 136 | /// If no messages are left, returns `None`. 137 | pub fn pop_message(&mut self) -> Option; 138 | ``` 139 | 140 | ## Code Examples 141 | 142 | ### Poke Example 143 | 144 | 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. 145 | 146 | ```rust 147 | // Import the `ShipInterface` struct 148 | use urbit_http_api::ShipInterface; 149 | 150 | fn main() { 151 | // Create a new `ShipInterface` for a local ~zod ship 152 | let mut ship_interface = 153 | ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap(); 154 | // Create a `Channel` 155 | let mut channel = ship_interface.create_channel().unwrap(); 156 | 157 | // Issue a poke over the channel 158 | let poke_res = channel.poke("hood", "helm-hi", &"This is a poke".into()); 159 | 160 | // Cleanup/delete the `Channel` once finished 161 | channel.delete_channel(); 162 | } 163 | ``` 164 | 165 | ### Graph Store Subscription Example 166 | 167 | 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. 168 | 169 | ```rust 170 | use std::thread; 171 | use std::time::Duration; 172 | use urbit_http_api::ShipInterface; 173 | 174 | fn main() { 175 | // Create a new `ShipInterface` for a local ~zod ship 176 | let mut ship_interface = 177 | ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap(); 178 | // Create a `Channel` 179 | let mut channel = ship_interface.create_channel().unwrap(); 180 | // Create a `Subscription` for the `graph-store` app with the `/updates` path. This `Subscription` 181 | // is automatically added to the `Channel`'s `subscription_list`. 182 | channel 183 | .create_new_subscription("graph-store", "/updates") 184 | .unwrap(); 185 | 186 | // Create a loop that iterates 10 times 187 | for _ in 0..10 { 188 | // Parse all of the event messages to move them into the correct 189 | // `Subscription`s in the `Channel`'s `subscription_list`. 190 | channel.parse_event_messages(); 191 | 192 | // Find our graph-store `Subscription` 193 | let gs_sub = channel.find_subscription("graph-store", "/updates").unwrap(); 194 | 195 | // Pop all of the messages from our `gs_sub` and print them 196 | loop { 197 | let pop_res = gs_sub.pop_message(); 198 | if let Some(mess) = &pop_res { 199 | println!("Message: {:?}", mess); 200 | } 201 | // If no messages left, stop 202 | if let None = &pop_res { 203 | break; 204 | } 205 | } 206 | 207 | // Wait for 1 second before trying to parse the event messages again 208 | thread::sleep(Duration::new(1, 0)); 209 | } 210 | 211 | // Once finished, unsubscribe/destroy our `Subscription` 212 | channel.unsubscribe("graph-store", "/updates"); 213 | // Delete the channel 214 | channel.delete_channel(); 215 | } 216 | ``` 217 | 218 | ### Urbit Chat Messaging Example 219 | 220 | This example displays how to connect to a ship and send a message to an Urbit chat using the `Chat` struct interface. 221 | 222 | ```rust 223 | // Import the `ShipInterface` struct 224 | use urbit_http_api::{ShipInterface, chat::Message}; 225 | 226 | fn main() { 227 | // Create a new `ShipInterface` for a local ~zod ship 228 | let mut ship_interface = 229 | ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap(); 230 | // Create a `Channel` 231 | let mut channel = ship_interface.create_channel().unwrap(); 232 | 233 | // Create a `Message` which is formatted properly for an Urbit chat 234 | let message = Message::new() 235 | // Add text to your message 236 | .add_text("Checkout this cool article by ~wicdev-wisryt:") 237 | // Add a URL link to your message after the previous text (which gets automatically added on a new line) 238 | .add_url("https://urbit.org/blog/io-in-hoon/") 239 | // Add an image URL to your message after the previous url (which gets automatically added on a new line as a rendered image) 240 | .add_url("https://media.urbit.org/site/posts/essays/zion-canyon-1.jpg"); 241 | // Send the message to a chat hosted by ~zod named "test-93". 242 | // Note the connected ship must already have joined the chat in order to send a message to the chat. 243 | let _mess_res = channel 244 | .chat() 245 | .send_message("~zod", "test-93", &message); 246 | 247 | // Cleanup/delete the `Channel` once finished 248 | channel.delete_channel(); 249 | } 250 | ``` 251 | 252 | ### Urbit Chat Subscription Example 253 | 254 | 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. 255 | 256 | ```rust 257 | use std::thread; 258 | use std::time::Duration; 259 | use urbit_http_api::ShipInterface; 260 | 261 | fn main() { 262 | // Create a new `ShipInterface` for a local ~zod ship 263 | let mut ship_interface = 264 | ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap(); 265 | // Create a `Channel` 266 | let mut channel = ship_interface.create_channel().unwrap(); 267 | // Subscribe to a specific chat, and obtain a `Receiver` back which contains a stream of messages from the chat 268 | let chat_receiver = channel 269 | .chat() 270 | .subscribe_to_chat("~mocrux-nomdep", "test-93") 271 | .unwrap(); 272 | 273 | // Create a loop that iterates 10 times 274 | for _ in 0..10 { 275 | // If a message has been posted to the chat, unwrap it and acquire the `AuthoredMessage` 276 | if let Ok(authored_message) = chat_receiver.try_recv() { 277 | // Pretty print the author ship @p and the message contents 278 | println!( 279 | "~{}:{}", 280 | authored_message.author, 281 | authored_message.message.to_formatted_string() 282 | ); 283 | } 284 | // Wait for 1 second before checking again 285 | thread::sleep(Duration::new(1, 0)); 286 | } 287 | 288 | // Delete the channel 289 | channel.delete_channel(); 290 | } 291 | ``` 292 | 293 | --- 294 | 295 | This library was created by ~mocrux-nomdep([Robert Kornacki](https://github.com/robkorn)). 296 | -------------------------------------------------------------------------------- /src/apps/chat.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use crate::traits::messaging::{AuthoredMessage, Message, Messaging}; 3 | use crate::Channel; 4 | use crossbeam::channel::Receiver; 5 | 6 | /// A struct that provides an interface for interacting with Urbit chats 7 | pub struct Chat<'a> { 8 | pub channel: &'a mut Channel, 9 | } 10 | 11 | impl<'a> Messaging for Chat<'a> { 12 | fn channel(&mut self) -> &mut Channel { 13 | self.channel 14 | } 15 | } 16 | 17 | impl<'a> Chat<'a> { 18 | /// Send a message to an Urbit chat. 19 | /// Returns the index of the node that was added to Graph Store. 20 | pub fn send_chat_message( 21 | &mut self, 22 | chat_ship: &str, 23 | chat_name: &str, 24 | message: &Message, 25 | ) -> Result { 26 | self.send_message(chat_ship, chat_name, message) 27 | } 28 | 29 | /// Extracts chat log automatically into a list of formatted `String`s 30 | pub fn export_chat_log(&mut self, chat_ship: &str, chat_name: &str) -> Result> { 31 | self.export_message_log(chat_ship, chat_name) 32 | } 33 | 34 | /// Extracts a chat's messages as `AuthoredMessage`s 35 | pub fn export_chat_authored_messages( 36 | &mut self, 37 | chat_ship: &str, 38 | chat_name: &str, 39 | ) -> Result> { 40 | self.export_authored_messages(chat_ship, chat_name) 41 | } 42 | 43 | /// Subscribe to and watch for messages. This method returns a `Receiver` with the 44 | /// `AuthoredMessage`s that are posted after subscribing. Simply call `receiver.try_recv()` 45 | /// to read the next `AuthoredMessage` if one has been posted. 46 | /// 47 | /// Technical Note: This method actually creates a new `Channel` with your Urbit Ship, and spawns a new unix thread 48 | /// locally that processes all messages on said channel. This is required due to borrowing mechanisms in Rust, however 49 | /// on the plus side this makes it potentially more performant by each subscription having it's own unix thread. 50 | pub fn subscribe_to_chat( 51 | &mut self, 52 | chat_ship: &str, 53 | chat_name: &str, 54 | ) -> Result> { 55 | self.subscribe_to_messages(chat_ship, chat_name) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/apps/collections.rs: -------------------------------------------------------------------------------- 1 | use crate::apps::notebook::Comment; 2 | use crate::graph::NodeContents; 3 | use crate::helper::{get_current_da_time, get_current_time}; 4 | use crate::AuthoredMessage; 5 | use crate::{Channel, Node, Result, UrbitAPIError}; 6 | 7 | /// A struct that provides an interface for interacting with Urbit collections 8 | pub struct Collection<'a> { 9 | pub channel: &'a mut Channel, 10 | } 11 | 12 | /// A struct that represents a Collection link 13 | #[derive(Clone, Debug)] 14 | pub struct Link { 15 | pub title: String, 16 | pub author: String, 17 | pub time_sent: String, 18 | pub url: String, 19 | pub comments: Vec, 20 | pub index: String, 21 | } 22 | 23 | impl Link { 24 | /// Create a new `Link` 25 | pub fn new( 26 | title: &str, 27 | author: &str, 28 | time_sent: &str, 29 | url: &str, 30 | comments: &Vec, 31 | index: &str, 32 | ) -> Link { 33 | Link { 34 | title: title.to_string(), 35 | author: author.to_string(), 36 | time_sent: time_sent.to_string(), 37 | url: url.to_string(), 38 | comments: comments.clone(), 39 | index: index.to_string(), 40 | } 41 | } 42 | 43 | /// Convert from a `Node` to a `Link` 44 | pub fn from_node(node: &Node) -> Result { 45 | let mut comments: Vec = vec![]; 46 | // Check the to see if the children exist 47 | if node.children.len() > 0 && node.children[0].children.len() > 0 { 48 | // Find the latest revision of each of the comments 49 | for comment_node in &node.children { 50 | let mut latest_comment_revision_node = comment_node.children[0].clone(); 51 | for revision_node in &comment_node.children { 52 | if revision_node.index_tail() > latest_comment_revision_node.index_tail() { 53 | latest_comment_revision_node = revision_node.clone(); 54 | } 55 | } 56 | comments.push(Comment::from_node(&latest_comment_revision_node)); 57 | } 58 | } 59 | 60 | // Acquire the title, which is the first item in the content_list 61 | let title = format!("{}", node.contents.content_list[0]["text"]); 62 | // Acquire the url, which is the second item in the content_list 63 | let url = format!("{}", node.contents.content_list[1]["url"]); 64 | let author = node.author.clone(); 65 | let time_sent = node.time_sent_formatted(); 66 | 67 | // Create the note 68 | Ok(Link::new( 69 | &title, 70 | &author, 71 | &time_sent, 72 | &url, 73 | &comments, 74 | &node.index, 75 | )) 76 | } 77 | } 78 | 79 | impl<'a> Collection<'a> { 80 | /// Extracts a Collection's graph from the connected ship and parses it into a vector of `Link`s. 81 | pub fn export_collection( 82 | &mut self, 83 | collection_ship: &str, 84 | collection_name: &str, 85 | ) -> Result> { 86 | let graph = &self 87 | .channel 88 | .graph_store() 89 | .get_graph(collection_ship, collection_name)?; 90 | 91 | // Parse each top level node (Link) in the collection graph 92 | let mut links = vec![]; 93 | for node in &graph.nodes { 94 | let link = Link::from_node(node)?; 95 | links.push(link); 96 | } 97 | 98 | Ok(links) 99 | } 100 | 101 | /// Adds a new link to the specified Collection that your ship has access to. 102 | /// Returns the index of the link. 103 | pub fn add_link( 104 | &mut self, 105 | collection_ship: &str, 106 | collection_name: &str, 107 | title: &str, 108 | url: &str, 109 | ) -> Result { 110 | let mut gs = self.channel.graph_store(); 111 | 112 | let link_contents = NodeContents::new().add_text(title).add_url(url); 113 | let link_node = gs.new_node(&link_contents); 114 | 115 | if let Ok(_) = gs.add_node(collection_ship, collection_name, &link_node) { 116 | Ok(link_node.index) 117 | } else { 118 | Err(UrbitAPIError::FailedToCreateNote( 119 | link_node.to_json().dump(), 120 | )) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/apps/dm.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use crate::traits::messaging::{AuthoredMessage, Message, Messaging}; 3 | use crate::Channel; 4 | use crossbeam::channel::Receiver; 5 | 6 | /// A struct that provides an interface for interacting with Urbit DMs 7 | pub struct DM<'a> { 8 | pub channel: &'a mut Channel, 9 | } 10 | 11 | impl<'a> Messaging for DM<'a> { 12 | fn channel(&mut self) -> &mut Channel { 13 | self.channel 14 | } 15 | } 16 | 17 | impl<'a> DM<'a> { 18 | /// Converts a ship @p to the `dm_name` string format used for DM channels 19 | pub fn ship_to_dm_name(&self, ship: &str) -> String { 20 | format!("dm--{}", ship) 21 | } 22 | 23 | /// Send a message to an Urbit DM chat. 24 | /// Returns the index of the node that was added to Graph Store. 25 | pub fn send_dm_message( 26 | &mut self, 27 | dm_ship: &str, 28 | dm_name: &str, 29 | message: &Message, 30 | ) -> Result { 31 | self.send_message(dm_ship, dm_name, message) 32 | } 33 | 34 | /// Extracts DM chat log automatically into a list of formatted `String`s 35 | pub fn export_dm_log(&mut self, dm_ship: &str, dm_name: &str) -> Result> { 36 | self.export_message_log(dm_ship, dm_name) 37 | } 38 | 39 | /// Extracts a DM chat's messages as `AuthoredMessage`s 40 | pub fn export_dm_authored_messages( 41 | &mut self, 42 | dm_ship: &str, 43 | dm_name: &str, 44 | ) -> Result> { 45 | self.export_authored_messages(dm_ship, dm_name) 46 | } 47 | 48 | /// Subscribe to and watch for messages. This method returns a `Receiver` with the 49 | /// `AuthoredMessage`s that are posted after subscribing. Simply call `receiver.try_recv()` 50 | /// to read the next `AuthoredMessage` if one has been posted. 51 | /// 52 | /// Technical Note: This method actually creates a new `Channel` with your Urbit Ship, and spawns a new unix thread 53 | /// locally that processes all messages on said channel. This is required due to borrowing mechanisms in Rust, however 54 | /// on the plus side this makes it potentially more performant by each subscription having it's own unix thread. 55 | pub fn subscribe_to_dm( 56 | &mut self, 57 | dm_ship: &str, 58 | dm_name: &str, 59 | ) -> Result> { 60 | self.subscribe_to_messages(dm_ship, dm_name) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/apps/harkstore.rs: -------------------------------------------------------------------------------- 1 | use crate::{Channel, Node, Result, UrbitAPIError}; 2 | use crossbeam::channel::{unbounded, Receiver}; 3 | use json::JsonValue; 4 | use std::thread; 5 | use std::time::Duration; 6 | 7 | /// A struct that provides an interface for interacting with hark-store 8 | pub struct HarkStore<'a> { 9 | pub channel: &'a mut Channel, 10 | } 11 | 12 | impl<'a> HarkStore<'a> {} 13 | -------------------------------------------------------------------------------- /src/apps/invitestore.rs: -------------------------------------------------------------------------------- 1 | use crate::{Channel, Node, Result, UrbitAPIError}; 2 | use crossbeam::channel::{unbounded, Receiver}; 3 | use json::JsonValue; 4 | use std::thread; 5 | use std::time::Duration; 6 | 7 | /// A struct that provides an interface for interacting with invite-store 8 | pub struct InviteStore<'a> { 9 | pub channel: &'a mut Channel, 10 | } 11 | 12 | impl<'a> InviteStore<'a> { 13 | /// Accept an invite 14 | pub fn accept_invite(&self, term: &str, uid: &str) { 15 | // let mut poke2_data = json::JsonValue::new_object(); 16 | // poke2_data["accept"] = json::JsonValue::new_object(); 17 | // poke2_data["accept"]["term"] = "graph".to_string().into(); 18 | // poke2_data["accept"]["uid"] = poke_channel.uid.clone().into(); 19 | // let _poke2_response = poke_channel.poke("invite-store", "invite-action", &poke_data); 20 | todo!(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/apps/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod chat; 2 | pub mod collections; 3 | pub mod dm; 4 | pub mod harkstore; 5 | pub mod invitestore; 6 | pub mod notebook; 7 | -------------------------------------------------------------------------------- /src/apps/notebook.rs: -------------------------------------------------------------------------------- 1 | use crate::graph::NodeContents; 2 | use crate::helper::{get_current_da_time, get_current_time}; 3 | use crate::AuthoredMessage; 4 | use crate::{Channel, Node, Result, UrbitAPIError}; 5 | 6 | /// A struct that provides an interface for interacting with Urbit notebooks 7 | pub struct Notebook<'a> { 8 | pub channel: &'a mut Channel, 9 | } 10 | 11 | /// A comment is effectively equivalent to an `AuthoredMessage`, and is stored as such. 12 | pub type Comment = AuthoredMessage; 13 | 14 | /// A struct that represents a Note from a Notebook 15 | #[derive(Clone, Debug)] 16 | pub struct Note { 17 | pub title: String, 18 | pub author: String, 19 | pub time_sent: String, 20 | pub contents: String, 21 | pub comments: Vec, 22 | pub index: String, 23 | } 24 | 25 | /// An internal helper struct for analysing Notebook node indices 26 | #[derive(Clone, Debug)] 27 | struct NotebookIndex<'a> { 28 | pub index: &'a str, 29 | pub index_split: Vec<&'a str>, 30 | } 31 | 32 | impl Note { 33 | /// Create a new `Note` 34 | pub fn new( 35 | title: &str, 36 | author: &str, 37 | time_sent: &str, 38 | contents: &str, 39 | comments: &Vec, 40 | index: &str, 41 | ) -> Note { 42 | Note { 43 | title: title.to_string(), 44 | author: author.to_string(), 45 | time_sent: time_sent.to_string(), 46 | contents: contents.to_string(), 47 | comments: comments.clone(), 48 | index: index.to_string(), 49 | } 50 | } 51 | 52 | /// Convert from a `Node` to a `Note` 53 | pub fn from_node(node: &Node, revision: Option) -> Result { 54 | let mut comments: Vec = vec![]; 55 | // Find the comments node which has an index tail of `2` 56 | let comments_node = node 57 | .children 58 | .iter() 59 | .find(|c| c.index_tail() == "2") 60 | .ok_or(UrbitAPIError::InvalidNoteGraphNode(node.to_json().dump()))?; 61 | // Find the note content node which has an index tail of `1` 62 | let content_node = node 63 | .children 64 | .iter() 65 | .find(|c| c.index_tail() == "1") 66 | .ok_or(UrbitAPIError::InvalidNoteGraphNode(node.to_json().dump()))?; 67 | 68 | // Find the latest revision of each of the notebook comments 69 | for comment_node in &comments_node.children { 70 | let mut latest_comment_revision_node = comment_node.children[0].clone(); 71 | for revision_node in &comment_node.children { 72 | if revision_node.index_tail() > latest_comment_revision_node.index_tail() { 73 | latest_comment_revision_node = revision_node.clone(); 74 | } 75 | } 76 | comments.push(Comment::from_node(&latest_comment_revision_node)); 77 | } 78 | 79 | let mut fetched_revision_node = content_node.children[0].clone(); 80 | 81 | match revision { 82 | Some(idx) => { 83 | // find a specific revision of the notebook content 84 | for revision_node in &content_node.children { 85 | if revision_node.index == idx { 86 | fetched_revision_node = revision_node.clone(); 87 | } 88 | } 89 | } 90 | None => { 91 | // Find the latest revision of the notebook content 92 | for revision_node in &content_node.children { 93 | if revision_node.index_tail() > fetched_revision_node.index_tail() { 94 | fetched_revision_node = revision_node.clone(); 95 | } 96 | } 97 | } 98 | } 99 | // Acquire the title, which is the first item in the revision node of the note 100 | let title = format!("{}", fetched_revision_node.contents.content_list[0]["text"]); 101 | // Acquire the note body, which is all in the second item in the revision node of the note 102 | let contents = format!("{}", fetched_revision_node.contents.content_list[1]["text"]); 103 | let author = fetched_revision_node.author.clone(); 104 | let time_sent = fetched_revision_node.time_sent_formatted(); 105 | 106 | // Create the note 107 | Ok(Note::new( 108 | &title, 109 | &author, 110 | &time_sent, 111 | &contents, 112 | &comments, 113 | &fetched_revision_node.index, 114 | )) 115 | } 116 | 117 | /// Convert the contents of the latest revision of the Note to 118 | /// a series of markdown `String`s 119 | pub fn content_as_markdown(&self) -> Vec { 120 | let formatted_string = self.contents.clone(); 121 | formatted_string 122 | .split("\\n") 123 | .map(|l| l.to_string()) 124 | .collect() 125 | } 126 | } 127 | 128 | impl<'a> Notebook<'a> { 129 | /// Extracts a Notebook's graph from the connected ship and parses it into a vector of `Note`s 130 | pub fn export_notebook( 131 | &mut self, 132 | notebook_ship: &str, 133 | notebook_name: &str, 134 | ) -> Result> { 135 | let graph = &self 136 | .channel 137 | .graph_store() 138 | .get_graph(notebook_ship, notebook_name)?; 139 | 140 | // Parse each top level node (Note) in the notebook graph 141 | let mut notes = vec![]; 142 | for node in &graph.nodes { 143 | let note = Note::from_node(node, None)?; 144 | notes.push(note); 145 | } 146 | 147 | Ok(notes) 148 | } 149 | 150 | /// Fetch a note object given an index `note_index`. This note index can be the root index of the note 151 | /// or any of the child indexes of the note. If a child index for a specific revision of the note is passed 152 | /// then that revision will be fetched, otherwise latest revision is the default. 153 | pub fn fetch_note( 154 | &mut self, 155 | notebook_ship: &str, 156 | notebook_name: &str, 157 | note_index: &str, 158 | ) -> Result { 159 | // check index 160 | let index = NotebookIndex::new(note_index); 161 | if !index.is_valid() { 162 | return Err(UrbitAPIError::InvalidNoteGraphNodeIndex( 163 | note_index.to_string(), 164 | )); 165 | } 166 | 167 | // root note index 168 | let note_root_index = index.note_root_index(); 169 | 170 | // get the note root node 171 | let node = 172 | &self 173 | .channel 174 | .graph_store() 175 | .get_node(notebook_ship, notebook_name, ¬e_root_index)?; 176 | let revision = match index.is_note_revision() { 177 | true => Some(note_index.to_string()), 178 | false => None, 179 | }; 180 | 181 | return Ok(Note::from_node(node, revision)?); 182 | } 183 | 184 | /// Fetches the latest version of a note based on providing the index of a comment on said note. 185 | /// This is technically just a wrapper around `fetch_note`, but is implemented as a separate method 186 | /// to prevent overloading method meaning/documentation thereby preventing confusion. 187 | pub fn fetch_note_with_comment_index( 188 | &mut self, 189 | notebook_ship: &str, 190 | notebook_name: &str, 191 | comment_index: &str, 192 | ) -> Result { 193 | self.fetch_note(notebook_ship, notebook_name, comment_index) 194 | } 195 | 196 | /// Find the index of the latest revision of a note given an index `note_index` 197 | /// `note_index` can be any valid note index (even an index of a comment on the note) 198 | pub fn fetch_note_latest_revision_index( 199 | &mut self, 200 | notebook_ship: &str, 201 | notebook_name: &str, 202 | note_index: &str, 203 | ) -> Result { 204 | // check index 205 | let index = NotebookIndex::new(note_index); 206 | if !index.is_valid() { 207 | return Err(UrbitAPIError::InvalidNoteGraphNodeIndex( 208 | note_index.to_string(), 209 | )); 210 | } 211 | 212 | // root note index 213 | let note_root_index = index.note_root_index(); 214 | 215 | // get note root node 216 | let node = 217 | &self 218 | .channel 219 | .graph_store() 220 | .get_node(notebook_ship, notebook_name, ¬e_root_index)?; 221 | for pnode in &node.children { 222 | if pnode.index_tail() == "1" { 223 | let mut latestindex = NotebookIndex::new(&pnode.children[0].index); 224 | for rev in &pnode.children { 225 | let revindex = NotebookIndex::new(&rev.index); 226 | if revindex.index_tail() > latestindex.index_tail() { 227 | latestindex = revindex.clone(); 228 | } 229 | } 230 | return Ok(latestindex.index.to_string()); 231 | } 232 | } 233 | 234 | Err(UrbitAPIError::InvalidNoteGraphNodeIndex( 235 | note_index.to_string(), 236 | )) 237 | } 238 | 239 | /// Fetch a comment given an index `comment_index`. 240 | /// Index can be the comment root node index, or index of any revision. 241 | /// Will fetch most recent revision if passed root node index 242 | pub fn fetch_comment( 243 | &mut self, 244 | notebook_ship: &str, 245 | notebook_name: &str, 246 | comment_index: &str, 247 | ) -> Result { 248 | // check index 249 | let index = NotebookIndex::new(comment_index); 250 | 251 | if !index.is_valid_comment_index() { 252 | return Err(UrbitAPIError::InvalidCommentGraphNodeIndex( 253 | comment_index.to_string(), 254 | )); 255 | } 256 | let comment_root_index = index.comment_root_index()?; 257 | 258 | // get comment root node 259 | let node = &self.channel.graph_store().get_node( 260 | notebook_ship, 261 | notebook_name, 262 | &comment_root_index, 263 | )?; 264 | 265 | if index.is_comment_root() { 266 | // find latest comment revision 267 | let mut newest = node.children[0].clone(); 268 | for rnode in &node.children { 269 | if rnode.index_tail() > newest.index_tail() { 270 | newest = rnode.clone(); 271 | } 272 | } 273 | return Ok(Comment::from_node(&newest)); 274 | } else { 275 | // find specific comment revision 276 | for rnode in &node.children { 277 | if rnode.index == comment_index { 278 | return Ok(Comment::from_node(&rnode)); 279 | } 280 | } 281 | } 282 | 283 | Err(UrbitAPIError::InvalidCommentGraphNodeIndex( 284 | comment_index.to_string(), 285 | )) 286 | } 287 | 288 | /// Fetch index of latest revision of a comment given an index `comment_index`. 289 | /// Index can be the comment root node index, or the index of any revision of the comment. 290 | pub fn fetch_comment_latest_revision_index( 291 | &mut self, 292 | notebook_ship: &str, 293 | notebook_name: &str, 294 | comment_index: &str, 295 | ) -> Result { 296 | // check index 297 | let index = NotebookIndex::new(comment_index); 298 | 299 | if !index.is_valid_comment_index() { 300 | return Err(UrbitAPIError::InvalidCommentGraphNodeIndex( 301 | comment_index.to_string(), 302 | )); 303 | } 304 | let comment_root_index = index.comment_root_index()?; 305 | 306 | // get comment root node 307 | let node = &self.channel.graph_store().get_node( 308 | notebook_ship, 309 | notebook_name, 310 | &comment_root_index, 311 | )?; 312 | 313 | if node.children.len() > 0 { 314 | let mut newestindex = NotebookIndex::new(&node.children[0].index); 315 | for rnode in &node.children { 316 | let revindex = NotebookIndex::new(&rnode.index); 317 | if revindex.index_tail() > newestindex.index_tail() { 318 | newestindex = revindex.clone(); 319 | } 320 | } 321 | return Ok(newestindex.index.to_string()); 322 | } 323 | 324 | Err(UrbitAPIError::InvalidCommentGraphNodeIndex( 325 | comment_index.to_string(), 326 | )) 327 | } 328 | 329 | /// Adds a new note to the notebook. 330 | /// Returns the index of the newly created first revision of the note. 331 | pub fn add_note( 332 | &mut self, 333 | notebook_ship: &str, 334 | notebook_name: &str, 335 | title: &str, 336 | body: &str, 337 | ) -> Result { 338 | let mut gs = self.channel.graph_store(); 339 | // make the root node for the note 340 | let node_root = gs.new_node(&NodeContents::new()); 341 | // save creation time for other nodes 342 | let unix_time = node_root.time_sent; 343 | // index helper 344 | let index = NotebookIndex::new(&node_root.index); 345 | 346 | // make child 1 for note content 347 | // make child 2 for comments 348 | // make child 1/1 for initial note revision 349 | let node_root = node_root 350 | .add_child(&gs.new_node_specified( 351 | &index.note_content_node_index(), 352 | unix_time, 353 | &NodeContents::new(), 354 | )) 355 | .add_child(&gs.new_node_specified( 356 | &index.note_comments_node_index(), 357 | unix_time, 358 | &NodeContents::new(), 359 | )) 360 | .add_child(&gs.new_node_specified( 361 | &index.note_revision_index(1), 362 | unix_time, 363 | &NodeContents::new().add_text(title).add_text(body), 364 | )); 365 | 366 | if let Ok(_) = gs.add_node(notebook_ship, notebook_name, &node_root) { 367 | Ok(index.note_revision_index(1)) 368 | } else { 369 | Err(UrbitAPIError::FailedToCreateNote( 370 | node_root.to_json().dump(), 371 | )) 372 | } 373 | } 374 | 375 | /// Update an existing note with a new title and body. 376 | /// `note_index` can be any valid note index. 377 | /// Returns index of the newly created revision. 378 | pub fn update_note( 379 | &mut self, 380 | notebook_ship: &str, 381 | notebook_name: &str, 382 | note_index: &str, 383 | title: &str, 384 | body: &str, 385 | ) -> Result { 386 | // fetch latest revision of note (will return error if not a valid note index) 387 | let note_latest_index = 388 | self.fetch_note_latest_revision_index(notebook_ship, notebook_name, note_index)?; 389 | // index helper 390 | let index = NotebookIndex::new(¬e_latest_index); 391 | // build new node index 392 | let note_new_index = index.next_revision_index()?; 393 | 394 | let mut gs = self.channel.graph_store(); 395 | let unix_time = get_current_time(); 396 | 397 | // add the node 398 | let node = gs.new_node_specified( 399 | ¬e_new_index, 400 | unix_time, 401 | &NodeContents::new().add_text(title).add_text(body), 402 | ); 403 | 404 | if let Ok(_) = gs.add_node(notebook_ship, notebook_name, &node) { 405 | Ok(node.index.clone()) 406 | } else { 407 | Err(UrbitAPIError::FailedToCreateNote(node.to_json().dump())) 408 | } 409 | } 410 | 411 | /// Add a new comment to a specific note inside of a notebook specified by `note_index` 412 | /// `note_index` can be any valid note/revision, and even the index of other comments. 413 | pub fn add_comment( 414 | &mut self, 415 | notebook_ship: &str, 416 | notebook_name: &str, 417 | note_index: &str, 418 | comment: &NodeContents, 419 | ) -> Result { 420 | // check index 421 | let index = NotebookIndex::new(note_index); 422 | if !index.is_valid() { 423 | return Err(UrbitAPIError::InvalidNoteGraphNodeIndex( 424 | note_index.to_string(), 425 | )); 426 | } 427 | 428 | let mut gs = self.channel.graph_store(); 429 | let unix_time = get_current_time(); 430 | 431 | // make a new node under the note comments node - this is root node for this comment 432 | let cmt_root_node = gs.new_node_specified( 433 | &index.new_comment_root_index(), 434 | unix_time, 435 | &NodeContents::new(), 436 | ); 437 | // update index helper from new node 438 | let index = NotebookIndex::new(&cmt_root_node.index); 439 | // make initial comment revision node 440 | let cmt_rev_index = index.comment_revision_index(1)?; 441 | let cmt_rev_node = gs.new_node_specified(&cmt_rev_index, unix_time, comment); 442 | // assemble node tree 443 | let cmt_root_node = cmt_root_node.add_child(&cmt_rev_node); 444 | // add the nodes 445 | if let Ok(_) = gs.add_node(notebook_ship, notebook_name, &cmt_root_node) { 446 | Ok(cmt_rev_index.clone()) 447 | } else { 448 | Err(UrbitAPIError::FailedToCreateComment( 449 | cmt_root_node.to_json().dump(), 450 | )) 451 | } 452 | } 453 | 454 | /// Update an existing comment on a note. `comment_index` must be a valid index for a comment 455 | /// for a note within the notebook specified which your ship has edit rights for. 456 | /// Returns index of the new comment revision 457 | pub fn update_comment( 458 | &mut self, 459 | notebook_ship: &str, 460 | notebook_name: &str, 461 | comment_index: &str, 462 | comment: &NodeContents, 463 | ) -> Result { 464 | // fetch latest comment revision index (will return error if not a valid comment index) 465 | let cmt_latest_index = 466 | self.fetch_comment_latest_revision_index(notebook_ship, notebook_name, comment_index)?; 467 | // index helper 468 | let index = NotebookIndex::new(&cmt_latest_index); 469 | // build new node index 470 | let cmt_new_index = index.next_revision_index()?; 471 | 472 | // add the node 473 | let mut gs = self.channel.graph_store(); 474 | let unix_time = get_current_time(); 475 | 476 | let node = gs.new_node_specified(&cmt_new_index, unix_time, comment); 477 | 478 | if let Ok(_) = gs.add_node(notebook_ship, notebook_name, &node) { 479 | Ok(node.index.clone()) 480 | } else { 481 | Err(UrbitAPIError::FailedToCreateComment(node.to_json().dump())) 482 | } 483 | } 484 | } 485 | 486 | impl<'a> NotebookIndex<'a> { 487 | /// Create a new `NotebookIndex` 488 | pub fn new(idx: &str) -> NotebookIndex { 489 | NotebookIndex { 490 | index: idx, 491 | index_split: idx.split("/").collect(), 492 | } 493 | } 494 | 495 | // notebook index slices 496 | // must have at least 2 slices to be valid notebook index 497 | // slice 0 must have len 0 - means index started with a "/" 498 | // slice 1 is note root node 499 | // slice 2 is "1" for note, "2" for comment 500 | // slice 3 is note revision or comment root node 501 | // slice 4 is comment revision 502 | 503 | /// is this any kind of valid notebook node index (comment or note)? 504 | pub fn is_valid(&self) -> bool { 505 | (self.index_split.len() >= 2) && (self.index_split[0].len() == 0) 506 | } 507 | 508 | /// is this the index of a note root node? 509 | pub fn is_note_root(&self) -> bool { 510 | (self.index_split.len() == 2) && (self.index_split[0].len() == 0) 511 | } 512 | 513 | /// is this the index of a specific note revision? 514 | pub fn is_note_revision(&self) -> bool { 515 | (self.index_split.len() == 4) 516 | && (self.index_split[0].len() == 0) 517 | && (self.index_split[2] == "1") 518 | } 519 | 520 | /// is this some kind of valid comment index? 521 | pub fn is_valid_comment_index(&self) -> bool { 522 | (self.index_split.len() >= 4) 523 | && (self.index_split[0].len() == 0) 524 | && (self.index_split[2] == "2") 525 | } 526 | 527 | /// is this the index of a comment root? 528 | pub fn is_comment_root(&self) -> bool { 529 | (self.index_split.len() == 4) 530 | && (self.index_split[0].len() == 0) 531 | && (self.index_split[2] == "2") 532 | } 533 | 534 | /// is this the index of a comment revision? 535 | pub fn is_comment_revision(&self) -> bool { 536 | (self.index_split.len() == 5) 537 | && (self.index_split[0].len() == 0) 538 | && (self.index_split[2] == "2") 539 | } 540 | 541 | /// root index of note 542 | pub fn note_root_index(&self) -> String { 543 | format!("/{}", self.index_split[1]) 544 | } 545 | 546 | /// index of note content node, note revisions are children of this 547 | pub fn note_content_node_index(&self) -> String { 548 | format!("/{}/1", self.index_split[1]) 549 | } 550 | 551 | /// index of note comments node, all note comments are children of this 552 | pub fn note_comments_node_index(&self) -> String { 553 | format!("/{}/2", self.index_split[1]) 554 | } 555 | 556 | /// root index of comment (if this is a valid comment index) 557 | /// all revisions of a comment are children of the comment root 558 | pub fn comment_root_index(&self) -> Result { 559 | if self.is_valid_comment_index() { 560 | Ok(format!( 561 | "/{}/2/{}", 562 | self.index_split[1], self.index_split[3] 563 | )) 564 | } else { 565 | Err(UrbitAPIError::InvalidCommentGraphNodeIndex( 566 | self.index.to_string(), 567 | )) 568 | } 569 | } 570 | /// generate a new comment root index using `get_current_da_time()` 571 | pub fn new_comment_root_index(&self) -> String { 572 | format!("/{}/2/{}", self.index_split[1], get_current_da_time()) 573 | } 574 | 575 | /// str slice of final element of index 576 | pub fn index_tail(&self) -> &str { 577 | self.index_split[self.index_split.len() - 1] 578 | } 579 | 580 | /// revision number if this is index of a specific revision 581 | pub fn revision(&self) -> Result { 582 | if self.is_note_revision() { 583 | if let Ok(r) = self.index_split[3].parse::() { 584 | return Ok(r); 585 | } 586 | } else if self.is_comment_revision() { 587 | if let Ok(r) = self.index_split[4].parse::() { 588 | return Ok(r); 589 | } 590 | } 591 | 592 | Err(UrbitAPIError::InvalidNoteGraphNodeIndex( 593 | self.index.to_string(), 594 | )) 595 | } 596 | 597 | /// generates the index of next revision, if this is a valid note or comment revision index 598 | pub fn next_revision_index(&self) -> Result { 599 | let rev = self.revision()?; 600 | let newrev = rev + 1; 601 | // we know index_split.len() is either 4 or 5 here as revision() was Ok 602 | if self.index_split.len() == 5 { 603 | Ok(format!( 604 | "/{}/2/{}/{}", 605 | self.index_split[1], 606 | self.index_split[3], 607 | &newrev.to_string() 608 | )) 609 | } else { 610 | Ok(format!( 611 | "/{}/1/{}", 612 | self.index_split[1], 613 | &newrev.to_string() 614 | )) 615 | } 616 | } 617 | 618 | /// generate a specific note revision index 619 | pub fn note_revision_index(&self, revision: u64) -> String { 620 | format!("/{}/1/{}", self.index_split[1], revision.to_string()) 621 | } 622 | 623 | /// generate a specific comment revision index (if this is a valid comment index) 624 | pub fn comment_revision_index(&self, revision: u64) -> Result { 625 | if self.is_valid_comment_index() { 626 | Ok(format!( 627 | "/{}/2/{}/{}", 628 | self.index_split[1], 629 | self.index_split[3], 630 | revision.to_string() 631 | )) 632 | } else { 633 | Err(UrbitAPIError::InvalidCommentGraphNodeIndex( 634 | self.index.to_string(), 635 | )) 636 | } 637 | } 638 | } 639 | -------------------------------------------------------------------------------- /src/channel.rs: -------------------------------------------------------------------------------- 1 | use crate::apps::chat::Chat; 2 | use crate::apps::collections::Collection; 3 | use crate::apps::notebook::Notebook; 4 | use crate::error::{Result, UrbitAPIError}; 5 | use crate::graphstore::GraphStore; 6 | use crate::interface::ShipInterface; 7 | use crate::subscription::{CreationID, Subscription}; 8 | use eventsource_threaded::{EventSource, ReceiverSource}; 9 | use json::{object, JsonValue}; 10 | use rand::Rng; 11 | use reqwest::blocking::Response; 12 | use reqwest::header::HeaderMap; 13 | use reqwest::Url; 14 | use std::time::SystemTime; 15 | 16 | /// A Channel which is used to interact with a ship 17 | #[derive(Debug)] 18 | pub struct Channel { 19 | /// `ShipInterface` this channel is created from 20 | pub ship_interface: ShipInterface, 21 | /// The uid of the channel 22 | pub uid: String, 23 | /// The url of the channel 24 | pub url: String, 25 | // The list of `Subscription`s for this channel 26 | pub subscription_list: Vec, 27 | // / The `EventSource` for this channel which reads all of 28 | // / the SSE events. 29 | event_receiver: ReceiverSource, 30 | /// The current number of messages that have been sent out (which are 31 | /// also defined as message ids) via this `Channel` 32 | pub message_id_count: u64, 33 | } 34 | 35 | /// Channel methods for basic functionality 36 | impl Channel { 37 | /// Create a new channel 38 | pub fn new(ship_interface: ShipInterface) -> Result { 39 | let mut rng = rand::thread_rng(); 40 | // Defining the uid as UNIX time, or random if error 41 | let uid = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { 42 | Ok(n) => n.as_micros(), 43 | Err(_) => rng.gen(), 44 | } 45 | .to_string(); 46 | 47 | // Channel url 48 | let channel_url = format!("{}/~/channel/{}", &ship_interface.url, uid); 49 | // Opening channel request json 50 | let mut body = json::parse(r#"[]"#).unwrap(); 51 | body[0] = object! { 52 | "id": 1, 53 | "action": "poke", 54 | "ship": ship_interface.ship_name.clone(), 55 | "app": "hood", 56 | "mark": "helm-hi", 57 | "json": "Opening channel", 58 | }; 59 | 60 | // Make the put request to create the channel. 61 | let resp = ship_interface.send_put_request(&channel_url, &body)?; 62 | 63 | if resp.status().as_u16() == 204 { 64 | // Create cookie header with the ship session auth val 65 | let mut headers = HeaderMap::new(); 66 | headers.append("cookie", ship_interface.session_auth.clone()); 67 | // Create the receiver 68 | let url_structured = 69 | Url::parse(&channel_url).map_err(|_| UrbitAPIError::FailedToCreateNewChannel)?; 70 | let receiver = EventSource::new(url_structured, headers); 71 | 72 | return Ok(Channel { 73 | ship_interface: ship_interface, 74 | uid: uid, 75 | url: channel_url, 76 | subscription_list: vec![], 77 | event_receiver: receiver, 78 | message_id_count: 2, 79 | }); 80 | } else { 81 | return Err(UrbitAPIError::FailedToCreateNewChannel); 82 | } 83 | } 84 | 85 | /// Sends a poke over the channel 86 | pub fn poke(&mut self, app: &str, mark: &str, json: &JsonValue) -> Result { 87 | let mut body = json::parse(r#"[]"#).unwrap(); 88 | body[0] = object! { 89 | "id": self.get_and_raise_message_id_count(), 90 | "action": "poke", 91 | "ship": self.ship_interface.ship_name.clone(), 92 | "app": app, 93 | "mark": mark, 94 | "json": json.clone(), 95 | }; 96 | 97 | // Make the put request for the poke 98 | self.ship_interface.send_put_request(&self.url, &body) 99 | } 100 | 101 | /// Sends a scry to the ship 102 | pub fn scry(&self, app: &str, path: &str, mark: &str) -> Result { 103 | self.ship_interface.scry(app, path, mark) 104 | } 105 | 106 | /// Run a thread via spider 107 | pub fn spider( 108 | &self, 109 | input_mark: &str, 110 | output_mark: &str, 111 | thread_name: &str, 112 | body: &JsonValue, 113 | ) -> Result { 114 | self.ship_interface 115 | .spider(input_mark, output_mark, thread_name, body) 116 | } 117 | 118 | /// Create a new `Subscription` and thus subscribes to events on the 119 | /// ship with the provided app/path. 120 | pub fn create_new_subscription(&mut self, app: &str, path: &str) -> Result { 121 | // Saves the message id to be reused 122 | let creation_id = self.get_and_raise_message_id_count(); 123 | // Create the json body 124 | let mut body = json::parse(r#"[]"#).unwrap(); 125 | body[0] = object! { 126 | "id": creation_id, 127 | "action": "subscribe", 128 | "ship": self.ship_interface.ship_name.clone(), 129 | "app": app.to_string(), 130 | "path": path.to_string(), 131 | }; 132 | 133 | // Make the put request to create the channel. 134 | let resp = self.ship_interface.send_put_request(&self.url, &body)?; 135 | 136 | if resp.status().as_u16() == 204 { 137 | // Create the `Subscription` 138 | let sub = Subscription { 139 | channel_uid: self.uid.clone(), 140 | creation_id: creation_id, 141 | app: app.to_string(), 142 | path: path.to_string(), 143 | message_list: vec![], 144 | }; 145 | // Add the `Subscription` to the list 146 | self.subscription_list.push(sub.clone()); 147 | return Ok(creation_id); 148 | } else { 149 | return Err(UrbitAPIError::FailedToCreateNewSubscription); 150 | } 151 | } 152 | 153 | /// Parses SSE messages for this channel and moves them into 154 | /// the proper corresponding `Subscription`'s `message_list`. 155 | pub fn parse_event_messages(&mut self) { 156 | let rec = &mut self.event_receiver; 157 | 158 | // Consume all messages 159 | loop { 160 | if let Ok(event_res) = rec.try_recv() { 161 | if let Err(e) = &event_res { 162 | println!("Error Event: {}", e); 163 | } 164 | if let Ok(event) = event_res { 165 | // Go through all subscriptions and find which 166 | // subscription this event is for. 167 | for sub in &mut self.subscription_list { 168 | // If adding the message succeeded (because found 169 | // correct `Subscription`) then stop. 170 | if let Some(_) = sub.add_to_message_list(&event) { 171 | // Send an ack for the processed event 172 | // Using unwrap because `add_to_message_list` 173 | // already does error checking. 174 | let eid: u64 = event.id.unwrap().parse().unwrap(); 175 | let mut json = json::parse(r#"[]"#).unwrap(); 176 | json[0] = object! { 177 | "id": self.message_id_count, 178 | "action": "ack", 179 | "event-id": eid, 180 | }; 181 | self.message_id_count += 1; 182 | let _ack_res = self.ship_interface.send_put_request(&self.url, &json); 183 | break; 184 | } 185 | } 186 | } 187 | continue; 188 | } 189 | break; 190 | } 191 | } 192 | 193 | /// Finds the first `Subscription` in the list which has a matching 194 | /// `app` and `path`; 195 | pub fn find_subscription(&mut self, app: &str, path: &str) -> Option<&mut Subscription> { 196 | for sub in &mut self.subscription_list { 197 | if sub.app == app && sub.path == path { 198 | return Some(sub); 199 | } 200 | } 201 | None 202 | } 203 | 204 | /// Finds the first `Subscription` in the list which has a matching 205 | /// `app` and `path`, removes it from the list, and tells the ship 206 | /// that you are unsubscribing. Returns `None` if failed to find 207 | /// a subscription with a matching app & path. 208 | pub fn unsubscribe(&mut self, app: &str, path: &str) -> Option { 209 | let index = self 210 | .subscription_list 211 | .iter() 212 | .position(|s| s.app == app && s.path == path)?; 213 | self.subscription_list.remove(index); 214 | Some(true) 215 | } 216 | 217 | /// Deletes the channel 218 | pub fn delete_channel(self) { 219 | let mut json = json::parse(r#"[]"#).unwrap(); 220 | json[0] = object! { 221 | "id": self.message_id_count, 222 | "action": "delete", 223 | }; 224 | let _res = self.ship_interface.send_put_request(&self.url, &json); 225 | std::mem::drop(self); 226 | } 227 | 228 | /// Acquires and returns the current `message_id_count` from the 229 | /// `ShipInterface` that this channel was created from while also 230 | /// increase said value by 1. 231 | fn get_and_raise_message_id_count(&mut self) -> u64 { 232 | let current_id_count = self.message_id_count; 233 | self.message_id_count += 1; 234 | current_id_count 235 | } 236 | } 237 | 238 | /// `Channel` methods which expose advanced functionality, typically by 239 | /// producing another struct which is built on top of `Channel`. 240 | impl Channel { 241 | /// Create a `Chat` struct which exposes an interface for interacting 242 | /// with chats on Urbit 243 | pub fn chat(&mut self) -> Chat { 244 | Chat { channel: self } 245 | } 246 | 247 | /// Create a `Notebook` struct which exposes an interface for interacting 248 | /// with notebooks on Urbit 249 | pub fn notebook(&mut self) -> Notebook { 250 | Notebook { channel: self } 251 | } 252 | 253 | /// Create a `GraphStore` struct which exposes an interface for interacting 254 | /// with a ship's Graph Store. 255 | pub fn graph_store(&mut self) -> GraphStore { 256 | GraphStore { channel: self } 257 | } 258 | 259 | /// Create a `Collection` struct which exposes an interface for interacting 260 | /// with collections on Urbit. 261 | pub fn collection(&mut self) -> Collection { 262 | Collection { channel: self } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Error as ReqError; 2 | use thiserror::Error; 3 | 4 | pub type Result = std::result::Result; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum UrbitAPIError { 8 | #[error("Failed logging in to the ship given the provided url and code.")] 9 | FailedToLogin, 10 | #[error("Failed to create a new channel.")] 11 | FailedToCreateNewChannel, 12 | #[error("Failed to create a new subscription.")] 13 | FailedToCreateNewSubscription, 14 | #[error("Failed to fetch Graph Store keys.")] 15 | FailedToFetchKeys, 16 | #[error("Failed to fetch Graph Store tags.")] 17 | FailedToFetchTags, 18 | #[error("Failed to send a chat message to chat {0}.")] 19 | FailedToSendChatMessage(String), 20 | #[error("Failed to acquire update log from Graph Store for resource {0}.")] 21 | FailedToGetUpdateLog(String), 22 | #[error("Failed to acquire graph from Graph Store for resource {0}.")] 23 | FailedToGetGraph(String), 24 | #[error("Failed to acquire graph node from Graph Store for resource + index {0}.")] 25 | FailedToGetGraphNode(String), 26 | #[error("Failed to archive graph from Graph Store for resource {0}.")] 27 | FailedToArchiveGraph(String), 28 | #[error("Failed to add tag to resource {0}.")] 29 | FailedToAddTag(String), 30 | #[error("Failed to remove tag from resource {0}.")] 31 | FailedToRemoveTag(String), 32 | #[error("Failed to add nodes to Graph Store for resource {0}.")] 33 | FailedToAddNodesToGraphStore(String), 34 | #[error("Failed to remove nodes from Graph Store for resource {0}.")] 35 | FailedToRemoveNodesFromGraphStore(String), 36 | #[error("Failed to remove graph from Graph Store for resource {0}.")] 37 | FailedToRemoveGraphFromGraphStore(String), 38 | #[error("Failed to create a Graph inside of Graph Store on connected ship for resource {0}")] 39 | FailedToCreateGraphInShip(String), 40 | #[error("Failed to build a Graph struct from supplied JsonValue.")] 41 | FailedToCreateGraphFromJSON, 42 | #[error("Failed to build a Node struct from supplied JsonValue.")] 43 | FailedToCreateGraphNodeFromJSON, 44 | #[error("Failed to insert a Node struct into a Graph because of the index.")] 45 | FailedToInsertGraphNode, 46 | #[error("The following graph node is not a valid Notebook Note node {0}")] 47 | InvalidNoteGraphNode(String), 48 | #[error("The following graph node is not a valid Collections Link node {0}")] 49 | InvalidLinkGraphNode(String), 50 | #[error("The following graph node index is not a valid Notebook Note node index {0}")] 51 | InvalidNoteGraphNodeIndex(String), 52 | #[error("Failed to create a Notebook Note from these nodes {0}")] 53 | FailedToCreateNote(String), 54 | #[error("Failed to create a Notebook Comment from these nodes {0}")] 55 | FailedToCreateComment(String), 56 | #[error("The following graph node index is not a valid Notebook Comment node index {0}")] 57 | InvalidCommentGraphNodeIndex(String), 58 | #[error("{0}")] 59 | Other(String), 60 | #[error(transparent)] 61 | ReqwestError(#[from] ReqError), 62 | } 63 | -------------------------------------------------------------------------------- /src/graph.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Result, UrbitAPIError}; 2 | use chrono::prelude::*; 3 | use json::{object, JsonValue}; 4 | use regex::Regex; 5 | 6 | /// Struct which represents a graph in Graph Store 7 | /// as a list of Nodes. 8 | #[derive(Clone, Debug)] 9 | pub struct Graph { 10 | /// List of nodes structured as a graph with children 11 | pub nodes: Vec, 12 | } 13 | 14 | /// Struct which represents a node in a graph in Graph Store 15 | #[derive(Clone, Debug)] 16 | pub struct Node { 17 | pub index: String, 18 | pub author: String, 19 | pub time_sent: u64, 20 | pub signatures: Vec, 21 | pub contents: NodeContents, 22 | pub hash: Option, 23 | pub children: Vec, 24 | } 25 | 26 | /// Struct which represents the contents inside of a node 27 | #[derive(Debug, Clone)] 28 | pub struct NodeContents { 29 | pub content_list: Vec, 30 | } 31 | 32 | /// Struct which represents a signature inside of a node 33 | #[derive(Debug, Clone)] 34 | pub struct Signature { 35 | signature: String, 36 | life: u64, 37 | ship: String, 38 | } 39 | 40 | impl Graph { 41 | /// Create a new `Graph` 42 | pub fn new(nodes: Vec) -> Graph { 43 | Graph { nodes: nodes } 44 | } 45 | 46 | /// Insert a `Node` into the top level of the `Graph`. 47 | pub fn insert(&mut self, node: Node) { 48 | self.nodes.push(node); 49 | } 50 | 51 | /// Convert from graph `JsonValue` to `Graph` 52 | pub fn from_json(graph_json: JsonValue) -> Result { 53 | // Create a new empty graph to insert nodes into 54 | let mut graph = Graph::new(vec![]); 55 | // Create a list of nodes all stripped of child associations 56 | let mut childless_nodes = vec![]; 57 | // Get the graph inner json 58 | let mut graph_text = format!("{}", graph_json["graph-update"]["add-graph"]["graph"]); 59 | if graph_text == "null" { 60 | graph_text = format!("{}", graph_json["graph-update"]["add-nodes"]["nodes"]); 61 | } 62 | 63 | // Check if the graph is valid but simply has no nodes 64 | if graph_text == "{}" { 65 | return Ok(Graph::new(vec![])); 66 | } 67 | 68 | // Create regex to capture each node json 69 | let re = Regex::new(r#"\d+":(.+?children":).+?"#) 70 | .map_err(|_| UrbitAPIError::FailedToCreateGraphFromJSON)?; 71 | // For each capture group, create a childless node 72 | for capture in re.captures_iter(&graph_text) { 73 | // Get the node json string without it's children 74 | let node_string = capture 75 | .get(1) 76 | .ok_or(UrbitAPIError::FailedToCreateGraphFromJSON)? 77 | .as_str() 78 | .to_string() 79 | + r#"null}"#; 80 | let json = json::parse(&node_string) 81 | .map_err(|_| UrbitAPIError::FailedToCreateGraphNodeFromJSON)?; 82 | // Skipping nodes which do not have a proper `post` value 83 | if json["post"].is_string() { 84 | continue; 85 | } 86 | // Finish processing node 87 | let processed_node_opt = Node::from_json(&json); 88 | if processed_node_opt.is_err() { 89 | println!("Failed to process graph node: \n{}", node_string); 90 | } 91 | childless_nodes.push(processed_node_opt?); 92 | } 93 | 94 | // Check if failed to extract nodes from json via Regex 95 | if childless_nodes.len() == 0 { 96 | return Err(UrbitAPIError::FailedToCreateGraphFromJSON); 97 | } 98 | 99 | // Create a placeholder node that accumulates all of the children 100 | // before being added to the graph 101 | let mut building_node = childless_nodes[0].clone(); 102 | // Insert all of the childless nodes into the graph 103 | // under the correct parent. 104 | for i in 1..childless_nodes.len() { 105 | if building_node.is_ancestor(&childless_nodes[i]) { 106 | // Add the child into the deepest depth possible and update building_node 107 | building_node = building_node.add_child(&childless_nodes[i]); 108 | } else { 109 | // Insert the finished `building_node` into the graph 110 | graph.insert(building_node.clone()); 111 | building_node = childless_nodes[i].clone(); 112 | } 113 | } 114 | // Add the final created `building_node` from the last 115 | // iteration of the for loop. 116 | graph.insert(building_node.clone()); 117 | 118 | // Return the finished graph 119 | Ok(graph) 120 | } 121 | 122 | // Converts to `JsonValue` 123 | pub fn to_json(&self) -> JsonValue { 124 | let nodes_json: Vec = self.nodes.iter().map(|n| n.to_json()).collect(); 125 | object! { 126 | "graph-update": { 127 | "add-graph": { 128 | "graph": nodes_json, 129 | } 130 | } 131 | } 132 | } 133 | } 134 | 135 | impl Node { 136 | // Create a new `Node` 137 | pub fn new( 138 | index: String, 139 | author: String, 140 | time_sent: u64, 141 | signatures: Vec, 142 | contents: NodeContents, 143 | hash: Option, 144 | ) -> Node { 145 | Node { 146 | index: index, 147 | author: author, 148 | time_sent: time_sent, 149 | signatures: signatures, 150 | contents: contents, 151 | hash: hash, 152 | children: vec![], 153 | } 154 | } 155 | 156 | /// Extract the node's final section (after the last `/`) of the index 157 | pub fn index_tail(&self) -> String { 158 | let split_index: Vec<&str> = self.index.split("/").collect(); 159 | split_index[split_index.len() - 1].to_string() 160 | } 161 | 162 | /// Extract the `Node`'s parent's index (if parent exists) 163 | pub fn parent_index(&self) -> Option { 164 | let rev_index = self.index.chars().rev().collect::(); 165 | let split_index: Vec<&str> = rev_index.splitn(2, "/").collect(); 166 | // Error check 167 | if split_index.len() < 2 { 168 | return None; 169 | } 170 | 171 | let parent_index = split_index[1].chars().rev().collect::(); 172 | 173 | Some(parent_index) 174 | } 175 | 176 | /// Check if a self is the direct parent of another `Node`. 177 | pub fn is_parent(&self, potential_child: &Node) -> bool { 178 | if let Some(index) = potential_child.parent_index() { 179 | return self.index == index; 180 | } 181 | false 182 | } 183 | 184 | /// Check if self is a parent (direct or indirect) of another `Node` 185 | pub fn is_ancestor(&self, potential_child: &Node) -> bool { 186 | let pc_split_index: Vec<&str> = potential_child.index.split("/").collect(); 187 | let parent_split_index: Vec<&str> = self.index.split("/").collect(); 188 | 189 | // Verify the parent has a smaller split index than child 190 | if parent_split_index.len() > pc_split_index.len() { 191 | return false; 192 | } 193 | 194 | // Check if every index split part of the parent is part of 195 | // the child 196 | let mut matching = false; 197 | for n in 0..parent_split_index.len() { 198 | matching = parent_split_index[n] == pc_split_index[n] 199 | } 200 | 201 | // Return if parent index is fully part of the child index 202 | matching 203 | } 204 | 205 | /// Creates a copy of self and searches through the children to find 206 | /// the deepest depth which the `new_child` can be placed. 207 | pub fn add_child(&self, new_child: &Node) -> Node { 208 | let mut new_self = self.clone(); 209 | for i in 0..self.children.len() { 210 | let child = &new_self.children[i]; 211 | if child.is_parent(new_child) { 212 | new_self.children[i].children.push(new_child.clone()); 213 | return new_self; 214 | } else if child.is_ancestor(new_child) { 215 | new_self.children[i] = child.add_child(new_child); 216 | return new_self; 217 | } 218 | } 219 | new_self.children.push(new_child.clone()); 220 | new_self 221 | } 222 | /// Formats the `time_sent` field to be human readable date-time in UTC 223 | pub fn time_sent_formatted(&self) -> String { 224 | let unix_time = self.time_sent as i64 / 1000; 225 | let date_time: DateTime = 226 | DateTime::from_utc(NaiveDateTime::from_timestamp(unix_time, 0), Utc); 227 | let new_date = date_time.format("%Y-%m-%d %H:%M:%S"); 228 | format!("{}", new_date) 229 | } 230 | 231 | /// Converts to `JsonValue` 232 | /// creates a json object with one field: key = the node's full index path, value = json representation of node 233 | pub fn to_json(&self) -> JsonValue { 234 | let mut node_json = object!(); 235 | node_json[self.index.clone()] = self.to_json_value(); 236 | node_json 237 | } 238 | 239 | /// Converts to `JsonValue` 240 | /// json representation of a node 241 | fn to_json_value(&self) -> JsonValue { 242 | let mut children = object!(); 243 | for child in &self.children { 244 | children[child.index_tail()] = child.to_json_value(); 245 | } 246 | 247 | let result_json = object! { 248 | "post": { 249 | "author": self.author.clone(), 250 | "index": self.index.clone(), 251 | "time-sent": self.time_sent, 252 | "contents": self.contents.to_json(), 253 | "hash": null, 254 | "signatures": [] 255 | }, 256 | "children": children 257 | }; 258 | result_json 259 | } 260 | 261 | /// Convert from node `JsonValue` which is wrapped up in a few wrapper fields 262 | /// into a `Node`, with children if they exist. 263 | pub fn from_graph_update_json(wrapped_json: &JsonValue) -> Result { 264 | let dumped = wrapped_json["graph-update"]["add-nodes"]["nodes"].dump(); 265 | let split: Vec<&str> = dumped.splitn(2, ":").collect(); 266 | if split.len() <= 1 { 267 | return Err(UrbitAPIError::FailedToCreateGraphNodeFromJSON); 268 | } 269 | 270 | let mut inner_string = split[1].to_string(); 271 | inner_string.remove(inner_string.len() - 1); 272 | 273 | let inner_json = json::parse(&inner_string) 274 | .map_err(|_| UrbitAPIError::FailedToCreateGraphNodeFromJSON)?; 275 | 276 | Self::from_json(&inner_json) 277 | } 278 | 279 | /// Convert from straight node `JsonValue` to `Node` 280 | pub fn from_json(json: &JsonValue) -> Result { 281 | // Process all of the json fields 282 | let children = json["children"].clone(); 283 | let post_json = json["post"].clone(); 284 | 285 | let index = post_json["index"] 286 | .as_str() 287 | .ok_or(UrbitAPIError::FailedToCreateGraphNodeFromJSON)?; 288 | let author = post_json["author"] 289 | .as_str() 290 | .ok_or(UrbitAPIError::FailedToCreateGraphNodeFromJSON)?; 291 | let time_sent = post_json["time-sent"] 292 | .as_u64() 293 | .ok_or(UrbitAPIError::FailedToCreateGraphNodeFromJSON)?; 294 | 295 | // Convert array JsonValue to vector for contents 296 | let mut json_contents = vec![]; 297 | for content in post_json["contents"].members() { 298 | json_contents.push(content.clone()); 299 | } 300 | let contents = NodeContents::from_json(json_contents); 301 | 302 | // Wrap hash in an Option for null case 303 | let hash = match post_json["contents"]["hash"].is_null() { 304 | true => None, 305 | false => Some( 306 | post_json["contents"]["hash"] 307 | .as_str() 308 | .ok_or(UrbitAPIError::FailedToCreateGraphNodeFromJSON)? 309 | .to_string(), 310 | ), 311 | }; 312 | 313 | // Convert array JsonValue to vector of Signatures 314 | let mut signatures = vec![]; 315 | for signature in post_json["signatures"].members() { 316 | let sig = Signature { 317 | signature: signature["signature"] 318 | .as_str() 319 | .ok_or(UrbitAPIError::FailedToCreateGraphNodeFromJSON)? 320 | .to_string(), 321 | life: signature["life"] 322 | .as_u64() 323 | .ok_or(UrbitAPIError::FailedToCreateGraphNodeFromJSON)?, 324 | ship: signature["ship"] 325 | .as_str() 326 | .ok_or(UrbitAPIError::FailedToCreateGraphNodeFromJSON)? 327 | .to_string(), 328 | }; 329 | 330 | signatures.push(sig); 331 | } 332 | 333 | // children 334 | let mut node_children: Vec = vec![]; 335 | if let JsonValue::Object(o) = children { 336 | for (_, val) in o.iter() { 337 | if let Ok(child_node) = Node::from_json(val) { 338 | node_children.push(child_node); 339 | } 340 | } 341 | } 342 | 343 | Ok(Node { 344 | index: index.to_string(), 345 | author: author.to_string(), 346 | time_sent: time_sent, 347 | signatures: signatures, 348 | contents: contents, 349 | hash: hash, 350 | children: node_children, 351 | }) 352 | } 353 | } 354 | 355 | /// Methods for `NodeContents` 356 | impl NodeContents { 357 | /// Create a new empty `NodeContents` 358 | pub fn new() -> NodeContents { 359 | NodeContents { 360 | content_list: vec![], 361 | } 362 | } 363 | 364 | /// Check if `NodeContents` is empty 365 | pub fn is_empty(&self) -> bool { 366 | self.content_list.len() == 0 367 | } 368 | 369 | /// Appends text to the end of the list of contents 370 | pub fn add_text(&self, text: &str) -> NodeContents { 371 | let formatted = object! { 372 | "text": text 373 | }; 374 | self.add_to_contents(formatted) 375 | } 376 | 377 | /// Appends a url to the end of the list of contents 378 | pub fn add_url(&self, url: &str) -> NodeContents { 379 | let formatted = object! { 380 | "url": url 381 | }; 382 | self.add_to_contents(formatted) 383 | } 384 | 385 | /// Appends a mention to another @p/ship to the end of the list of contents 386 | pub fn add_mention(&self, referenced_ship: &str) -> NodeContents { 387 | let formatted = object! { 388 | "mention": referenced_ship 389 | }; 390 | self.add_to_contents(formatted) 391 | } 392 | 393 | /// Appends a code block to the end of the list of contents 394 | pub fn add_code(&self, expression: &str, output: &str) -> NodeContents { 395 | let formatted = object! { 396 | "code": { 397 | "expression": expression, 398 | "output": [[output]] 399 | } 400 | }; 401 | self.add_to_contents(formatted) 402 | } 403 | 404 | /// Create a `NodeContents` from a list of `JsonValue`s 405 | pub fn from_json(json_contents: Vec) -> NodeContents { 406 | NodeContents { 407 | content_list: json_contents, 408 | } 409 | } 410 | 411 | /// Convert the `NodeContents` into a json array in a `JsonValue` 412 | pub fn to_json(&self) -> JsonValue { 413 | self.content_list.clone().into() 414 | } 415 | 416 | /// Convert the `NodeContents` into a single `String` that is formatted 417 | /// for human reading. 418 | pub fn to_formatted_string(&self) -> String { 419 | let mut result = "".to_string(); 420 | for item in &self.content_list { 421 | // Convert item into text 422 | let text = Self::extract_content_text(item); 423 | result = result + " " + text.trim(); 424 | } 425 | result 426 | } 427 | 428 | /// Converts the `NodeContents` into a `String` that is formatted 429 | /// for human reading, which is then split at every whitespace. 430 | /// Useful for parsing a message. 431 | pub fn to_formatted_words(&self) -> Vec { 432 | let formatted_string = self.to_formatted_string(); 433 | formatted_string 434 | .split_whitespace() 435 | .map(|s| s.to_string()) 436 | .collect() 437 | } 438 | 439 | /// Extracts content from a content list item `JsonValue` 440 | fn extract_content_text(json: &JsonValue) -> String { 441 | let mut result = " ".to_string(); 442 | if !json["text"].is_empty() { 443 | result = json["text"].dump(); 444 | } else if !json["url"].is_empty() { 445 | result = json["url"].dump(); 446 | } else if !json["mention"].is_empty() { 447 | result = json["mention"].dump(); 448 | result.remove(0); 449 | result.remove(result.len() - 1); 450 | return format!("~{}", result); 451 | } else if !json["code"].is_empty() { 452 | result = json["code"].dump(); 453 | } 454 | result.remove(0); 455 | result.remove(result.len() - 1); 456 | result 457 | } 458 | 459 | /// Internal method to append `JsonValue` to the end of the list of contents 460 | fn add_to_contents(&self, json: JsonValue) -> NodeContents { 461 | let mut contents = self.content_list.clone(); 462 | contents.append(&mut vec![json]); 463 | NodeContents { 464 | content_list: contents, 465 | } 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /src/graphstore.rs: -------------------------------------------------------------------------------- 1 | use crate::graph::{Graph, Node, NodeContents}; 2 | use crate::helper::{get_current_da_time, get_current_time, index_dec_to_ud}; 3 | use crate::{Channel, Result, UrbitAPIError}; 4 | use json::{object, JsonValue}; 5 | 6 | /// The type of module a given graph is. 7 | pub enum Module { 8 | Chat, 9 | Notebook, 10 | Collection, 11 | Null, 12 | } 13 | 14 | /// A struct which exposes Graph Store functionality 15 | pub struct GraphStore<'a> { 16 | pub channel: &'a mut Channel, 17 | } 18 | 19 | impl<'a> GraphStore<'a> { 20 | /// Create a new Graph Store node using defaults from the connected ship and local time. 21 | /// This is a wrapper method around `Node::new()` which fills out a lot of boilerplate. 22 | pub fn new_node(&self, contents: &NodeContents) -> Node { 23 | // Add the ~ to the ship name to be used within the post as author 24 | let ship = format!("~{}", self.channel.ship_interface.ship_name); 25 | // The index. For chat the default is current `@da` time as atom encoding with a `/` in front. 26 | let index = format!("/{}", get_current_da_time()); 27 | 28 | // Get the current Unix Time 29 | let unix_time = get_current_time(); 30 | 31 | Node::new( 32 | index, 33 | ship.clone(), 34 | unix_time, 35 | vec![], 36 | contents.clone(), 37 | None, 38 | ) 39 | } 40 | 41 | /// Create a new Graph Store node using a specified index and creation time 42 | /// using the connected ship as author 43 | pub fn new_node_specified( 44 | &self, 45 | node_index: &str, 46 | unix_time: u64, 47 | contents: &NodeContents, 48 | ) -> Node { 49 | // Add the ~ to the ship name to be used within the post as author 50 | let ship = format!("~{}", self.channel.ship_interface.ship_name); 51 | Node::new( 52 | node_index.to_string(), 53 | ship.clone(), 54 | unix_time, 55 | vec![], 56 | contents.clone(), 57 | None, 58 | ) 59 | } 60 | 61 | /// Add node to Graph Store 62 | pub fn add_node( 63 | &mut self, 64 | resource_ship: &str, 65 | resource_name: &str, 66 | node: &Node, 67 | ) -> Result<()> { 68 | let prepped_json = object! { 69 | "add-nodes": { 70 | "resource": { 71 | "ship": resource_ship, 72 | "name": resource_name 73 | }, 74 | "nodes": node.to_json() 75 | } 76 | }; 77 | 78 | let resp = (&mut self.channel).poke("graph-push-hook", "graph-update-3", &prepped_json)?; 79 | 80 | if resp.status().as_u16() == 204 { 81 | Ok(()) 82 | } else { 83 | return Err(UrbitAPIError::FailedToAddNodesToGraphStore( 84 | resource_name.to_string(), 85 | )); 86 | } 87 | } 88 | 89 | /// Add node to Graph Store via spider thread 90 | pub fn add_node_spider( 91 | &mut self, 92 | resource_ship: &str, 93 | resource_name: &str, 94 | node: &Node, 95 | ) -> Result<()> { 96 | let prepped_json = object! { 97 | "add-nodes": { 98 | "resource": { 99 | "ship": resource_ship, 100 | "name": resource_name 101 | }, 102 | "nodes": node.to_json() 103 | } 104 | }; 105 | 106 | let resp = self.channel.ship_interface.spider( 107 | "graph-update", 108 | "graph-view-action", 109 | "graph-add-nodes", 110 | &prepped_json, 111 | )?; 112 | 113 | if resp.status().as_u16() == 200 { 114 | Ok(()) 115 | } else { 116 | return Err(UrbitAPIError::FailedToAddNodesToGraphStore( 117 | resource_name.to_string(), 118 | )); 119 | } 120 | } 121 | 122 | /// Remove nodes from Graph Store using the provided list of indices 123 | pub fn remove_nodes( 124 | &mut self, 125 | resource_ship: &str, 126 | resource_name: &str, 127 | indices: Vec<&str>, 128 | ) -> Result<()> { 129 | let prepped_json = object! { 130 | "remove-nodes": { 131 | "resource": { 132 | "ship": resource_ship, 133 | "name": resource_name 134 | }, 135 | "indices": indices 136 | } 137 | }; 138 | 139 | let resp = (&mut self.channel).poke("graph-push-hook", "graph-update-3", &prepped_json)?; 140 | 141 | if resp.status().as_u16() == 204 { 142 | Ok(()) 143 | } else { 144 | return Err(UrbitAPIError::FailedToRemoveNodesFromGraphStore( 145 | resource_name.to_string(), 146 | )); 147 | } 148 | } 149 | 150 | /// Acquire a node from Graph Store 151 | pub fn get_node( 152 | &mut self, 153 | resource_ship: &str, 154 | resource_name: &str, 155 | node_index: &str, 156 | ) -> Result { 157 | let path_nodes = index_dec_to_ud(node_index); 158 | let path = format!("/node/{}/{}{}", resource_ship, resource_name, &path_nodes); 159 | let res = self 160 | .channel 161 | .ship_interface 162 | .scry("graph-store", &path, "json")?; 163 | 164 | // If successfully acquired node json 165 | if res.status().as_u16() == 200 { 166 | if let Ok(body) = res.text() { 167 | if let Ok(node_json) = json::parse(&body) { 168 | return Node::from_graph_update_json(&node_json); 169 | } 170 | } 171 | } 172 | // Else return error 173 | Err(UrbitAPIError::FailedToGetGraphNode(format!( 174 | "/{}/{}/{}", 175 | resource_ship, resource_name, node_index 176 | ))) 177 | } 178 | 179 | /// Acquire a subset of children of a node from Graph Store by specifying the start and end indices 180 | /// of the subset children. 181 | pub fn get_node_subset( 182 | &mut self, 183 | resource_ship: &str, 184 | resource_name: &str, 185 | node_index: &str, 186 | start_index: &str, 187 | end_index: &str, 188 | ) -> Result { 189 | let path = format!( 190 | "/node-children-subset/{}/{}/{}/{}/{}", 191 | resource_ship, resource_name, node_index, end_index, start_index 192 | ); 193 | let res = self 194 | .channel 195 | .ship_interface 196 | .scry("graph-store", &path, "json")?; 197 | 198 | // If successfully acquired node json 199 | if res.status().as_u16() == 200 { 200 | if let Ok(body) = res.text() { 201 | if let Ok(graph_json) = json::parse(&body) { 202 | return Graph::from_json(graph_json); 203 | } 204 | } 205 | } 206 | // Else return error 207 | Err(UrbitAPIError::FailedToGetGraph(resource_name.to_string())) 208 | } 209 | 210 | /// Create a new graph on the connected Urbit ship that is managed 211 | /// (meaning associated with a specific group) 212 | pub fn create_managed_graph( 213 | &mut self, 214 | graph_resource_name: &str, 215 | graph_title: &str, 216 | graph_description: &str, 217 | graph_module: Module, 218 | managed_group_ship: &str, 219 | managed_group_name: &str, 220 | ) -> Result<()> { 221 | let create_req = object! { 222 | "create": { 223 | "resource": { 224 | "ship": format!("~{}", &self.channel.ship_interface.ship_name), 225 | "name": graph_resource_name 226 | }, 227 | "title": graph_title, 228 | "description": graph_description, 229 | "associated": { 230 | "group": { 231 | "ship": managed_group_ship, 232 | "name": managed_group_name, 233 | }, 234 | }, 235 | "module": module_to_validator_string(&graph_module), 236 | "mark": module_to_mark(&graph_module) 237 | } 238 | }; 239 | 240 | let resp = self 241 | .channel 242 | .ship_interface 243 | .spider("graph-view-action", "json", "graph-create", &create_req) 244 | .unwrap(); 245 | 246 | if resp.status().as_u16() == 200 { 247 | Ok(()) 248 | } else { 249 | Err(UrbitAPIError::FailedToCreateGraphInShip( 250 | graph_resource_name.to_string(), 251 | )) 252 | } 253 | } 254 | 255 | /// Create a new graph on the connected Urbit ship that is unmanaged 256 | /// (meaning not associated with any group) 257 | pub fn create_unmanaged_graph( 258 | &mut self, 259 | graph_resource_name: &str, 260 | graph_title: &str, 261 | graph_description: &str, 262 | graph_module: Module, 263 | ) -> Result<()> { 264 | let create_req = object! { 265 | "create": { 266 | "resource": { 267 | "ship": self.channel.ship_interface.ship_name_with_sig(), 268 | "name": graph_resource_name 269 | }, 270 | "title": graph_title, 271 | "description": graph_description, 272 | "associated": { 273 | "policy": { 274 | "invite": { 275 | "pending": [] 276 | } 277 | } 278 | }, 279 | "module": module_to_validator_string(&graph_module), 280 | "mark": module_to_mark(&graph_module) 281 | } 282 | }; 283 | 284 | let resp = self 285 | .channel 286 | .ship_interface 287 | .spider("graph-view-action", "json", "graph-create", &create_req) 288 | .unwrap(); 289 | 290 | if resp.status().as_u16() == 200 { 291 | Ok(()) 292 | } else { 293 | Err(UrbitAPIError::FailedToCreateGraphInShip( 294 | graph_resource_name.to_string(), 295 | )) 296 | } 297 | } 298 | 299 | // /// Create a new graph on the connected Urbit ship that is unmanaged 300 | // /// (meaning not associated with any group) and "raw", meaning created 301 | // /// directly via poking graph-store and not set up to deal with networking 302 | // pub fn create_unmanaged_graph_raw(&mut self, graph_resource_name: &str) -> Result<()> { 303 | // // [%add-graph =resource =graph mark=(unit mark)] 304 | 305 | // let prepped_json = object! { 306 | // "add-graph": { 307 | // "resource": { 308 | // "ship": self.channel.ship_interface.ship_name_with_sig(), 309 | // "name": graph_resource_name 310 | // }, 311 | // "graph": "", 312 | // "mark": "", 313 | 314 | // } 315 | // }; 316 | 317 | // let resp = (&mut self.channel).poke("graph-store", "graph-update-3", &prepped_json)?; 318 | 319 | // if resp.status().as_u16() == 200 { 320 | // Ok(()) 321 | // } else { 322 | // Err(UrbitAPIError::FailedToCreateGraphInShip( 323 | // graph_resource_name.to_string(), 324 | // )) 325 | // } 326 | // } 327 | 328 | /// Acquire a graph from Graph Store 329 | pub fn get_graph(&mut self, resource_ship: &str, resource_name: &str) -> Result { 330 | let path = format!("/graph/{}/{}", resource_ship, resource_name); 331 | let res = self 332 | .channel 333 | .ship_interface 334 | .scry("graph-store", &path, "json")?; 335 | 336 | // If successfully acquired graph json 337 | if res.status().as_u16() == 200 { 338 | if let Ok(body) = res.text() { 339 | if let Ok(graph_json) = json::parse(&body) { 340 | return Graph::from_json(graph_json); 341 | } 342 | } 343 | } 344 | // Else return error 345 | Err(UrbitAPIError::FailedToGetGraph(resource_name.to_string())) 346 | } 347 | 348 | /// Acquire a subset of a graph from Graph Store by specifying the start and end indices 349 | /// of the subset of the graph. 350 | pub fn get_graph_subset( 351 | &mut self, 352 | resource_ship: &str, 353 | resource_name: &str, 354 | start_index: &str, 355 | end_index: &str, 356 | ) -> Result { 357 | let path = format!( 358 | "/graph-subset/{}/{}/{}/{}", 359 | resource_ship, resource_name, end_index, start_index 360 | ); 361 | let res = self 362 | .channel 363 | .ship_interface 364 | .scry("graph-store", &path, "json")?; 365 | 366 | // If successfully acquired graph json 367 | if res.status().as_u16() == 200 { 368 | if let Ok(body) = res.text() { 369 | if let Ok(graph_json) = json::parse(&body) { 370 | return Graph::from_json(graph_json); 371 | } 372 | } 373 | } 374 | // Else return error 375 | Err(UrbitAPIError::FailedToGetGraph(resource_name.to_string())) 376 | } 377 | 378 | /// Delete graph from Graph Store 379 | pub fn delete_graph(&mut self, resource_ship: &str, resource_name: &str) -> Result<()> { 380 | let prepped_json = object! { 381 | "delete": { 382 | "resource": { 383 | "ship": resource_ship, 384 | "name": resource_name 385 | } 386 | } 387 | }; 388 | 389 | let resp = 390 | (&mut self.channel).poke("graph-view-action", "graph-update-3", &prepped_json)?; 391 | 392 | if resp.status().as_u16() == 204 { 393 | Ok(()) 394 | } else { 395 | return Err(UrbitAPIError::FailedToRemoveGraphFromGraphStore( 396 | resource_name.to_string(), 397 | )); 398 | } 399 | } 400 | 401 | /// Leave graph in Graph Store 402 | pub fn leave_graph(&mut self, resource_ship: &str, resource_name: &str) -> Result<()> { 403 | let prepped_json = object! { 404 | "leave": { 405 | "resource": { 406 | "ship": resource_ship, 407 | "name": resource_name 408 | } 409 | } 410 | }; 411 | 412 | let resp = 413 | (&mut self.channel).poke("graph-view-action", "graph-update-3", &prepped_json)?; 414 | 415 | if resp.status().as_u16() == 204 { 416 | Ok(()) 417 | } else { 418 | return Err(UrbitAPIError::FailedToRemoveGraphFromGraphStore( 419 | resource_name.to_string(), 420 | )); 421 | } 422 | } 423 | 424 | /// Archive a graph in Graph Store 425 | pub fn archive_graph(&mut self, resource_ship: &str, resource_name: &str) -> Result { 426 | let path = format!("/archive/{}/{}", resource_ship, resource_name); 427 | let res = self 428 | .channel 429 | .ship_interface 430 | .scry("graph-store", &path, "json")?; 431 | 432 | if res.status().as_u16() == 200 { 433 | if let Ok(body) = res.text() { 434 | return Ok(body); 435 | } 436 | } 437 | return Err(UrbitAPIError::FailedToArchiveGraph( 438 | resource_name.to_string(), 439 | )); 440 | } 441 | 442 | /// Unarchive a graph in Graph Store 443 | pub fn unarchive_graph(&mut self, resource_ship: &str, resource_name: &str) -> Result { 444 | let path = format!("/unarchive/{}/{}", resource_ship, resource_name); 445 | let res = self 446 | .channel 447 | .ship_interface 448 | .scry("graph-store", &path, "json")?; 449 | 450 | if res.status().as_u16() == 200 { 451 | if let Ok(body) = res.text() { 452 | return Ok(body); 453 | } 454 | } 455 | return Err(UrbitAPIError::FailedToArchiveGraph( 456 | resource_name.to_string(), 457 | )); 458 | } 459 | 460 | /// Add a tag to a graph 461 | pub fn add_tag(&mut self, resource_ship: &str, resource_name: &str, tag: &str) -> Result<()> { 462 | let prepped_json = object! { 463 | "add-tag": { 464 | "resource": { 465 | "ship": resource_ship, 466 | "name": resource_name 467 | }, 468 | "term": tag 469 | } 470 | }; 471 | 472 | let resp = (&mut self.channel).poke("graph-push-hook", "graph-update-3", &prepped_json)?; 473 | 474 | if resp.status().as_u16() == 204 { 475 | Ok(()) 476 | } else { 477 | return Err(UrbitAPIError::FailedToAddTag(resource_name.to_string())); 478 | } 479 | } 480 | 481 | /// Remove a tag from a graph 482 | pub fn remove_tag( 483 | &mut self, 484 | resource_ship: &str, 485 | resource_name: &str, 486 | tag: &str, 487 | ) -> Result<()> { 488 | let prepped_json = object! { 489 | "remove-tag": { 490 | "resource": { 491 | "ship": resource_ship, 492 | "name": resource_name 493 | }, 494 | "term": tag 495 | } 496 | }; 497 | 498 | let resp = (&mut self.channel).poke("graph-push-hook", "graph-update-3", &prepped_json)?; 499 | 500 | if resp.status().as_u16() == 204 { 501 | Ok(()) 502 | } else { 503 | return Err(UrbitAPIError::FailedToRemoveTag(resource_name.to_string())); 504 | } 505 | } 506 | 507 | /// Performs a scry to get all keys 508 | pub fn get_keys(&mut self) -> Result> { 509 | let resp = self 510 | .channel 511 | .ship_interface 512 | .scry("graph-store", "/keys", "json")?; 513 | 514 | if resp.status().as_u16() == 200 { 515 | let json_text = resp.text()?; 516 | if let Ok(json) = json::parse(&json_text) { 517 | let keys = json["graph-update"]["keys"].clone(); 518 | let mut keys_list = vec![]; 519 | for key in keys.members() { 520 | keys_list.push(key.clone()) 521 | } 522 | return Ok(keys_list); 523 | } 524 | } 525 | return Err(UrbitAPIError::FailedToFetchKeys); 526 | } 527 | 528 | /// Performs a scry to get all tags 529 | pub fn get_tags(&mut self) -> Result> { 530 | let resp = self 531 | .channel 532 | .ship_interface 533 | .scry("graph-store", "/tags", "json")?; 534 | 535 | if resp.status().as_u16() == 200 { 536 | let json_text = resp.text()?; 537 | if let Ok(json) = json::parse(&json_text) { 538 | let tags = json["graph-update"]["tags"].clone(); 539 | let mut tags_list = vec![]; 540 | for tag in tags.members() { 541 | tags_list.push(tag.clone()) 542 | } 543 | return Ok(tags_list); 544 | } 545 | } 546 | return Err(UrbitAPIError::FailedToFetchTags); 547 | } 548 | 549 | /// Performs a scry to get all tags 550 | pub fn get_tag_queries(&mut self) -> Result> { 551 | let resp = self 552 | .channel 553 | .ship_interface 554 | .scry("graph-store", "/tag-queries", "json")?; 555 | 556 | if resp.status().as_u16() == 200 { 557 | let json_text = resp.text()?; 558 | if let Ok(json) = json::parse(&json_text) { 559 | let tags = json["graph-update"]["tag-queries"].clone(); 560 | let mut tags_list = vec![]; 561 | for tag in tags.members() { 562 | tags_list.push(tag.clone()) 563 | } 564 | return Ok(tags_list); 565 | } 566 | } 567 | return Err(UrbitAPIError::FailedToFetchTags); 568 | } 569 | 570 | /// Acquire the time the update log of a given resource was last updated 571 | pub fn peek_update_log(&mut self, resource_ship: &str, resource_name: &str) -> Result { 572 | let path = format!("/peek-update-log/{}/{}", resource_ship, resource_name); 573 | let res = self 574 | .channel 575 | .ship_interface 576 | .scry("graph-store", &path, "json")?; 577 | 578 | // If successfully acquired node json 579 | if res.status().as_u16() == 200 { 580 | if let Ok(body) = res.text() { 581 | return Ok(body); 582 | } 583 | } 584 | // Else return error 585 | Err(UrbitAPIError::FailedToGetGraph(resource_name.to_string())) 586 | } 587 | 588 | /// Acquire the update log for a given resource 589 | pub fn get_update_log(&mut self, resource_ship: &str, resource_name: &str) -> Result { 590 | let path = format!("/update-log/{}/{}", resource_ship, resource_name); 591 | let res = self 592 | .channel 593 | .ship_interface 594 | .scry("graph-store", &path, "json")?; 595 | 596 | // If successfully acquired node json 597 | if res.status().as_u16() == 200 { 598 | if let Ok(body) = res.text() { 599 | return Ok(body); 600 | } 601 | } 602 | // Else return error 603 | Err(UrbitAPIError::FailedToGetGraph(resource_name.to_string())) 604 | } 605 | 606 | /// Acquire a subset of the update log for a given resource 607 | pub fn get_update_log_subset( 608 | &mut self, 609 | resource_ship: &str, 610 | resource_name: &str, 611 | start_index: &str, 612 | end_index: &str, 613 | ) -> Result { 614 | let path = format!( 615 | "/update-log-subset/{}/{}/{}/{}", 616 | resource_ship, resource_name, end_index, start_index 617 | ); 618 | let res = self 619 | .channel 620 | .ship_interface 621 | .scry("graph-store", &path, "json")?; 622 | 623 | // If successfully acquired node json 624 | if res.status().as_u16() == 200 { 625 | if let Ok(body) = res.text() { 626 | return Ok(body); 627 | } 628 | } 629 | // Else return error 630 | Err(UrbitAPIError::FailedToGetUpdateLog( 631 | resource_name.to_string(), 632 | )) 633 | } 634 | } 635 | 636 | pub fn module_to_validator_string(module: &Module) -> String { 637 | match module { 638 | Module::Chat => "graph-validator-chat".to_string(), 639 | Module::Notebook => "graph-validator-publish".to_string(), 640 | Module::Collection => "graph-validator-link".to_string(), 641 | Module::Null => "".to_string(), 642 | } 643 | } 644 | 645 | pub fn module_to_mark(module: &Module) -> String { 646 | match module { 647 | Module::Chat => "chat".to_string(), 648 | Module::Notebook => "publish".to_string(), 649 | Module::Collection => "link".to_string(), 650 | Module::Null => "".to_string(), 651 | } 652 | } 653 | -------------------------------------------------------------------------------- /src/helper.rs: -------------------------------------------------------------------------------- 1 | use std::time::{SystemTime, UNIX_EPOCH}; 2 | 3 | // `@ud` ~1970.1.1 4 | static DA_UNIX_EPOCH: u128 = 170141184475152167957503069145530368000; 5 | // `@ud` ~s1 6 | static DA_SECOND: u128 = 18446744073709551616; 7 | 8 | /// Convert from Unix time in milliseconds to Urbit `@da` time 9 | pub fn unix_time_to_da(unix_time: u64) -> u128 { 10 | let time_since_epoch = (unix_time as u128 * DA_SECOND) / 1000; 11 | DA_UNIX_EPOCH + time_since_epoch 12 | } 13 | 14 | /// Acquire the current time as u64 15 | pub fn get_current_time() -> u64 { 16 | SystemTime::now() 17 | .duration_since(UNIX_EPOCH) 18 | .unwrap() 19 | .as_millis() as u64 20 | } 21 | 22 | /// Acquire the current time in Urbit `@da` encoding 23 | pub fn get_current_da_time() -> u128 { 24 | let unix_time = get_current_time(); 25 | unix_time_to_da(unix_time) 26 | } 27 | 28 | /// Encode an index path into urbit ud format 29 | /// /12345678901234/1/10987654321 -> /12.345.678.901.234/1/10.987.654.321 30 | pub fn index_dec_to_ud(index: &str) -> String { 31 | // Split the index 32 | let index_split: Vec<&str> = index.split("/").collect(); 33 | let mut udindex = String::new(); 34 | // Handle each segment 35 | for i in 0..index_split.len() { 36 | if index_split[i].len() > 0 { 37 | let mut rev: String = index_split[i].chars().rev().collect(); 38 | let mut out = String::new(); 39 | while rev.len() >= 3 { 40 | let chunk: String = rev.drain(..3).collect(); 41 | out += &chunk; 42 | if rev.len() > 0 { 43 | out += "."; 44 | } 45 | } 46 | out += &rev; 47 | let seg: String = out.chars().rev().collect(); 48 | udindex += &format!("/{}", &seg); 49 | } 50 | } 51 | udindex 52 | } 53 | -------------------------------------------------------------------------------- /src/interface.rs: -------------------------------------------------------------------------------- 1 | use crate::channel::Channel; 2 | use crate::error::{Result, UrbitAPIError}; 3 | use json::JsonValue; 4 | use reqwest::blocking::{Client, Response}; 5 | use reqwest::header::{HeaderValue, COOKIE}; 6 | 7 | // The struct which holds the details for connecting to a given Urbit ship 8 | #[derive(Debug, Clone)] 9 | pub struct ShipInterface { 10 | /// The URL of the ship given as `http://ip:port` such as 11 | /// `http://0.0.0.0:8080`. 12 | pub url: String, 13 | /// The session auth string header value 14 | pub session_auth: HeaderValue, 15 | /// The ship name (without a leading ~) 16 | pub ship_name: String, 17 | /// The Reqwest `Client` to be reused for making requests 18 | req_client: Client, 19 | } 20 | 21 | impl ShipInterface { 22 | /// Logs into the given ship and creates a new `ShipInterface`. 23 | /// `ship_url` should be `http://ip:port` of the given ship. Example: 24 | /// `http://0.0.0.0:8080`. `ship_code` is the code acquire from your ship 25 | /// by typing `+code` in dojo. 26 | pub fn new(ship_url: &str, ship_code: &str) -> Result { 27 | let client = Client::new(); 28 | let login_url = format!("{}/~/login", ship_url); 29 | let resp = client 30 | .post(&login_url) 31 | .body("password=".to_string() + &ship_code) 32 | .send()?; 33 | 34 | // Check for status code 35 | if resp.status().as_u16() != 204 { 36 | return Err(UrbitAPIError::FailedToLogin); 37 | } 38 | 39 | // Acquire the session auth header value 40 | let session_auth = resp 41 | .headers() 42 | .get("set-cookie") 43 | .ok_or(UrbitAPIError::FailedToLogin)?; 44 | 45 | // Convert sessions auth to a string 46 | let auth_string = session_auth 47 | .to_str() 48 | .map_err(|_| UrbitAPIError::FailedToLogin)?; 49 | 50 | // Trim the auth string to acquire the ship name 51 | let end_pos = auth_string.find('=').ok_or(UrbitAPIError::FailedToLogin)?; 52 | let ship_name = &auth_string[9..end_pos]; 53 | 54 | Ok(ShipInterface { 55 | url: ship_url.to_string(), 56 | session_auth: session_auth.clone(), 57 | ship_name: ship_name.to_string(), 58 | req_client: client, 59 | }) 60 | } 61 | 62 | /// Returns the ship name with a leading `~` (By default ship_name does not have one) 63 | pub fn ship_name_with_sig(&self) -> String { 64 | format!("~{}", self.ship_name) 65 | } 66 | 67 | /// Create a `Channel` using this `ShipInterface` 68 | pub fn create_channel(&self) -> Result { 69 | Channel::new(self.clone()) 70 | } 71 | 72 | // Send a put request using the `ShipInterface` 73 | pub fn send_put_request(&self, url: &str, body: &JsonValue) -> Result { 74 | let json = body.dump(); 75 | let resp = self 76 | .req_client 77 | .put(url) 78 | .header(COOKIE, self.session_auth.clone()) 79 | .header("Content-Type", "application/json") 80 | .body(json); 81 | 82 | Ok(resp.send()?) 83 | } 84 | 85 | /// Sends a scry to the ship 86 | pub fn scry(&self, app: &str, path: &str, mark: &str) -> Result { 87 | let scry_url = format!("{}/~/scry/{}{}.{}", self.url, app, path, mark); 88 | let resp = self 89 | .req_client 90 | .get(&scry_url) 91 | .header(COOKIE, self.session_auth.clone()) 92 | .header("Content-Type", "application/json"); 93 | 94 | Ok(resp.send()?) 95 | } 96 | 97 | /// Run a thread via spider 98 | pub fn spider( 99 | &self, 100 | input_mark: &str, 101 | output_mark: &str, 102 | thread_name: &str, 103 | body: &JsonValue, 104 | ) -> Result { 105 | let json = body.dump(); 106 | let spider_url = format!( 107 | "{}/spider/{}/{}/{}.json", 108 | self.url, input_mark, thread_name, output_mark 109 | ); 110 | 111 | let resp = self 112 | .req_client 113 | .post(&spider_url) 114 | .header(COOKIE, self.session_auth.clone()) 115 | .header("Content-Type", "application/json") 116 | .body(json); 117 | 118 | Ok(resp.send()?) 119 | } 120 | } 121 | 122 | impl Default for ShipInterface { 123 | fn default() -> Self { 124 | ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap() 125 | } 126 | } 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use super::*; 131 | use crate::subscription::Subscription; 132 | use json::object; 133 | #[test] 134 | // Verify that we can login to a local `~zod` dev ship. 135 | fn can_login() { 136 | let ship_interface = 137 | ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap(); 138 | } 139 | 140 | #[test] 141 | // Verify that we can create a channel 142 | fn can_create_channel() { 143 | let ship_interface = 144 | ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap(); 145 | let channel = ship_interface.create_channel().unwrap(); 146 | channel.delete_channel(); 147 | } 148 | 149 | #[test] 150 | // Verify that we can create a channel 151 | fn can_subscribe() { 152 | let ship_interface = 153 | ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap(); 154 | let mut channel = ship_interface.create_channel().unwrap(); 155 | channel 156 | .create_new_subscription("chat-view", "/primary") 157 | .unwrap(); 158 | 159 | channel.find_subscription("chat-view", "/primary"); 160 | channel.unsubscribe("chat-view", "/primary"); 161 | channel.delete_channel(); 162 | } 163 | 164 | #[test] 165 | // Verify that we can make a poke 166 | fn can_poke() { 167 | let ship_interface = 168 | ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap(); 169 | let mut channel = ship_interface.create_channel().unwrap(); 170 | let poke_res = channel 171 | .poke("hood", "helm-hi", &"A poke has been made".into()) 172 | .unwrap(); 173 | assert!(poke_res.status().as_u16() == 204); 174 | channel.delete_channel(); 175 | } 176 | 177 | #[test] 178 | // Verify we can scry 179 | fn can_scry() { 180 | let mut ship_interface = 181 | ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap(); 182 | let scry_res = ship_interface.scry("graph-store", "/keys", "json").unwrap(); 183 | 184 | assert!(scry_res.status().as_u16() == 200); 185 | } 186 | 187 | #[test] 188 | // Verify we can run threads 189 | fn can_spider() { 190 | let mut ship_interface = 191 | ShipInterface::new("http://0.0.0.0:8080", "lidlut-tabwed-pillex-ridrup").unwrap(); 192 | let create_req = object! { 193 | "create": { 194 | "resource": { 195 | "ship": "~zod", 196 | "name": "test", 197 | }, 198 | "title": "Testing creation", 199 | "description": "test", 200 | "associated": { 201 | "policy": { 202 | "invite": { 203 | "pending": [] 204 | } 205 | } 206 | }, 207 | "module": "chat", 208 | "mark": "graph-validator-chat" 209 | } 210 | }; 211 | 212 | let spider_res = ship_interface 213 | .spider("graph-view-action", "json", "graph-create", &create_req) 214 | .unwrap(); 215 | 216 | assert!(spider_res.status().as_u16() == 200); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod apps; 2 | pub mod channel; 3 | pub mod error; 4 | pub mod graph; 5 | pub mod graphstore; 6 | pub mod helper; 7 | pub mod interface; 8 | pub mod local_config; 9 | pub mod subscription; 10 | pub mod traits; 11 | 12 | pub use apps::collections::{Collection, Link}; 13 | pub use apps::harkstore::HarkStore; 14 | pub use apps::invitestore::InviteStore; 15 | pub use apps::notebook::Note; 16 | pub use channel::Channel; 17 | pub use error::{Result, UrbitAPIError}; 18 | pub use graph::{Graph, Node, NodeContents}; 19 | pub use graphstore::Module; 20 | pub use helper::get_current_da_time; 21 | pub use interface::ShipInterface; 22 | pub use local_config::{ 23 | create_new_ship_config_file, default_cli_ship_interface_setup, ship_interface_from_config, 24 | ship_interface_from_local_config, 25 | }; 26 | pub use subscription::Subscription; 27 | pub use traits::messaging::{AuthoredMessage, Message, Messaging}; 28 | -------------------------------------------------------------------------------- /src/local_config.rs: -------------------------------------------------------------------------------- 1 | use crate::ShipInterface; 2 | use std::fs::File; 3 | use std::io::prelude::*; 4 | use std::path::Path; 5 | use yaml_rust::{Yaml, YamlLoader}; 6 | 7 | static BAREBONES_SHIP_CONFIG_YAML: &str = r#" 8 | # IP Address of your Urbit ship (default is local) 9 | ship_ip: "0.0.0.0" 10 | # Port that the ship is on 11 | ship_port: "8080" 12 | # The `+code` of your ship 13 | ship_code: "lidlut-tabwed-pillex-ridrup" 14 | "#; 15 | 16 | /// Attempts to create a new `ship_config.yaml` with the barebones yaml inside. 17 | /// Returns `None` if file already exists. 18 | pub fn create_new_ship_config_file() -> Option<()> { 19 | let file_path = Path::new("ship_config.yaml"); 20 | if file_path.exists() == false { 21 | let mut file = File::create(file_path).ok()?; 22 | file.write_all(&BAREBONES_SHIP_CONFIG_YAML.to_string().into_bytes()) 23 | .ok()?; 24 | return Some(()); 25 | } 26 | None 27 | } 28 | 29 | /// Based on the provided input config yaml, create a ShipInterface 30 | fn ship_interface_from_yaml(config: Yaml) -> Option { 31 | let ip = config["ship_ip"].as_str()?; 32 | let port = config["ship_port"].as_str()?; 33 | let url = format!("http://{}:{}", ip, port); 34 | let code = config["ship_code"].as_str()?; 35 | 36 | ShipInterface::new(&url, code).ok() 37 | } 38 | 39 | /// Opens a local `ship_config.yaml` file and uses the 40 | /// data inside to create a `ShipInterface` 41 | pub fn ship_interface_from_local_config() -> Option { 42 | ship_interface_from_config("ship_config.yaml") 43 | } 44 | 45 | /// Opens the yaml file specified by `path_to_file` and uses the 46 | /// data inside to create a `ShipInterface` 47 | pub fn ship_interface_from_config(path_to_file: &str) -> Option { 48 | let yaml_str = std::fs::read_to_string(path_to_file).ok()?; 49 | let yaml = YamlLoader::load_from_str(&yaml_str).ok()?[0].clone(); 50 | ship_interface_from_yaml(yaml) 51 | } 52 | 53 | /// A function for CLI apps which first attempts to create a new local ship config file if one does not exist and exits with a helpful message. 54 | /// If a config does exist, then it tries to connect to the Urbit Ship specified in the config. 55 | /// If connection fails then prints a message telling the user to check their local config. 56 | pub fn default_cli_ship_interface_setup() -> ShipInterface { 57 | if let Some(_) = create_new_ship_config_file() { 58 | println!("Ship configuration file created. Please edit `ship_config.yaml` with your ship info and restart the application."); 59 | std::process::exit(0); 60 | } 61 | if let Some(ship) = ship_interface_from_local_config() { 62 | return ship; 63 | } 64 | println!("Failed to connect to Ship using information from local config."); 65 | std::process::exit(1); 66 | } 67 | -------------------------------------------------------------------------------- /src/subscription.rs: -------------------------------------------------------------------------------- 1 | use eventsource_threaded::event::Event; 2 | use json; 3 | 4 | // ID of the message that created a `Subscription` 5 | pub type CreationID = u64; 6 | 7 | // A subscription on a given Channel 8 | #[derive(Debug, Clone)] 9 | pub struct Subscription { 10 | /// The uid of the channel this subscription was made in 11 | pub channel_uid: String, 12 | /// The id of the message that created this subscription 13 | pub creation_id: CreationID, 14 | /// The app that is being subscribed to 15 | pub app: String, 16 | /// The path of the app being subscribed to 17 | pub path: String, 18 | // A list of messages from the given subscription. 19 | pub message_list: Vec, 20 | } 21 | 22 | impl Subscription { 23 | /// Verifies if the id of the message id in the event matches thea 24 | /// `Subscription` `creation_id`. 25 | fn event_matches(&self, event: &Event) -> bool { 26 | if let Some(json) = &json::parse(&event.data).ok() { 27 | return self.creation_id.to_string() == json["id"].dump(); 28 | } 29 | false 30 | } 31 | 32 | /// Parses an event and adds it to the message list if it's id 33 | /// matches the `Subscription` `creation_id`. On success returns 34 | /// the length of the message list. 35 | pub fn add_to_message_list(&mut self, event: &Event) -> Option { 36 | if self.event_matches(&event) { 37 | let json = &json::parse(&event.data).ok()?["json"]; 38 | if !json.is_null() { 39 | self.message_list.push(json.dump()); 40 | return Some(self.message_list.len() as u64); 41 | } 42 | } 43 | None 44 | } 45 | 46 | /// Pops a message from the front of `Subscription`'s `message_list`. 47 | /// If no messages are left, returns `None`. 48 | pub fn pop_message(&mut self) -> Option { 49 | if self.message_list.len() == 0 { 50 | return None; 51 | } 52 | let messages = self.message_list.clone(); 53 | let (head, tail) = messages.split_at(1); 54 | self.message_list = tail.to_vec(); 55 | Some(head.to_owned()[0].clone()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/traits/messaging.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Result, UrbitAPIError}; 2 | use crate::graph::{Node, NodeContents}; 3 | use crate::Channel; 4 | use crossbeam::channel::{unbounded, Receiver}; 5 | use json::JsonValue; 6 | use std::thread; 7 | use std::time::Duration; 8 | 9 | /// A struct that represents a message that is to be submitted to Urbit. 10 | /// `Message` provides methods to build a message in chunks, thereby allowing you 11 | /// to add content which needs to be parsed, for example links @p mentions. 12 | /// It is technically an alias for the `NodeContents` struct. 13 | pub type Message = NodeContents; 14 | 15 | /// A `Message` with the author @p, post time and index also included 16 | #[derive(Clone, Debug)] 17 | pub struct AuthoredMessage { 18 | pub author: String, 19 | pub contents: Message, 20 | pub time_sent: String, 21 | pub index: String, 22 | } 23 | 24 | impl AuthoredMessage { 25 | /// Create a new `AuthoredMessage` 26 | pub fn new(author: &str, contents: &Message, time_sent: &str, index: &str) -> Self { 27 | AuthoredMessage { 28 | author: author.to_string(), 29 | contents: contents.clone(), 30 | time_sent: time_sent.to_string(), 31 | index: index.to_string(), 32 | } 33 | } 34 | /// Parses a `Node` into `Self` 35 | pub fn from_node(node: &Node) -> Self { 36 | Self::new( 37 | &node.author, 38 | &node.contents, 39 | &node.time_sent_formatted(), 40 | &node.index, 41 | ) 42 | } 43 | 44 | /// Converts self into a human readable formatted string which 45 | /// includes the author, date, and node contents. 46 | pub fn to_formatted_string(&self) -> String { 47 | let content = self.contents.to_formatted_string(); 48 | format!("{} - ~{}:{}", self.time_sent, self.author, content) 49 | } 50 | } 51 | 52 | /// A trait which wraps both chats & DMs. 53 | pub trait Messaging { 54 | /// Returns the reference to the Channel being used 55 | fn channel(&mut self) -> &mut Channel; 56 | 57 | /// Send a message to an Urbit chat/DM. 58 | /// Returns the index of the node that was added to Graph Store. 59 | fn send_message( 60 | &mut self, 61 | resource_ship: &str, 62 | resource_name: &str, 63 | message: &Message, 64 | ) -> Result { 65 | let node = self.channel().graph_store().new_node(message); 66 | 67 | if let Ok(_) = self 68 | .channel() 69 | .graph_store() 70 | .add_node(resource_ship, resource_name, &node) 71 | { 72 | Ok(node.index) 73 | } else { 74 | Err(UrbitAPIError::FailedToSendChatMessage( 75 | message.to_json().dump(), 76 | )) 77 | } 78 | } 79 | 80 | /// Extracts messages automatically into a list of formatted `String`s 81 | fn export_message_log( 82 | &mut self, 83 | resource_ship: &str, 84 | resource_name: &str, 85 | ) -> Result> { 86 | let mut export_log = vec![]; 87 | let authored_messages = self.export_authored_messages(resource_ship, resource_name)?; 88 | 89 | for am in authored_messages { 90 | if !am.contents.is_empty() { 91 | export_log.push(am.to_formatted_string()); 92 | } 93 | } 94 | 95 | Ok(export_log) 96 | } 97 | 98 | /// Extracts messages as `AuthoredMessage`s 99 | fn export_authored_messages( 100 | &mut self, 101 | resource_ship: &str, 102 | resource_name: &str, 103 | ) -> Result> { 104 | let mut authored_messages = vec![]; 105 | let nodes = self.export_message_nodes(resource_ship, resource_name)?; 106 | 107 | for node in nodes { 108 | if !node.contents.is_empty() { 109 | let authored_message = AuthoredMessage::from_node(&node); 110 | authored_messages.push(authored_message); 111 | } 112 | } 113 | 114 | Ok(authored_messages) 115 | } 116 | 117 | /// Extracts a message nodes 118 | fn export_message_nodes( 119 | &mut self, 120 | resource_ship: &str, 121 | resource_name: &str, 122 | ) -> Result> { 123 | let messages_graph = &self 124 | .channel() 125 | .graph_store() 126 | .get_graph(resource_ship, resource_name)?; 127 | 128 | let mut nodes = messages_graph.clone().nodes; 129 | nodes.sort_by(|a, b| a.time_sent.cmp(&b.time_sent)); 130 | 131 | Ok(nodes) 132 | } 133 | 134 | /// Subscribe to and watch for messages. This method returns a `Receiver` with the 135 | /// `AuthoredMessage`s that are posted after subscribing. Simply call `receiver.try_recv()` 136 | /// to read the next `AuthoredMessage` if one has been posted. 137 | /// 138 | /// Technical Note: This method actually creates a new `Channel` with your Urbit Ship, and spawns a new unix thread 139 | /// locally that processes all messages on said channel. This is required due to borrowing mechanisms in Rust, however 140 | /// on the plus side this makes it potentially more performant by each subscription having it's own unix thread. 141 | fn subscribe_to_messages( 142 | &mut self, 143 | resource_ship: &str, 144 | resource_name: &str, 145 | ) -> Result> { 146 | let resource_ship = resource_ship.to_string(); 147 | let resource_name = resource_name.to_string(); 148 | // Create sender/receiver 149 | let (s, r) = unbounded(); 150 | // Creating a new Ship Interface Channel to pass into the new thread 151 | // to be used to communicate with the Urbit ship 152 | let mut new_channel = self.channel().ship_interface.create_channel()?; 153 | 154 | thread::spawn(move || { 155 | // Infinitely watch for new graph store updates 156 | let channel = &mut new_channel; 157 | channel 158 | .create_new_subscription("graph-store", "/updates") 159 | .ok(); 160 | loop { 161 | channel.parse_event_messages(); 162 | let res_graph_updates = &mut channel.find_subscription("graph-store", "/updates"); 163 | if let Some(graph_updates) = res_graph_updates { 164 | // Read all of the current SSE messages to find if any are for the resource 165 | // we are looking for. 166 | loop { 167 | let pop_res = graph_updates.pop_message(); 168 | // Acquire the message 169 | if let Some(mess) = &pop_res { 170 | // Parse it to json 171 | if let Ok(json) = json::parse(mess) { 172 | // If the graph-store node update is not for the correct resource 173 | // then continue to next message. 174 | if !check_resource_json(&resource_ship, &resource_name, &json) { 175 | continue; 176 | } 177 | // Otherwise, parse json to a `Node` 178 | if let Ok(node) = Node::from_graph_update_json(&json) { 179 | // Parse it as an `AuthoredMessage` 180 | let authored_message = AuthoredMessage::from_node(&node); 181 | let _ = s.send(authored_message); 182 | } 183 | } 184 | } 185 | // If no messages left, stop 186 | if let None = &pop_res { 187 | break; 188 | } 189 | } 190 | } 191 | // Pause for half a second 192 | thread::sleep(Duration::new(0, 500000000)); 193 | } 194 | }); 195 | Ok(r) 196 | } 197 | } 198 | 199 | /// Checks whether the resource json matches the resource_name & resource_ship 200 | fn check_resource_json( 201 | resource_ship: &str, 202 | resource_name: &str, 203 | resource_json: &JsonValue, 204 | ) -> bool { 205 | let resource = resource_json["graph-update"]["add-nodes"]["resource"].clone(); 206 | let json_resource_name = format!("{}", resource["name"]); 207 | let json_resource_ship = format!("~{}", resource["ship"]); 208 | if json_resource_name == resource_name && json_resource_ship == resource_ship { 209 | return true; 210 | } 211 | false 212 | } 213 | -------------------------------------------------------------------------------- /src/traits/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod messaging; 2 | --------------------------------------------------------------------------------