From a231bd6c0ca2052b0dd36306f4b619916f141364 Mon Sep 17 00:00:00 2001 From: Badewanne3 <41148446+MaxOhn@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:01:29 +0200 Subject: [PATCH] feat: add friends endpoint (#38) --- README.md | 1 + src/client/mod.rs | 53 +++++++++++++++++++++++++++------------------ src/client/token.rs | 3 +-- src/lib.rs | 1 + src/request/user.rs | 38 +++++++++++++++++++++++++++++++- src/routing.rs | 3 +++ 6 files changed, 75 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index c950bb4..f604349 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ The following endpoints are currently supported: - `comments`: Most recent comments and their replies up to two levels deep - `events`: Collection of events in order of creation time - `forums/topics/{topic_id}`: A forum topic and its posts +- `friends`: List of authenticated user's friends - `matches`: List of currently open multiplayer lobbies - `matches/{match_id}`: More specific data about a specific multiplayer lobby including participating players and occured events - `me[/{mode}]`: Detailed info about the authenticated user [in the specified mode] (requires OAuth) diff --git a/src/client/mod.rs b/src/client/mod.rs index ea77052..e7df59a 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -268,6 +268,17 @@ impl Osu { GetForumPosts::new(self, topic_id) } + /// Get all friends of the authenticated user as a vec of [`User`](crate::model::user::User). + /// + /// Note that the client has to be initialized with the `FriendsRead` scope + /// through the OAuth process in order for this endpoint to not return an error. + /// + /// See [`OsuBuilder::with_authorization`](crate::OsuBuilder::with_authorization). + #[inline] + pub const fn friends(&self) -> GetFriends<'_> { + GetFriends::new(self) + } + /// Get the kudosu history of a user in form of a vec of /// [`KudosuHistory`](crate::model::kudosu::KudosuHistory). #[cfg(not(feature = "cache"))] @@ -671,7 +682,7 @@ impl OsuRef { let bytes = self.request_raw(req).await?; // let text = String::from_utf8_lossy(&bytes); - // println!("Response:\n{}", text); + // println!("Response:\n{text}"); parse_bytes(&bytes) } @@ -715,31 +726,31 @@ impl OsuRef { let url = Url::parse(&url).map_err(|source| OsuError::Url { source, url })?; debug!("URL: {url}"); - if let Some(ref token) = self.token.read().await.access { - let value = HeaderValue::from_str(token) - .map_err(|source| OsuError::CreatingTokenHeader { source })?; - - let bytes = BodyBytes::from(body); + let Some(ref token) = self.token.read().await.access else { + return Err(OsuError::NoToken); + }; - let mut req_builder = HyperRequest::builder() - .method(method) - .uri(url.as_str()) - .header(AUTHORIZATION, value) - .header(USER_AGENT, MY_USER_AGENT) - .header(X_API_VERSION, api_version) - .header(ACCEPT, APPLICATION_JSON) - .header(CONTENT_LENGTH, bytes.len()); + let value = HeaderValue::from_str(token) + .map_err(|source| OsuError::CreatingTokenHeader { source })?; - if !bytes.is_empty() { - req_builder = req_builder.header(CONTENT_TYPE, APPLICATION_JSON); - } + let bytes = BodyBytes::from(body); - let req = req_builder.body(bytes)?; + let mut req_builder = HyperRequest::builder() + .method(method) + .uri(url.as_str()) + .header(AUTHORIZATION, value) + .header(USER_AGENT, MY_USER_AGENT) + .header(X_API_VERSION, api_version) + .header(ACCEPT, APPLICATION_JSON) + .header(CONTENT_LENGTH, bytes.len()); - self.send_request(req).await - } else { - Err(OsuError::NoToken) + if !bytes.is_empty() { + req_builder = req_builder.header(CONTENT_TYPE, APPLICATION_JSON); } + + let req = req_builder.body(bytes)?; + + self.send_request(req).await } async fn send_request(&self, req: HyperRequest) -> OsuResult> { diff --git a/src/client/token.rs b/src/client/token.rs index d1c4334..76820a9 100644 --- a/src/client/token.rs +++ b/src/client/token.rs @@ -159,9 +159,8 @@ impl AuthorizationBuilder { &response_type=code", ); - url.push_str("&scopes=%22"); + url.push_str("&scope="); scopes.format(&mut url, '+'); - url.push_str("%22"); println!("Authorize yourself through the following url:\n{url}"); info!("Awaiting manual authorization..."); diff --git a/src/lib.rs b/src/lib.rs index c463799..ed56e20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,6 +30,7 @@ //! - `comments`: Most recent comments and their replies up to two levels deep //! - `events`: Collection of events in order of creation time //! - `forums/topics/{topic_id}`: A forum topic and its posts +//! - `friends`: List of authenticated user's friends //! - `matches`: List of currently open multiplayer lobbies //! - `matches/{match_id}`: More specific data about a specific multiplayer lobby including participating players and occured events //! - `me[/{mode}]`: Detailed info about the authenticated user [in the specified mode] (requires OAuth) diff --git a/src/request/user.rs b/src/request/user.rs index 6369e57..c5eac7e 100644 --- a/src/request/user.rs +++ b/src/request/user.rs @@ -81,7 +81,7 @@ impl fmt::Display for UserId { /// Get the [`UserExtended`](crate::model::user::UserExtended) of the authenticated user. /// -/// Note that the client has to be initialized with the `identify` scope +/// Note that the client has to be initialized with the `Identify` scope /// through the OAuth process in order for this endpoint to not return an error. /// /// See [`OsuBuilder::with_authorization`](crate::OsuBuilder::with_authorization). @@ -124,6 +124,42 @@ impl<'a> GetOwnData<'a> { poll_req!(GetOwnData => UserExtended); +/// Get all friends of the authenticated user as a vec of [`User`](crate::model::user::User). +/// +/// Note that the client has to be initialized with the `FriendsRead` scope +/// through the OAuth process in order for this endpoint to not return an error. +/// +/// See [`OsuBuilder::with_authorization`](crate::OsuBuilder::with_authorization). +#[must_use = "futures do nothing unless you `.await` or poll them"] +pub struct GetFriends<'a> { + fut: Option>>, + osu: &'a Osu, +} + +impl<'a> GetFriends<'a> { + #[inline] + pub(crate) const fn new(osu: &'a Osu) -> Self { + Self { fut: None, osu } + } + + fn start(&mut self) -> Pending<'a, Vec> { + let req = Request::new(Route::GetFriends); + let osu = self.osu; + let fut = osu.request::>(req); + + #[cfg(feature = "cache")] + let fut = fut.inspect_ok(move |users| { + for user in users.iter() { + osu.update_cache(user.user_id, &user.username); + } + }); + + Box::pin(fut) + } +} + +poll_req!(GetFriends => Vec); + /// Get a [`UserExtended`](crate::model::user::UserExtended) by their id. #[must_use = "futures do nothing unless you `.await` or poll them"] #[derive(Serialize)] diff --git a/src/routing.rs b/src/routing.rs index 63659e4..0e12846 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -36,6 +36,7 @@ pub(crate) enum Route { GetForumPosts { topic_id: u64, }, + GetFriends, GetMatch { match_id: Option, }, @@ -117,6 +118,7 @@ impl Route { Self::GetForumPosts { topic_id } => { (Method::GET, format!("forums/topics/{topic_id}").into()) } + Self::GetFriends => (Method::GET, "friends".into()), Self::GetMatch { match_id } => { let path = match match_id { Some(id) => format!("matches/{id}").into(), @@ -217,6 +219,7 @@ impl Route { Self::GetComments => "GetComments", Self::GetEvents => "GetEvents", Self::GetForumPosts { .. } => "GetForumPosts", + Self::GetFriends => "GetFriends", Self::GetMatch { match_id } => match match_id { Some(_) => "GetMatch/match_id", None => "GetMatch",