urbit_http_api/apps/
notebook.rs

1use crate::graph::NodeContents;
2use crate::helper::{get_current_da_time, get_current_time};
3use crate::AuthoredMessage;
4use crate::{Channel, Node, Result, UrbitAPIError};
5
6/// A struct that provides an interface for interacting with Urbit notebooks
7pub 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.
12pub type Comment = AuthoredMessage;
13
14/// A struct that represents a Note from a Notebook
15#[derive(Clone, Debug)]
16pub struct Note {
17    pub title: String,
18    pub author: String,
19    pub time_sent: String,
20    pub contents: String,
21    pub comments: Vec<Comment>,
22    pub index: String,
23}
24
25/// An internal helper struct for analysing Notebook node indices
26#[derive(Clone, Debug)]
27struct NotebookIndex<'a> {
28    pub index: &'a str,
29    pub index_split: Vec<&'a str>,
30}
31
32impl 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<Comment>,
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<String>) -> Result<Note> {
54        let mut comments: Vec<Comment> = 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<String> {
120        let formatted_string = self.contents.clone();
121        formatted_string
122            .split("\\n")
123            .map(|l| l.to_string())
124            .collect()
125    }
126}
127
128impl<'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<Vec<Note>> {
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<Note> {
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, &note_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<Note> {
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<String> {
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, &note_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<Comment> {
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<String> {
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<String> {
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<String> {
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(&note_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            &note_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<String> {
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<String> {
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
486impl<'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<String> {
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<u64> {
582        if self.is_note_revision() {
583            if let Ok(r) = self.index_split[3].parse::<u64>() {
584                return Ok(r);
585            }
586        } else if self.is_comment_revision() {
587            if let Ok(r) = self.index_split[4].parse::<u64>() {
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<String> {
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<String> {
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}