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