aboutsummaryrefslogtreecommitdiff
path: root/planetwars-server/src/db
diff options
context:
space:
mode:
Diffstat (limited to 'planetwars-server/src/db')
-rw-r--r--planetwars-server/src/db/maps.rs35
-rw-r--r--planetwars-server/src/db/matches.rs152
-rw-r--r--planetwars-server/src/db/mod.rs1
-rw-r--r--planetwars-server/src/db/users.rs28
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()