urbit_http_api/traits/
messaging.rs

1use crate::error::{Result, UrbitAPIError};
2use crate::graph::{Node, NodeContents};
3use crate::Channel;
4use crossbeam::channel::{unbounded, Receiver};
5use json::JsonValue;
6use std::thread;
7use 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.
13pub type Message = NodeContents;
14
15/// A `Message` with the author @p, post time and index also included
16#[derive(Clone, Debug)]
17pub struct AuthoredMessage {
18    pub author: String,
19    pub contents: Message,
20    pub time_sent: String,
21    pub index: String,
22}
23
24impl 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.
53pub 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<String> {
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<Vec<String>> {
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<Vec<AuthoredMessage>> {
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<Vec<Node>> {
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<Receiver<AuthoredMessage>> {
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
200fn 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}