rosu_v2/model/
user.rs

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>, // TODO: Can be removed?
16    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
36/// Country codes are at most 2 ASCII characters long
37pub 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/// Counts of grades of a [`UserExtended`].
138#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
139#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
140pub struct GradeCounts {
141    /// Number of SS ranked scores
142    #[serde(deserialize_with = "deserialize_i32_default")]
143    pub ss: i32,
144    /// Number of Silver SS ranked scores
145    #[serde(deserialize_with = "deserialize_i32_default")]
146    pub ssh: i32,
147    /// Number of S ranked scores
148    #[serde(deserialize_with = "deserialize_i32_default")]
149    pub s: i32,
150    /// Number of Silver S ranked scores
151    #[serde(deserialize_with = "deserialize_i32_default")]
152    pub sh: i32,
153    /// Number of A ranked scores
154    #[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/// Describes a Group membership of a [`UserExtended`].
164#[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    /// Whether this group associates [`GameMode`]s with users' memberships.
171    #[serde(rename = "has_playmodes")]
172    pub has_modes: bool,
173    pub id: u32,
174    /// Unique string to identify the group.
175    pub identifier: String,
176    /// Whether members of this group are considered probationary.
177    pub is_probationary: bool,
178    /// [`GameMode`]s associated with this membership (`None` if `has_modes` is
179    /// unset).
180    #[serde(default, rename = "playmodes", skip_serializing_if = "Option::is_none")]
181    pub modes: Option<Vec<GameMode>>,
182    pub name: String,
183    /// Short name of the group for display.
184    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/// Represents a User. Extends [`User`] object with additional attributes.
293#[derive(Clone, Debug, Deserialize, PartialEq)]
294#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
295pub struct UserExtended {
296    /// url of user's avatar
297    pub avatar_url: String,
298    /// number of forum comments
299    pub comments_count: usize,
300    /// country of the user
301    #[serde(deserialize_with = "deserialize_country")]
302    pub country: String,
303    /// two-letter code representing user's country
304    pub country_code: CountryCode,
305    /// urls for the profile cover
306    pub cover: UserCover,
307    /// Identifier of the default [`Group`] the user belongs to.
308    #[serde(deserialize_with = "serde_util::from_option::deserialize")]
309    pub default_group: String,
310    /// discord tag, `None` if not specified by the user
311    #[serde(default, skip_serializing_if = "Option::is_none")]
312    pub discord: Option<String>,
313    /// whether or not ever being a supporter in the past
314    pub has_supported: bool,
315    /// interests, `None` if not specified by the user
316    #[serde(default, skip_serializing_if = "Option::is_none")]
317    pub interests: Option<String>,
318    /// has this account been active in the last x months?
319    pub is_active: bool,
320    /// is this a bot account?
321    pub is_bot: bool,
322    /// has this user been deleted?
323    pub is_deleted: bool,
324    /// is the user currently online? (either on lazer or the new website)
325    pub is_online: bool,
326    /// does this user have supporter?
327    pub is_supporter: bool,
328    /// date of account creation
329    #[serde(with = "serde_util::datetime")]
330    pub join_date: OffsetDateTime,
331    /// current kudosu of the user
332    pub kudosu: UserKudosu,
333    /// last access time. `None` if the user hides online presence
334    #[serde(
335        default,
336        skip_serializing_if = "Option::is_none",
337        with = "serde_util::option_datetime"
338    )]
339    pub last_visit: Option<OffsetDateTime>,
340    /// location of the user, `None` if disabled by the user
341    #[serde(default, skip_serializing_if = "Option::is_none")]
342    pub location: Option<String>,
343    /// maximum number of users allowed to be blocked
344    pub max_blocks: u32,
345    /// maximum number of friends allowed to be added
346    pub max_friends: u32,
347    /// mode for this struct
348    #[serde(rename = "playmode")]
349    pub mode: GameMode,
350    /// occupation, `None` if not specified by the user
351    #[serde(default, skip_serializing_if = "Option::is_none")]
352    pub occupation: Option<String>,
353    /// Device choices of the user
354    #[serde(default, skip_serializing_if = "Option::is_none")]
355    pub playstyle: Option<Vec<Playstyle>>,
356    /// whether or not the user allows PM from other than friends
357    pub pm_friends_only: bool,
358    /// number of forum posts
359    #[serde(rename = "post_count")]
360    pub forum_post_count: u32,
361    /// colour of username/profile highlight, hex code (e.g. `"#333333"`)
362    #[serde(
363        default,
364        rename = "profile_colour",
365        skip_serializing_if = "Option::is_none"
366    )]
367    pub profile_color: Option<String>,
368    /// ordered list of sections in user profile page
369    pub profile_order: Vec<ProfilePage>,
370    #[serde(default, skip_serializing_if = "Option::is_none")]
371    pub team: Option<Team>,
372    /// user-specific title
373    #[serde(default, skip_serializing_if = "Option::is_none")]
374    pub title: Option<String>,
375    /// URL to the user title
376    #[serde(default, skip_serializing_if = "Option::is_none")]
377    pub title_url: Option<String>,
378    /// twitter handle, `None` if not specified by the user
379    #[serde(default, skip_serializing_if = "Option::is_none")]
380    pub twitter: Option<String>,
381    /// unique identifier for user
382    #[serde(rename = "id")]
383    pub user_id: u32,
384    /// user's display name
385    pub username: Username,
386    /// website, `None` if not specified by the user
387    #[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    // pub active_tournament_banner: Option<ProfileBanner>, // TODO
393    #[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    // friends: Option<>,
408    #[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    /// Counts both ranked and approved mapsets
465    #[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/// Mainly used for embedding in certain responses to save additional api lookups.
510#[derive(Clone, Debug, Deserialize, PartialEq)]
511#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
512pub struct User {
513    /// url of user's avatar
514    pub avatar_url: String,
515    /// two-letter code representing user's country
516    pub country_code: CountryCode,
517    /// Identifier of the default [`Group`] the user belongs to.
518    #[serde(deserialize_with = "serde_util::from_option::deserialize")]
519    pub default_group: String,
520    /// has this account been active in the last x months?
521    pub is_active: bool,
522    /// is this a bot account?
523    pub is_bot: bool,
524    /// has this user been deleted?
525    pub is_deleted: bool,
526    /// is the user currently online? (either on lazer or the new website)
527    pub is_online: bool,
528    /// does this user have supporter?
529    pub is_supporter: bool,
530    /// last access time. `None` if the user hides online presence
531    #[serde(
532        default,
533        skip_serializing_if = "Option::is_none",
534        with = "serde_util::option_datetime"
535    )]
536    pub last_visit: Option<OffsetDateTime>,
537    /// whether or not the user allows PM from other than friends
538    pub pm_friends_only: bool,
539    /// colour of username/profile highlight, hex code (e.g. `"#333333"`)
540    #[serde(
541        default,
542        rename = "profile_colour",
543        skip_serializing_if = "Option::is_none"
544    )]
545    pub profile_color: Option<String>,
546    /// unique identifier for user
547    #[serde(rename = "id")]
548    pub user_id: u32,
549    /// user's display name
550    pub username: Username,
551
552    #[serde(default, skip_serializing_if = "Option::is_none")]
553    pub account_history: Option<Vec<AccountHistory>>,
554    // pub active_tournament_banner: Option<ProfileBanner>, // TODO
555    #[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    // friends: Option<>,
576    #[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    /// Counts both ranked and approved mapsets
637    #[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/// Specifies the type of mapsets returned by [`Osu::user_beatmapsets`].
733///
734/// [`Osu::user_beatmapsets`]: crate::Osu::user_beatmapsets
735#[derive(Copy, Clone, Debug)]
736pub enum UserBeatmapsetsKind {
737    Favourite,
738    Graveyard,
739    Guest,
740    Loved,
741    Nominated,
742    Pending,
743    /// Both ranked and approved
744    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/// Kudosu of a [`UserExtended`]
780#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
781#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
782pub struct UserKudosu {
783    /// Currently available kudosu
784    pub available: i32,
785    /// Total gained kudosu
786    pub total: i32,
787}
788
789/// Level progression of a [`UserExtended`].
790#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
791#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
792pub struct UserLevel {
793    /// The current level
794    pub current: u32,
795    /// Percentage to the next level between `0.0` and `100.0`
796    pub progress: u32,
797}
798
799impl UserLevel {
800    /// Combine `self.current` and `self.progress` into a corresponding f32.
801    ///
802    /// # Example
803    ///
804    /// ```
805    /// use rosu_v2::model::user::UserLevel;
806    ///
807    /// let level = UserLevel { current: 100, progress: 25 };
808    /// assert_eq!(level.float(), 100.25);
809    /// ```
810    #[inline]
811    pub fn float(&self) -> f32 {
812        self.current as f32 + self.progress as f32 / 100.0
813    }
814}
815
816/// osu! usernames are at most 15 ASCII characters long
817pub 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/// A summary of various gameplay statistics for a [`UserExtended`]. Specific to a [`GameMode`]
827#[derive(Clone, Debug, Deserialize, PartialEq)]
828#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
829pub struct UserStatistics {
830    /// Hit accuracy percentage
831    #[serde(rename = "hit_accuracy")]
832    pub accuracy: f32,
833    /// Total number of n300s
834    pub count_300: u32,
835    /// Total number of n100s
836    pub count_100: u32,
837    /// Total number of n50s
838    pub count_50: u32,
839    /// Total number of misses
840    pub count_miss: u32,
841    /// Current country rank according to pp
842    #[serde(default, skip_serializing_if = "Option::is_none")]
843    pub country_rank: Option<u32>,
844    /// Current global rank according to pp
845    pub global_rank: Option<u32>,
846    /// Counts of grades
847    pub grade_counts: GradeCounts,
848    /// Is actively ranked
849    pub is_ranked: bool,
850    /// The user's level progression
851    pub level: UserLevel,
852    /// Highest maximum combo
853    #[serde(rename = "maximum_combo")]
854    pub max_combo: u32,
855    /// Number of maps played
856    #[serde(rename = "play_count")]
857    pub playcount: u32,
858    /// Cumulative time played in seconds
859    #[serde(rename = "play_time", deserialize_with = "maybe_u32")]
860    pub playtime: u32,
861    /// Performance points
862    #[serde(deserialize_with = "deserialize_f32_default")]
863    pub pp: f32,
864    /// Current ranked score
865    pub ranked_score: u64,
866    /// Rank change in the last 30 days
867    #[serde(default, skip_serializing_if = "Option::is_none")]
868    pub rank_change_since_30_days: Option<i32>,
869    /// Number of replays watched by other users
870    #[serde(rename = "replays_watched_by_others")]
871    pub replays_watched: u32,
872    /// Total number of hits
873    pub total_hits: u64,
874    /// Total score
875    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}