diff --git a/readme.md b/readme.md index 1fc274d..f474275 100644 --- a/readme.md +++ b/readme.md @@ -34,7 +34,7 @@ _these features are implemented_ ## todo -- swap to chrono instead of `(TO_CHAR(CURRENT_TIMESTAMP, 'YYYY-MM-DD HH24:MI:SS.MS'))` (thats REALLY ugly!!) +- swap to chrono instead of `(TO_CHAR(CURRENT_TIMESTAMP, 'YYYY-MM-DD HH24:MI:SS.MS'))` (thats REALLY ugly!!) this would also make the `28` and `29` parameters work on downloadlevel - 2.2 friends only unlisted - add more old endpoints + better support for older versions - add dailies, events, weekly @@ -43,4 +43,7 @@ _these features are implemented_ - sqlite would make sense for this - unscuff difficulties - moderation utilities -- probably make more things bools in the database \ No newline at end of file +- probably make more things bools in the database +- ip actions +- fix downlopading levels +- better song support \ No newline at end of file diff --git a/src/endpoints/levels.rs b/src/endpoints/levels.rs index 81e45af..d3c618e 100644 --- a/src/endpoints/levels.rs +++ b/src/endpoints/levels.rs @@ -1,2 +1,3 @@ +pub mod download_level; pub mod get_levels; pub mod upload_level; \ No newline at end of file diff --git a/src/endpoints/levels/download_level.rs b/src/endpoints/levels/download_level.rs new file mode 100644 index 0000000..ebcc15b --- /dev/null +++ b/src/endpoints/levels/download_level.rs @@ -0,0 +1,206 @@ +use rocket::form::Form; +use rocket::http::Status; +use rocket::response::status; + +use diesel::prelude::*; + +use base64::{Engine as _, engine::general_purpose}; + +use flate2::read::GzDecoder; + +use std::fs; + +use std::io::prelude::*; + +use crate::helpers; +use crate::db; + +#[derive(FromForm)] +pub struct FormDownloadLevel { + levelID: i32, + gameVersion: Option, + extras: Option +} + +#[post("/downloadGJLevel22.php", data = "")] +pub fn download_level(input: Form) -> status::Custom<&'static str> { + let connection = &mut db::establish_connection_pg(); + + use crate::schema::{levels, users}; + + use crate::models::{Level, User}; + + let mut response: Vec = Vec::new(); + + let query = levels::table.into_boxed(); + + match input.levelID { + -1 => { + unimplemented!("no daily support") + }, + -2 => { + unimplemented!("no weeky support") + }, + -3 => { + unimplemented!("what is an event level.") + }, + _ => { + // do nothing special + } + } + + // database query + { + let result = query + .filter(levels::id.eq(input.levelID)) + .get_result::(connection) + .expect("fatal error loading levels"); + + let user: User = users::table.find(result.user_id).get_result::(connection).expect("couldnt get user from lvl"); + let level: Level = result; + + let set_difficulty = match level.difficulty { + Some(diff) => { + Some(helpers::difficulty::LevelDifficulty::new(diff)) + }, + None => None + }; + let community_difficulty = match level.community_difficulty { + Some(diff) => { + Some(helpers::difficulty::LevelDifficulty::new(diff)) + }, + None => None + }; + let difficulty = match set_difficulty { + Some(diff) => { + Some(diff) + }, + None => { + match community_difficulty { + Some(diff) => { + Some(diff) + }, + None => None + } + } + }; + let demon_difficulty = match level.demon_difficulty { + Some(diff) => { + Some(helpers::difficulty::DemonDifficulty::new(diff)) + }, + None => None + }; + + let xor_pass: String; + if input.gameVersion.unwrap_or(19) >= 20 { + xor_pass = helpers::encryption::cyclic_xor_string(&level.password.clone().unwrap_or(String::from("0")), "26364") + } else { + xor_pass = level.password.clone().unwrap_or(String::from("0")); + } + + let compressed_level_data = fs::read(format!("{}/{}/{}.lvl", crate::CONFIG.db.data_folder, "levels", level.id)).expect("couldnt read level file"); + + let mut level_data_decoder = GzDecoder::new(compressed_level_data.as_slice()); + + let mut uncompressed_level_data = String::new(); + level_data_decoder.read_to_string(&mut uncompressed_level_data).expect("err unzipping level"); + + let level_data = uncompressed_level_data.as_bytes(); + + response.push(helpers::format::format(hashmap! { + 1 => level.id.to_string(), + 2 => level.name, + 3 => if input.gameVersion.unwrap_or(19) >= 20 { + general_purpose::URL_SAFE.encode(level.description) + } else { + level.description + }, + 4 => String::from_utf8(level_data.to_vec()).expect("invalid utf-8 sequence"), + 5 => level.version.to_string(), + 6 => user.id.to_string(), + // this argument is weird. its the "difficulty divisor" + // used to be vote count but yeah + 8 => 10.to_string(), + 9 => (match difficulty { + Some(diff) => diff.to_star_difficulty(), + None => 0 + } * 10).to_string(), + 10 => level.downloads.to_string(), + 12 => (if level.song_id < 50 { level.song_id } else { 0 }).to_string(), + 13 => level.game_version.to_string(), + 14 => level.likes.to_string(), + 16 => (-level.likes).to_string(), + 15 => level.length.to_string(), + 17 => match difficulty { + Some(diff) => { + if diff == helpers::difficulty::LevelDifficulty::Demon { + 1 + } else { + 0 + } + }, + None => 0 + }.to_string(), + 18 => (if let Some(stars) = level.stars { stars } else { 0 }).to_string(), + 19 => level.featured.to_string(), + 25 => match difficulty { + Some(diff) => { + if diff == helpers::difficulty::LevelDifficulty::Auto { + 1 + } else { + 0 + } + }, + None => 0 + }.to_string(), + 27 => xor_pass, + 28 => "1".to_string(), // unimplemented + 29 => "1".to_string(), // unimplemented + 30 => (if let Some(original) = level.original { original } else { 0 }).to_string(), + 31 => level.two_player.to_string(), + 35 => (if level.song_id >= 50 { level.song_id } else { 0 }).to_string(), + 36 => String::from_utf8(if input.extras.is_some() { level.extra_data } else { Vec::new() }).expect("invalid utf-8 sequence"), + 37 => level.coins.to_string(), + 38 => level.rated_coins.to_string(), + 39 => (if let Some(requested_stars) = level.requested_stars { requested_stars } else { 0 }).to_string(), + 40 => level.has_ldm.to_string(), + 41 => "".to_string(), // unimplemented + 42 => level.epic.to_string(), + 43 => match demon_difficulty { + Some(diff) => { + diff + }, + None => helpers::difficulty::DemonDifficulty::Hard + }.to_demon_difficulty().to_string(), + 44 => "0".to_string(), // unimplemented + 45 => level.objects.to_string(), + 46 => level.editor_time.to_string(), + 47 => level.editor_time_copies.to_string() + })); + response.push(helpers::encryption::gen_solo(String::from_utf8(level_data.to_vec()).expect("invalid utf-8 sequence"))); + + let thing = [ + user.id.to_string(), + level.stars.unwrap_or(0).to_string(), + match difficulty { + Some(diff) => { + if diff == helpers::difficulty::LevelDifficulty::Demon { + 1 + } else { + 0 + } + }, + None => 0 + }.to_string(), + level.id.to_string(), + level.rated_coins.to_string(), + level.featured.to_string(), + level.password.unwrap_or(String::new()).to_string(), + 0.to_string() + ]; + response.push(helpers::encryption::gen_solo_2(thing.join(","))); + } + + println!("{:?}", response); + return status::Custom(Status::Ok, Box::leak(response.join("#").into_boxed_str())) +} \ No newline at end of file diff --git a/src/helpers/encryption.rs b/src/helpers/encryption.rs index 303c1de..c161990 100644 --- a/src/helpers/encryption.rs +++ b/src/helpers/encryption.rs @@ -46,5 +46,35 @@ pub fn gen_multi(level_hash_data: Vec<(i32, i32, bool)>) -> String { bytes.extend(input_str.as_bytes().to_vec()); bytes.extend("xI25fpAapCQg".as_bytes().to_vec()); - return Sha1::default().digest(bytes.as_mut_slice()).to_hex(); + return Sha1::default().digest(bytes.as_slice()).to_hex(); +} + +pub fn gen_solo(level_string: String) -> String { + let mut hash = String::new(); + let divided = level_string.len() as i32 / 40; + let mut i = 0; + let mut k = 0; + while k < level_string.len() { + if i > 39 { + break + } + + hash += level_string.chars().nth(k).expect("invalid char access").to_string().as_str(); + i += 1; + k += divided as usize; + } + + let mut bytes: Vec = Vec::new(); + bytes.extend(format!("{:a<5}", hash).as_bytes().to_vec()); + bytes.extend("xI25fpAapCQg".as_bytes().to_vec()); + + return Sha1::default().digest(bytes.as_slice()).to_hex(); +} + +pub fn gen_solo_2(level_mutli_string: String) -> String { + let mut bytes: Vec = Vec::new(); + bytes.extend(level_mutli_string.as_bytes().to_vec()); + bytes.extend("xI25fpAapCQg".as_bytes().to_vec()); + + return Sha1::default().digest(bytes.as_slice()).to_hex(); } \ No newline at end of file diff --git a/src/helpers/levels.rs b/src/helpers/levels.rs index eb5da08..a71833d 100644 --- a/src/helpers/levels.rs +++ b/src/helpers/levels.rs @@ -21,7 +21,6 @@ macro_rules! object_prop_bool { }; } - #[derive(Clone)] pub struct ObjectData { raw: HashMap diff --git a/src/main.rs b/src/main.rs index ab78110..6ca4c01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,6 +58,7 @@ fn rocket() -> _ { endpoints::users::get_users::get_users, + endpoints::levels::download_level::download_level, endpoints::levels::get_levels::get_levels, endpoints::levels::upload_level::upload_level ])