diff --git a/src/database/queries/calculate_tts_model_leaderboard.rs b/src/database/queries/calculate_tts_model_leaderboard.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/database/queries/calculate_w2l_template_leaderboard.rs b/src/database/queries/calculate_w2l_template_leaderboard.rs new file mode 100644 index 0000000000..71a0d4e9a1 --- /dev/null +++ b/src/database/queries/calculate_w2l_template_leaderboard.rs @@ -0,0 +1,81 @@ +use anyhow::anyhow; +use crate::util::anyhow_result::AnyhowResult; +use log::warn; +use sqlx::MySqlPool; + +#[derive(Serialize)] +pub struct W2lLeaderboardRecordForList { + pub username: String, + pub display_name: String, + pub gravatar_hash: String, + pub creator_user_token: String, + pub uploaded_count: i64, +} + +#[derive(Serialize)] +pub struct W2lLeaderboardRecordForListRaw { + pub username: String, + pub display_name: String, + pub gravatar_hash: String, + pub creator_user_token: String, + pub uploaded_count: i64, +} + +// NB: This may need to become an offline query if it is expensive. +pub async fn calculate_w2l_template_leaderboard( + mysql_pool: &MySqlPool +) -> AnyhowResult> { + let maybe_results = sqlx::query_as!( + W2lLeaderboardRecordForListRaw, + r#" +SELECT + users.username, + users.display_name, + users.email_gravatar_hash as gravatar_hash, + creator_user_token, + count(*) as uploaded_count +FROM w2l_templates +JOIN users +ON + users.token = creator_user_token +WHERE + is_public_listing_approved IS TRUE +AND + users.is_banned IS FALSE +GROUP BY creator_user_token +ORDER BY uploaded_count desc +LIMIT 25; + "#, + ) + .fetch_all(mysql_pool) + .await; + + let results : Vec = match maybe_results { + Ok(results) => results, + Err(err) => { + warn!("Error: {:?}", err); + + match err { + RowNotFound => { + return Ok(Vec::new()); + }, + _ => { + warn!("w2l leaderboard query error: {:?}", err); + return Err(anyhow!("w2l leaderboard query error")); + } + } + } + }; + + Ok(results.into_iter() + .map(|result| { + W2lLeaderboardRecordForList { + username: result.username.to_string(), + display_name: result.display_name.to_string(), + gravatar_hash: result.gravatar_hash.to_string(), + creator_user_token: result.creator_user_token.to_string(), + uploaded_count: result.uploaded_count, + } + }) + .collect::>()) +} diff --git a/src/database/queries/mod.rs b/src/database/queries/mod.rs index 0702a3f63b..602a7800f8 100644 --- a/src/database/queries/mod.rs +++ b/src/database/queries/mod.rs @@ -1,9 +1,12 @@ pub mod badges; +pub mod calculate_tts_model_leaderboard; +pub mod calculate_w2l_template_leaderboard; pub mod create_session; pub mod delete_tts_model; pub mod delete_tts_result; pub mod delete_w2l_result; pub mod delete_w2l_template; +pub mod get_user_profile_by_username; pub mod list_ip_bans; pub mod list_tts_models; pub mod list_user_badges; @@ -11,6 +14,5 @@ pub mod list_user_roles; pub mod list_w2l_templates; pub mod query_tts_model; pub mod query_tts_result; -pub mod get_user_profile_by_username; pub mod query_w2l_result; pub mod query_w2l_template; \ No newline at end of file diff --git a/src/http_server/endpoints/leaderboard/get_leaderboard.rs b/src/http_server/endpoints/leaderboard/get_leaderboard.rs new file mode 100644 index 0000000000..92d3dfa69c --- /dev/null +++ b/src/http_server/endpoints/leaderboard/get_leaderboard.rs @@ -0,0 +1,101 @@ +use actix_http::Error; +use actix_http::http::header; +use actix_web::cookie::Cookie; +use actix_web::dev::HttpResponseBuilder; +use actix_web::error::ResponseError; +use actix_web::http::StatusCode; +use actix_web::{Responder, web, HttpResponse, error, HttpRequest, HttpMessage}; +use chrono::{DateTime, Utc}; +use crate::AnyhowResult; +use crate::database::queries::calculate_w2l_template_leaderboard::W2lLeaderboardRecordForList; +use crate::database::queries::calculate_w2l_template_leaderboard::calculate_w2l_template_leaderboard; +use crate::http_server::web_utils::response_error_helpers::to_simple_json_error; +use crate::http_server::web_utils::serialize_as_json_error::serialize_as_json_error; +use crate::server_state::ServerState; +use derive_more::{Display, Error}; +use log::{info, warn, log}; +use regex::Regex; +use sqlx::MySqlPool; +use sqlx::error::DatabaseError; +use sqlx::error::Error::Database; +use sqlx::mysql::MySqlDatabaseError; +use std::fmt; +use std::sync::Arc; + +#[derive(Serialize)] +pub struct LeaderboardResponse { + success: bool, + w2l_leaderboard: Vec +} + +#[derive(Serialize, Debug)] +pub struct LeaderboardErrorResponse { + pub success: bool, + pub error_type: LeaderboardErrorType, + pub error_message: String, +} + +#[derive(Copy, Clone, Debug, Serialize)] +pub enum LeaderboardErrorType { + ServerError, +} + +impl LeaderboardErrorResponse { + fn server_error() -> Self { + Self { + success: false, + error_type: LeaderboardErrorType::ServerError, + error_message: "server error".to_string() + } + } +} + +impl ResponseError for LeaderboardErrorResponse { + fn status_code(&self) -> StatusCode { + match self.error_type { + LeaderboardErrorType::ServerError => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + serialize_as_json_error(self) + } +} + +// NB: Not using DeriveMore since Clion doesn't understand it. +impl fmt::Display for LeaderboardErrorResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.error_type) + } +} + +pub async fn leaderboard_handler( + http_request: HttpRequest, + server_state: web::Data> +) -> Result { + + // TODO: Fire off both requests in parallel. + let maybe_results = + calculate_w2l_template_leaderboard(&server_state.mysql_pool).await; + + + let results = match maybe_results { + Ok(results) => results, + Err(e) => { + warn!("Query error: {:?}", e); + return Err(LeaderboardErrorResponse::server_error()); + } + }; + + let response = LeaderboardResponse { + success: true, + w2l_leaderboard: results, + }; + + let body = serde_json::to_string(&response) + .map_err(|e| LeaderboardErrorResponse::server_error())?; + + Ok(HttpResponse::Ok() + .content_type("application/json") + .body(body)) +} diff --git a/src/http_server/endpoints/leaderboard/mod.rs b/src/http_server/endpoints/leaderboard/mod.rs new file mode 100644 index 0000000000..4fad116a64 --- /dev/null +++ b/src/http_server/endpoints/leaderboard/mod.rs @@ -0,0 +1 @@ +pub mod get_leaderboard; \ No newline at end of file diff --git a/src/http_server/endpoints/mod.rs b/src/http_server/endpoints/mod.rs index 304cc633f3..f585acfc72 100644 --- a/src/http_server/endpoints/mod.rs +++ b/src/http_server/endpoints/mod.rs @@ -1,5 +1,6 @@ pub mod default_route_404; pub mod events; +pub mod leaderboard; pub mod misc; pub mod moderation; pub mod root_index; diff --git a/src/main.rs b/src/main.rs index 9fca5cec6d..efff6b912a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,6 +106,7 @@ use sqlx::mysql::MySqlPoolOptions; use std::sync::Arc; use std::time::Duration; use crate::http_server::endpoints::moderation::users::list_users::list_users_handler; +use crate::http_server::endpoints::leaderboard::get_leaderboard::leaderboard_handler; // TODO TODO TODO TODO // TODO TODO TODO TODO @@ -591,6 +592,11 @@ pub async fn serve(server_state: ServerState) -> AnyhowResult<()> .route(web::get().to(list_events_handler)) .route(web::head().to(|| HttpResponse::Ok())) ) + .service( + web::resource("/leaderboard") + .route(web::get().to(leaderboard_handler)) + .route(web::head().to(|| HttpResponse::Ok())) + ) .service(get_root_index) .service(enable_alpha_handler) .service(enable_alpha_easy_handler)