rosu_v2/request/
user.rs

1use std::fmt;
2
3use itoa::Buffer;
4use serde::Serialize;
5use smallstr::SmallString;
6
7use crate::{
8    model::{
9        beatmap::{BeatmapsetExtended, MostPlayedMap},
10        event::Event,
11        kudosu::KudosuHistory,
12        score::Score,
13        user::{User, UserBeatmapsetsKind, UserExtended, Username},
14        DeserializedList, GameMode,
15    },
16    request::{
17        serialize::{maybe_bool_as_u8, maybe_mode_as_str, user_id_type},
18        Query, Request,
19    },
20    routing::Route,
21    Osu,
22};
23
24/// Either a user id as `u32` or a username as [`Username`].
25///
26/// Use the `From` implementations to create this enum.
27///
28/// # Example
29///
30/// ```
31/// use rosu_v2::request::UserId;
32///
33/// let user_id: UserId = 123_456.into();
34/// let user_id: UserId = "my username".into();
35/// ```
36#[derive(Clone, Debug, Eq, Hash, PartialEq)]
37pub enum UserId {
38    /// Represents a user through their user id
39    Id(u32),
40    /// Represents a user through their username
41    Name(Username),
42}
43
44impl From<u32> for UserId {
45    #[inline]
46    fn from(id: u32) -> Self {
47        Self::Id(id)
48    }
49}
50
51impl From<&str> for UserId {
52    #[inline]
53    fn from(name: &str) -> Self {
54        Self::Name(SmallString::from_str(name))
55    }
56}
57
58impl From<&String> for UserId {
59    #[inline]
60    fn from(name: &String) -> Self {
61        Self::Name(SmallString::from_str(name))
62    }
63}
64
65impl From<String> for UserId {
66    #[inline]
67    fn from(name: String) -> Self {
68        Self::Name(SmallString::from_string(name))
69    }
70}
71
72impl fmt::Display for UserId {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match self {
75            Self::Id(id) => write!(f, "{id}"),
76            Self::Name(name) => f.write_str(name),
77        }
78    }
79}
80
81/// Get the [`UserExtended`] of the authenticated user.
82///
83/// Note that the client has to be initialized with the `Identify` scope
84/// through the OAuth process in order for this endpoint to not return an error.
85///
86/// See [`OsuBuilder::with_authorization`](crate::OsuBuilder::with_authorization).
87#[must_use = "requests must be configured and executed"]
88pub struct GetOwnData<'a> {
89    osu: &'a Osu,
90    mode: Option<GameMode>,
91}
92
93impl<'a> GetOwnData<'a> {
94    pub(crate) const fn new(osu: &'a Osu) -> Self {
95        Self { osu, mode: None }
96    }
97
98    /// Specify the mode for which the user data should be retrieved
99    #[inline]
100    pub const fn mode(mut self, mode: GameMode) -> Self {
101        self.mode = Some(mode);
102
103        self
104    }
105}
106
107into_future! {
108    |self: GetOwnData<'_>| -> UserExtended {
109        Request::new(Route::GetOwnData { mode: self.mode })
110    }
111}
112
113/// Get all friends of the authenticated user as a vec of [`User`].
114///
115/// Note that the client has to be initialized with the `FriendsRead` scope
116/// through the OAuth process in order for this endpoint to not return an error.
117///
118/// See [`OsuBuilder::with_authorization`](crate::OsuBuilder::with_authorization).
119#[must_use = "requests must be configured and executed"]
120pub struct GetFriends<'a> {
121    osu: &'a Osu,
122}
123
124impl<'a> GetFriends<'a> {
125    pub(crate) const fn new(osu: &'a Osu) -> Self {
126        Self { osu }
127    }
128}
129
130into_future! {
131    |self: GetFriends<'_>| -> Vec<User> {
132        Request::new(Route::GetFriends)
133    }
134}
135
136/// Get a [`UserExtended`].
137#[must_use = "requests must be configured and executed"]
138pub struct GetUser<'a> {
139    osu: &'a Osu,
140    user_id: UserId,
141    mode: Option<GameMode>,
142}
143
144impl<'a> GetUser<'a> {
145    pub(crate) const fn new(osu: &'a Osu, user_id: UserId) -> Self {
146        Self {
147            osu,
148            user_id,
149            mode: None,
150        }
151    }
152
153    /// Specify the mode for which the user data should be retrieved
154    #[inline]
155    pub const fn mode(mut self, mode: GameMode) -> Self {
156        self.mode = Some(mode);
157
158        self
159    }
160
161    /// Auxiliary function so that [`GetUser`]'s future can be created without
162    /// an actual [`GetUser`] instance.
163    ///
164    /// Used for username caching.
165    pub(crate) fn create_request(user_id: UserId, mode: Option<GameMode>) -> Request {
166        #[derive(Serialize)]
167        pub struct UserQuery {
168            #[serde(rename(serialize = "key"), serialize_with = "user_id_type")]
169            user_id: UserId,
170        }
171
172        let user_query = UserQuery { user_id };
173        let query = Query::encode(&user_query);
174        let user_id = user_query.user_id;
175
176        let route = Route::GetUser { user_id, mode };
177
178        Request::with_query(route, query)
179    }
180}
181
182into_future! {
183    |self: GetUser<'_>| -> UserExtended {
184        Self::create_request(self.user_id, self.mode)
185    }
186}
187
188/// Get the [`BeatmapsetExtended`]s of a user.
189#[must_use = "requests must be configured and executed"]
190#[derive(Serialize)]
191pub struct GetUserBeatmapsets<'a> {
192    #[serde(skip)]
193    osu: &'a Osu,
194    #[serde(skip)]
195    map_kind: UserBeatmapsetsKind,
196    limit: Option<usize>,
197    offset: Option<usize>,
198    #[serde(skip)]
199    user_id: UserId,
200}
201
202impl<'a> GetUserBeatmapsets<'a> {
203    pub(crate) const fn new(osu: &'a Osu, user_id: UserId, kind: UserBeatmapsetsKind) -> Self {
204        Self {
205            osu,
206            user_id,
207            map_kind: kind,
208            limit: None,
209            offset: None,
210        }
211    }
212
213    /// Limit the amount of results in the response
214    #[inline]
215    pub const fn limit(mut self, limit: usize) -> Self {
216        self.limit = Some(limit);
217
218        self
219    }
220
221    /// Set an offset for the requested elements
222    /// e.g. skip the first `offset` amount in the list
223    #[inline]
224    pub const fn offset(mut self, offset: usize) -> Self {
225        self.offset = Some(offset);
226
227        self
228    }
229
230    /// Only include mapsets of the specified type
231    #[inline]
232    pub const fn kind(mut self, kind: UserBeatmapsetsKind) -> Self {
233        self.map_kind = kind;
234
235        self
236    }
237}
238
239into_future! {
240    |self: GetUserBeatmapsets<'_>| -> Vec<BeatmapsetExtended> {
241        GetUserBeatmapsetsData {
242            map_kind: UserBeatmapsetsKind = self.map_kind,
243            query: String = Query::encode(&self),
244        }
245    } => |user_id, data| {
246        Request::with_query(
247            Route::GetUserBeatmapsets {
248                user_id,
249                map_type: data.map_kind.as_str(),
250            },
251            data.query,
252        )
253    }
254}
255
256/// Get a user's kudosu history as a vec of [`KudosuHistory`].
257#[must_use = "requests must be configured and executed"]
258#[derive(Serialize)]
259pub struct GetUserKudosu<'a> {
260    #[serde(skip)]
261    osu: &'a Osu,
262    limit: Option<usize>,
263    offset: Option<usize>,
264    #[serde(skip)]
265    user_id: UserId,
266}
267
268impl<'a> GetUserKudosu<'a> {
269    pub(crate) const fn new(osu: &'a Osu, user_id: UserId) -> Self {
270        Self {
271            osu,
272            user_id,
273            limit: None,
274            offset: None,
275        }
276    }
277
278    /// Limit the amount of results in the response
279    #[inline]
280    pub const fn limit(mut self, limit: usize) -> Self {
281        self.limit = Some(limit);
282
283        self
284    }
285
286    /// Set an offset for the requested elements
287    /// e.g. skip the first `offset` amount in the list
288    #[inline]
289    pub const fn offset(mut self, offset: usize) -> Self {
290        self.offset = Some(offset);
291
292        self
293    }
294}
295
296into_future! {
297    |self: GetUserKudosu<'_>| -> Vec<KudosuHistory> {
298        GetUserKudosuData {
299            query: String = Query::encode(&self),
300        }
301    } => |user_id, data| {
302        Request::with_query(Route::GetUserKudosu { user_id }, data.query)
303    }
304}
305
306/// Get the most played beatmaps of a user as a vec of [`MostPlayedMap`].
307#[must_use = "requests must be configured and executed"]
308#[derive(Serialize)]
309pub struct GetUserMostPlayed<'a> {
310    #[serde(skip)]
311    osu: &'a Osu,
312    limit: Option<usize>,
313    offset: Option<usize>,
314    #[serde(skip)]
315    user_id: UserId,
316}
317
318impl<'a> GetUserMostPlayed<'a> {
319    pub(crate) const fn new(osu: &'a Osu, user_id: UserId) -> Self {
320        Self {
321            osu,
322            user_id,
323            limit: None,
324            offset: None,
325        }
326    }
327
328    /// The API provides at most 51 results per requests.
329    #[inline]
330    pub const fn limit(mut self, limit: usize) -> Self {
331        self.limit = Some(limit);
332
333        self
334    }
335
336    /// Set an offset for the requested elements
337    /// e.g. skip the first `offset` amount in the list
338    #[inline]
339    pub const fn offset(mut self, offset: usize) -> Self {
340        self.offset = Some(offset);
341
342        self
343    }
344}
345
346into_future! {
347    |self: GetUserMostPlayed<'_>| -> Vec<MostPlayedMap> {
348        GetUserMostPlayedData {
349            query: String = Query::encode(&self),
350        }
351    } => |user_id, data| {
352        let route = Route::GetUserBeatmapsets {
353            user_id,
354            map_type: "most_played",
355        };
356
357        Request::with_query(route, data.query)
358    }
359}
360
361/// Get a vec of [`Event`] of a user.
362#[must_use = "requests must be configured and executed"]
363#[derive(Serialize)]
364pub struct GetRecentActivity<'a> {
365    #[serde(skip)]
366    osu: &'a Osu,
367    limit: Option<usize>,
368    offset: Option<usize>,
369    #[serde(skip)]
370    user_id: UserId,
371}
372
373impl<'a> GetRecentActivity<'a> {
374    pub(crate) const fn new(osu: &'a Osu, user_id: UserId) -> Self {
375        Self {
376            osu,
377            user_id,
378            limit: None,
379            offset: None,
380        }
381    }
382
383    /// Limit the amount of results in the response
384    #[inline]
385    pub const fn limit(mut self, limit: usize) -> Self {
386        self.limit = Some(limit);
387
388        self
389    }
390
391    /// Set an offset for the requested elements
392    /// e.g. skip the first `offset` amount in the list
393    #[inline]
394    pub const fn offset(mut self, offset: usize) -> Self {
395        self.offset = Some(offset);
396
397        self
398    }
399}
400
401into_future! {
402    |self: GetRecentActivity<'_>| -> Vec<Event> {
403        GetRecentActivityData {
404            query: String = Query::encode(&self),
405        }
406    } => |user_id, data| {
407        Request::with_query(Route::GetRecentActivity { user_id }, data.query)
408    }
409}
410
411#[derive(Copy, Clone, Debug)]
412pub(crate) enum ScoreType {
413    Best,
414    First,
415    Pinned,
416    Recent,
417}
418
419impl ScoreType {
420    pub(crate) const fn as_str(self) -> &'static str {
421        match self {
422            Self::Best => "best",
423            Self::First => "firsts",
424            Self::Pinned => "pinned",
425            Self::Recent => "recent",
426        }
427    }
428}
429
430/// Get a vec of [`Score`]s of a user.
431///
432/// If no score type is specified by either
433/// [`best`](crate::request::GetUserScores::best),
434/// [`firsts`](crate::request::GetUserScores::firsts),
435/// or [`recent`](crate::request::GetUserScores::recent), it defaults to `best`.
436#[must_use = "requests must be configured and executed"]
437#[derive(Serialize)]
438pub struct GetUserScores<'a> {
439    #[serde(skip)]
440    osu: &'a Osu,
441    #[serde(skip)]
442    score_type: ScoreType,
443    limit: Option<usize>,
444    offset: Option<usize>,
445    #[serde(serialize_with = "maybe_bool_as_u8")]
446    include_fails: Option<bool>,
447    #[serde(serialize_with = "maybe_mode_as_str")]
448    mode: Option<GameMode>,
449    legacy_only: bool,
450    #[serde(skip)]
451    legacy_scores: bool,
452    #[serde(skip)]
453    user_id: UserId,
454}
455
456impl<'a> GetUserScores<'a> {
457    pub(crate) const fn new(osu: &'a Osu, user_id: UserId) -> Self {
458        Self {
459            osu,
460            user_id,
461            score_type: ScoreType::Best,
462            limit: None,
463            offset: None,
464            include_fails: None,
465            mode: None,
466            legacy_only: false,
467            legacy_scores: false,
468        }
469    }
470
471    /// The API provides at most 100 results per requests.
472    #[inline]
473    pub const fn limit(mut self, limit: usize) -> Self {
474        self.limit = Some(limit);
475
476        self
477    }
478
479    /// Set an offset for the requested elements
480    /// e.g. skip the first `offset` amount in the list
481    #[inline]
482    pub const fn offset(mut self, offset: usize) -> Self {
483        self.offset = Some(offset);
484
485        self
486    }
487
488    /// Specify the mode of the scores
489    #[inline]
490    pub const fn mode(mut self, mode: GameMode) -> Self {
491        self.mode = Some(mode);
492
493        self
494    }
495
496    /// Specify whether failed scores can be included.
497    ///
498    /// Only relevant for [`recent`](GetUserScores::recent)
499    #[inline]
500    pub const fn include_fails(mut self, include_fails: bool) -> Self {
501        self.include_fails = Some(include_fails);
502
503        self
504    }
505
506    /// Get top scores of a user
507    #[inline]
508    pub const fn best(mut self) -> Self {
509        self.score_type = ScoreType::Best;
510
511        self
512    }
513
514    /// Get global #1 scores of a user.
515    #[inline]
516    pub const fn firsts(mut self) -> Self {
517        self.score_type = ScoreType::First;
518
519        self
520    }
521
522    /// Get the pinned scores of a user.
523    #[inline]
524    pub const fn pinned(mut self) -> Self {
525        self.score_type = ScoreType::Pinned;
526
527        self
528    }
529
530    /// Get recent scores of a user.
531    #[inline]
532    pub const fn recent(mut self) -> Self {
533        self.score_type = ScoreType::Recent;
534
535        self
536    }
537
538    /// Whether or not to exclude lazer scores.
539    #[inline]
540    pub const fn legacy_only(mut self, legacy_only: bool) -> Self {
541        self.legacy_only = legacy_only;
542
543        self
544    }
545
546    /// Specify whether the scores should contain legacy data or not.
547    ///
548    /// Legacy data consists of a different grade calculation, less
549    /// populated statistics, legacy mods, and a different score kind.
550    #[inline]
551    pub const fn legacy_scores(mut self, legacy_scores: bool) -> Self {
552        self.legacy_scores = legacy_scores;
553
554        self
555    }
556}
557
558into_future! {
559    |self: GetUserScores<'_>| -> Vec<Score> {
560        GetUserScoresData {
561            query: String = Query::encode(&self),
562            score_type: ScoreType = self.score_type,
563            legacy_scores: bool = self.legacy_scores,
564        }
565    } => |user_id, data| {
566        let route = Route::GetUserScores {
567            user_id,
568            score_type: data.score_type,
569        };
570
571        let mut req = Request::with_query(route, data.query);
572
573        if data.legacy_scores {
574            req.api_version(0);
575        }
576
577        req
578    }
579}
580
581/// Get a vec of [`User`].
582#[must_use = "requests must be configured and executed"]
583pub struct GetUsers<'a> {
584    osu: &'a Osu,
585    query: String,
586}
587
588impl<'a> GetUsers<'a> {
589    pub(crate) fn new<I>(osu: &'a Osu, user_ids: I) -> Self
590    where
591        I: IntoIterator<Item = u32>,
592    {
593        let mut query = String::new();
594        let mut buf = Buffer::new();
595
596        let mut iter = user_ids.into_iter().take(50);
597
598        if let Some(user_id) = iter.next() {
599            query.push_str("ids[]=");
600            query.push_str(buf.format(user_id));
601
602            for user_id in iter {
603                query.push_str("&ids[]=");
604                query.push_str(buf.format(user_id));
605            }
606        }
607
608        Self { osu, query }
609    }
610}
611
612into_future! {
613    |self: GetUsers<'_>| -> DeserializedList<User> {
614        Request::with_query(Route::GetUsers, self.query)
615    } => |users, _| -> Vec<User> {
616        Ok(users.0)
617    }
618}