diff --git a/.gitignore b/.gitignore index 68b3e7e..cfb4bb0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /target +/data + .env config.toml \ No newline at end of file diff --git a/config.example.toml b/config.example.toml index 20e2599..76fb6e2 100644 --- a/config.example.toml +++ b/config.example.toml @@ -16,4 +16,8 @@ port = 8000 [accounts] # allow new accounts to be created -allow_registration = true \ No newline at end of file +allow_registration = true + +[db] +# path for your data to be stored +data_folder = "data" \ No newline at end of file diff --git a/readme.md b/readme.md index 9d7eb6f..115f664 100644 --- a/readme.md +++ b/readme.md @@ -30,11 +30,14 @@ _these features are implemented_ ### building -- run `cargo build` +- run `cargo build --release` ## todo - probably work on the code warnings we get hehe - green name users... (add udid auth to auth function, use userName instead of accountID in uploading levels, and it goes on and on and on and on...) +- patch uploadlevel to fix color tags, use 1.9 [levelInfo](https://github.com/Cvolton/GMDprivateServer/blob/master/incl/levels/uploadGJLevel.php#L56)???, and use 2.2 unlisted?? +- clean up uploadlevel +- add updating levels - add level parsing - maybe swap to timestamp type instead of `(TO_CHAR(CURRENT_TIMESTAMP, 'YYYY-MM-DD HH24:MI:SS.MS'))` (thats REALLY ugly!!) \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index ee9a2aa..c6b37ae 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,7 +5,8 @@ use std::sync::LazyLock; #[derive(Deserialize)] pub struct Config { pub general: ConfigGeneral, - pub accounts: ConfigAccounts + pub accounts: ConfigAccounts, + pub db: ConfigDB } #[derive(Deserialize)] @@ -19,6 +20,11 @@ pub struct ConfigAccounts { pub allow_registration: bool } +#[derive(Deserialize)] +pub struct ConfigDB { + pub data_folder: String +} + impl Config { pub fn load_from_file(file_path: &str) -> Self { let toml_str = fs::read_to_string(file_path).expect("Error finding toml config:"); diff --git a/src/db/models.rs b/src/db/models.rs index 0dd7f17..b78af61 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -121,4 +121,29 @@ pub struct Level { pub featured: i32, pub epic: i32, pub rated_coins: i32 +} + +#[derive(Insertable, Deserialize)] +#[diesel(table_name = levels)] +pub struct NewLevel { + pub name: String, + pub user_id: i32, + pub description: String, + pub original: i32, + pub game_version: i32, + pub binary_version: i32, + pub password: Option, + pub requested_stars: i32, + pub unlisted: i32, + pub version: i32, + pub extra_data: Vec, + pub level_info: Vec, + pub editor_time: i32, + pub editor_time_copies: i32, + pub song_id: i32, + pub length: i32, + pub objects: i32, + pub coins: i32, + pub has_ldm: i32, + pub two_player: i32 } \ No newline at end of file diff --git a/src/endpoints/levels/upload_level.rs b/src/endpoints/levels/upload_level.rs index 2444bca..fb930cc 100644 --- a/src/endpoints/levels/upload_level.rs +++ b/src/endpoints/levels/upload_level.rs @@ -5,6 +5,10 @@ use rocket::response::status; use diesel::prelude::*; +use base64::{Engine as _, engine::general_purpose}; + +use std::fs; + use crate::helpers; use crate::db; @@ -12,9 +16,27 @@ use crate::db; pub struct FormUploadLevel { accountID: i32, - password: Option, gjp: Option, gjp2: Option, + + password: Option, + songID: i32, + audioTrack: i32, + levelName: String, + levelDesc: String, + levelID: i32, + levelVersion: i32, + levelInfo: String, + levelString: String, + gameVersion: i32, + extraString: Option, + requestedStars: Option, + binaryVersion: Option, + unlisted: Option, + original: Option, + wt: Option, + wt2: Option, + ldm: Option } #[post("/uploadGJLevel21.php", data = "")] @@ -22,15 +44,99 @@ pub fn upload_level(input: Form) -> status::Custom<&'static str let connection = &mut db::establish_connection_pg(); // account verification - let (user_id_val, account_id_val): (i32, i32); + let (user_id_val, _account_id_val): (i32, i32); - match helpers::accounts::auth(input.accountID.clone(), input.password.clone(), input.gjp.clone(), input.gjp2.clone()) { + // password argument is used for the level, so + match helpers::accounts::auth(input.accountID.clone(), None, input.gjp.clone(), input.gjp2.clone()) { Ok((user_id, account_id)) => { user_id_val = user_id; - account_id_val = account_id; + _account_id_val = account_id; }, Err(_) => return status::Custom(Status::Ok, "-1") }; - return status::Custom(Status::Ok, "1") + let description_val; + if input.gameVersion >= 20 { + description_val = String::from_utf8(general_purpose::URL_SAFE.decode(input.levelDesc.clone()).expect("couldn't decode base64")).expect("invalid UTF-8 sequence (how)") + } else { + description_val = input.levelDesc.clone() + } + + let song_id_val = if input.songID == 0 { + input.audioTrack + } else { + input.songID + }; + + let extra_string; + match input.extraString.clone() { + Some(extra_string_val) => { extra_string = extra_string_val }, + None => { extra_string = helpers::levels::DEFAULT_EXTRA_STRING.to_owned() } + } + + // db shit + use crate::models::{Level, NewLevel}; + + { + use crate::schema::levels::dsl::*; + + if levels + .filter(id.eq(input.levelID)) + .count() + .get_result::(connection) + .expect("couldnt get count of levels") > 0 { + // update level + let level_user_id = levels + .filter(id.eq(input.levelID)) + .select(user_id) + .get_result::(connection) + .expect("couldnt query levels"); + + if level_user_id != user_id_val { + return status::Custom(Status::Ok, "-1") + } + + // TODO: add level to db, file + + return status::Custom(Status::Ok, Box::leak(input.levelID.to_string().into_boxed_str())) + } else { + // upload level + let new_level = NewLevel { + // TODO: clean and filter this to 20 charfacters + name: input.levelName.clone(), + user_id: user_id_val, + // TODO: filter this to 140 characters + description: description_val, + original: input.original.unwrap_or(0), + game_version: input.gameVersion, + binary_version: input.binaryVersion.unwrap_or(0), + password: input.password.clone(), + requested_stars: match input.requestedStars { + Some(requested_stars_val) => requested_stars_val.clamp(0, 10), + None => 0 + }, + unlisted: input.unlisted.unwrap_or(0), + version: input.levelVersion, + extra_data: extra_string.as_bytes().to_owned(), + level_info: input.levelInfo.clone().into_bytes(), + editor_time: input.wt.unwrap_or(0), + editor_time_copies: input.wt2.unwrap_or(0), + song_id: song_id_val, + length: 0, // not implemeneted + objects: 0, // not implemeneted + coins: 0, // not implemeneted + has_ldm: input.ldm.unwrap_or(0), + two_player: 0 // not implemented + }; + + let inserted_level = diesel::insert_into(levels) + .values(&new_level) + .get_result::(connection) + .expect("failed to insert level"); + + fs::write(format!("{}/levels/{}.lvl", crate::CONFIG.db.data_folder, inserted_level.id), general_purpose::URL_SAFE.decode(input.levelString.clone()).expect("user provided invalid level string")).expect("couldnt write level to file"); + + return status::Custom(Status::Ok, "1") + } + } } \ No newline at end of file diff --git a/src/helpers.rs b/src/helpers.rs index 49728fc..dcc409e 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,4 +1,5 @@ pub mod accounts; pub mod clean; pub mod encryption; -pub mod format; \ No newline at end of file +pub mod format; +pub mod levels; \ No newline at end of file diff --git a/src/helpers/levels.rs b/src/helpers/levels.rs new file mode 100644 index 0000000..d85a395 --- /dev/null +++ b/src/helpers/levels.rs @@ -0,0 +1,7 @@ +use std::sync::LazyLock; + +pub static DEFAULT_EXTRA_STRING: LazyLock = LazyLock::new(|| { + let string = String::from("29_29_29_40_29_29_29_29_29_29_29_29_29_29_29_29"); + + return string; +}); \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 6c30eb8..d9dfe45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,8 @@ #[macro_use] extern crate maplit; #[macro_use] extern crate rocket; +use std::fs; + mod db; use db::*; @@ -23,6 +25,8 @@ fn index() -> String { #[launch] fn rocket() -> _ { + fs::create_dir_all(&CONFIG.db.data_folder).expect("failed to create data directory!"); + fs::create_dir_all(format!("{}/levels", &CONFIG.db.data_folder)).expect("failed to create data directory for levels"); rocket::build() .configure(rocket::Config::figment().merge(("port", CONFIG.general.port))) .mount("/", routes![