diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e47d9bb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "rust-analyzer.linkedProjects": [ + "./Cargo.toml", + "./Cargo.toml" + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 58989db..6203242 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chrono = { version = "0.4.24", features = ["serde"] } +dotenv = "0.15.0" rocket = { version = "0.5.0-rc.2", features = ["json"] } -mysql = "22.2.0" -mysql_common = { version = "0.28.2", features = ["chrono"] } -dotenv = "0.15.0" \ No newline at end of file +tokio = { version = "1.27.0", features = ["full"] } +tokio-postgres = { version = "0.7.8", features = ["with-chrono-0_4"] } +tokio-test = "0.4.2" +serde = "1.0.154" \ No newline at end of file diff --git a/src/database/map.rs b/src/database/map.rs deleted file mode 100644 index dd5e644..0000000 --- a/src/database/map.rs +++ /dev/null @@ -1,123 +0,0 @@ -use mysql_common::{chrono::NaiveDateTime, frunk::HList}; -use rocket::serde::{Deserialize, Serialize}; - -pub type MapRow = HList!( - String, - String, - i8, - i8, - String, - NaiveDateTime, - String, - i32, - i32, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool, - bool -); - -// Different tile types and whether they appear in the map. -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct MapTileData { - pub death: bool, - pub through: bool, - pub jump: bool, - pub dfreeze: bool, - pub ehook_start: bool, - pub hit_end: bool, - pub solo_start: bool, - pub tele_gun: bool, - pub tele_grenade: bool, - pub tele_laser: bool, - pub npc_start: bool, - pub super_start: bool, - pub jetpack_start: bool, - pub walljump: bool, - pub nph_start: bool, - pub weapon_shotgun: bool, - pub weapon_grenade: bool, - pub powerup_ninja: bool, - pub weapon_rifle: bool, - pub laser_stop: bool, - pub crazy_shotgun: bool, - pub dragger: bool, - pub door: bool, - pub switch_timed: bool, - pub switch: bool, - pub stop: bool, - pub through_all: bool, - pub tune: bool, - pub oldlaser: bool, - pub teleinevil: bool, - pub telein: bool, - pub telecheck: bool, - pub teleinweapon: bool, - pub teleinhook: bool, - pub checkpoint_first: bool, - pub bonus: bool, - pub boost: bool, - pub plasmaf: bool, - pub plasmae: bool, - pub plasmau: bool, -} - -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct Map { - pub name: String, - pub server: String, - pub points: i8, - pub stars: i8, - pub mapper: String, - pub timestamp: NaiveDateTime, - pub width: i32, - pub height: i32, - pub tile_data: MapTileData, -} - -/* - IMPORTANT: For this to work the following SQL query must be executed: - ```SQL - UPDATE record_maps SET Timestamp='1970-01-01 01:01:01' WHERE Timestamp='1990-01-01 00:00:00'; - ``` - Else the reads on `record_maps` will fail whenever the timestamp is '0000-00-00 00:00:00'! - TODO: Fix this server-side. -*/ diff --git a/src/database/mod.rs b/src/database/mod.rs index 092d725..ed45c38 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,256 +1,148 @@ -use mysql::prelude::*; -use mysql::*; -use mysql_common::frunk::hlist_pat; -use std::error::Error; -use std::result::Result; -use std::{env, result}; +use std::{env, error::Error}; -pub mod map; -pub mod race; -pub mod teamrace; +use dotenv::dotenv; +use tokio_postgres::{Client, NoTls}; -pub fn create_pool() -> Pool { - let url = match env::var("DB_URI") { - Ok(uri) => uri, - Err(err) => { - println!("You must provide an env var: 'DB_URI'!"); - panic!("{}", err); - } - }; +use self::models::map::Map; - let pool = match Pool::new(url.as_str()) { - Ok(pool) => pool, - Err(err) => { - println!("Couldn't connect to the database!"); - panic!("{}", err); - } - }; +mod models; - pool +pub struct DatabaseHandler { + pub client: Client, } -pub struct DatabasePoolStore(pub Pool); +impl DatabaseHandler { + pub async fn create() -> Result> { + // Load the env file + dotenv()?; -pub fn get_maps(pool: &Pool) -> Result, Box> { - let mut conn = pool.get_conn()?; + // Load in the environment variables + let connection_host = env::var("DB_HOST")?; + let connection_user = env::var("DB_USER")?; + let connection_db = env::var("DB_NAME")?; - let maps = conn.query_map( - "SELECT * FROM record_maps AS maps JOIN record_mapinfo AS mapinfo ON maps.Map = mapinfo.Map", - |row: map::MapRow| { - let hlist_pat![ name, server, points, stars, mapper, timestamp, _name_again, width, height, death, through, jump, dfreeze, hit_end, ehook_start, solo_start, tele_gun, tele_grenade, tele_laser, npc_start, super_start, jetpack_start, walljump, nph_start, weapon_shotgun, weapon_grenade, powerup_ninja, weapon_rifle, laser_stop, crazy_shotgun, dragger, door, switch_timed, switch, stop, through_all, tune, oldlaser, teleinevil, telein, telecheck, teleinweapon, teleinhook, checkpoint_first, bonus, boost, plasmaf, plasmae, plasmau ] = row; - map::Map { - name, - server, points, - stars, mapper, - timestamp, - width, - height, - tile_data: map::MapTileData { death, through, jump, dfreeze, hit_end, ehook_start, solo_start, tele_gun, tele_grenade, tele_laser, npc_start, super_start, jetpack_start, walljump, nph_start, weapon_shotgun, weapon_grenade, powerup_ninja, weapon_rifle, laser_stop, crazy_shotgun, dragger, door, switch_timed, switch, stop, through_all, tune, oldlaser, teleinevil, telein, telecheck, teleinweapon, teleinhook, checkpoint_first, bonus, boost, plasmaf, plasmae, plasmau } + let connection_string = format!( + "host={} user={} dbname={}", + connection_host, connection_user, connection_db + ); + + // Connect to the database + let (client, connection) = tokio_postgres::connect(&connection_string, NoTls).await?; + + // NOTE: This comes directly from the official documentation (https://docs.rs/tokio-postgres/latest/tokio_postgres/#example) + // I hace no idea what in the flying flop it means... + tokio::spawn(async move { + if let Err(e) = connection.await { + eprintln!("Error while connecting to the database: {}", e); } + }); + + Ok(DatabaseHandler { client }) + } + + pub async fn get_all_maps(&self) -> Result, Box> { + // FIXME: Why does this have to be mutable? Should fix. + let mut results = self + .client + .query( + " + SELECT record_maps.map, type, points, stars, mapper, release, width, height, + CONCAT_WS(',', + CASE WHEN DEATH = '1' THEN 'DEATH' END, + CASE WHEN THROUGH = '1' THEN 'THROUGH' END, + CASE WHEN JUMP = '1' THEN 'JUMP' END, + CASE WHEN DFREEZE = '1' THEN 'DFREEZE' END, + CASE WHEN EHOOK_START = '1' THEN 'EHOOK_START' END, + CASE WHEN HIT_END = '1' THEN 'HIT_END' END, + CASE WHEN SOLO_START = '1' THEN 'SOLO_START' END, + CASE WHEN TELE_GUN = '1' THEN 'TELE_GUN' END, + CASE WHEN TELE_GRENADE = '1' THEN 'TELE_GRENADE' END, + CASE WHEN TELE_LASER = '1' THEN 'TELE_LASER' END, + CASE WHEN NPC_START = '1' THEN 'NPC_START' END, + CASE WHEN SUPER_START = '1' THEN 'SUPER_START' END, + CASE WHEN JETPACK_START = '1' THEN 'JETPACK_START' END, + CASE WHEN WALLJUMP = '1' THEN 'WALLJUMP' END, + CASE WHEN NPH_START = '1' THEN 'NPH_START' END, + CASE WHEN WEAPON_SHOTGUN = '1' THEN 'WEAPON_SHOTGUN' END, + CASE WHEN WEAPON_GRENADE = '1' THEN 'WEAPON_GRENADE' END, + CASE WHEN POWERUP_NINJA = '1' THEN 'POWERUP_NINJA' END, + CASE WHEN WEAPON_RIFLE = '1' THEN 'WEAPON_RIFLE' END, + CASE WHEN LASER_STOP = '1' THEN 'LASER_STOP' END, + CASE WHEN CRAZY_SHOTGUN = '1' THEN 'CRAZY_SHOTGUN' END, + CASE WHEN DRAGGER = '1' THEN 'DRAGGER' END, + CASE WHEN DOOR = '1' THEN 'DOOR' END, + CASE WHEN SWITCH_TIMED = '1' THEN 'SWITCH_TIMED' END, + CASE WHEN SWITCH = '1' THEN 'SWITCH' END, + CASE WHEN STOP = '1' THEN 'STOP' END, + CASE WHEN THROUGH_ALL = '1' THEN 'THROUGH_ALL' END, + CASE WHEN TUNE = '1' THEN 'TUNE' END, + CASE WHEN OLDLASER = '1' THEN 'OLDLASER' END, + CASE WHEN TELEINEVIL = '1' THEN 'TELEINEVIL' END, + CASE WHEN TELEIN = '1' THEN 'TELEIN' END, + CASE WHEN TELECHECK = '1' THEN 'TELECHECK' END, + CASE WHEN TELEINWEAPON = '1' THEN 'TELEINWEAPON' END, + CASE WHEN TELEINHOOK = '1' THEN 'TELEINHOOK' END, + CASE WHEN CHECKPOINT_FIRST = '1' THEN 'CHECKPOINT_FIRST' END, + CASE WHEN BONUS = '1' THEN 'BONUS' END, + CASE WHEN BOOST = '1' THEN 'BOOST' END, + CASE WHEN PLASMAF = '1' THEN 'PLASMAF' END, + CASE WHEN PLASMAE = '1' THEN 'PLASMAE' END, + CASE WHEN PLASMAU = '1' THEN 'PLASMAU' END) AS tiles + FROM record_maps JOIN record_mapinfo ON record_maps.map = record_mapinfo.map + ", + &[], + ) + .await? + .into_iter() + .map(|row| Map::from_db_row(&row)); + + // If the result has errors, return it. Otherwise, return all the rows. + match results.find(|row| row.is_err()) { + Some(row) => row.map(|row| vec![row]), + None => Ok(results.map(|row| row.unwrap()).collect()), } - )?; - - Ok(maps) -} - -pub fn get_map_by_name(pool: &Pool, name: &str) -> Result, Box> { - let mut conn = pool.get_conn()?; - let stmt = conn.prep("SELECT * FROM record_maps AS maps JOIN record_mapinfo AS mapinfo ON maps.Map = mapinfo.Map WHERE maps.Map = ?")?; - - let map: Option = conn.exec_first(&stmt, (name,))?; - - Ok(match map { - Some(row) => { - let hlist_pat![ - name, - server, - points, - stars, - mapper, - timestamp, - _name_again, - width, - height, - death, - through, - jump, - dfreeze, - hit_end, - ehook_start, - solo_start, - tele_gun, - tele_grenade, - tele_laser, - npc_start, - super_start, - jetpack_start, - walljump, - nph_start, - weapon_shotgun, - weapon_grenade, - powerup_ninja, - weapon_rifle, - laser_stop, - crazy_shotgun, - dragger, - door, - switch_timed, - switch, - stop, - through_all, - tune, - oldlaser, - teleinevil, - telein, - telecheck, - teleinweapon, - teleinhook, - checkpoint_first, - bonus, - boost, - plasmaf, - plasmae, - plasmau - ] = row; - Some(map::Map { - name, - server, - points, - stars, - mapper, - timestamp, - width, - height, - tile_data: map::MapTileData { - death, - through, - jump, - dfreeze, - hit_end, - ehook_start, - solo_start, - tele_gun, - tele_grenade, - tele_laser, - npc_start, - super_start, - jetpack_start, - walljump, - nph_start, - weapon_shotgun, - weapon_grenade, - powerup_ninja, - weapon_rifle, - laser_stop, - crazy_shotgun, - dragger, - door, - switch_timed, - switch, - stop, - through_all, - tune, - oldlaser, - teleinevil, - telein, - telecheck, - teleinweapon, - teleinhook, - checkpoint_first, - bonus, - boost, - plasmaf, - plasmae, - plasmau, - }, - }) - } - None => None, - }) -} - -fn races_result(row: race::RaceRow) -> race::Race { - let hlist_pat![ - map, name, timestamp, time, server, cp1, cp2, cp3, cp4, cp5, cp6, cp7, cp8, cp9, cp10, - cp11, cp12, cp13, cp14, cp15, cp16, cp17, cp18, cp19, cp20, cp21, cp22, cp23, cp24, cp25, - gameid, ddnet7 - ] = row; - race::Race { - map, - name, - timestamp, - time, - server, - cp1, - cp2, - cp3, - cp4, - cp5, - cp6, - cp7, - cp8, - cp9, - cp10, - cp11, - cp12, - cp13, - cp14, - cp15, - cp16, - cp17, - cp18, - cp19, - cp20, - cp21, - cp22, - cp23, - cp24, - cp25, - gameid, - ddnet7, } } -pub fn get_races_by_player(pool: &Pool, player: &str) -> Result, Box> { - let mut conn = pool.get_conn()?; - let stmt = conn.prep("SELECT * FROM record_race WHERE Name = ?")?; +#[cfg(test)] +mod tests { + use super::*; - let races = conn.exec_map(&stmt, (player,), races_result)?; + #[test] + fn test_database_connection() { + async fn test() { + let db = match DatabaseHandler::create().await { + Ok(db) => db, + Err(err) => panic!("Could not get a client!\n{}", err), + }; - Ok(races) -} + let msg = "Hello World!"; + let rows = match db.client.query("SELECT $1::TEXT", &[&msg]).await { + Ok(rows) => rows, + Err(err) => panic!("Could not create query!\n{}", err), + }; -pub fn get_races_by_id(pool: &Pool, id: &str) -> Result, Box> { - let mut conn = pool.get_conn()?; - let stmt = conn.prep("SELECT * FROM record_race WHERE GameID = ?")?; + let value: &str = rows[0].get(0); + assert_eq!(value, msg) + } - let races = conn.exec_map(&stmt, (id,), races_result)?; + tokio_test::block_on(test()) + } - Ok(races) -} + #[test] + fn test_get_all_maps() { + async fn test() { + let db = match DatabaseHandler::create().await { + Ok(db) => db, + Err(err) => panic!("Could not get a client!\n{}", err), + }; -pub fn get_races_by_map(pool: &Pool, map: &str) -> Result, Box> { - let mut conn = pool.get_conn()?; - let stmt = conn.prep("SELECT * FROM record_race WHERE Map = ?")?; + match db.get_all_maps().await { + Ok(maps) => println!("Found maps: {:?}", maps.len()), + Err(err) => panic!("Could not get all maps!\n{}", err), + }; + } - let races = conn.exec_map(&stmt, (map,), races_result)?; - - Ok(races) -} - -pub fn get_player_total_playtime( - pool: &Pool, - player: &str, -) -> Result<(Option, Option), Box> { - let mut conn = pool.get_conn()?; - let stmt = conn.prep( - "SELECT COALESCE(SUM(Time),0), COALESCE(COUNT(Time),0) FROM record_race WHERE Name = ?", - )?; - - let result = conn.exec_first(&stmt, (player,))?; - - match result { - Some((total_playtime, total_games)) => Ok((total_playtime, total_games)), - None => Ok((None::, None::)), + tokio_test::block_on(test()) } } diff --git a/src/database/models/map.rs b/src/database/models/map.rs new file mode 100644 index 0000000..74fa569 --- /dev/null +++ b/src/database/models/map.rs @@ -0,0 +1,43 @@ +use std::error::Error; + +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; +use tokio_postgres::Row; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Map { + map: String, + mapper: String, + category: String, + points: u8, + stars: u8, + release: Option, + width: u16, + height: u16, + tiles: Vec, +} + +impl Map { + pub fn from_db_row(db_row: &Row) -> Result> { + let map: String = db_row.try_get(0)?; + let category: String = db_row.try_get(1)?; + let points_i16: i16 = db_row.try_get(2)?; + let points: u8 = points_i16 as u8; + let stars_i16: i16 = db_row.try_get(3)?; + let stars: u8 = stars_i16 as u8; + let mapper: String = db_row.try_get(4)?; + let release: Option = db_row.try_get(5)?; + + Ok(Map { + map, + mapper, + category, + points, + stars, + release, + width: 1, + height: 1, + tiles: vec![], + }) + } +} diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs new file mode 100644 index 0000000..1d7f53b --- /dev/null +++ b/src/database/models/mod.rs @@ -0,0 +1 @@ +pub mod map; diff --git a/src/database/race.rs b/src/database/race.rs deleted file mode 100644 index 6ab0d61..0000000 --- a/src/database/race.rs +++ /dev/null @@ -1,74 +0,0 @@ -use mysql_common::{chrono::NaiveDateTime, frunk::HList}; -use rocket::serde::{Deserialize, Serialize}; - -pub type RaceRow = HList!( - String, - String, - NaiveDateTime, - f64, - String, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - f64, - Option, - bool -); - -#[derive(Debug, Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct Race { - pub map: String, - pub name: String, - pub timestamp: NaiveDateTime, - pub time: f64, - pub server: String, - pub cp1: f64, - pub cp2: f64, - pub cp3: f64, - pub cp4: f64, - pub cp5: f64, - pub cp6: f64, - pub cp7: f64, - pub cp8: f64, - pub cp9: f64, - pub cp10: f64, - pub cp11: f64, - pub cp12: f64, - pub cp13: f64, - pub cp14: f64, - pub cp15: f64, - pub cp16: f64, - pub cp17: f64, - pub cp18: f64, - pub cp19: f64, - pub cp20: f64, - pub cp21: f64, - pub cp22: f64, - pub cp23: f64, - pub cp24: f64, - pub cp25: f64, - pub gameid: Option, - pub ddnet7: bool, -} diff --git a/src/database/teamrace.rs b/src/database/teamrace.rs deleted file mode 100644 index 7c3352f..0000000 --- a/src/database/teamrace.rs +++ /dev/null @@ -1,16 +0,0 @@ -use mysql_common::{chrono::NaiveDateTime, frunk::HList}; -use rocket::serde::{Deserialize, Serialize}; - -pub type TeamRaceRow = HList!(String, String, f64, NaiveDateTime, String, String, bool); - -#[derive(Debug, Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct TeamRace { - pub name: String, - pub map: String, - pub time: f64, - pub timestamp: NaiveDateTime, - pub id: String, - pub gameid: String, - pub ddnet7: bool, -} diff --git a/src/main.rs b/src/main.rs index a33ae22..0449c22 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,103 +1,28 @@ +use database::DatabaseHandler; + #[macro_use] extern crate rocket; - -use database::create_pool; -use rocket::serde::json::Json; -use rocket::State; - extern crate dotenv; -use dotenv::dotenv; mod database; -use database::{map::Map, race::Race, DatabasePoolStore}; -#[get("/maps")] -fn get_all_maps(db_pool: &State) -> Option>> { - match database::get_maps(&db_pool.0) { - Ok(maps) => Some(Json(maps)), - Err(err) => { - println!("{err}"); - None - } +#[rocket::main] +async fn main() { + let client = match DatabaseHandler::create().await { + Ok(client) => client, + Err(err) => panic!( + "Encountered an error while connecting to the database!\n{}", + err + ), + }; + + match rocket::build() + .manage(client) + .mount("/", routes![]) + .launch() + .await + { + Ok(_) => (), + Err(err) => println!("Encountered an error while starting rocket!\n{}", err), } } - -#[get("/maps/")] -fn get_map_by_name(db_pool: &State, map: &str) -> Option> { - match database::get_map_by_name(&db_pool.0, map) { - Ok(map) => map.map(Json), - Err(err) => { - println!("{err}"); - None - } - } -} - -#[get("/races/by-player/")] -fn get_races_by_player( - db_pool: &State, - player: &str, -) -> Option>> { - match database::get_races_by_player(&db_pool.0, player) { - Ok(races) => Some(Json(races)), - Err(err) => { - println!("{err}"); - None - } - } -} - -#[get("/races/by-id/")] -fn get_races_by_id(db_pool: &State, id: &str) -> Option>> { - match database::get_races_by_id(&db_pool.0, id) { - Ok(races) => Some(Json(races)), - Err(err) => { - println!("{err}"); - None - } - } -} - -#[get("/races/by-map/")] -fn get_races_by_map(db_pool: &State, map: &str) -> Option>> { - match database::get_races_by_map(&db_pool.0, map) { - Ok(races) => Some(Json(races)), - Err(err) => { - println!("{err}"); - None - } - } -} - -#[get("/player//playtime")] -fn get_player_playtime( - db_pool: &State, - player: &str, -) -> Option, Option)>> { - match database::get_player_total_playtime(&db_pool.0, player) { - Ok(playtime) => Some(Json(playtime)), - Err(err) => { - println!("{err}"); - None - } - } -} - -#[launch] -fn rocket() -> _ { - dotenv().ok(); - - let db_pool = create_pool(); - - rocket::build().manage(DatabasePoolStore(db_pool)).mount( - "/", - routes![ - get_all_maps, - get_map_by_name, - get_races_by_player, - get_races_by_id, - get_races_by_map, - get_player_playtime - ], - ) -}