diff --git a/src/statistics/database.rs b/src/statistics/database.rs index e5aba71..13dff77 100644 --- a/src/statistics/database.rs +++ b/src/statistics/database.rs @@ -415,8 +415,8 @@ impl StatisticDatabaseController { Ok(data) } - async fn wrapped_data(&self, player_id: &Uuid) -> StatisticsDatabaseResult { - let result = self.wrapped.build_wrapped(player_id).await?; + async fn wrapped_data(&self, player_id: Uuid, year: u16) -> StatisticsDatabaseResult { + let result = self.wrapped.build_wrapped(player_id, year).await?; Ok(result) } } @@ -549,13 +549,16 @@ impl Handler for StatisticDatabaseController { } } -pub struct WrappedData(pub Uuid); +pub struct WrappedData { + pub player_id: Uuid, + pub year: u16, +} impl Handler for StatisticDatabaseController { type Return = StatisticsDatabaseResult; async fn handle(&mut self, message: WrappedData, _ctx: &mut Context) -> Self::Return { - self.wrapped_data(&message.0).await + self.wrapped_data(message.player_id, message.year).await } } @@ -567,6 +570,8 @@ pub enum StatisticsDatabaseError { Postgres(#[from] tokio_postgres::Error), #[error("a database pool error occurred: {0}")] Pool(#[from] deadpool_postgres::PoolError), + #[error("nucleoid wrapped is not available for this year")] + UnWrappedYear, #[error("unknown error")] Unknown, } diff --git a/src/statistics/wrapped.rs b/src/statistics/wrapped.rs index a168bed..e23c0f0 100644 --- a/src/statistics/wrapped.rs +++ b/src/statistics/wrapped.rs @@ -1,6 +1,28 @@ +use std::convert::TryFrom; + use serde::{Deserialize, Serialize}; use uuid::Uuid; +use super::database::{StatisticsDatabaseError, StatisticsDatabaseResult}; + +#[derive(Clone, Copy)] +pub enum WrappedYear { + Y2023, + Y2024, +} + +impl TryFrom for WrappedYear { + type Error = StatisticsDatabaseError; + + fn try_from(value: u16) -> Result { + match value { + 2023 => Ok(WrappedYear::Y2023), + 2024 => Ok(WrappedYear::Y2024), + _ => Err(StatisticsDatabaseError::UnWrappedYear), + } + } +} + pub struct NucleoidWrapped { clickhouse_pool: clickhouse_rs::Pool, } @@ -10,7 +32,29 @@ impl NucleoidWrapped { Self { clickhouse_pool } } - async fn played_count(&self, player: &Uuid) -> Result { + fn start_date(year: WrappedYear) -> &'static str { + match year { + WrappedYear::Y2023 => "2022-12-31 00:00:00", + WrappedYear::Y2024 => "2023-12-01 00:00:00", + } + } + + fn end_date(year: WrappedYear) -> &'static str { + match year { + WrappedYear::Y2023 => "2023-12-01 00:00:00", + WrappedYear::Y2024 => "2024-12-21 00:00:00", + } + } + + fn date_range(year: WrappedYear) -> String { + format!( + "(games.date_played < '{end_date}') AND (games.date_played > '{start_date}')", + start_date = Self::start_date(year), + end_date = Self::end_date(year), + ) + } + + async fn played_count(&self, player: Uuid, year: WrappedYear) -> Result { let mut ch_handle = self.clickhouse_pool.get_handle().await?; let results = ch_handle.query(format!( r#" @@ -18,11 +62,12 @@ impl NucleoidWrapped { COUNT(DISTINCT game_id) AS total FROM player_statistics INNER JOIN games ON player_statistics.game_id = games.game_id - WHERE (player_id = '{player_id}') AND (games.date_played < '2023-12-01 00:00:00') AND (games.date_played > '2022-12-31 00:00:00') + WHERE (player_id = '{player_id}') AND {date_range} ORDER BY total DESC "#, // safety: player is a uuid, which has a fixed format which is safe to insert directly into the sql - player_id = player + player_id = player, + date_range = Self::date_range(year), )).fetch_all().await?; if let Some(row) = results.rows().next() { Ok(row.get("total")?) @@ -33,7 +78,8 @@ impl NucleoidWrapped { async fn top_games( &self, - player: &Uuid, + player: Uuid, + year: WrappedYear, ) -> Result, clickhouse_rs::errors::Error> { let mut ch_handle = self.clickhouse_pool.get_handle().await?; let results = ch_handle.query(format!( @@ -43,12 +89,13 @@ impl NucleoidWrapped { COUNT(DISTINCT game_id) AS total FROM player_statistics INNER JOIN games ON player_statistics.game_id = games.game_id - WHERE (player_id = '{player_id}') AND (games.date_played < '2023-12-01 00:00:00') AND (games.date_played > '2022-12-31 00:00:00') + WHERE (player_id = '{player_id}') AND {date_range} GROUP BY games.namespace ORDER BY total DESC "#, // safety: player is a uuid, which has a fixed format which is safe to insert directly into the sql - player_id = player + player_id = player, + date_range = Self::date_range(year), )).fetch_all().await?; let mut top_games = Vec::with_capacity(results.row_count()); @@ -62,7 +109,7 @@ impl NucleoidWrapped { Ok(top_games) } - async fn days_played(&self, player: &Uuid) -> Result { + async fn days_played(&self, player: Uuid, year: WrappedYear,) -> Result { let mut ch_handle = self.clickhouse_pool.get_handle().await?; let results = ch_handle.query(format!( r#" @@ -70,11 +117,12 @@ impl NucleoidWrapped { COUNT(DISTINCT toDayOfYear(date_played)) AS total FROM player_statistics INNER JOIN games ON player_statistics.game_id = games.game_id - WHERE (player_id = '{player_id}') AND (games.date_played < '2023-12-01 00:00:00') AND (games.date_played > '2022-12-31 00:00:00') + WHERE (player_id = '{player_id}') AND {date_range} ORDER BY total DESC "#, // safety: player is a uuid, which has a fixed format which is safe to insert directly into the sql - player_id = player + player_id = player, + date_range = Self::date_range(year), )).fetch_all().await?; if let Some(row) = results.rows().next() { Ok(row.get("total")?) @@ -85,7 +133,8 @@ impl NucleoidWrapped { async fn days_played_games( &self, - player: &Uuid, + player: Uuid, + year: WrappedYear, ) -> Result, clickhouse_rs::errors::Error> { let mut ch_handle = self.clickhouse_pool.get_handle().await?; let results = ch_handle.query(format!( @@ -95,12 +144,13 @@ impl NucleoidWrapped { COUNT(DISTINCT toDayOfYear(date_played)) AS total FROM player_statistics INNER JOIN games ON player_statistics.game_id = games.game_id - WHERE (player_id = '{player_id}') AND (games.date_played < '2023-12-01 00:00:00') AND (games.date_played > '2022-12-31 00:00:00') + WHERE (player_id = '{player_id}') AND {date_range} GROUP BY games.namespace ORDER BY total DESC "#, // safety: player is a uuid, which has a fixed format which is safe to insert directly into the sql - player_id = player + player_id = player, + date_range = Self::date_range(year), )).fetch_all().await?; let mut top_games = Vec::with_capacity(results.row_count()); @@ -114,7 +164,7 @@ impl NucleoidWrapped { Ok(top_games) } - async fn most_players(&self, player: &Uuid) -> Result { + async fn most_players(&self, player: Uuid, year: WrappedYear,) -> Result { let mut ch_handle = self.clickhouse_pool.get_handle().await?; let results = ch_handle .query(format!( @@ -126,20 +176,22 @@ impl NucleoidWrapped { game_id FROM player_statistics INNER JOIN games ON player_statistics.game_id = games.game_id - WHERE (player_id = '{player_id}') - AND (games.date_played < '2023-12-01 00:00:00') - AND (games.date_played > '2022-12-31 00:00:00') + WHERE (player_id = '{player_id}') AND {date_range} GROUP BY game_id) AS games INNER JOIN player_statistics ON player_statistics.game_id = games.game_id "#, // safety: player is a uuid, which has a fixed format which is safe to insert directly into the sql - player_id = player + player_id = player, + date_range = Self::date_range(year), )) .fetch_all() .await?; if let Some(row) = results.rows().next() { let mut total: u64 = row.get("total")?; - total -= 1; + // lets maybe not crash here + if total > 0 { + total -= 1; + } Ok(total) } else { Ok(0) @@ -148,7 +200,8 @@ impl NucleoidWrapped { async fn most_players_games( &self, - player: &Uuid, + player: Uuid, + year: WrappedYear, ) -> Result, clickhouse_rs::errors::Error> { let mut ch_handle = self.clickhouse_pool.get_handle().await?; let results = ch_handle @@ -163,16 +216,15 @@ impl NucleoidWrapped { namespace FROM player_statistics INNER JOIN games ON player_statistics.game_id = games.game_id - WHERE (player_id = '{player_id}') - AND (games.date_played < '2023-12-01 00:00:00') - AND (games.date_played > '2022-12-31 00:00:00') + WHERE (player_id = '{player_id}') AND {date_range} GROUP BY game_id, namespace) AS games INNER JOIN player_statistics ON player_statistics.game_id = games.game_id GROUP BY namespace ORDER BY total DESC "#, // safety: player is a uuid, which has a fixed format which is safe to insert directly into the sql - player_id = player + player_id = player, + date_range = Self::date_range(year), )) .fetch_all() .await?; @@ -191,14 +243,17 @@ impl NucleoidWrapped { pub async fn build_wrapped( &self, - player: &Uuid, - ) -> Result { - let played_count = self.played_count(player).await?; - let top_games = self.top_games(player).await?; - let days_played = self.days_played(player).await?; - let days_played_games = self.days_played_games(player).await?; - let most_players = self.most_players(player).await?; - let most_players_games = self.most_players_games(player).await?; + player: Uuid, + year: u16, + ) -> StatisticsDatabaseResult { + let year = WrappedYear::try_from(year)?; + + let played_count = self.played_count(player, year).await?; + let top_games = self.top_games(player, year).await?; + let days_played = self.days_played(player, year).await?; + let days_played_games = self.days_played_games(player, year).await?; + let most_players = self.most_players(player, year).await?; + let most_players_games = self.most_players_games(player, year).await?; Ok(PlayerWrappedData { played_count, top_games, diff --git a/src/web.rs b/src/web.rs index c496553..b854094 100644 --- a/src/web.rs +++ b/src/web.rs @@ -121,9 +121,10 @@ pub async fn run(controller: Address, config: WebServerConfig) { let nucleoid_wrapped = warp::path("player") .and(warp::path::param::()) .and(warp::path("wrapped")) + .and(warp::query()) .and_then({ let controller = controller.clone(); - move |id| nucleoid_wrapped(controller.clone(), id) + move |id, query: WrappedQuery| nucleoid_wrapped(controller.clone(), id, query.year) }) .with(&cors); @@ -263,10 +264,13 @@ async fn get_player_username(mojang_client: Address, id: Uuid) handle_option_result(profile) } -async fn nucleoid_wrapped(controller: Address, player_id: Uuid) -> ApiResult { +async fn nucleoid_wrapped(controller: Address, player_id: Uuid, year: Option) -> ApiResult { let statistics = get_statistics_controller(controller).await?; let res = statistics - .send(WrappedData(player_id)) + .send(WrappedData { + player_id, + year: year.unwrap_or(2023), + }) .await .expect("controller disconnected"); handle_result(res) @@ -283,6 +287,11 @@ struct DataQueryQuery { query: DataQueryType, } +#[derive(Deserialize)] +struct WrappedQuery { + year: Option, +} + async fn get_statistics_controller( controller: Address, ) -> Result, warp::Rejection> {