diff options
Diffstat (limited to 'planetwars-server/src')
-rw-r--r-- | planetwars-server/src/cli.rs | 54 | ||||
-rw-r--r-- | planetwars-server/src/db/maps.rs | 35 | ||||
-rw-r--r-- | planetwars-server/src/db/matches.rs | 152 | ||||
-rw-r--r-- | planetwars-server/src/db/mod.rs | 1 | ||||
-rw-r--r-- | planetwars-server/src/db/users.rs | 28 | ||||
-rw-r--r-- | planetwars-server/src/lib.rs | 12 | ||||
-rw-r--r-- | planetwars-server/src/modules/client_api.rs | 11 | ||||
-rw-r--r-- | planetwars-server/src/modules/matches.rs | 22 | ||||
-rw-r--r-- | planetwars-server/src/modules/ranking.rs | 22 | ||||
-rw-r--r-- | planetwars-server/src/modules/registry.rs | 4 | ||||
-rw-r--r-- | planetwars-server/src/routes/demo.rs | 14 | ||||
-rw-r--r-- | planetwars-server/src/routes/maps.rs | 19 | ||||
-rw-r--r-- | planetwars-server/src/routes/matches.rs | 75 | ||||
-rw-r--r-- | planetwars-server/src/routes/mod.rs | 1 | ||||
-rw-r--r-- | planetwars-server/src/schema.rs | 15 |
15 files changed, 414 insertions, 51 deletions
diff --git a/planetwars-server/src/cli.rs b/planetwars-server/src/cli.rs new file mode 100644 index 0000000..f33506e --- /dev/null +++ b/planetwars-server/src/cli.rs @@ -0,0 +1,54 @@ +extern crate planetwars_server; +extern crate tokio; + +use clap::Parser; +use planetwars_server::db; +use planetwars_server::{create_db_pool, get_config}; + +#[derive(clap::Parser)] +struct Args { + #[clap(subcommand)] + action: Action, +} + +#[derive(clap::Subcommand)] +enum Action { + SetPassword(SetPassword), +} + +impl Action { + async fn run(self) { + match self { + Action::SetPassword(set_password) => set_password.run().await, + } + } +} + +#[derive(clap::Parser)] +struct SetPassword { + #[clap(value_parser)] + username: String, + + #[clap(value_parser)] + new_password: String, +} + +impl SetPassword { + async fn run(self) { + let global_config = get_config().unwrap(); + let pool = create_db_pool(&global_config).await; + + let conn = pool.get().await.expect("could not get database connection"); + let credentials = db::users::Credentials { + username: &self.username, + password: &self.new_password, + }; + db::users::set_user_password(credentials, &conn).expect("could not set password"); + } +} + +#[tokio::main] +pub async fn main() { + let args = Args::parse(); + args.action.run().await; +} diff --git a/planetwars-server/src/db/maps.rs b/planetwars-server/src/db/maps.rs new file mode 100644 index 0000000..dffe4fd --- /dev/null +++ b/planetwars-server/src/db/maps.rs @@ -0,0 +1,35 @@ +use diesel::prelude::*; + +use crate::schema::maps; + +#[derive(Insertable)] +#[table_name = "maps"] +pub struct NewMap<'a> { + pub name: &'a str, + pub file_path: &'a str, +} + +#[derive(Queryable, Clone, Debug)] +pub struct Map { + pub id: i32, + pub name: String, + pub file_path: String, +} + +pub fn create_map(new_map: NewMap, conn: &PgConnection) -> QueryResult<Map> { + diesel::insert_into(maps::table) + .values(new_map) + .get_result(conn) +} + +pub fn find_map(id: i32, conn: &PgConnection) -> QueryResult<Map> { + maps::table.find(id).get_result(conn) +} + +pub fn find_map_by_name(name: &str, conn: &PgConnection) -> QueryResult<Map> { + maps::table.filter(maps::name.eq(name)).first(conn) +} + +pub fn list_maps(conn: &PgConnection) -> QueryResult<Vec<Map>> { + maps::table.get_results(conn) +} diff --git a/planetwars-server/src/db/matches.rs b/planetwars-server/src/db/matches.rs index 061e2ea..2041296 100644 --- a/planetwars-server/src/db/matches.rs +++ b/planetwars-server/src/db/matches.rs @@ -1,20 +1,27 @@ pub use crate::db_types::MatchState; use chrono::NaiveDateTime; use diesel::associations::BelongsTo; +use diesel::pg::Pg; +use diesel::query_builder::BoxedSelectStatement; +use diesel::query_source::{AppearsInFromClause, Once}; use diesel::{ BelongingToDsl, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl, RunQueryDsl, }; use diesel::{Connection, GroupedBy, PgConnection, QueryResult}; +use std::collections::{HashMap, HashSet}; -use crate::schema::{bot_versions, bots, match_players, matches}; +use crate::schema::{bot_versions, bots, maps, match_players, matches}; use super::bots::{Bot, BotVersion}; +use super::maps::Map; #[derive(Insertable)] #[table_name = "matches"] pub struct NewMatch<'a> { pub state: MatchState, pub log_path: &'a str, + pub is_public: bool, + pub map_id: Option<i32>, } #[derive(Insertable)] @@ -36,6 +43,8 @@ pub struct MatchBase { pub log_path: String, pub created_at: NaiveDateTime, pub winner: Option<i32>, + pub is_public: bool, + pub map_id: Option<i32>, } #[derive(Queryable, Identifiable, Associations, Clone)] @@ -87,42 +96,133 @@ pub struct MatchData { pub match_players: Vec<MatchPlayer>, } +/// Add player information to MatchBase instances +fn fetch_full_match_data( + matches: Vec<MatchBase>, + conn: &PgConnection, +) -> QueryResult<Vec<FullMatchData>> { + let map_ids: HashSet<i32> = matches.iter().filter_map(|m| m.map_id).collect(); + + let maps_by_id: HashMap<i32, Map> = maps::table + .filter(maps::id.eq_any(map_ids)) + .load::<Map>(conn)? + .into_iter() + .map(|m| (m.id, m)) + .collect(); + + let match_players = MatchPlayer::belonging_to(&matches) + .left_join( + bot_versions::table.on(match_players::bot_version_id.eq(bot_versions::id.nullable())), + ) + .left_join(bots::table.on(bot_versions::bot_id.eq(bots::id.nullable()))) + .order_by(( + match_players::match_id.asc(), + match_players::player_id.asc(), + )) + .load::<FullMatchPlayerData>(conn)? + .grouped_by(&matches); + + let res = matches + .into_iter() + .zip(match_players.into_iter()) + .map(|(base, players)| FullMatchData { + match_players: players.into_iter().collect(), + map: base + .map_id + .and_then(|map_id| maps_by_id.get(&map_id).cloned()), + base, + }) + .collect(); + + Ok(res) +} + +// TODO: this method should disappear pub fn list_matches(amount: i64, conn: &PgConnection) -> QueryResult<Vec<FullMatchData>> { conn.transaction(|| { let matches = matches::table + .filter(matches::state.eq(MatchState::Finished)) .order_by(matches::created_at.desc()) .limit(amount) .get_results::<MatchBase>(conn)?; - let match_players = MatchPlayer::belonging_to(&matches) - .left_join( - bot_versions::table - .on(match_players::bot_version_id.eq(bot_versions::id.nullable())), - ) - .left_join(bots::table.on(bot_versions::bot_id.eq(bots::id.nullable()))) - .order_by(( - match_players::match_id.asc(), - match_players::player_id.asc(), - )) - .load::<FullMatchPlayerData>(conn)? - .grouped_by(&matches); - - let res = matches - .into_iter() - .zip(match_players.into_iter()) - .map(|(base, players)| FullMatchData { - base, - match_players: players.into_iter().collect(), - }) - .collect(); + fetch_full_match_data(matches, conn) + }) +} - Ok(res) +pub fn list_public_matches( + amount: i64, + before: Option<NaiveDateTime>, + after: Option<NaiveDateTime>, + conn: &PgConnection, +) -> QueryResult<Vec<FullMatchData>> { + conn.transaction(|| { + // TODO: how can this common logic be abstracted? + let query = matches::table + .filter(matches::state.eq(MatchState::Finished)) + .filter(matches::is_public.eq(true)) + .into_boxed(); + + let matches = + select_matches_page(query, amount, before, after).get_results::<MatchBase>(conn)?; + fetch_full_match_data(matches, conn) }) } +pub fn list_bot_matches( + bot_id: i32, + amount: i64, + before: Option<NaiveDateTime>, + after: Option<NaiveDateTime>, + conn: &PgConnection, +) -> QueryResult<Vec<FullMatchData>> { + let query = matches::table + .filter(matches::state.eq(MatchState::Finished)) + .filter(matches::is_public.eq(true)) + .order_by(matches::created_at.desc()) + .inner_join(match_players::table) + .inner_join( + bot_versions::table.on(match_players::bot_version_id.eq(bot_versions::id.nullable())), + ) + .filter(bot_versions::bot_id.eq(bot_id)) + .select(matches::all_columns) + .into_boxed(); + + let matches = + select_matches_page(query, amount, before, after).get_results::<MatchBase>(conn)?; + fetch_full_match_data(matches, conn) +} + +fn select_matches_page<QS>( + query: BoxedSelectStatement<'static, matches::SqlType, QS, Pg>, + amount: i64, + before: Option<NaiveDateTime>, + after: Option<NaiveDateTime>, +) -> BoxedSelectStatement<'static, matches::SqlType, QS, Pg> +where + QS: AppearsInFromClause<matches::table, Count = Once>, +{ + // TODO: this is not nice. Replace this with proper cursor logic. + match (before, after) { + (None, None) => query.order_by(matches::created_at.desc()), + (Some(before), None) => query + .filter(matches::created_at.lt(before)) + .order_by(matches::created_at.desc()), + (None, Some(after)) => query + .filter(matches::created_at.gt(after)) + .order_by(matches::created_at.asc()), + (Some(before), Some(after)) => query + .filter(matches::created_at.lt(before)) + .filter(matches::created_at.gt(after)) + .order_by(matches::created_at.desc()), + } + .limit(amount) +} + // TODO: maybe unify this with matchdata? pub struct FullMatchData { pub base: MatchBase, + pub map: Option<Map>, pub match_players: Vec<FullMatchPlayerData>, } @@ -151,6 +251,11 @@ pub fn find_match(id: i32, conn: &PgConnection) -> QueryResult<FullMatchData> { conn.transaction(|| { let match_base = matches::table.find(id).get_result::<MatchBase>(conn)?; + let map = match match_base.map_id { + None => None, + Some(map_id) => Some(super::maps::find_map(map_id, conn)?), + }; + let match_players = MatchPlayer::belonging_to(&match_base) .left_join( bot_versions::table @@ -163,6 +268,7 @@ pub fn find_match(id: i32, conn: &PgConnection) -> QueryResult<FullMatchData> { let res = FullMatchData { base: match_base, match_players, + map, }; Ok(res) diff --git a/planetwars-server/src/db/mod.rs b/planetwars-server/src/db/mod.rs index 84ed2a6..f014cea 100644 --- a/planetwars-server/src/db/mod.rs +++ b/planetwars-server/src/db/mod.rs @@ -1,4 +1,5 @@ pub mod bots; +pub mod maps; pub mod matches; pub mod ratings; pub mod sessions; diff --git a/planetwars-server/src/db/users.rs b/planetwars-server/src/db/users.rs index ebb2268..9676dae 100644 --- a/planetwars-server/src/db/users.rs +++ b/planetwars-server/src/db/users.rs @@ -42,11 +42,17 @@ fn argon2_config() -> argon2::Config<'static> { } } -pub fn create_user(credentials: &Credentials, conn: &PgConnection) -> QueryResult<User> { +pub fn hash_password(password: &str) -> (Vec<u8>, [u8; 32]) { let argon_config = argon2_config(); - let salt: [u8; 32] = rand::thread_rng().gen(); - let hash = argon2::hash_raw(credentials.password.as_bytes(), &salt, &argon_config).unwrap(); + let hash = argon2::hash_raw(password.as_bytes(), &salt, &argon_config).unwrap(); + + (hash, salt) +} + +pub fn create_user(credentials: &Credentials, conn: &PgConnection) -> QueryResult<User> { + let (hash, salt) = hash_password(&credentials.password); + let new_user = NewUser { username: credentials.username, password_salt: &salt, @@ -69,6 +75,22 @@ pub fn find_user_by_name(username: &str, db_conn: &PgConnection) -> QueryResult< .first::<User>(db_conn) } +pub fn set_user_password(credentials: Credentials, db_conn: &PgConnection) -> QueryResult<()> { + let (hash, salt) = hash_password(&credentials.password); + + let n_changes = diesel::update(users::table.filter(users::username.eq(&credentials.username))) + .set(( + users::password_salt.eq(salt.as_slice()), + users::password_hash.eq(hash.as_slice()), + )) + .execute(db_conn)?; + if n_changes == 0 { + Err(diesel::result::Error::NotFound) + } else { + Ok(()) + } +} + pub fn authenticate_user(credentials: &Credentials, db_conn: &PgConnection) -> Option<User> { find_user_by_name(credentials.username, db_conn) .optional() diff --git a/planetwars-server/src/lib.rs b/planetwars-server/src/lib.rs index e16b232..fa1af8d 100644 --- a/planetwars-server/src/lib.rs +++ b/planetwars-server/src/lib.rs @@ -94,11 +94,9 @@ pub async fn seed_simplebot(config: &GlobalConfig, pool: &ConnectionPool) { pub type DbPool = Pool<DieselConnectionManager<PgConnection>>; -pub async fn prepare_db(config: &GlobalConfig) -> DbPool { +pub async fn create_db_pool(config: &GlobalConfig) -> DbPool { let manager = DieselConnectionManager::<PgConnection>::new(&config.database_url); - let pool = bb8::Pool::builder().build(manager).await.unwrap(); - seed_simplebot(config, &pool).await; - pool + bb8::Pool::builder().build(manager).await.unwrap() } // create all directories required for further operation @@ -129,12 +127,13 @@ pub fn api() -> Router { "/bots/:bot_name/upload", post(routes::bots::upload_code_multipart), ) - .route("/matches", get(routes::matches::list_matches)) + .route("/matches", get(routes::matches::list_recent_matches)) .route("/matches/:match_id", get(routes::matches::get_match_data)) .route( "/matches/:match_id/log", get(routes::matches::get_match_log), ) + .route("/maps", get(routes::maps::list_maps)) .route("/leaderboard", get(routes::bots::get_ranking)) .route("/submit_bot", post(routes::demo::submit_bot)) .route("/save_bot", post(routes::bots::save_bot)) @@ -165,7 +164,8 @@ async fn run_registry(config: Arc<GlobalConfig>, db_pool: DbPool) { pub async fn run_app() { let global_config = Arc::new(get_config().unwrap()); - let db_pool = prepare_db(&global_config).await; + let db_pool = create_db_pool(&global_config).await; + seed_simplebot(&global_config, &db_pool).await; init_directories(&global_config).unwrap(); if global_config.ranker_enabled { diff --git a/planetwars-server/src/modules/client_api.rs b/planetwars-server/src/modules/client_api.rs index 7026671..3402964 100644 --- a/planetwars-server/src/modules/client_api.rs +++ b/planetwars-server/src/modules/client_api.rs @@ -111,14 +111,23 @@ impl pb::client_api_service_server::ClientApiService for ClientApiServer { db::bots::find_bot_with_version_by_name(&match_request.opponent_name, &conn) .map_err(|_| Status::not_found("opponent not found"))?; + let map_name = match match_request.map_name.as_str() { + "" => "hex", + name => name, + }; + let map = db::maps::find_map_by_name(map_name, &conn) + .map_err(|_| Status::not_found("map not found"))?; + let player_key = gen_alphanumeric(32); let remote_bot_spec = Box::new(RemoteBotSpec { player_key: player_key.clone(), router: self.router.clone(), }); - let run_match = RunMatch::from_players( + let run_match = RunMatch::new( self.runner_config.clone(), + false, + map, vec![ MatchPlayer::BotSpec { spec: remote_bot_spec, diff --git a/planetwars-server/src/modules/matches.rs b/planetwars-server/src/modules/matches.rs index 4f538ed..ecc7976 100644 --- a/planetwars-server/src/modules/matches.rs +++ b/planetwars-server/src/modules/matches.rs @@ -7,6 +7,7 @@ use tokio::task::JoinHandle; use crate::{ db::{ self, + maps::Map, matches::{MatchData, MatchResult}, }, util::gen_alphanumeric, @@ -17,6 +18,11 @@ pub struct RunMatch { log_file_name: String, players: Vec<MatchPlayer>, config: Arc<GlobalConfig>, + is_public: bool, + // Map is mandatory for now. + // It would be nice to allow "anonymous" (eg. randomly generated) maps + // in the future, too. + map: Map, } pub enum MatchPlayer { @@ -30,19 +36,27 @@ pub enum MatchPlayer { } impl RunMatch { - pub fn from_players(config: Arc<GlobalConfig>, players: Vec<MatchPlayer>) -> Self { + // TODO: create a MatchParams struct + pub fn new( + config: Arc<GlobalConfig>, + is_public: bool, + map: Map, + players: Vec<MatchPlayer>, + ) -> Self { let log_file_name = format!("{}.log", gen_alphanumeric(16)); RunMatch { config, log_file_name, players, + is_public, + map, } } fn into_runner_config(self) -> runner::MatchConfig { runner::MatchConfig { - map_path: PathBuf::from(&self.config.maps_directory).join("hex.json"), - map_name: "hex".to_string(), + map_path: PathBuf::from(&self.config.maps_directory).join(self.map.file_path), + map_name: self.map.name, log_path: PathBuf::from(&self.config.match_logs_directory).join(&self.log_file_name), players: self .players @@ -80,6 +94,8 @@ impl RunMatch { let new_match_data = db::matches::NewMatch { state: db::matches::MatchState::Playing, log_path: &self.log_file_name, + is_public: self.is_public, + map_id: Some(self.map.id), }; let new_match_players = self .players diff --git a/planetwars-server/src/modules/ranking.rs b/planetwars-server/src/modules/ranking.rs index cb699fe..90c4a56 100644 --- a/planetwars-server/src/modules/ranking.rs +++ b/planetwars-server/src/modules/ranking.rs @@ -1,4 +1,5 @@ use crate::db::bots::BotVersion; +use crate::db::maps::Map; use crate::{db::bots::Bot, DbPool, GlobalConfig}; use crate::db; @@ -25,22 +26,31 @@ pub async fn run_ranker(config: Arc<GlobalConfig>, db_pool: DbPool) { .expect("could not get database connection"); loop { interval.tick().await; - let bots = db::bots::all_active_bots_with_version(&db_conn).unwrap(); + let bots = db::bots::all_active_bots_with_version(&db_conn).expect("could not load bots"); if bots.len() < 2 { // not enough bots to play a match continue; } - let selected_bots: Vec<(Bot, BotVersion)> = { - let mut rng = &mut rand::thread_rng(); - bots.choose_multiple(&mut rng, 2).cloned().collect() + + let selected_bots: Vec<(Bot, BotVersion)> = bots + .choose_multiple(&mut rand::thread_rng(), 2) + .cloned() + .collect(); + + let maps = db::maps::list_maps(&db_conn).expect("could not load map"); + let map = match maps.choose(&mut rand::thread_rng()).cloned() { + None => continue, // no maps available + Some(map) => map, }; - play_ranking_match(config.clone(), selected_bots, db_pool.clone()).await; + + play_ranking_match(config.clone(), map, selected_bots, db_pool.clone()).await; recalculate_ratings(&db_conn).expect("could not recalculate ratings"); } } async fn play_ranking_match( config: Arc<GlobalConfig>, + map: Map, selected_bots: Vec<(Bot, BotVersion)>, db_pool: DbPool, ) { @@ -53,7 +63,7 @@ async fn play_ranking_match( players.push(player); } - let (_, handle) = RunMatch::from_players(config, players) + let (_, handle) = RunMatch::new(config, true, map, players) .run(db_pool.clone()) .await .expect("failed to run match"); diff --git a/planetwars-server/src/modules/registry.rs b/planetwars-server/src/modules/registry.rs index 297404a..4a79d59 100644 --- a/planetwars-server/src/modules/registry.rs +++ b/planetwars-server/src/modules/registry.rs @@ -300,8 +300,10 @@ async fn put_upload( while let Some(Ok(chunk)) = stream.next().await { file.write_all(&chunk).await.unwrap(); } - file.flush().await.unwrap(); let range_end = last_byte_pos(&file).await.unwrap(); + // Close the file to ensure all data has been flushed to the kernel. + // If we don't do this, calculating the checksum can fail. + std::mem::drop(file); let expected_digest = params.digest.strip_prefix("sha256:").unwrap(); let digest = file_sha256_digest(&upload_path).unwrap(); diff --git a/planetwars-server/src/routes/demo.rs b/planetwars-server/src/routes/demo.rs index dad9453..1ec8825 100644 --- a/planetwars-server/src/routes/demo.rs +++ b/planetwars-server/src/routes/demo.rs @@ -14,12 +14,13 @@ use serde::{Deserialize, Serialize}; use super::matches::ApiMatch; const DEFAULT_OPPONENT_NAME: &str = "simplebot"; +const DEFAULT_MAP_NAME: &str = "hex"; #[derive(Serialize, Deserialize, Debug)] pub struct SubmitBotParams { pub code: String, - // TODO: would it be better to pass an ID here? pub opponent_name: Option<String>, + pub map_name: Option<String>, } #[derive(Serialize, Deserialize)] @@ -40,16 +41,24 @@ pub async fn submit_bot( .opponent_name .unwrap_or_else(|| DEFAULT_OPPONENT_NAME.to_string()); + let map_name = params + .map_name + .unwrap_or_else(|| DEFAULT_MAP_NAME.to_string()); + let (opponent_bot, opponent_bot_version) = db::bots::find_bot_with_version_by_name(&opponent_name, &conn) .map_err(|_| StatusCode::BAD_REQUEST)?; + let map = db::maps::find_map_by_name(&map_name, &conn).map_err(|_| StatusCode::BAD_REQUEST)?; + let player_bot_version = save_code_string(¶ms.code, None, &conn, &config) // TODO: can we recover from this? .expect("could not save bot code"); - let run_match = RunMatch::from_players( + let run_match = RunMatch::new( config, + false, + map.clone(), vec![ MatchPlayer::BotVersion { bot: None, @@ -81,6 +90,7 @@ pub async fn submit_bot( bot: Some(opponent_bot), }, ], + map: Some(map), }; let api_match = super::matches::match_data_to_api(full_match_data); diff --git a/planetwars-server/src/routes/maps.rs b/planetwars-server/src/routes/maps.rs new file mode 100644 index 0000000..689b11e --- /dev/null +++ b/planetwars-server/src/routes/maps.rs @@ -0,0 +1,19 @@ +use crate::{db, DatabaseConnection}; +use axum::Json; +use hyper::StatusCode; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct ApiMap { + pub name: String, +} + +pub async fn list_maps(conn: DatabaseConnection) -> Result<Json<Vec<ApiMap>>, StatusCode> { + let maps = db::maps::list_maps(&conn).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let api_maps = maps + .into_iter() + .map(|map| ApiMap { name: map.name }) + .collect(); + Ok(Json(api_maps)) +} diff --git a/planetwars-server/src/routes/matches.rs b/planetwars-server/src/routes/matches.rs index 58ca478..10b4507 100644 --- a/planetwars-server/src/routes/matches.rs +++ b/planetwars-server/src/routes/matches.rs @@ -1,19 +1,30 @@ -use axum::{extract::Path, Extension, Json}; +use axum::{ + extract::{Path, Query}, + Extension, Json, +}; +use chrono::NaiveDateTime; use hyper::StatusCode; use serde::{Deserialize, Serialize}; use std::{path::PathBuf, sync::Arc}; use crate::{ - db::matches::{self, MatchState}, + db::{ + self, + matches::{self, MatchState}, + }, DatabaseConnection, GlobalConfig, }; +use super::maps::ApiMap; + #[derive(Serialize, Deserialize)] pub struct ApiMatch { id: i32, timestamp: chrono::NaiveDateTime, state: MatchState, players: Vec<ApiMatchPlayer>, + winner: Option<i32>, + map: Option<ApiMap>, } #[derive(Serialize, Deserialize)] @@ -23,10 +34,60 @@ pub struct ApiMatchPlayer { bot_name: Option<String>, } -pub async fn list_matches(conn: DatabaseConnection) -> Result<Json<Vec<ApiMatch>>, StatusCode> { - matches::list_matches(100, &conn) - .map_err(|_| StatusCode::BAD_REQUEST) - .map(|matches| Json(matches.into_iter().map(match_data_to_api).collect())) +#[derive(Serialize, Deserialize)] +pub struct ListRecentMatchesParams { + count: Option<usize>, + // TODO: should timezone be specified here? + before: Option<NaiveDateTime>, + after: Option<NaiveDateTime>, + + bot: Option<String>, +} + +const MAX_NUM_RETURNED_MATCHES: usize = 100; +const DEFAULT_NUM_RETURNED_MATCHES: usize = 50; + +#[derive(Serialize, Deserialize)] +pub struct ListMatchesResponse { + matches: Vec<ApiMatch>, + has_next: bool, +} + +pub async fn list_recent_matches( + Query(params): Query<ListRecentMatchesParams>, + conn: DatabaseConnection, +) -> Result<Json<ListMatchesResponse>, StatusCode> { + let requested_count = std::cmp::min( + params.count.unwrap_or(DEFAULT_NUM_RETURNED_MATCHES), + MAX_NUM_RETURNED_MATCHES, + ); + + // fetch one additional record to check whether a next page exists + let count = (requested_count + 1) as i64; + + let matches_result = match params.bot { + Some(bot_name) => { + let bot = db::bots::find_bot_by_name(&bot_name, &conn) + .map_err(|_| StatusCode::BAD_REQUEST)?; + matches::list_bot_matches(bot.id, count, params.before, params.after, &conn) + } + None => matches::list_public_matches(count, params.before, params.after, &conn), + }; + + let mut matches = matches_result.map_err(|_| StatusCode::BAD_REQUEST)?; + + let mut has_next = false; + if matches.len() > requested_count { + has_next = true; + matches.truncate(requested_count); + } + + let api_matches = matches.into_iter().map(match_data_to_api).collect(); + + Ok(Json(ListMatchesResponse { + matches: api_matches, + has_next, + })) } pub fn match_data_to_api(data: matches::FullMatchData) -> ApiMatch { @@ -43,6 +104,8 @@ pub fn match_data_to_api(data: matches::FullMatchData) -> ApiMatch { bot_name: _p.bot.as_ref().map(|b| b.name.clone()), }) .collect(), + winner: data.base.winner, + map: data.map.map(|m| ApiMap { name: m.name }), } } diff --git a/planetwars-server/src/routes/mod.rs b/planetwars-server/src/routes/mod.rs index b3decb8..9510fd4 100644 --- a/planetwars-server/src/routes/mod.rs +++ b/planetwars-server/src/routes/mod.rs @@ -1,4 +1,5 @@ pub mod bots; pub mod demo; +pub mod maps; pub mod matches; pub mod users; diff --git a/planetwars-server/src/schema.rs b/planetwars-server/src/schema.rs index 5993115..adc6555 100644 --- a/planetwars-server/src/schema.rs +++ b/planetwars-server/src/schema.rs @@ -30,6 +30,17 @@ table! { use diesel::sql_types::*; use crate::db_types::*; + maps (id) { + id -> Int4, + name -> Text, + file_path -> Text, + } +} + +table! { + use diesel::sql_types::*; + use crate::db_types::*; + match_players (match_id, player_id) { match_id -> Int4, player_id -> Int4, @@ -47,6 +58,8 @@ table! { log_path -> Text, created_at -> Timestamp, winner -> Nullable<Int4>, + is_public -> Bool, + map_id -> Nullable<Int4>, } } @@ -86,12 +99,14 @@ table! { joinable!(bots -> users (owner_id)); joinable!(match_players -> bot_versions (bot_version_id)); joinable!(match_players -> matches (match_id)); +joinable!(matches -> maps (map_id)); joinable!(ratings -> bots (bot_id)); joinable!(sessions -> users (user_id)); allow_tables_to_appear_in_same_query!( bot_versions, bots, + maps, match_players, matches, ratings, |