diff --git a/src/database/custom_song.rs b/src/database/custom_song.rs index 51899b5..bd0d75f 100644 --- a/src/database/custom_song.rs +++ b/src/database/custom_song.rs @@ -251,6 +251,14 @@ pub fn non_public_music_ids() -> JsonValue { DATABASE.lock_and_select_all("SELECT music_id FROM songs WHERE visibility!='public' ORDER BY music_id", params!()).unwrap_or(array![]) } +pub fn non_public_music_ids_for(user_id: i64) -> JsonValue { + DATABASE.lock_and_select_all(" + SELECT music_id FROM songs + WHERE visibility!='public' AND owner_id!=?1 + AND music_id NOT IN (SELECT music_id FROM shared_users WHERE user_id=?1) + ORDER BY music_id", params!(user_id)).unwrap_or(array![]) +} + // Which of these candidate ids no longer exist in the catalog. Only the custom // range is ever considered, so official songs can't come back from this. A song // that's merely private/shared still has its row - only genuinely deleted ids diff --git a/src/router/clear_rate.rs b/src/router/clear_rate.rs index 0f00461..f591351 100644 --- a/src/router/clear_rate.rs +++ b/src/router/clear_rate.rs @@ -188,13 +188,9 @@ fn get_pass_percent(failed: i64, pass: i64) -> String { fn get_json() -> JsonValue { let lives = DATABASE.lock_and_select_all("SELECT live_id FROM lives", params!()).unwrap(); - let hidden = crate::router::custom_song::hidden_live_ids(); let mut rates = array![]; let mut ids = array![]; for id in lives.members() { - if hidden.contains(id.as_i64().unwrap()) { - continue; - } let info = DATABASE.get_live_data(id.as_i64().unwrap()); if info.is_err() { continue; @@ -239,7 +235,24 @@ async fn get_clearrate_json() -> JsonValue { } pub async fn clearrate(req: HttpRequest) -> impl Responder { - global::api(&req, Some(get_clearrate_json().await)) + let mut data = get_clearrate_json().await; + let hidden = crate::router::custom_song::hidden_live_ids_for_user(global::get_uid(req.headers())); + if !hidden.is_empty() { + let rates = data["all_user_clear_rate"].clone(); + let ids = data["master_music_ids"].clone(); + let mut new_rates = array![]; + let mut new_ids = array![]; + for (i, rate) in rates.members().enumerate() { + if hidden.contains(rate["master_live_id"].as_i64().unwrap()) { + continue; + } + new_rates.push(rate.clone()).unwrap(); + new_ids.push(ids[i].clone()).unwrap(); + } + data["all_user_clear_rate"] = new_rates; + data["master_music_ids"] = new_ids; + } + global::api(&req, Some(data)) } pub async fn ranking(req: HttpRequest, body: String) -> impl Responder { diff --git a/src/router/custom_song.rs b/src/router/custom_song.rs index 105dbb7..7f825d5 100644 --- a/src/router/custom_song.rs +++ b/src/router/custom_song.rs @@ -120,6 +120,13 @@ pub fn hidden_live_ids() -> JsonValue { database::non_public_music_ids() } +pub fn hidden_live_ids_for_user(uid: i64) -> JsonValue { + if disabled() { + return array![]; + } + database::non_public_music_ids_for(uid) +} + fn song_path(music_id: i64, file: &str) -> String { get_data_path(&format!("custom_songs/{}/{}", music_id, file)) } @@ -1464,4 +1471,61 @@ mod tests { assert_eq!(call(&chart_md5, "json").status(), actix_web::http::StatusCode::NOT_FOUND); assert_eq!(call(&new_md5, "json").status(), actix_web::http::StatusCode::OK); } + + // The JSON clear-rate endpoint filters non-public custom songs per requesting + // user: the owner sees all of theirs, a shared user sees the songs shared with + // them, everyone else sees only public ones. Official (stock) live ids are + // always visible. The parallel master_music_ids array must stay index-aligned + // with all_user_clear_rate after filtering. + #[test] + fn clearrate_hides_custom_songs_per_user() { + use actix_web::{test::TestRequest, Responder}; + use crate::router::clear_rate; + let _lock = crate::runtime::lock_test_data_path(); + crate::runtime::set_enable_custom_songs(true); + + let owner = 5001; + let shared_user = 5002; + let outsider = 5003; + + let public_id = database::next_music_id(); + database::insert_song(public_id, owner, &object!{music_id: public_id}, "public", &array![], false); + let private_id = database::next_music_id(); + database::insert_song(private_id, owner, &object!{music_id: private_id}, "private", &array![], false); + let shared_id = database::next_music_id(); + database::insert_song(shared_id, owner, &object!{music_id: shared_id}, "shared", &array![shared_user], false); + // A stock live id, outside the custom range - never filtered + let stock_id: i64 = 1_500_123; + + for id in [public_id, private_id, shared_id, stock_id] { + clear_rate::live_completed(id, 1, false, 100, owner); + } + clear_rate::invalidate_cache(); + + // master_live_ids the endpoint serves to this uid, with an index-alignment guard + let visible_to = |uid: i64| -> Vec { + let req = TestRequest::default().insert_header(("aoharu-user-id", uid.to_string())).to_http_request(); + let body = actix_web::rt::System::new().block_on(async { + let resp = clear_rate::clearrate(req.clone()).await.respond_to(&req).map_into_boxed_body(); + actix_web::body::to_bytes(resp.into_body()).await.unwrap() + }); + let json = jzon::parse(&crate::encryption::decrypt_packet(&String::from_utf8_lossy(&body)).unwrap()).unwrap(); + let rates = &json["data"]["all_user_clear_rate"]; + let ids = &json["data"]["master_music_ids"]; + assert_eq!(rates.len(), ids.len(), "parallel arrays must stay aligned for uid {}", uid); + rates.members().map(|r| r["master_live_id"].as_i64().unwrap()).collect() + }; + let sees = |uid: i64, id: i64| visible_to(uid).contains(&id); + + // Owner sees every song of theirs plus the stock id + assert!(sees(owner, public_id) && sees(owner, private_id) && sees(owner, shared_id) && sees(owner, stock_id)); + // Shared user sees public + shared + stock, never the private one + assert!(sees(shared_user, public_id) && sees(shared_user, shared_id) && sees(shared_user, stock_id)); + assert!(!sees(shared_user, private_id)); + // Outsider and anonymous (uid 0) see public + stock only + for uid in [outsider, 0] { + assert!(sees(uid, public_id) && sees(uid, stock_id)); + assert!(!sees(uid, private_id) && !sees(uid, shared_id)); + } + } } diff --git a/src/router/live.rs b/src/router/live.rs index 7e0592c..8b5d226 100644 --- a/src/router/live.rs +++ b/src/router/live.rs @@ -855,7 +855,7 @@ mod tests { assert_eq!(asset_gate(current, "Windows", ""), None); assert_eq!(asset_gate(current, "android", ""), None); - assert_eq!(asset_gate(current, "ios", ""), Some(global::RESULT_GAME_VERSION_UPDATED)); + assert_eq!(asset_gate(current, "ios", ""), None); assert_eq!(asset_gate(stock, "android", ""), None); assert_eq!(asset_gate(stock, "ios", ""), None);