Skip to content

Commit

Permalink
feat: add endpoints for nucleoid wrapped
Browse files Browse the repository at this point in the history
  • Loading branch information
ashhhleyyy committed Dec 1, 2023
1 parent ee7b6c0 commit 992691c
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 1 deletion.
28 changes: 28 additions & 0 deletions src/statistics/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ use crate::statistics::model::{
};
use crate::{Controller, StatisticsConfig};

use super::wrapped::{NucleoidWrapped, PlayerWrappedData};

pub struct StatisticDatabaseController {
_controller: Address<Controller>,
pool: Pool,
_config: StatisticsConfig,
leaderboards: LeaderboardsDatabase,
wrapped: NucleoidWrapped,
}

impl StatisticDatabaseController {
Expand All @@ -33,12 +36,15 @@ impl StatisticDatabaseController {
) -> StatisticsDatabaseResult<Self> {
let pool = Pool::new(config.database_url.clone());

let wrapped = NucleoidWrapped::new(pool.clone());

let handler = Self {
_controller: controller.clone(),
pool: pool.clone(),
_config: config.clone(),
leaderboards: LeaderboardsDatabase::new(postgres_pool.clone(), pool, leaderboards)
.await?,
wrapped,
};

initialise_database(&handler.pool).await?;
Expand Down Expand Up @@ -408,6 +414,11 @@ impl StatisticDatabaseController {

Ok(data)
}

async fn wrapped_data(&self, player_id: &Uuid) -> StatisticsDatabaseResult<PlayerWrappedData> {
let result = self.wrapped.build_wrapped(player_id).await?;
Ok(result)
}
}

impl Actor for StatisticDatabaseController {}
Expand Down Expand Up @@ -587,6 +598,23 @@ impl Handler<DataQuery> for StatisticDatabaseController {
}
}

pub struct WrappedData(pub Uuid);

impl Message for WrappedData {
type Result = StatisticsDatabaseResult<PlayerWrappedData>;
}

#[async_trait]
impl Handler<WrappedData> for StatisticDatabaseController {
async fn handle(
&mut self,
message: WrappedData,
_ctx: &mut Context<Self>,
) -> <WrappedData as Message>::Result {
self.wrapped_data(&message.0).await
}
}

