urbit_http_api/
interface.rs

1use crate::channel::Channel;
2use crate::error::{Result, UrbitAPIError};
3use json::JsonValue;
4use reqwest::blocking::{Client, Response};
5use reqwest::header::{HeaderValue, COOKIE};
6
7// The struct which holds the details for connecting to a given Urbit ship
8#[derive(Debug, Clone)]
9pub 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
21impl 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<ShipInterface> {
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<Channel> {
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<Response> {
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<Response> {
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<Response> {
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
122impl 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)]
129mod 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}