dcss_api/
lib.rs

1//! An API library to interact with [DCSS Webtile](http://crawl.develz.org/wordpress/howto).
2
3mod api_errors;
4mod common;
5mod lobby;
6mod play;
7
8pub use api_errors::{BlockingError, Error};
9
10use api_errors::blocking_messages;
11use flate2::Decompress;
12use serde_json::Value;
13use std::collections::VecDeque;
14use std::net::TcpStream;
15use std::result::Result;
16use std::str;
17use std::thread;
18use std::time::{Duration, SystemTime};
19use tungstenite::Message;
20use tungstenite::{self, protocol::WebSocket, stream::MaybeTlsStream};
21
22/// Webtile connection, using websocket ([tungstenite]) and a Deflate decoder ([flate2]).
23#[derive(Debug)]
24pub struct Webtile {
25    /// Websocket (using [tungstenite::WebSocket]) to send and receive data from
26    /// [DCSS Webtile](http://crawl.develz.org/wordpress/howto).
27    socket: WebSocket<MaybeTlsStream<TcpStream>>,
28    /// A [flate2::Decompress] decompression object (Deflate) to decompress data received
29    /// by [DCSS Webtile](http://crawl.develz.org/wordpress/howto).
30    decompressor: Decompress,
31    /// [SystemTime] of the last sent message. Used to limit the rate for
32    /// running the bot on someone else's server.
33    last_send: SystemTime,
34    /// A [bool] of if the searched for data (in the websocket) has been found.
35    message_found: bool,
36    /// Speed limit in milliseconds between each command sent to DCSS Webtiles.
37    speed_ms: u32,
38    /// [VecDeque] of messages received from DCSS.
39    received_messages: VecDeque<Value>,
40}
41
42impl Webtile {
43    /// Connects to a websocket URL, prepares the decompressor (using [flate2::Decompress]) and
44    /// returns a [Webtile] connection object.
45    ///
46    /// # Arguments
47    ///
48    /// * `url` - A [&str] that holds the `ws://` or `wss://` URL
49    /// * `speed_ms` - A [u32] that depicts the speed limit in milliseconds between
50    ///   each command sent to DCSS Webtiles.
51    /// * `_version` - Currently a placeholder for the version number of DCSS, in case
52    ///   the API changes in the future.
53    ///     
54    /// # Example
55    ///
56    /// ```no_run
57    /// let mut webtile = Webtile::connect("ws://localhost:8080/socket", 100, "0.29")?;
58    /// ```
59    pub fn connect(url: &str, speed_ms: u32, _version: &str) -> Result<Self, Box<Error>> {
60        // Open connection
61        let (socket, _response) = tungstenite::connect(url).map_err(Error::Websocket)?;
62
63        // Init decompressor (see https://rustpython.github.io/website/src/rustpython_vm/stdlib/zlib.rs.html)
64        let wbits = 15; // Windows bits fixed (goes to -15 in flate2 because of zlib_header = false)
65        let decompressor = Decompress::new_with_window_bits(false, wbits);
66
67        // Create webtile object
68        let mut webtile = Self {
69            socket,
70            decompressor,
71            last_send: SystemTime::now(),
72            speed_ms,
73            message_found: false,
74            received_messages: VecDeque::new(),
75        };
76
77        // Wait until the "lobby_complete" message is received -- meaning a
78        // successful connection
79        webtile.read_until("lobby_complete", None, None)?;
80
81        Ok(webtile)
82    }
83
84    /// Close the websocket connection.
85    ///
86    /// # Example
87    ///
88    /// ```no_run
89    /// webtile.disconnect()?;
90    /// ```
91    pub fn disconnect(&mut self) -> Result<(), Box<Error>> {
92        self.socket.close(None).map_err(Error::Websocket)?;
93
94        Ok(())
95    }
96
97    /// Read the websocket messages until a specified message is found. Stores the
98    /// messages in a [VecDeque] that can be accessed by the user through the
99    /// [`Webtile::get_message()`] function. Any known blocking message (e.g.
100    /// a 'more' log statement) will return a [api_errors::BlockingError].
101    ///
102    /// Will block forever if the expected message never comes.
103    ///
104    /// # Arguments
105    ///
106    /// * `msg` - A [&str] that holds the value expected in the "msg" field of any returned message.
107    /// * `key` - A optional [&str] with the name of the specific key in the json data to search for.
108    /// * `value` - A optional [u64] with the value of the `key`, only if u64. Specifically meant to
109    ///   distinguish between types of "modes" for the input_mode.
110    ///
111    /// # Example
112    ///
113    /// ```no_run
114    ///
115    /// // Read until the "close_all_menu" message is received
116    /// webtile.read_until("close_all_menus", None, None)
117    ///
118    /// // Read until the "input_mode" message is received, with mode == 1
119    /// webtile.read_until("input_mode", Some("mode"), Some(1))
120    /// ```
121    pub fn read_until(
122        &mut self,
123        msg: &str,
124        key: Option<&str>,
125        value: Option<u64>,
126    ) -> Result<(), Box<Error>> {
127        // loop until break (found expected results or found a blocking type)
128        // use self variable in order to retain the info when there is a blocking error
129        while !self.message_found {
130            // Read the message from the socket into Vec<u8> -- it will be compressed
131            let mut compressed_msg = self
132                .socket
133                .read()
134                .map_err(Error::Websocket)?
135                .into_data()
136                .to_vec();
137
138            // Decompress the message and return JSON Value
139            let messages = common::deflate_to_json(&mut self.decompressor, &mut compressed_msg)?;
140
141            // Alert if blocking
142            let mut blocking = Ok(());
143
144            // Will get array of message, go through them until what is expected is found
145            for message in messages["msgs"].as_array().unwrap() {
146                // Send data to a VecDeque to be pulled by user;
147                self.received_messages.push_back(message.to_owned());
148
149                // Pre-process the data to identify blocking
150                if let Err(e) = blocking_messages(message) {
151                    match *e {
152                        Error::Blocking(BlockingError::Died) => return Err(e), // Automatic return when death
153                        _ => blocking = Err(e),
154                    }
155                };
156
157                // If searching for key-value
158                let message_msg = message["msg"].as_str().unwrap().to_owned();
159
160                // Same message
161                if msg == message_msg &&
162                    // And no key
163                    (key.is_none() ||
164                    // or And contains key
165                    (message.as_object().unwrap().contains_key(&key.unwrap().to_owned()) &&
166                        // And no value
167                        (value.is_none() ||
168                        // or And value correct
169                        message[key.unwrap()].as_u64().unwrap() == value.unwrap())))
170                {
171                    self.message_found = true;
172                }
173            }
174
175            blocking?
176        }
177
178        self.message_found = false;
179
180        Ok(())
181    }
182
183    /// Write a [serde_json::Value] to the websocket. Will only send if sufficient time has
184    /// elapsed since the last sent data, according to the [`Webtile::connect`] speed_ms option.
185    ///
186    /// # Arguments
187    ///
188    /// * `json_val` - A [serde_json::Value] to send to DCSS Webtiles.
189    ///
190    /// # Example
191    ///
192    /// ```no_run
193    /// // Send the login command
194    /// webtile.write_json(json!({
195    ///     "msg": "login",
196    ///     "username": "Username",
197    ///     "password": "Password",
198    /// }))?;
199    /// ```
200    pub fn write_json(&mut self, json_val: Value) -> Result<(), Box<Error>> {
201        // Pause while min time not met
202        while SystemTime::now()
203            .duration_since(self.last_send)
204            .expect("Time failed")
205            .as_millis()
206            < self.speed_ms as u128
207        {
208            thread::sleep(Duration::from_millis(10));
209        }
210        self.last_send = SystemTime::now();
211
212        self.socket
213            .send(Message::Text(json_val.to_string().into()))
214            .map_err(Error::Websocket)?;
215
216        Ok(())
217    }
218
219    /// Write a string slice (processed by the crate) to the websocket. Special
220    /// characters starting with `key_` will be sent as a keycode (e.g. `key_esc` will
221    /// send the `esc` character). Will only send if sufficient time has elapsed since
222    /// the last sent data, according to the [`Webtile::connect`] speed_ms option.
223    ///
224    /// Special keys:
225    /// * CTRL+char = `key_ctrl_a` to `key_ctrl_z`
226    /// * Special chars = `key_tab`, `key_esc` and `key_enter`
227    /// * Cardinal directions: `key_dir_n`, `key_dir_ne`, `key_dir_e`, `key_dir_se`,
228    ///   `key_dir_s`, `key_dir_sw`, `key_dir_w` and `key_dir_nw`
229    /// * Stairs: `key_stair_down` and `key_stair_up`
230    ///
231    /// # Arguments
232    ///
233    /// * `key` - A string slice to be sent to DCSS (after processing).
234    ///
235    /// # Example
236    ///
237    /// ```no_run
238    /// // Send the `esc` key
239    /// webtile.write_key("key_esc")
240    ///
241    /// // Send the 'a' key
242    /// webtile.write_key("a")
243    ///
244    /// // Send a string of keys that will move left, open a menu and drop an item (slot a)
245    /// webtile.write_key("6iad")
246    /// ```
247    pub fn write_key(&mut self, key: &str) -> Result<(), Box<Error>> {
248        // Pause while min time not met
249        while SystemTime::now()
250            .duration_since(self.last_send)
251            .expect("Time failed")
252            .as_millis()
253            < self.speed_ms as u128
254        {
255            thread::sleep(Duration::from_millis(10));
256        }
257        self.last_send = SystemTime::now();
258
259        let json_key = common::keys(key);
260        self.socket
261            .send(Message::Text(json_key.to_string().into()))
262            .map_err(Error::Websocket)?;
263
264        Ok(())
265    }
266
267    /// Get the messages received by the DCSS Webtile (as [serde_json::Value]), in
268    /// order of reception. Will return [None] if the queue is empty.
269    ///
270    /// # Example
271    ///
272    /// ```no_run
273    /// // Print the messages received, until the queue is empty
274    /// while let Some(message) = webtile.get_message() {
275    ///     println!("{:?}", message)
276    /// }
277    /// ```
278    pub fn get_message(&mut self) -> Option<Value> {
279        self.received_messages.pop_front()
280    }
281
282    /// Get a copy of the messages currently in the received queue.
283    pub(crate) fn read_only_messages(&self) -> Vec<Value> {
284        self.received_messages
285            .clone()
286            .into_iter()
287            .collect::<Vec<Value>>()
288    }
289}