1mod structs;
2pub use structs::*;
3
4use etterna::*;
5
6use crate::extension_traits::*;
7use crate::Error;
8
9fn difficulty_from_eo(string: &str) -> Result<etterna::Difficulty, Error> {
10 Ok(match string {
11 "Beginner" => Difficulty::Beginner,
12 "Easy" => Difficulty::Easy,
13 "Medium" => Difficulty::Medium,
14 "Hard" => Difficulty::Hard,
15 "Challenge" => Difficulty::Challenge,
16 "Edit" => Difficulty::Edit,
17 other => {
18 return Err(Error::InvalidDataStructure(format!(
19 "Unexpected difficulty name '{}'",
20 other
21 )))
22 }
23 })
24}
25
26fn parse_judgements(json: &serde_json::Value) -> Result<etterna::FullJudgements, Error> {
27 Ok(etterna::FullJudgements {
28 marvelouses: json["marvelous"].u32_()?,
29 perfects: json["perfect"].u32_()?,
30 greats: json["great"].u32_()?,
31 goods: json["good"].u32_()?,
32 bads: json["bad"].u32_()?,
33 misses: json["miss"].u32_()?,
34 hit_mines: json["hitMines"].u32_()?,
35 held_holds: json["heldHold"].u32_()?,
36 let_go_holds: json["letGoHold"].u32_()?,
37 missed_holds: json["missedHold"].u32_()?,
38 })
39}
40
41pub struct Session {
73 username: String,
75 password: String,
76 client_data: String,
77
78 authorization: crate::common::AuthorizationManager<Option<String>>,
80
81 last_request: std::sync::Mutex<std::time::Instant>,
83 cooldown: std::time::Duration,
84
85 timeout: Option<std::time::Duration>,
86}
87
88impl Session {
89 pub fn new_from_login(
112 username: String,
113 password: String,
114 client_data: String,
115 cooldown: std::time::Duration,
116 timeout: Option<std::time::Duration>,
117 ) -> Result<Self, Error> {
118 let session = Self {
119 username,
120 password,
121 client_data,
122 cooldown,
123 timeout,
124 authorization: crate::common::AuthorizationManager::new(None),
125 last_request: std::sync::Mutex::new(std::time::Instant::now() - cooldown),
126 };
127 session.login()?;
128
129 Ok(session)
130 }
131
132 fn login(&self) -> Result<(), Error> {
138 self.authorization.refresh(|| {
139 let form: &[(&str, &str)] = &[
140 ("username", &self.username.clone()),
143 ("password", &self.password.clone()),
144 ("clientData", &self.client_data.clone()),
145 ];
146
147 let json = self.generic_request(
148 "POST",
149 "login",
150 |mut request| request.send_form(form),
151 false,
152 )?;
153
154 Ok(Some(format!(
155 "Bearer {}",
156 json["attributes"]["accessToken"].str_()?,
157 )))
158 })
159 }
160
161 fn generic_request(
165 &self,
166 method: &str,
167 path: &str,
168 request_callback: impl Fn(ureq::Request) -> ureq::Response,
169 do_authorization: bool,
170 ) -> Result<serde_json::Value, Error> {
171 crate::rate_limit(&mut *self.last_request.lock().unwrap(), self.cooldown);
173
174 let mut request = ureq::request(
175 method,
176 &format!("https://api.etternaonline.com/v2/{}", path),
177 );
178 if let Some(timeout) = self.timeout {
179 request.timeout(timeout);
180 }
181 if do_authorization {
182 let auth = self
183 .authorization
184 .get_authorization()
185 .as_ref()
186 .expect("No authorization set even though it was requested??")
187 .clone();
188 request.set("Authorization", &auth);
189 }
190
191 let response = request_callback(request);
192
193 if let Some(ureq::Error::Io(io_err)) = response.synthetic_error() {
194 if io_err.kind() == std::io::ErrorKind::TimedOut {
195 return Err(Error::Timeout);
196 }
197 }
198
199 let status = response.status();
200 let response = match response.into_string() {
201 Ok(response) => response,
202 Err(e) => {
203 return if e.to_string().contains("timed out reading response") {
204 Err(Error::Timeout)
207 } else {
208 Err(e.into())
209 };
210 }
211 };
212
213 if status >= 500 {
214 return Err(Error::ServerIsDown {
215 status_code: status,
216 });
217 }
218
219 if response.is_empty() {
220 return Err(Error::EmptyServerResponse);
221 }
222
223 let mut json: serde_json::Value = serde_json::from_str(&response)?;
226
227 if status >= 400 {
229 return match json["errors"][0]["title"].str_()? {
230 "Unauthorized" => {
231 self.login()?;
233 return self.generic_request(method, path, request_callback, do_authorization);
234 }
235 "Score not found" => Err(Error::ScoreNotFound),
236 "Chart not tracked" => Err(Error::ChartNotTracked),
237 "User not found" => Err(Error::UserNotFound),
238 "Favorite already exists" => Err(Error::ChartAlreadyFavorited),
239 "Database error" => Err(Error::DatabaseError),
240 "Goal already exist" => Err(Error::GoalAlreadyExists),
241 "Chart already exists" => Err(Error::ChartAlreadyAdded),
242 "Malformed XML file" => Err(Error::InvalidXml),
243 "No users found" => Err(Error::NoUsersFound),
244 other => Err(Error::UnknownApiError(other.to_owned())),
245 };
246 } else if status != 200 {
247 println!("Warning: status code {}", status);
249 }
250
251 Ok(json["data"].take())
252 }
253
254 fn request(
255 &self,
256 method: &str,
257 path: &str,
258 request_callback: impl Fn(ureq::Request) -> ureq::Response,
259 ) -> Result<serde_json::Value, Error> {
260 self.generic_request(method, path, request_callback, true)
261 }
262
263 fn get(&self, path: &str) -> Result<serde_json::Value, Error> {
264 self.request("GET", path, |mut request| request.call())
265 }
266
267 pub fn user_details(&self, username: &str) -> Result<UserDetails, Error> {
284 let json = self.get(&format!("user/{}", username))?;
285 let json = &json["attributes"];
286
287 Ok(UserDetails {
288 username: json["userName"].string()?,
289 about_me: json["aboutMe"].string()?,
290 is_moderator: json["moderator"].bool_()?,
291 is_patreon: json["patreon"].bool_()?,
292 avatar_url: json["avatar"].string()?,
293 country_code: json["countryCode"].string()?,
294 player_rating: json["playerRating"].f32_()?,
295 default_modifiers: match json["defaultModifiers"].str_()? {
296 "" => None,
297 modifiers => Some(modifiers.to_owned()),
298 },
299 rating: etterna::Skillsets8 {
300 overall: json["playerRating"].f32_()?,
301 stream: json["skillsets"]["Stream"].f32_()?,
302 jumpstream: json["skillsets"]["Jumpstream"].f32_()?,
303 handstream: json["skillsets"]["Handstream"].f32_()?,
304 stamina: json["skillsets"]["Stamina"].f32_()?,
305 jackspeed: json["skillsets"]["JackSpeed"].f32_()?,
306 chordjack: json["skillsets"]["Chordjack"].f32_()?,
307 technical: json["skillsets"]["Technical"].f32_()?,
308 },
309 })
310 }
311
312 fn parse_top_scores(&self, url: &str) -> Result<Vec<TopScore>, Error> {
313 let json = self.get(url)?;
314
315 json.array()?
316 .iter()
317 .map(|json| {
318 Ok(TopScore {
319 scorekey: json["id"].parse()?,
320 song_name: json["attributes"]["songName"].string()?,
321 ssr_overall: json["attributes"]["Overall"].f32_()?,
322 wifescore: json["attributes"]["wife"].wifescore_percent_float()?,
323 rate: json["attributes"]["rate"].rate_float()?,
324 difficulty: json["attributes"]["difficulty"].parse()?,
325 chartkey: json["attributes"]["chartKey"].parse()?,
326 base_msd: etterna::Skillsets8 {
327 overall: json["attributes"]["Overall"].f32_()?,
328 stream: json["attributes"]["skillsets"]["Stream"].f32_()?,
329 jumpstream: json["attributes"]["skillsets"]["Jumpstream"].f32_()?,
330 handstream: json["attributes"]["skillsets"]["Handstream"].f32_()?,
331 stamina: json["attributes"]["skillsets"]["Stamina"].f32_()?,
332 jackspeed: json["attributes"]["skillsets"]["JackSpeed"].f32_()?,
333 chordjack: json["attributes"]["skillsets"]["Chordjack"].f32_()?,
334 technical: json["attributes"]["skillsets"]["Technical"].f32_()?,
335 },
336 })
337 })
338 .collect()
339 }
340
341 pub fn user_top_skillset_scores(
358 &self,
359 username: &str,
360 skillset: etterna::Skillset7,
361 limit: u32,
362 ) -> Result<Vec<TopScore>, Error> {
363 self.parse_top_scores(&format!(
364 "user/{}/top/{}/{}",
365 username,
366 crate::common::skillset_to_eo(skillset),
367 limit
368 ))
369 }
370
371 pub fn user_top_10_scores(&self, username: &str) -> Result<Vec<TopScore>, Error> {
387 self.parse_top_scores(&format!("user/{}/top//", username))
388 }
389
390 pub fn user_latest_scores(&self, username: &str) -> Result<Vec<LatestScore>, Error> {
405 let json = self.get(&format!("user/{}/latest", username))?;
406
407 json.array()?
408 .iter()
409 .map(|json| {
410 Ok(LatestScore {
411 scorekey: json["id"].parse()?,
412 song_name: json["attributes"]["songName"].string()?,
413 ssr_overall: json["attributes"]["Overall"].f32_()?,
414 wifescore: json["attributes"]["wife"].wifescore_percent_float()?,
415 rate: json["attributes"]["rate"].rate_float()?,
416 difficulty: difficulty_from_eo(json["attributes"]["difficulty"].str_()?)?,
417 })
418 })
419 .collect()
420 }
421
422 pub fn user_ranks_per_skillset(&self, username: &str) -> Result<etterna::UserRank, Error> {
437 let json = self.get(&format!("user/{}/ranks", username))?;
438 let json = &json["attributes"];
439
440 Ok(etterna::UserRank {
441 overall: json["Overall"].u32_()?,
442 stream: json["Stream"].u32_()?,
443 jumpstream: json["Jumpstream"].u32_()?,
444 handstream: json["Handstream"].u32_()?,
445 stamina: json["Stamina"].u32_()?,
446 jackspeed: json["JackSpeed"].u32_()?,
447 chordjack: json["Chordjack"].u32_()?,
448 technical: json["Technical"].u32_()?,
449 })
450 }
451
452 pub fn user_top_scores_per_skillset(
468 &self,
469 username: &str,
470 ) -> Result<UserTopScoresPerSkillset, Error> {
471 let json = self.get(&format!("user/{}/all", username))?;
472
473 let parse_skillset_top_scores = |array: &serde_json::Value| -> Result<Vec<_>, Error> {
474 array
475 .array()?
476 .iter()
477 .map(|json| {
478 Ok(TopScorePerSkillset {
479 song_name: json["songname"].string()?,
480 rate: json["user_chart_rate_rate"].rate_float()?,
481 wifescore: json["wifescore"].wifescore_proportion_float()?,
482 chartkey: json["chartkey"].parse()?,
483 scorekey: json["scorekey"].parse()?,
484 difficulty: difficulty_from_eo(json["difficulty"].str_()?)?,
485 ssr: etterna::Skillsets8 {
486 overall: json["Overall"].f32_()?,
487 stream: json["Stream"].f32_()?,
488 jumpstream: json["Jumpstream"].f32_()?,
489 handstream: json["Handstream"].f32_()?,
490 stamina: json["Stamina"].f32_()?,
491 jackspeed: json["JackSpeed"].f32_()?,
492 chordjack: json["Chordjack"].f32_()?,
493 technical: json["Technical"].f32_()?,
494 },
495 })
496 })
497 .collect()
498 };
499
500 Ok(UserTopScoresPerSkillset {
501 overall: parse_skillset_top_scores(&json["attributes"]["Overall"])?,
502 stream: parse_skillset_top_scores(&json["attributes"]["Stream"])?,
503 jumpstream: parse_skillset_top_scores(&json["attributes"]["Jumpstream"])?,
504 handstream: parse_skillset_top_scores(&json["attributes"]["Handstream"])?,
505 stamina: parse_skillset_top_scores(&json["attributes"]["Stamina"])?,
506 jackspeed: parse_skillset_top_scores(&json["attributes"]["JackSpeed"])?,
507 chordjack: parse_skillset_top_scores(&json["attributes"]["Chordjack"])?,
508 technical: parse_skillset_top_scores(&json["attributes"]["Technical"])?,
509 })
510 }
511
512 pub fn score_data(&self, scorekey: impl AsRef<str>) -> Result<ScoreData, Error> {
528 let json = self.get(&format!("score/{}", scorekey.as_ref()))?;
529
530 let scorekey = json["id"].parse()?;
531 let json = &json["attributes"];
532
533 Ok(ScoreData {
534 scorekey,
535 modifiers: json["modifiers"].string()?,
536 wifescore: json["wife"].wifescore_proportion_float()?,
537 rate: json["rate"].rate_float()?,
538 max_combo: json["maxCombo"].u32_()?,
539 is_valid: json["valid"].bool_()?,
540 has_chord_cohesion: !json["nocc"].bool_()?,
541 song_name: json["song"]["songName"].string()?,
542 artist: json["song"]["artist"].string()?,
543 song_id: json["song"]["id"].u32_()?,
544 ssr: etterna::Skillsets8 {
545 overall: json["skillsets"]["Overall"].f32_()?,
546 stream: json["skillsets"]["Stream"].f32_()?,
547 jumpstream: json["skillsets"]["Jumpstream"].f32_()?,
548 handstream: json["skillsets"]["Handstream"].f32_()?,
549 stamina: json["skillsets"]["Stamina"].f32_()?,
550 jackspeed: json["skillsets"]["JackSpeed"].f32_()?,
551 chordjack: json["skillsets"]["Chordjack"].f32_()?,
552 technical: json["skillsets"]["Technical"].f32_()?,
553 },
554 judgements: parse_judgements(&json["judgements"])?,
555 replay: crate::common::parse_replay(&json["replay"])?,
556 user: ScoreUser {
557 username: json["user"]["username"].string()?,
558 avatar: json["user"]["avatar"].string()?,
559 country_code: json["user"]["countryCode"].string()?,
560 overall_rating: json["user"]["Overall"].f32_()?,
561 },
562 })
563 }
564
565 pub fn chart_leaderboard(
582 &self,
583 chartkey: impl AsRef<str>,
584 ) -> Result<Vec<ChartLeaderboardScore>, Error> {
585 let json = self.get(&format!("charts/{}/leaderboards", chartkey.as_ref()))?;
586
587 json.array()?
588 .iter()
589 .map(|json| {
590 Ok(ChartLeaderboardScore {
591 scorekey: json["id"].parse()?,
592 wifescore: json["attributes"]["wife"].wifescore_percent_float()?,
593 max_combo: json["attributes"]["maxCombo"].u32_()?,
594 is_valid: json["attributes"]["valid"].bool_()?,
595 modifiers: json["attributes"]["modifiers"].string()?,
596 has_chord_cohesion: !json["attributes"]["noCC"].bool_()?,
597 rate: json["attributes"]["rate"].rate_float()?,
598 datetime: json["attributes"]["datetime"].string()?,
599 ssr: etterna::Skillsets8 {
600 overall: json["attributes"]["skillsets"]["Overall"].f32_()?,
601 stream: json["attributes"]["skillsets"]["Stream"].f32_()?,
602 jumpstream: json["attributes"]["skillsets"]["Jumpstream"].f32_()?,
603 handstream: json["attributes"]["skillsets"]["Handstream"].f32_()?,
604 stamina: json["attributes"]["skillsets"]["Stamina"].f32_()?,
605 jackspeed: json["attributes"]["skillsets"]["JackSpeed"].f32_()?,
606 chordjack: json["attributes"]["skillsets"]["Chordjack"].f32_()?,
607 technical: json["attributes"]["skillsets"]["Technical"].f32_()?,
608 },
609 judgements: parse_judgements(&json["attributes"]["judgements"])?,
610 has_replay: json["attributes"]["hasReplay"].bool_()?, user: ScoreUser {
612 username: json["attributes"]["user"]["userName"].string()?,
613 avatar: json["attributes"]["user"]["avatar"].string()?,
614 country_code: json["attributes"]["user"]["countryCode"].string()?,
615 overall_rating: json["attributes"]["user"]["playerRating"].f32_()?,
616 },
617 })
618 })
619 .collect()
620 }
621
622 pub fn country_leaderboard(&self, country_code: &str) -> Result<Vec<LeaderboardEntry>, Error> {
642 let json = self.get(&format!("leaderboard/{}", country_code))?;
643
644 json.array()?
645 .iter()
646 .map(|json| {
647 Ok(LeaderboardEntry {
648 user: ScoreUser {
649 username: json["attributes"]["user"]["username"].string()?,
650 avatar: json["attributes"]["user"]["avatar"].string()?,
651 country_code: json["attributes"]["user"]["countryCode"].string()?,
652 overall_rating: json["attributes"]["user"]["Overall"].f32_()?,
653 },
654 rating: etterna::Skillsets8 {
655 overall: json["attributes"]["user"]["Overall"].f32_()?,
656 stream: json["attributes"]["skillsets"]["Stream"].f32_()?,
657 jumpstream: json["attributes"]["skillsets"]["Jumpstream"].f32_()?,
658 handstream: json["attributes"]["skillsets"]["Handstream"].f32_()?,
659 stamina: json["attributes"]["skillsets"]["Stamina"].f32_()?,
660 jackspeed: json["attributes"]["skillsets"]["JackSpeed"].f32_()?,
661 chordjack: json["attributes"]["skillsets"]["Chordjack"].f32_()?,
662 technical: json["attributes"]["skillsets"]["Technical"].f32_()?,
663 },
664 })
665 })
666 .collect()
667 }
668
669 pub fn world_leaderboard(&self) -> Result<Vec<LeaderboardEntry>, Error> {
686 self.country_leaderboard("")
687 }
688
689 pub fn user_favorites(&self, username: &str) -> Result<Vec<String>, Error> {
704 let json = self.get(&format!("user/{}/favorites", username))?;
705
706 json.array()?
707 .iter()
708 .map(|obj| Ok(obj["attributes"]["chartkey"].string()?))
709 .collect()
710 }
711
712 pub fn add_user_favorite(
728 &self,
729 username: &str,
730 chartkey: impl AsRef<str>,
731 ) -> Result<(), Error> {
732 self.request(
733 "POST",
734 &format!("user/{}/favorites", username),
735 |mut req| req.send_form(&[("chartkey", chartkey.as_ref())]),
736 )?;
737
738 Ok(())
739 }
740
741 pub fn remove_user_favorite(
753 &self,
754 username: &str,
755 chartkey: impl AsRef<str>,
756 ) -> Result<(), Error> {
757 self.request(
758 "DELETE",
759 &format!("user/{}/favorites/{}", username, chartkey.as_ref()),
760 |mut request| request.call(),
761 )?;
762
763 Ok(())
764 }
765
766 pub fn user_goals(&self, username: &str) -> Result<Vec<ScoreGoal>, Error> {
783 let json = self.get(&format!("user/{}/goals", username))?;
784
785 json.array()?
786 .iter()
787 .map(|json| {
788 Ok(ScoreGoal {
789 chartkey: json["attributes"]["chartkey"].parse()?,
790 rate: json["attributes"]["rate"].rate_float()?,
791 wifescore: json["attributes"]["wife"].wifescore_proportion_float()?,
792 time_assigned: json["attributes"]["timeAssigned"].string()?,
793 time_achieved: if json["attributes"]["achieved"].bool_int()? {
794 Some(json["attributes"]["timeAchieved"].string()?)
795 } else {
796 None
797 },
798 })
799 })
800 .collect()
801 }
802
803 pub fn add_user_goal(
827 &self,
828 username: &str,
829 chartkey: impl AsRef<str>,
830 rate: f64,
831 wifescore: f64,
832 time_assigned: &str,
833 ) -> Result<(), Error> {
834 self.request(
835 "POST",
836 &format!("user/{}/goals", username),
837 |mut request| {
838 request.send_form(&[
839 ("chartkey", chartkey.as_ref()),
840 ("rate", &format!("{}", rate)),
841 ("wife", &format!("{}", wifescore)),
842 ("timeAssigned", time_assigned),
843 ])
844 },
845 )?;
846
847 Ok(())
848 }
849
850 pub fn remove_user_goal(
872 &self,
873 username: &str,
874 chartkey: impl AsRef<str>,
875 rate: Rate,
876 wifescore: Wifescore,
877 ) -> Result<(), Error> {
878 self.request(
879 "DELETE",
880 &format!(
881 "user/{}/goals/{}/{}/{}",
882 username,
883 chartkey.as_ref(),
884 wifescore.as_proportion(),
885 rate.as_f32()
886 ),
887 |mut request| request.call(),
888 )?;
889
890 Ok(())
891 }
892
893 pub fn update_user_goal(&self, username: &str, goal: &ScoreGoal) -> Result<(), Error> {
912 self.request(
913 "POST",
914 &format!("user/{}/goals/update", username),
915 |mut request| {
916 request.send_form(&[
917 ("chartkey", goal.chartkey.as_ref()),
918 ("timeAssigned", &goal.time_assigned),
919 (
920 "achieved",
921 if goal.time_achieved.is_some() {
922 "1"
923 } else {
924 "0"
925 },
926 ),
927 ("rate", &format!("{}", goal.rate)),
928 ("wife", &format!("{}", goal.wifescore)),
929 (
930 "timeAchieved",
931 goal.time_achieved
932 .as_deref()
933 .unwrap_or("0000-00-00 00:00:00"),
934 ),
935 ])
936 },
937 )?;
938
939 Ok(())
940 }
941
942 }