Changing to postgres

This commit is contained in:
BurnyLlama 2023-04-13 17:38:47 +02:00
parent dbd52f31c6
commit 2dcfb991b4
9 changed files with 202 additions and 545 deletions

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"rust-analyzer.linkedProjects": [
"./Cargo.toml",
"./Cargo.toml"
]
}

View File

@ -6,7 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
rocket = { version = "0.5.0-rc.2", features = ["json"] } chrono = { version = "0.4.24", features = ["serde"] }
mysql = "22.2.0"
mysql_common = { version = "0.28.2", features = ["chrono"] }
dotenv = "0.15.0" dotenv = "0.15.0"
rocket = { version = "0.5.0-rc.2", features = ["json"] }
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"

View File

@ -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.
*/

View File

@ -1,256 +1,148 @@
use mysql::prelude::*; use std::{env, error::Error};
use mysql::*;
use mysql_common::frunk::hlist_pat;
use std::error::Error;
use std::result::Result;
use std::{env, result};
pub mod map; use dotenv::dotenv;
pub mod race; use tokio_postgres::{Client, NoTls};
pub mod teamrace;
pub fn create_pool() -> Pool { use self::models::map::Map;
let url = match env::var("DB_URI") {
Ok(uri) => uri, mod models;
Err(err) => {
println!("You must provide an env var: 'DB_URI'!"); pub struct DatabaseHandler {
panic!("{}", err); pub client: Client,
}
impl DatabaseHandler {
pub async fn create() -> Result<DatabaseHandler, Box<dyn Error>> {
// Load the env file
dotenv()?;
// 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 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<Vec<Map>, Box<dyn Error>> {
// 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()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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),
}; };
let pool = match Pool::new(url.as_str()) { let msg = "Hello World!";
Ok(pool) => pool, let rows = match db.client.query("SELECT $1::TEXT", &[&msg]).await {
Err(err) => { Ok(rows) => rows,
println!("Couldn't connect to the database!"); Err(err) => panic!("Could not create query!\n{}", err),
panic!("{}", err);
}
}; };
pool let value: &str = rows[0].get(0);
} assert_eq!(value, msg)
pub struct DatabasePoolStore(pub Pool);
pub fn get_maps(pool: &Pool) -> Result<Vec<map::Map>, Box<dyn Error>> {
let mut conn = pool.get_conn()?;
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 }
} }
tokio_test::block_on(test())
} }
)?;
Ok(maps) #[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_map_by_name(pool: &Pool, name: &str) -> Result<Option<map::Map>, Box<dyn Error>> { match db.get_all_maps().await {
let mut conn = pool.get_conn()?; Ok(maps) => println!("Found maps: {:?}", maps.len()),
let stmt = conn.prep("SELECT * FROM record_maps AS maps JOIN record_mapinfo AS mapinfo ON maps.Map = mapinfo.Map WHERE maps.Map = ?")?; Err(err) => panic!("Could not get all maps!\n{}", err),
};
let map: Option<map::MapRow> = 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 { tokio_test::block_on(test())
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<Vec<race::Race>, Box<dyn Error>> {
let mut conn = pool.get_conn()?;
let stmt = conn.prep("SELECT * FROM record_race WHERE Name = ?")?;
let races = conn.exec_map(&stmt, (player,), races_result)?;
Ok(races)
}
pub fn get_races_by_id(pool: &Pool, id: &str) -> Result<Vec<race::Race>, Box<dyn Error>> {
let mut conn = pool.get_conn()?;
let stmt = conn.prep("SELECT * FROM record_race WHERE GameID = ?")?;
let races = conn.exec_map(&stmt, (id,), races_result)?;
Ok(races)
}
pub fn get_races_by_map(pool: &Pool, map: &str) -> Result<Vec<race::Race>, Box<dyn Error>> {
let mut conn = pool.get_conn()?;
let stmt = conn.prep("SELECT * FROM record_race WHERE Map = ?")?;
let races = conn.exec_map(&stmt, (map,), races_result)?;
Ok(races)
}
pub fn get_player_total_playtime(
pool: &Pool,
player: &str,
) -> Result<(Option<f64>, Option<i32>), Box<dyn Error>> {
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::<f64>, None::<i32>)),
} }
} }

View File

@ -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<NaiveDateTime>,
width: u16,
height: u16,
tiles: Vec<String>,
}
impl Map {
pub fn from_db_row(db_row: &Row) -> Result<Self, Box<dyn Error>> {
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<NaiveDateTime> = db_row.try_get(5)?;
Ok(Map {
map,
mapper,
category,
points,
stars,
release,
width: 1,
height: 1,
tiles: vec![],
})
}
}

View File

@ -0,0 +1 @@
pub mod map;

View File

@ -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<String>,
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<String>,
pub ddnet7: bool,
}

View File

@ -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,
}

View File

@ -1,103 +1,28 @@
use database::DatabaseHandler;
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
use database::create_pool;
use rocket::serde::json::Json;
use rocket::State;
extern crate dotenv; extern crate dotenv;
use dotenv::dotenv;
mod database; mod database;
use database::{map::Map, race::Race, DatabasePoolStore};
#[get("/maps")] #[rocket::main]
fn get_all_maps(db_pool: &State<DatabasePoolStore>) -> Option<Json<Vec<Map>>> { async fn main() {
match database::get_maps(&db_pool.0) { let client = match DatabaseHandler::create().await {
Ok(maps) => Some(Json(maps)), Ok(client) => client,
Err(err) => { Err(err) => panic!(
println!("{err}"); "Encountered an error while connecting to the database!\n{}",
None err
} ),
};
match rocket::build()
.manage(client)
.mount("/", routes![])
.launch()
.await
{
Ok(_) => (),
Err(err) => println!("Encountered an error while starting rocket!\n{}", err),
} }
} }
#[get("/maps/<map>")]
fn get_map_by_name(db_pool: &State<DatabasePoolStore>, map: &str) -> Option<Json<Map>> {
match database::get_map_by_name(&db_pool.0, map) {
Ok(map) => map.map(Json),
Err(err) => {
println!("{err}");
None
}
}
}
#[get("/races/by-player/<player>")]
fn get_races_by_player(
db_pool: &State<DatabasePoolStore>,
player: &str,
) -> Option<Json<Vec<Race>>> {
match database::get_races_by_player(&db_pool.0, player) {
Ok(races) => Some(Json(races)),
Err(err) => {
println!("{err}");
None
}
}
}
#[get("/races/by-id/<id>")]
fn get_races_by_id(db_pool: &State<DatabasePoolStore>, id: &str) -> Option<Json<Vec<Race>>> {
match database::get_races_by_id(&db_pool.0, id) {
Ok(races) => Some(Json(races)),
Err(err) => {
println!("{err}");
None
}
}
}
#[get("/races/by-map/<map>")]
fn get_races_by_map(db_pool: &State<DatabasePoolStore>, map: &str) -> Option<Json<Vec<Race>>> {
match database::get_races_by_map(&db_pool.0, map) {
Ok(races) => Some(Json(races)),
Err(err) => {
println!("{err}");
None
}
}
}
#[get("/player/<player>/playtime")]
fn get_player_playtime(
db_pool: &State<DatabasePoolStore>,
player: &str,
) -> Option<Json<(Option<f64>, Option<i32>)>> {
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
],
)
}