Skip to main content

urbit_chatbot_framework/
lib.rs

1use json::JsonValue;
2use std::thread;
3use std::time::Duration;
4use urbit_http_api::{default_cli_ship_interface_setup, Node, ShipInterface};
5pub use urbit_http_api::{AuthoredMessage, Message};
6
7/// This struct represents a chatbot that is connected to a given `ship`,
8/// is watching/posting to a specific `chat_ship`/`chat_name`
9/// and is using the function `respond_to_message` to process any messages
10/// which are posted in said chat.
11pub struct Chatbot {
12    /// `respond_to_message` is a function defined by the user of this framework.
13    /// This function receives any messages that get posted to the connected chat,
14    /// and if the function returns `Some(message)`, then `message` is posted to the
15    /// chat as a response. If it returns `None`, then no message is posted.
16    respond_to_message: fn(AuthoredMessage) -> Option<Message>,
17    ship: ShipInterface,
18    chat_ship: String,
19    chat_name: String,
20}
21
22impl Chatbot {
23    /// Create a new `Chatbot` with a manually provided `ShipInterface`
24    pub fn new(
25        respond_to_message: fn(AuthoredMessage) -> Option<Message>,
26        ship: ShipInterface,
27        chat_ship: &str,
28        chat_name: &str,
29    ) -> Self {
30        Chatbot {
31            respond_to_message: respond_to_message,
32            ship: ship,
33            chat_ship: chat_ship.to_string(),
34            chat_name: chat_name.to_string(),
35        }
36    }
37
38    /// Create a new `Chatbot` with a `ShipInterface` derived automatically
39    /// from a local config file. If the config file does not exist, the
40    /// `Chatbot` will create the config file, exit, and prompt the user to
41    /// fill it out.
42    pub fn new_with_local_config(
43        respond_to_message: fn(AuthoredMessage) -> Option<Message>,
44        chat_ship: &str,
45        chat_name: &str,
46    ) -> Self {
47        let ship = default_cli_ship_interface_setup();
48        Self::new(respond_to_message, ship, chat_ship, chat_name)
49    }
50
51    /// Run the `Chatbot`
52    pub fn run(&self) -> Option<()> {
53        println!("=======================================\nPowered By The Urbit Chatbot Framework\n=======================================");
54        // Create a `Subscription`
55        let channel = &mut self.ship.create_channel().ok()?;
56        // Subscribe to all graph-store updates
57        channel
58            .create_new_subscription("graph-store", "/updates")
59            .ok()?;
60
61        // Infinitely watch for new graph store updates
62        loop {
63            channel.parse_event_messages();
64            let graph_updates = &mut channel.find_subscription("graph-store", "/updates")?;
65            let mut messages_to_send = vec![];
66
67            // Read all of the current SSE messages to find if any are for the chat
68            // we are looking for.
69            loop {
70                let pop_res = graph_updates.pop_message();
71                // Acquire the message
72                if let Some(mess) = &pop_res {
73                    // Parse it to json
74                    if let Ok(json) = json::parse(mess) {
75                        // If the graph-store node update is not for the chat the `Chatbot`
76                        // is watching, then continue to next message.
77                        if !self.check_resource_json(&json) {
78                            continue;
79                        }
80                        // Otherwise, parse json to a `Node`
81                        if let Ok(node) = Node::from_graph_update_json(&json) {
82                            // If the message is posted by the Chatbot ship, ignore
83                            // if node.author == self.ship.ship_name
84                            if node.author == self.ship.ship_name {
85                                continue;
86                            }
87
88                            // Else parse it as an `AuthoredMessage`
89                            let authored_message = AuthoredMessage::new(
90                                &node.author,
91                                &node.contents,
92                                &node.time_sent_formatted(),
93                                &node.index,
94                            );
95                            // If the Chatbot intends to respond to the provided message
96                            if let Some(message) = (self.respond_to_message)(authored_message) {
97                                println!("Replied to message.");
98                                messages_to_send.push(message)
99                            } else {
100                                println!("Message ignored.")
101                            }
102                        }
103                    }
104                }
105                // If no messages left, stop
106                if let None = &pop_res {
107                    break;
108                }
109            }
110
111            // Send each response message that was returned by the `respond_to_message`
112            // function. This is separated until after done parsing messages due to mutable borrows.
113            for message in messages_to_send {
114                channel
115                    .chat()
116                    .send_chat_message(&self.chat_ship, &self.chat_name, &message)
117                    .ok();
118            }
119            thread::sleep(Duration::new(0, 500000000));
120        }
121    }
122
123    /// Checks whether the resource json matches the chat_name & chat_ship
124    /// that this `Chatbot` is interacting with
125    fn check_resource_json(&self, resource_json: &JsonValue) -> bool {
126        let resource = resource_json["graph-update"]["add-nodes"]["resource"].clone();
127        let chat_name = format!("{}", resource["name"]);
128        let chat_ship = format!("~{}", resource["ship"]);
129        if chat_name == self.chat_name && chat_ship == self.chat_ship {
130            return true;
131        }
132        false
133    }
134}