diff --git a/src/client/mod.rs b/src/client/mod.rs index f13693f..bc8c0a1 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -205,6 +205,16 @@ impl Osu { GetComments::new(self) } + #[inline] + pub const fn changelog_build(&self, stream: String, build: String) -> GetChangelogBuild<'_> { + GetChangelogBuild::new(self, stream, build) + } + + #[inline] + pub fn changelog_listing(&self) -> GetChangelogListing<'_> { + GetChangelogListing::new(self) + } + /// Get a [`ChartRankings`](crate::model::ranking::ChartRankings) struct /// containing a [`Spotlight`](crate::model::ranking::Spotlight), its /// [`BeatmapsetExtended`](crate::model::beatmap::BeatmapsetExtended)s, and participating diff --git a/src/lib.rs b/src/lib.rs index b042023..70525ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -181,6 +181,7 @@ pub mod prelude { error::OsuError, model::{ beatmap::*, + changelog::*, comments::*, event::*, forum::*, diff --git a/src/model/changelog.rs b/src/model/changelog.rs new file mode 100644 index 0000000..a358df0 --- /dev/null +++ b/src/model/changelog.rs @@ -0,0 +1,124 @@ +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +use super::serde_util; +use crate::model::ContainedUsers; + +/// Changelog listing entry +#[derive(Clone, Debug, Deserialize)] +pub struct ChangelogListing { + /// List of all game update streams (stable, lazer, etc) + pub streams: Vec, + /// List of builds included + pub builds: Vec, + /// Search query inputs + pub search: Search, +} + +impl ContainedUsers for ChangelogListing { + fn apply_to_users(&self, _f: impl super::CacheUserFn) {} +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Stream { + /// Build stream ID + pub id: i64, + /// Build stream title (stable40, lazer, etc) + pub name: String, + /// User-friendly build stream name + pub display_name: String, + /// Whether the build is displayed + pub is_featured: bool, + /// Latest deployed build information + #[serde(skip_serializing_if = "Option::is_none")] + pub latest_build: Option>, + /// Current live user count + #[serde(skip_serializing_if = "Option::is_none")] + pub user_count: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Build { + /// Release build ID + pub id: i64, + /// Release build version + pub version: Option, + /// User-friendly release build version + pub display_version: String, + /// Current live user count for the build + pub users: i64, + /// Build release date + #[serde(with = "serde_util::datetime")] + pub created_at: OffsetDateTime, + pub update_stream: Option, // it is tagged as nullable but why would it be? + pub changelog_entries: Option>, + pub youtube_id: Option, + /// Previous and next versions to this build + pub versions: Option, +} + +impl ContainedUsers for Build { + fn apply_to_users(&self, _f: impl super::CacheUserFn) {} +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Versions { + pub next: Option>, + pub previous: Option>, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct ChangelogEntry { + pub id: Option, + pub repository: Option, + pub github_pull_request_id: Option, + pub github_url: Option, + /// Changelog entry URL in the news listing + pub url: Option, + #[serde(rename = "type")] + /// Changelog entry type + pub entry_type: String, // TODO, technically defined but I can't read PHP + /// Changelog category + pub category: Option, // TODO, technically defined but I can't read PHP + /// Changelog entry title + pub title: Option, + #[cfg_attr( + feature = "serialize", + serde(default, skip_serializing_if = "Option::is_none") + )] + pub message: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub message_html: Option, + pub major: bool, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "serde_util::option_datetime" + )] + pub created_at: Option, + pub github_user: GithubUser, +} + +/// Github user behind the specific change +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct GithubUser { + /// Display name of the user, may differ from github + pub display_name: String, + /// Github profile URL + pub github_url: Option, + /// Github username + pub github_username: Option, + pub id: Option, + pub osu_username: Option, + pub user_id: Option, + pub user_url: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Search { + pub stream: Option, + pub from: Option, + pub to: Option, + pub max_id: Option, + pub limit: i64, +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 2b9348f..bbd401e 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -171,6 +171,7 @@ pub mod seasonal_backgrounds; /// User related types pub mod user; +pub mod changelog; /// Wiki related types pub mod wiki; diff --git a/src/request/changelog.rs b/src/request/changelog.rs new file mode 100644 index 0000000..ae7c9aa --- /dev/null +++ b/src/request/changelog.rs @@ -0,0 +1,98 @@ +use crate::{ + prelude::{Build, ChangelogListing}, + request::{Query, Request}, + routing::Route, + Osu, +}; + +use serde::Serialize; + +#[must_use = "requests must be configured and executed"] +#[derive(Serialize)] +pub struct GetChangelogBuild<'a> { + #[serde(skip)] + osu: &'a Osu, + stream: String, + build: String, +} + +impl<'a> GetChangelogBuild<'a> { + pub(crate) const fn new(osu: &'a Osu, stream: String, build: String) -> Self { + Self { osu, stream, build } + } +} + +into_future! { + |self: GetChangelogBuild<'_>| -> Build { + let route = Route::GetChangelogBuild { + stream: self.stream, + build: self.build, + }; + + Request::new(route) + } +} + +#[must_use = "requests must be configured and executed"] +#[derive(Serialize)] +pub struct GetChangelogListing<'a> { + #[serde(skip)] + osu: &'a Osu, + from: Option<&'a str>, + to: Option<&'a str>, + max_id: Option<&'a str>, + stream: Option<&'a str>, + #[serde(skip_serializing_if = "Vec::is_empty")] + message_formats: Vec<&'a str>, +} + +impl<'a> GetChangelogListing<'a> { + pub(crate) fn new(osu: &'a Osu) -> Self { + Self { + osu, + from: None, + to: None, + max_id: None, + // There are only two supported formats, it should be fine + message_formats: Vec::with_capacity(2).into(), + stream: None, + } + } + + /// Specify minimum build version + #[inline] + pub fn from(mut self, from: &'a str) -> Self { + self.from = Some(from); + + self + } + + /// Specify maximum build version + pub fn to(mut self, to: &'a str) -> Self { + self.to = Some(to); + + self + } + + /// Specify the release stream + pub fn stream(mut self, stream: &'a str) -> Self { + self.stream = Some(stream); + + self + } + + pub fn message_formats(mut self, message_formats: I) -> Self + where + I: IntoIterator, + { + self.message_formats = message_formats.into_iter().collect(); + + self + } +} + +into_future! { + |self: GetChangelogListing<'_>| -> ChangelogListing { + Request::with_query(Route::GetChangelogListing, Query::encode(&self)) + } +} diff --git a/src/request/mod.rs b/src/request/mod.rs index 338112b..2484731 100644 --- a/src/request/mod.rs +++ b/src/request/mod.rs @@ -139,11 +139,12 @@ use crate::routing::Route; pub use crate::future::OsuFuture; pub use self::{ - beatmap::*, comments::*, event::*, forum::*, matches::*, news::*, ranking::*, replay::*, - score::*, seasonal_backgrounds::*, user::*, wiki::*, + beatmap::*, changelog::*, comments::*, event::*, forum::*, matches::*, news::*, ranking::*, + replay::*, score::*, seasonal_backgrounds::*, user::*, wiki::*, }; mod beatmap; +mod changelog; mod comments; mod event; mod forum; diff --git a/src/routing.rs b/src/routing.rs index f499102..80b19d8 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -30,6 +30,11 @@ pub(crate) enum Route { GetBeatmapsetFromMapId, GetBeatmapsetEvents, GetBeatmapsetSearch, + GetChangelogBuild { + stream: String, + build: String, + }, + GetChangelogListing, GetComments, GetEvents, GetForumPosts { @@ -112,6 +117,10 @@ impl Route { Self::GetBeatmapsetFromMapId => (Method::Get, "beatmapsets/lookup".into()), Self::GetBeatmapsetEvents => (Method::Get, "beatmapsets/events".into()), Self::GetBeatmapsetSearch => (Method::Get, "beatmapsets/search".into()), + Self::GetChangelogBuild { stream, build } => { + (Method::Get, format!("changelog/{stream}/{build}").into()) + } + Self::GetChangelogListing => (Method::Get, "changelog".into()), Self::GetComments => (Method::Get, "comments".into()), Self::GetEvents => (Method::Get, "events".into()), Self::GetForumPosts { topic_id } => { @@ -216,6 +225,8 @@ impl Route { Self::GetBeatmapsetFromMapId => "GetBeatmapsetFromMapId", Self::GetBeatmapsetEvents => "GetBeatmapsetEvents", Self::GetBeatmapsetSearch => "GetBeatmapsetSearch", + Self::GetChangelogBuild { .. } => "GetChangelogBuild", + Self::GetChangelogListing => "GetChangelogListing", Self::GetComments => "GetComments", Self::GetEvents => "GetEvents", Self::GetForumPosts { .. } => "GetForumPosts", diff --git a/tests/requests.rs b/tests/requests.rs index 9411def..5207c71 100644 --- a/tests/requests.rs +++ b/tests/requests.rs @@ -657,3 +657,12 @@ async fn wiki() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn changelogs() -> Result<()> { + let changelog = OSU.get().await?.changelog_listing().await?; + + println!("Received {} changelog listings", changelog.builds.len()); + + Ok(()) +}