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#[derive(Clone, Debug, Eq, Hash, PartialEq)]
37pub enum UserId {
38 Id(u32),
40 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#[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 #[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#[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#[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 #[inline]
155 pub const fn mode(mut self, mode: GameMode) -> Self {
156 self.mode = Some(mode);
157
158 self
159 }
160
161 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#[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 #[inline]
215 pub const fn limit(mut self, limit: usize) -> Self {
216 self.limit = Some(limit);
217
218 self
219 }
220
221 #[inline]
224 pub const fn offset(mut self, offset: usize) -> Self {
225 self.offset = Some(offset);
226
227 self
228 }
229
230 #[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#[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 #[inline]
280 pub const fn limit(mut self, limit: usize) -> Self {
281 self.limit = Some(limit);
282
283 self
284 }
285
286 #[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#[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 #[inline]
330 pub const fn limit(mut self, limit: usize) -> Self {
331 self.limit = Some(limit);
332
333 self
334 }
335
336 #[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#[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 #[inline]
385 pub const fn limit(mut self, limit: usize) -> Self {
386 self.limit = Some(limit);
387
388 self
389 }
390
391 #[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#[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 #[inline]
473 pub const fn limit(mut self, limit: usize) -> Self {
474 self.limit = Some(limit);
475
476 self
477 }
478
479 #[inline]
482 pub const fn offset(mut self, offset: usize) -> Self {
483 self.offset = Some(offset);
484
485 self
486 }
487
488 #[inline]
490 pub const fn mode(mut self, mode: GameMode) -> Self {
491 self.mode = Some(mode);
492
493 self
494 }
495
496 #[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 #[inline]
508 pub const fn best(mut self) -> Self {
509 self.score_type = ScoreType::Best;
510
511 self
512 }
513
514 #[inline]
516 pub const fn firsts(mut self) -> Self {
517 self.score_type = ScoreType::First;
518
519 self
520 }
521
522 #[inline]
524 pub const fn pinned(mut self) -> Self {
525 self.score_type = ScoreType::Pinned;
526
527 self
528 }
529
530 #[inline]
532 pub const fn recent(mut self) -> Self {
533 self.score_type = ScoreType::Recent;
534
535 self
536 }
537
538 #[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 #[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#[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}