sf_api/response.rs
1// TODO: Remove this once ouroboros is updated:
2// https://github.com/someguynamedjosh/ouroboros/issues/140
3#![allow(clippy::extra_unused_lifetimes)]
4
5use std::{collections::HashMap, fmt::Debug, str::FromStr};
6
7use chrono::NaiveDateTime;
8use log::{debug, error, trace, warn};
9
10use crate::error::SFError;
11
12/// A bunch of new information about the state of the server and/or the
13/// player
14///
15/// NOTE: This has a weird syntax to access, because we do not want to create
16/// 10000 strings on each request and instead just store the raw response body
17/// and references into it. This is faster & uses less memory, but because of
18/// rusts borrow checker requires some weird syntax here.
19// Technically we could do this safely with an iterator, that parses on demand,
20// but send_command() needs to access specific response keys to keep the session
21// running, which means a HashMap needs to be constructed no matter what
22#[ouroboros::self_referencing]
23pub struct Response {
24 body: String,
25 #[borrows(body)]
26 #[covariant]
27 resp: HashMap<&'this str, ResponseVal<'this>>,
28 /// We store this to make sure the time calculations are still correct, if
29 /// this response is held any amount of time before being used to update
30 /// character state
31 received_at: NaiveDateTime,
32}
33
34impl Clone for Response {
35 // This is not a good clone..
36 #[allow(clippy::expect_used)]
37 fn clone(&self) -> Self {
38 Self::parse(self.raw_response().to_string(), self.received_at())
39 .expect("Invalid response cloned")
40 }
41}
42
43impl Debug for Response {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.debug_map()
46 .entries(self.values().iter().map(|a| (a.0, a.1.as_str())))
47 .finish()
48 }
49}
50
51#[cfg(feature = "serde")]
52impl serde::Serialize for Response {
53 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
54 where
55 S: serde::Serializer,
56 {
57 use serde::ser::SerializeStruct;
58 let mut s = serializer.serialize_struct("Response", 2)?;
59 s.serialize_field("body", self.borrow_body())?;
60 s.serialize_field("received_at", &self.received_at())?;
61 s.end()
62 }
63}
64
65#[cfg(feature = "serde")]
66impl<'de> serde::Deserialize<'de> for Response {
67 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
68 where
69 D: serde::Deserializer<'de>,
70 {
71 struct AVisitor;
72
73 impl<'de> serde::de::Visitor<'de> for AVisitor {
74 type Value = Response;
75
76 fn expecting(
77 &self,
78 formatter: &mut std::fmt::Formatter,
79 ) -> std::fmt::Result {
80 formatter.write_str(
81 "struct Response with fields body and received_at",
82 )
83 }
84
85 fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
86 where
87 V: serde::de::MapAccess<'de>,
88 {
89 let mut body = None;
90 let mut received_at = None;
91
92 while let Some(key) = map.next_key()? {
93 match key {
94 "body" => {
95 body = Some(map.next_value()?);
96 }
97 "received_at" => {
98 received_at = Some(map.next_value()?);
99 }
100 _ => {
101 // Ignore unknown fields
102 map.next_value::<serde::de::IgnoredAny>()?;
103 }
104 }
105 }
106
107 let body: String =
108 body.ok_or_else(|| serde::de::Error::missing_field("q"))?;
109 let received_at: NaiveDateTime = received_at
110 .ok_or_else(|| serde::de::Error::missing_field("j"))?;
111
112 Response::parse(body, received_at).map_err(|_| {
113 serde::de::Error::custom("invalid response body")
114 })
115 }
116 }
117
118 deserializer.deserialize_struct(
119 "Response",
120 &["body", "received_at"],
121 AVisitor,
122 )
123 }
124}
125
126impl Response {
127 /// Returns a reference to the hashmap, that contains mappings of response
128 /// keys to values
129 #[must_use]
130 pub fn values(&self) -> &HashMap<&str, ResponseVal<'_>> {
131 self.borrow_resp()
132 }
133
134 /// Returns the raw response from the server. This should only ever be
135 /// necessary for debugging, caching, or in case there is ever a new
136 /// response format in a response, that is not yet supported. You can of
137 /// course also use this to look at how horrible the S&F encoding is..
138 #[must_use]
139 pub fn raw_response(&self) -> &str {
140 self.borrow_body()
141 }
142
143 /// Returns the time, at which the response was received
144 #[must_use]
145 pub fn received_at(&self) -> NaiveDateTime {
146 self.with_received_at(|a| *a)
147 }
148
149 /// Parses a response body from the server into a usable format
150 /// You might want to use this, if you are analyzing responses from the
151 /// browsers network tab. If you are trying to store/read responses to/from
152 /// disk to cache them, or otherwise, you should use the sso feature to
153 /// serialize/deserialize them instead
154 ///
155 /// # Errors
156 /// - `ServerError`: If the server responsed with an error
157 /// - `ParsingError`: If the response does not follow the standard S&F
158 /// server response schema
159 pub fn parse(
160 og_body: String,
161 received_at: NaiveDateTime,
162 ) -> Result<Response, SFError> {
163 // We can not return from the closure below, so we have to do this work
164 // twice (sadly)
165
166 // NOTE: I think the trims might actually be completely unnecessary.
167 // Pretty sure I mixed them up with command encoding, which is actually
168 // '|' padded
169
170 let body = og_body
171 .trim_end_matches('|')
172 .trim_start_matches(|a: char| !a.is_alphabetic());
173 trace!("Received raw response: {body}");
174
175 if !body.contains(':')
176 && !body.starts_with("success")
177 && !body.starts_with("Success")
178 {
179 return Err(SFError::ParsingError(
180 "unexpected server response",
181 body.to_string(),
182 ));
183 }
184
185 if body.starts_with("error") || body.starts_with("Error") {
186 let raw_error = body.split_once(':').unwrap_or_default().1;
187
188 let error_msg = match raw_error {
189 "adventure index must be 1-3" => "quest index must be 0-2",
190 x => x,
191 };
192
193 return Err(SFError::ServerError(error_msg.to_string()));
194 }
195
196 let resp = ResponseBuilder {
197 body: og_body,
198 resp_builder: |body: &String| {
199 let mut res = HashMap::new();
200 for part in body
201 .trim_start_matches(|a: char| !a.is_alphabetic())
202 .trim_end_matches('|')
203 .split('&')
204 .filter(|a| !a.is_empty())
205 {
206 let Some((full_key, value)) = part.split_once(':') else {
207 warn!("weird k/v in resp: {part}");
208 continue;
209 };
210
211 let (key, sub_key) = match full_key.split_once('.') {
212 Some(x) => {
213 // full_key == key.subkey
214 x
215 }
216 None => {
217 if let Some((k, sk)) = full_key.split_once('(') {
218 // full_key == key(4)
219 (k, sk.trim_matches(')'))
220 } else {
221 // full_key == key
222 (full_key, "")
223 }
224 }
225 };
226 if key.is_empty() {
227 continue;
228 }
229
230 let old_val =
231 res.insert(key, ResponseVal { value, sub_key });
232 if let Some(old_val) = old_val {
233 let old = old_val.as_str();
234 debug!("Overwrote [{key}]: {old} => {value}");
235 }
236 }
237 res
238 },
239 received_at,
240 }
241 .build();
242
243 Ok(resp)
244 }
245}
246
247/// This is the raw &str, that the server sent as a value to some key. This
248/// often requires extra conversions/parsing to use practically, so we associate
249/// the most common parsing functions as methods to this data.
250#[derive(Debug, Clone, Copy)]
251#[allow(clippy::module_name_repetitions)]
252pub struct ResponseVal<'a> {
253 value: &'a str,
254 sub_key: &'a str,
255}
256
257impl ResponseVal<'_> {
258 /// Converts the response value into the required type
259 ///
260 /// # Errors
261 /// If the response value can not be parsed into the output
262 /// value, a `ParsingError` will be returned
263 pub fn into<T: FromStr>(self, name: &'static str) -> Result<T, SFError> {
264 self.value.trim().parse().map_err(|_| {
265 error!("Could not convert {name} into target type: {self}");
266 SFError::ParsingError(name, self.value.to_string())
267 })
268 }
269
270 /// Converts the repsponse into a list, by splitting the raw value by '/'
271 /// and converting each value into the required type. If any conversion
272 /// fails, an error is returned
273 ///
274 /// # Errors
275 /// If any of the values in the string can not be parsed into the output
276 /// value, the `ParsingError` for that value will be returned
277 pub fn into_list<T: FromStr>(
278 self,
279 name: &'static str,
280 ) -> Result<Vec<T>, SFError> {
281 let x = &self.value;
282 if x.is_empty() {
283 return Ok(Vec::new());
284 }
285 // Trimming ` ` & `\n` is not required. Might remove this later
286 x.trim_matches(|a| ['/', ' ', '\n'].contains(&a))
287 .split('/')
288 .map(|c| {
289 c.trim().parse::<T>().map_err(|_| {
290 error!(
291 "Could not convert {name} into list because of {c}: \
292 {self}"
293 );
294 SFError::ParsingError(name, format!("{c:?}"))
295 })
296 })
297 .collect()
298 }
299
300 /// The way keys are parsed will trim some info from the string. The key for
301 /// the player save `ownplayersave` is actually `ownplayersave.playerSave`.
302 /// As this `.playerSave` is not relevant here and not in most cases, I
303 /// decided to trim that off. More common, this is also just `s`, `r`, or a
304 /// size hint like `(10)`. In some cases though, this information can be
305 /// helpful for parsing. Thus, you can access it here
306 #[must_use]
307 pub fn sub_key(&self) -> &str {
308 self.sub_key
309 }
310
311 /// Returns the raw reference to the internal &str, that the server sent
312 #[must_use]
313 pub fn as_str(&self) -> &str {
314 self.value
315 }
316}
317
318impl std::fmt::Display for ResponseVal<'_> {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 f.write_str(self.value)
321 }
322}