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::{ self, matches::{self, BotMatchOutcome, MatchState}, }, DatabaseConnection, GlobalConfig, }; use super::maps::ApiMap; #[derive(Serialize, Deserialize)] pub struct ApiMatch { id: i32, timestamp: chrono::NaiveDateTime, state: MatchState, players: Vec, winner: Option, map: Option, } #[derive(Serialize, Deserialize)] pub struct ApiMatchPlayer { bot_version_id: Option, bot_id: Option, bot_name: Option, owner_id: Option, had_errors: Option, } #[derive(Serialize, Deserialize)] pub struct ListRecentMatchesParams { count: Option, // TODO: should timezone be specified here? before: Option, after: Option, bot: Option, opponent: Option, map: Option, had_errors: Option, outcome: Option, } const MAX_NUM_RETURNED_MATCHES: usize = 100; const DEFAULT_NUM_RETURNED_MATCHES: usize = 50; #[derive(Serialize, Deserialize)] pub struct ListMatchesResponse { matches: Vec, has_next: bool, } pub async fn list_recent_matches( Query(params): Query, mut conn: DatabaseConnection, ) -> Result, 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) => { // TODO: do we prefer BAD_REQUEST for invalid parameters, or do we want to return an empty response? let bot = db::bots::find_bot_by_name(&bot_name, &mut conn) .map_err(|_| StatusCode::BAD_REQUEST)?; let opponent_id = if let Some(ref opponent_name) = params.opponent { let opponent = db::bots::find_bot_by_name(opponent_name, &mut conn) .map_err(|_| StatusCode::BAD_REQUEST)?; Some(opponent.id) } else { None }; let map_id = if let Some(ref map_name) = params.map { let map = db::maps::find_map_by_name(map_name, &mut conn) .map_err(|_| StatusCode::BAD_REQUEST)?; Some(map.id) } else { None }; matches::list_bot_matches( bot.id, opponent_id, map_id, params.outcome, params.had_errors, count, params.before, params.after, &mut conn, ) } None => matches::list_public_matches(count, params.before, params.after, &mut conn), }; let mut matches = matches_result.expect("failed to get matches"); //.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 { ApiMatch { id: data.base.id, timestamp: data.base.created_at, state: data.base.state, players: data .match_players .iter() .map(|p| ApiMatchPlayer { bot_version_id: p.bot_version.as_ref().map(|cb| cb.id), bot_id: p.bot.as_ref().map(|b| b.id), bot_name: p.bot.as_ref().map(|b| b.name.clone()), owner_id: p.bot.as_ref().and_then(|b| b.owner_id), had_errors: p.base.had_errors, }) .collect(), winner: data.base.winner, map: data.map.map(|m| ApiMap { name: m.name }), } } pub async fn get_match_data( Path(match_id): Path, mut conn: DatabaseConnection, ) -> Result, StatusCode> { let match_data = matches::find_match(match_id, &mut conn) .map_err(|_| StatusCode::NOT_FOUND) .map(match_data_to_api)?; Ok(Json(match_data)) } pub async fn get_match_log( Path(match_id): Path, mut conn: DatabaseConnection, Extension(config): Extension>, ) -> Result, StatusCode> { let match_base = matches::find_match_base(match_id, &mut conn).map_err(|_| StatusCode::NOT_FOUND)?; let log_path = PathBuf::from(&config.match_logs_directory).join(&match_base.log_path); let log_contents = std::fs::read(log_path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(log_contents) }