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}