#[derive(thiserror::Error, Debug)]
pub enum StatisticsDatabaseError {
#[error("a database error occurred: {0}")]
Expand Down
1 change: 1 addition & 0 deletions src/statistics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::{Controller, RegisterStatisticsDatabaseController, StatisticsConfig,
pub mod database;
pub mod leaderboards;
pub mod model;
mod wrapped;

pub async fn run(
controller: Address<Controller>,
Expand Down
228 changes: 228 additions & 0 deletions src/statistics/wrapped.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;

pub struct NucleoidWrapped {
clickhouse_pool: clickhouse_rs::Pool,
}

impl NucleoidWrapped {
pub fn new(
clickhouse_pool: clickhouse_rs::Pool,
) -> Self {
Self {
clickhouse_pool,
}
}

async fn played_count(&self, player: &Uuid) -> Result<u64, clickhouse_rs::errors::Error> {
let mut ch_handle = self.clickhouse_pool.get_handle().await?;
let results = ch_handle.query(format!(
r#"
SELECT
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')
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
)).fetch_all().await?;
if let Some(row) = results.rows().nth(0) {
Ok(row.get("total")?)
} else {
Ok(0)
}
}

async fn top_games(&self, player: &Uuid) -> Result<Vec<PerGameStat>, clickhouse_rs::errors::Error> {
let mut ch_handle = self.clickhouse_pool.get_handle().await?;
let results = ch_handle.query(format!(
r#"
SELECT
games.namespace as namespace,
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')
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
)).fetch_all().await?;

let mut top_games = Vec::with_capacity(results.row_count());

for row in results.rows() {
let namespace: String = row.get("namespace")?;
let total = row.get("total")?;
top_games.push(PerGameStat {
namespace,
total,
});
}

Ok(top_games)
}

async fn days_played(&self, player: &Uuid) -> Result<u64, clickhouse_rs::errors::Error> {
let mut ch_handle = self.clickhouse_pool.get_handle().await?;
let results = ch_handle.query(format!(
r#"
SELECT
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')
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
)).fetch_all().await?;
if let Some(row) = results.rows().nth(0) {
Ok(row.get("total")?)
} else {
Ok(0)
}
}

async fn days_played_games(&self, player: &Uuid) -> Result<Vec<PerGameStat>, clickhouse_rs::errors::Error> {
let mut ch_handle = self.clickhouse_pool.get_handle().await?;
let results = ch_handle.query(format!(
r#"
SELECT
games.namespace as namespace,
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')
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
)).fetch_all().await?;

let mut top_games = Vec::with_capacity(results.row_count());

for row in results.rows() {
let namespace: String = row.get("namespace")?;
let total = row.get("total")?;
top_games.push(PerGameStat {
namespace,
total,
});
}

Ok(top_games)
}

async fn most_players(&self, player: &Uuid) -> Result<u64, clickhouse_rs::errors::Error> {
let mut ch_handle = self.clickhouse_pool.get_handle().await?;
let results = ch_handle.query(format!(
r#"
SELECT
MAX(total) as total
FROM
(SELECT
COUNT(DISTINCT player_id) as total
FROM
(SELECT
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')
GROUP BY game_id) AS games
INNER JOIN player_statistics ON player_statistics.game_id = games.game_id
GROUP BY game_id)
"#,
// safety: player is a uuid, which has a fixed format which is safe to insert directly into the sql
player_id = player
)).fetch_all().await?;
if let Some(row) = results.rows().nth(0) {
Ok(row.get("total")?)
} else {
Ok(0)
}
}

async fn most_players_games(&self, player: &Uuid) -> Result<Vec<PerGameStat>, clickhouse_rs::errors::Error> {
let mut ch_handle = self.clickhouse_pool.get_handle().await?;
let results = ch_handle.query(format!(
r#"
SELECT
MAX(total) as total,
namespace
FROM
(SELECT
COUNT(DISTINCT player_id) as total,
namespace
FROM
(SELECT
game_id,
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')
GROUP BY game_id, namespace) AS games
INNER JOIN player_statistics ON player_statistics.game_id = games.game_id
GROUP BY game_id, namespace)
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
)).fetch_all().await?;

let mut top_games = Vec::with_capacity(results.row_count());

for row in results.rows() {
let namespace: String = row.get("namespace")?;
let total = row.get("total")?;
top_games.push(PerGameStat {
namespace,
total,
});
}

Ok(top_games)
}

pub async fn build_wrapped(&self, player: &Uuid) -> Result<PlayerWrappedData, clickhouse_rs::errors::Error> {
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?;
Ok(PlayerWrappedData {
played_count,
top_games,
days_played,
days_played_games,
most_players,
most_players_games,
})
}
}

#[derive(Deserialize, Serialize)]
pub struct PlayerWrappedData {
played_count: u64,
top_games: Vec<PerGameStat>,
days_played: u64,
days_played_games: Vec<PerGameStat>,
most_players: u64,
most_players_games: Vec<PerGameStat>,
}

#[derive(Deserialize, Serialize)]
pub struct PerGameStat {
pub namespace: String,
pub total: u64,
}
20 changes: 19 additions & 1 deletion src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ pub async fn run(controller: Address<Controller>, config: WebServerConfig) {
})
.with(&cors);

let nucleoid_wrapped = warp::path("player")
.and(warp::path::param::<Uuid>())
.and(warp::path("wrapped"))
.and_then({
let controller = controller.clone();
move |id| nucleoid_wrapped(controller.clone(), id)
})
.with(&cors);

let combined = status
.or(player_game_stats)
.or(all_player_game_stats)
Expand All @@ -128,7 +137,8 @@ pub async fn run(controller: Address<Controller>, config: WebServerConfig) {
.or(list_leaderboards)
.or(get_player_rankings)
.or(data_query)
.or(get_player_username);
.or(get_player_username)
.or(nucleoid_wrapped);

warp::serve(combined)
.run(([127, 0, 0, 1], config.port))
Expand Down Expand Up @@ -256,6 +266,14 @@ async fn get_player_username(mojang_client: Address<MojangApiClient>, id: Uuid)
handle_option_result(profile)
}

async fn nucleoid_wrapped(controller: Address<Controller>, player_id: Uuid) -> ApiResult {
let statistics = get_statistics_controller(controller).await?;
let res = statistics.send(WrappedData(player_id))
.await
.expect("controller disconnected");
handle_result(res)
}

#[derive(Deserialize)]
struct RecentGamesQuery {
limit: u32,
Expand Down

0 comments on commit 992691c

Please sign in to comment.