diff options
Diffstat (limited to 'planetwars-server/src/db')
-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 |
4 files changed, 190 insertions, 26 deletions
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() |