urbit_http_api/
graphstore.rs

1use crate::graph::{Graph, Node, NodeContents};
2use crate::helper::{get_current_da_time, get_current_time, index_dec_to_ud};
3use crate::{Channel, Result, UrbitAPIError};
4use json::{object, JsonValue};
5
6/// The type of module a given graph is.
7pub enum Module {
8    Chat,
9    Notebook,
10    Collection,
11    Null,
12}
13
14/// A struct which exposes Graph Store functionality
15pub struct GraphStore<'a> {
16    pub channel: &'a mut Channel,
17}
18
19impl<'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<Node> {
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<Graph> {
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<Graph> {
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<Graph> {
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<String> {
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<String> {
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<Vec<JsonValue>> {
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<Vec<JsonValue>> {
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<Vec<JsonValue>> {
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<String> {
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<String> {
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<String> {
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
636pub 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
645pub 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}