1use super::{serde_util, CacheUserFn, ContainedUsers, GameMode};
2
3use serde::{
4 de::{Error, IgnoredAny, MapAccess, SeqAccess, Visitor},
5 Deserialize, Deserializer,
6};
7use smallstr::SmallString;
8use std::fmt;
9use time::{Date, OffsetDateTime};
10
11#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
12#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
13pub struct AccountHistory {
14 #[serde(default, skip_serializing_if = "Option::is_none")]
15 pub id: Option<u32>, pub description: Option<String>,
17 #[serde(rename = "type")]
18 pub history_type: HistoryType,
19 #[serde(with = "serde_util::datetime")]
20 pub timestamp: OffsetDateTime,
21 #[serde(rename = "length")]
22 pub seconds: u32,
23 pub permanent: bool,
24}
25
26#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
27#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
28pub struct Badge {
29 #[serde(with = "serde_util::datetime")]
30 pub awarded_at: OffsetDateTime,
31 pub description: String,
32 pub image_url: String,
33 pub url: String,
34}
35
36pub type CountryCode = SmallString<[u8; 2]>;
38
39struct CountryVisitor;
40
41impl<'de> Visitor<'de> for CountryVisitor {
42 type Value = String;
43
44 #[inline]
45 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 f.write_str("a string or a map containing a `name` field")
47 }
48
49 #[inline]
50 fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
51 Ok(v.to_owned())
52 }
53
54 #[inline]
55 fn visit_string<E: Error>(self, v: String) -> Result<Self::Value, E> {
56 Ok(v)
57 }
58
59 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
60 let mut country = None;
61
62 while let Some(key) = map.next_key()? {
63 match key {
64 "name" => country = Some(map.next_value()?),
65 _ => {
66 let _: IgnoredAny = map.next_value()?;
67 }
68 }
69 }
70
71 country.ok_or_else(|| Error::missing_field("name"))
72 }
73}
74
75struct OptionCountryVisitor;
76
77impl<'de> Visitor<'de> for OptionCountryVisitor {
78 type Value = Option<String>;
79
80 #[inline]
81 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 f.write_str("a string, a map containing a `name` field, or null")
83 }
84
85 #[inline]
86 fn visit_some<D: Deserializer<'de>>(self, d: D) -> Result<Self::Value, D::Error> {
87 d.deserialize_any(CountryVisitor).map(Some)
88 }
89
90 #[inline]
91 fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
92 self.visit_unit()
93 }
94
95 #[inline]
96 fn visit_unit<E: Error>(self) -> Result<Self::Value, E> {
97 Ok(None)
98 }
99}
100
101pub(crate) fn deserialize_country<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
102 d.deserialize_any(CountryVisitor)
103}
104
105pub(crate) fn deserialize_maybe_country<'de, D>(d: D) -> Result<Option<String>, D::Error>
106where
107 D: Deserializer<'de>,
108{
109 d.deserialize_option(OptionCountryVisitor)
110}
111
112#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
113#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
114pub struct DailyChallengeUserStatistics {
115 pub daily_streak_best: u32,
116 pub daily_streak_current: u32,
117 #[serde(
118 default,
119 skip_serializing_if = "Option::is_none",
120 with = "serde_util::option_datetime"
121 )]
122 pub last_update: Option<OffsetDateTime>,
123 #[serde(
124 default,
125 skip_serializing_if = "Option::is_none",
126 with = "serde_util::option_datetime"
127 )]
128 pub last_weekly_streak: Option<OffsetDateTime>,
129 pub playcount: u32,
130 pub top_10p_placements: u32,
131 pub top_50p_placements: u32,
132 pub user_id: u32,
133 pub weekly_streak_best: u32,
134 pub weekly_streak_current: u32,
135}
136
137#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
139#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
140pub struct GradeCounts {
141 #[serde(deserialize_with = "deserialize_i32_default")]
143 pub ss: i32,
144 #[serde(deserialize_with = "deserialize_i32_default")]
146 pub ssh: i32,
147 #[serde(deserialize_with = "deserialize_i32_default")]
149 pub s: i32,
150 #[serde(deserialize_with = "deserialize_i32_default")]
152 pub sh: i32,
153 #[serde(deserialize_with = "deserialize_i32_default")]
155 pub a: i32,
156}
157
158#[inline]
159fn deserialize_i32_default<'de, D: Deserializer<'de>>(d: D) -> Result<i32, D::Error> {
160 <Option<i32> as Deserialize>::deserialize(d).map(Option::unwrap_or_default)
161}
162
163#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
165#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
166pub struct Group {
167 #[serde(rename = "colour")]
168 pub color: Option<String>,
169 pub description: Option<String>,
170 #[serde(rename = "has_playmodes")]
172 pub has_modes: bool,
173 pub id: u32,
174 pub identifier: String,
176 pub is_probationary: bool,
178 #[serde(default, rename = "playmodes", skip_serializing_if = "Option::is_none")]
181 pub modes: Option<Vec<GameMode>>,
182 pub name: String,
183 pub short_name: String,
185}
186
187#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
188#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
189#[serde(rename_all = "snake_case")]
190pub enum HistoryType {
191 Note,
192 Restriction,
193 TournamentBan,
194 Silence,
195}
196
197#[derive(Clone, Debug, Deserialize)]
198#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
199pub struct Medal {
200 pub description: String,
201 pub grouping: String,
202 pub icon_url: String,
203 pub instructions: Option<String>,
204 #[serde(rename = "id")]
205 pub medal_id: u32,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub mode: Option<GameMode>,
208 pub name: String,
209 pub ordering: u32,
210 pub slug: String,
211}
212
213impl PartialEq for Medal {
214 #[inline]
215 fn eq(&self, other: &Self) -> bool {
216 self.medal_id == other.medal_id
217 }
218}
219
220impl Eq for Medal {}
221
222#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
223#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
224pub struct MedalCompact {
225 #[serde(with = "serde_util::datetime")]
226 pub achieved_at: OffsetDateTime,
227 #[serde(rename = "achievement_id")]
228 pub medal_id: u32,
229}
230
231#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
232#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
233pub struct MonthlyCount {
234 #[serde(with = "serde_util::date")]
235 pub start_date: Date,
236 pub count: i32,
237}
238
239#[derive(Clone, Debug, Deserialize)]
240#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
241pub struct ProfileBanner {
242 pub id: u32,
243 pub tournament_id: u32,
244 pub image: String,
245}
246
247impl PartialEq for ProfileBanner {
248 #[inline]
249 fn eq(&self, other: &Self) -> bool {
250 self.id == other.id && self.tournament_id == other.tournament_id
251 }
252}
253
254impl Eq for ProfileBanner {}
255
256#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
257#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
258pub enum Playstyle {
259 #[serde(rename = "mouse")]
260 Mouse,
261 #[serde(rename = "keyboard")]
262 Keyboard,
263 #[serde(rename = "tablet")]
264 Tablet,
265 #[serde(rename = "touch")]
266 Touch,
267}
268
269#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
270#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
271#[serde(rename_all = "snake_case")]
272pub enum ProfilePage {
273 Beatmaps,
274 Historical,
275 Kudosu,
276 Me,
277 Medals,
278 RecentActivity,
279 TopRanks,
280}
281
282#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
283#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
284pub struct Team {
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub flag_url: Option<String>,
287 pub id: u32,
288 pub name: String,
289 pub short_name: String,
290}
291
292#[derive(Clone, Debug, Deserialize, PartialEq)]
294#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
295pub struct UserExtended {
296 pub avatar_url: String,
298 pub comments_count: usize,
300 #[serde(deserialize_with = "deserialize_country")]
302 pub country: String,
303 pub country_code: CountryCode,
305 pub cover: UserCover,
307 #[serde(deserialize_with = "serde_util::from_option::deserialize")]
309 pub default_group: String,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub discord: Option<String>,
313 pub has_supported: bool,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub interests: Option<String>,
318 pub is_active: bool,
320 pub is_bot: bool,
322 pub is_deleted: bool,
324 pub is_online: bool,
326 pub is_supporter: bool,
328 #[serde(with = "serde_util::datetime")]
330 pub join_date: OffsetDateTime,
331 pub kudosu: UserKudosu,
333 #[serde(
335 default,
336 skip_serializing_if = "Option::is_none",
337 with = "serde_util::option_datetime"
338 )]
339 pub last_visit: Option<OffsetDateTime>,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
342 pub location: Option<String>,
343 pub max_blocks: u32,
345 pub max_friends: u32,
347 #[serde(rename = "playmode")]
349 pub mode: GameMode,
350 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub occupation: Option<String>,
353 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub playstyle: Option<Vec<Playstyle>>,
356 pub pm_friends_only: bool,
358 #[serde(rename = "post_count")]
360 pub forum_post_count: u32,
361 #[serde(
363 default,
364 rename = "profile_colour",
365 skip_serializing_if = "Option::is_none"
366 )]
367 pub profile_color: Option<String>,
368 pub profile_order: Vec<ProfilePage>,
370 #[serde(default, skip_serializing_if = "Option::is_none")]
371 pub team: Option<Team>,
372 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub title: Option<String>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub title_url: Option<String>,
378 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub twitter: Option<String>,
381 #[serde(rename = "id")]
383 pub user_id: u32,
384 pub username: Username,
386 #[serde(default, skip_serializing_if = "Option::is_none")]
388 pub website: Option<String>,
389
390 #[serde(default, skip_serializing_if = "Option::is_none")]
391 pub account_history: Option<Vec<AccountHistory>>,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
394 pub badges: Option<Vec<Badge>>,
395 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub beatmap_playcounts_count: Option<u32>,
397 #[serde(rename = "daily_challenge_user_stats")]
398 pub daily_challenge_stats: DailyChallengeUserStatistics,
399 #[serde(
400 default,
401 rename = "favourite_beatmapset_count",
402 skip_serializing_if = "Option::is_none"
403 )]
404 pub favourite_mapset_count: Option<u32>,
405 #[serde(default, skip_serializing_if = "Option::is_none")]
406 pub follower_count: Option<u32>,
407 #[serde(
409 default,
410 rename = "graveyard_beatmapset_count",
411 skip_serializing_if = "Option::is_none"
412 )]
413 pub graveyard_mapset_count: Option<u32>,
414 #[serde(default, skip_serializing_if = "Option::is_none")]
415 pub groups: Option<Vec<Group>>,
416 #[serde(
417 default,
418 rename = "guest_beatmapset_count",
419 skip_serializing_if = "Option::is_none"
420 )]
421 pub guest_mapset_count: Option<u32>,
422 #[serde(
423 default,
424 rename = "rank_highest",
425 skip_serializing_if = "Option::is_none"
426 )]
427 pub highest_rank: Option<UserHighestRank>,
428 #[serde(default, skip_serializing_if = "Option::is_none")]
429 pub is_admin: Option<bool>,
430 #[serde(default, skip_serializing_if = "Option::is_none")]
431 pub is_bng: Option<bool>,
432 #[serde(default, skip_serializing_if = "Option::is_none")]
433 pub is_full_bn: Option<bool>,
434 #[serde(default, skip_serializing_if = "Option::is_none")]
435 pub is_gmt: Option<bool>,
436 #[serde(default, skip_serializing_if = "Option::is_none")]
437 pub is_limited_bn: Option<bool>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
439 pub is_moderator: Option<bool>,
440 #[serde(default, skip_serializing_if = "Option::is_none")]
441 pub is_nat: Option<bool>,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub is_silenced: Option<bool>,
444 #[serde(
445 default,
446 rename = "loved_beatmapset_count",
447 skip_serializing_if = "Option::is_none"
448 )]
449 pub loved_mapset_count: Option<u32>,
450 #[serde(default, skip_serializing_if = "Option::is_none")]
451 pub mapping_follower_count: Option<u32>,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
453 pub monthly_playcounts: Option<Vec<MonthlyCount>>,
454 #[serde(default, skip_serializing_if = "Option::is_none")]
455 pub page: Option<UserPage>,
456 #[serde(default, skip_serializing_if = "Option::is_none")]
457 pub previous_usernames: Option<Vec<Username>>,
458 #[serde(
459 default,
460 deserialize_with = "rank_history_vec",
461 skip_serializing_if = "Option::is_none"
462 )]
463 pub rank_history: Option<Vec<u32>>,
464 #[serde(
466 default,
467 rename = "ranked_beatmapset_count",
468 skip_serializing_if = "Option::is_none"
469 )]
470 pub ranked_mapset_count: Option<u32>,
471 #[serde(default, skip_serializing_if = "Option::is_none")]
472 pub replays_watched_counts: Option<Vec<MonthlyCount>>,
473 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub scores_best_count: Option<u32>,
475 #[serde(default, skip_serializing_if = "Option::is_none")]
476 pub scores_first_count: Option<u32>,
477 #[serde(default, skip_serializing_if = "Option::is_none")]
478 pub scores_recent_count: Option<u32>,
479 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub statistics: Option<UserStatistics>,
481 #[serde(
482 default,
483 alias = "statistics_rulesets",
484 deserialize_with = "serde_util::from_option::deserialize"
485 )]
486 pub statistics_modes: UserStatisticsModes,
487 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub support_level: Option<u8>,
489 #[serde(
490 default,
491 rename = "pending_beatmapset_count",
492 skip_serializing_if = "Option::is_none"
493 )]
494 pub pending_mapset_count: Option<u32>,
495 #[serde(
496 default,
497 rename = "user_achievements",
498 skip_serializing_if = "Option::is_none"
499 )]
500 pub medals: Option<Vec<MedalCompact>>,
501}
502
503impl ContainedUsers for UserExtended {
504 fn apply_to_users(&self, f: impl CacheUserFn) {
505 f(self.user_id, &self.username);
506 }
507}
508
509#[derive(Clone, Debug, Deserialize, PartialEq)]
511#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
512pub struct User {
513 pub avatar_url: String,
515 pub country_code: CountryCode,
517 #[serde(deserialize_with = "serde_util::from_option::deserialize")]
519 pub default_group: String,
520 pub is_active: bool,
522 pub is_bot: bool,
524 pub is_deleted: bool,
526 pub is_online: bool,
528 pub is_supporter: bool,
530 #[serde(
532 default,
533 skip_serializing_if = "Option::is_none",
534 with = "serde_util::option_datetime"
535 )]
536 pub last_visit: Option<OffsetDateTime>,
537 pub pm_friends_only: bool,
539 #[serde(
541 default,
542 rename = "profile_colour",
543 skip_serializing_if = "Option::is_none"
544 )]
545 pub profile_color: Option<String>,
546 #[serde(rename = "id")]
548 pub user_id: u32,
549 pub username: Username,
551
552 #[serde(default, skip_serializing_if = "Option::is_none")]
553 pub account_history: Option<Vec<AccountHistory>>,
554 #[serde(default, skip_serializing_if = "Option::is_none")]
556 pub badges: Option<Vec<Badge>>,
557 #[serde(default, skip_serializing_if = "Option::is_none")]
558 pub beatmap_playcounts_count: Option<u32>,
559 #[serde(
560 default,
561 deserialize_with = "deserialize_maybe_country",
562 skip_serializing_if = "Option::is_none"
563 )]
564 pub country: Option<String>,
565 #[serde(default, skip_serializing_if = "Option::is_none")]
566 pub cover: Option<UserCover>,
567 #[serde(
568 default,
569 rename = "favourite_beatmapset_count",
570 skip_serializing_if = "Option::is_none"
571 )]
572 pub favourite_mapset_count: Option<u32>,
573 #[serde(default, skip_serializing_if = "Option::is_none")]
574 pub follower_count: Option<u32>,
575 #[serde(
577 default,
578 rename = "graveyard_beatmapset_count",
579 skip_serializing_if = "Option::is_none"
580 )]
581 pub graveyard_mapset_count: Option<u32>,
582 #[serde(default, skip_serializing_if = "Option::is_none")]
583 pub groups: Option<Vec<Group>>,
584 #[serde(
585 default,
586 rename = "guest_beatmapset_count",
587 skip_serializing_if = "Option::is_none"
588 )]
589 pub guest_mapset_count: Option<u32>,
590 #[serde(
591 default,
592 rename = "rank_highest",
593 skip_serializing_if = "Option::is_none"
594 )]
595 pub highest_rank: Option<UserHighestRank>,
596 #[serde(default, skip_serializing_if = "Option::is_none")]
597 pub is_admin: Option<bool>,
598 #[serde(default, skip_serializing_if = "Option::is_none")]
599 pub is_bng: Option<bool>,
600 #[serde(default, skip_serializing_if = "Option::is_none")]
601 pub is_full_bn: Option<bool>,
602 #[serde(default, skip_serializing_if = "Option::is_none")]
603 pub is_gmt: Option<bool>,
604 #[serde(default, skip_serializing_if = "Option::is_none")]
605 pub is_limited_bn: Option<bool>,
606 #[serde(default, skip_serializing_if = "Option::is_none")]
607 pub is_moderator: Option<bool>,
608 #[serde(default, skip_serializing_if = "Option::is_none")]
609 pub is_nat: Option<bool>,
610 #[serde(default, skip_serializing_if = "Option::is_none")]
611 pub is_silenced: Option<bool>,
612 #[serde(
613 default,
614 rename = "loved_beatmapset_count",
615 skip_serializing_if = "Option::is_none"
616 )]
617 pub loved_mapset_count: Option<u32>,
618 #[serde(
619 default,
620 rename = "user_achievements",
621 skip_serializing_if = "Option::is_none"
622 )]
623 pub medals: Option<Vec<MedalCompact>>,
624 #[serde(default, skip_serializing_if = "Option::is_none")]
625 pub monthly_playcounts: Option<Vec<MonthlyCount>>,
626 #[serde(default, skip_serializing_if = "Option::is_none")]
627 pub page: Option<UserPage>,
628 #[serde(default, skip_serializing_if = "Option::is_none")]
629 pub previous_usernames: Option<Vec<Username>>,
630 #[serde(
631 default,
632 deserialize_with = "rank_history_vec",
633 skip_serializing_if = "Option::is_none"
634 )]
635 pub rank_history: Option<Vec<u32>>,
636 #[serde(
638 default,
639 rename = "ranked_beatmapset_count",
640 skip_serializing_if = "Option::is_none"
641 )]
642 pub ranked_mapset_count: Option<u32>,
643 #[serde(default, skip_serializing_if = "Option::is_none")]
644 pub replays_watched_counts: Option<Vec<MonthlyCount>>,
645 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub scores_best_count: Option<u32>,
647 #[serde(default, skip_serializing_if = "Option::is_none")]
648 pub scores_first_count: Option<u32>,
649 #[serde(default, skip_serializing_if = "Option::is_none")]
650 pub scores_recent_count: Option<u32>,
651 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub statistics: Option<UserStatistics>,
653 #[serde(
654 default,
655 alias = "statistics_rulesets",
656 deserialize_with = "serde_util::from_option::deserialize"
657 )]
658 pub statistics_modes: UserStatisticsModes,
659 #[serde(default, skip_serializing_if = "Option::is_none")]
660 pub support_level: Option<u8>,
661 #[serde(
662 default,
663 rename = "pending_beatmapset_count",
664 skip_serializing_if = "Option::is_none"
665 )]
666 pub pending_mapset_count: Option<u32>,
667 #[serde(default, skip_serializing_if = "Option::is_none")]
668 pub team: Option<Team>,
669}
670
671impl ContainedUsers for User {
672 fn apply_to_users(&self, f: impl CacheUserFn) {
673 f(self.user_id, &self.username);
674 }
675}
676
677impl From<UserExtended> for User {
678 fn from(user: UserExtended) -> Self {
679 Self {
680 avatar_url: user.avatar_url,
681 country_code: user.country_code,
682 default_group: user.default_group,
683 is_active: user.is_active,
684 is_bot: user.is_bot,
685 is_deleted: user.is_deleted,
686 is_online: user.is_online,
687 is_supporter: user.is_supporter,
688 last_visit: user.last_visit,
689 pm_friends_only: user.pm_friends_only,
690 profile_color: user.profile_color,
691 user_id: user.user_id,
692 username: user.username,
693 account_history: user.account_history,
694 badges: user.badges,
695 beatmap_playcounts_count: user.beatmap_playcounts_count,
696 country: Some(user.country),
697 cover: Some(user.cover),
698 favourite_mapset_count: user.favourite_mapset_count,
699 follower_count: user.follower_count,
700 graveyard_mapset_count: user.graveyard_mapset_count,
701 groups: user.groups,
702 guest_mapset_count: user.guest_mapset_count,
703 highest_rank: user.highest_rank,
704 is_admin: user.is_admin,
705 is_bng: user.is_bng,
706 is_full_bn: user.is_full_bn,
707 is_gmt: user.is_gmt,
708 is_limited_bn: user.is_limited_bn,
709 is_moderator: user.is_moderator,
710 is_nat: user.is_nat,
711 is_silenced: user.is_silenced,
712 loved_mapset_count: user.loved_mapset_count,
713 medals: user.medals,
714 monthly_playcounts: user.monthly_playcounts,
715 page: user.page,
716 previous_usernames: user.previous_usernames,
717 rank_history: user.rank_history,
718 ranked_mapset_count: user.ranked_mapset_count,
719 replays_watched_counts: user.replays_watched_counts,
720 scores_best_count: user.scores_best_count,
721 scores_first_count: user.scores_first_count,
722 scores_recent_count: user.scores_recent_count,
723 statistics: user.statistics,
724 statistics_modes: UserStatisticsModes::default(),
725 support_level: user.support_level,
726 pending_mapset_count: user.pending_mapset_count,
727 team: user.team,
728 }
729 }
730}
731
732#[derive(Copy, Clone, Debug)]
736pub enum UserBeatmapsetsKind {
737 Favourite,
738 Graveyard,
739 Guest,
740 Loved,
741 Nominated,
742 Pending,
743 Ranked,
745}
746
747impl UserBeatmapsetsKind {
748 pub(crate) const fn as_str(self) -> &'static str {
749 match self {
750 Self::Favourite => "favourite",
751 Self::Graveyard => "graveyard",
752 Self::Guest => "guest",
753 Self::Loved => "loved",
754 Self::Nominated => "nominated",
755 Self::Pending => "pending",
756 Self::Ranked => "ranked",
757 }
758 }
759}
760
761#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
762#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
763pub struct UserCover {
764 #[serde(default, skip_serializing_if = "Option::is_none")]
765 pub custom_url: Option<String>,
766 pub url: String,
767 #[serde(default, skip_serializing_if = "Option::is_none")]
768 pub id: Option<String>,
769}
770
771#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
772#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
773pub struct UserHighestRank {
774 pub rank: u32,
775 #[serde(with = "serde_util::datetime")]
776 pub updated_at: OffsetDateTime,
777}
778
779#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
781#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
782pub struct UserKudosu {
783 pub available: i32,
785 pub total: i32,
787}
788
789#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
791#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
792pub struct UserLevel {
793 pub current: u32,
795 pub progress: u32,
797}
798
799impl UserLevel {
800 #[inline]
811 pub fn float(&self) -> f32 {
812 self.current as f32 + self.progress as f32 / 100.0
813 }
814}
815
816pub type Username = SmallString<[u8; 15]>;
818
819#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
820#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
821pub struct UserPage {
822 pub html: String,
823 pub raw: String,
824}
825
826#[derive(Clone, Debug, Deserialize, PartialEq)]
828#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
829pub struct UserStatistics {
830 #[serde(rename = "hit_accuracy")]
832 pub accuracy: f32,
833 pub count_300: u32,
835 pub count_100: u32,
837 pub count_50: u32,
839 pub count_miss: u32,
841 #[serde(default, skip_serializing_if = "Option::is_none")]
843 pub country_rank: Option<u32>,
844 pub global_rank: Option<u32>,
846 pub grade_counts: GradeCounts,
848 pub is_ranked: bool,
850 pub level: UserLevel,
852 #[serde(rename = "maximum_combo")]
854 pub max_combo: u32,
855 #[serde(rename = "play_count")]
857 pub playcount: u32,
858 #[serde(rename = "play_time", deserialize_with = "maybe_u32")]
860 pub playtime: u32,
861 #[serde(deserialize_with = "deserialize_f32_default")]
863 pub pp: f32,
864 pub ranked_score: u64,
866 #[serde(default, skip_serializing_if = "Option::is_none")]
868 pub rank_change_since_30_days: Option<i32>,
869 #[serde(rename = "replays_watched_by_others")]
871 pub replays_watched: u32,
872 pub total_hits: u64,
874 pub total_score: u64,
876}
877
878#[inline]
879fn deserialize_f32_default<'de, D: Deserializer<'de>>(d: D) -> Result<f32, D::Error> {
880 <Option<f32> as Deserialize>::deserialize(d).map(Option::unwrap_or_default)
881}
882
883#[inline]
884fn maybe_u32<'de, D: Deserializer<'de>>(d: D) -> Result<u32, D::Error> {
885 <Option<u32> as Deserialize>::deserialize(d).map(Option::unwrap_or_default)
886}
887
888#[inline]
889fn rank_history_vec<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<u32>>, D::Error> {
890 d.deserialize_option(RankHistoryVisitor)
891}
892
893#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
894#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
895pub struct UserStatisticsModes {
896 pub osu: Option<UserStatistics>,
897 pub taiko: Option<UserStatistics>,
898 #[serde(alias = "fruits")]
899 pub catch: Option<UserStatistics>,
900 pub mania: Option<UserStatistics>,
901}
902
903struct RankHistoryVisitor;
904
905impl<'de> Visitor<'de> for RankHistoryVisitor {
906 type Value = Option<Vec<u32>>;
907
908 #[inline]
909 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
910 f.write_str("a map containing the field `data`, or a list of u32")
911 }
912
913 #[inline]
914 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
915 let capacity = seq.size_hint().unwrap_or(0);
916 let mut rank_history_vec = Vec::with_capacity(capacity);
917
918 while let Some(next) = seq.next_element()? {
919 rank_history_vec.push(next);
920 }
921
922 Ok(Some(rank_history_vec))
923 }
924
925 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
926 let mut rank_history_vec: Option<Option<Vec<u32>>> = None;
927
928 while let Some(key) = map.next_key::<&str>()? {
929 if key == "data" && rank_history_vec.is_none() {
930 rank_history_vec = Some(map.next_value()?);
931 } else {
932 map.next_value::<IgnoredAny>()?;
933 }
934 }
935
936 rank_history_vec.ok_or_else(|| Error::missing_field("data"))
937 }
938
939 #[inline]
940 fn visit_some<D: Deserializer<'de>>(self, d: D) -> Result<Self::Value, D::Error> {
941 d.deserialize_any(RankHistoryVisitor)
942 }
943
944 #[inline]
945 fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
946 self.visit_unit()
947 }
948
949 #[inline]
950 fn visit_unit<E: Error>(self) -> Result<Self::Value, E> {
951 Ok(None)
952 }
953}