diff --git a/src/router/clear_rate.rs b/src/router/clear_rate.rs index 5483e41..82b41ef 100644 --- a/src/router/clear_rate.rs +++ b/src/router/clear_rate.rs @@ -230,8 +230,29 @@ async fn get_clearrate_json() -> JsonValue { rv } +fn strip_custom_clear_rates(mut data: JsonValue) -> JsonValue { + let ids = data["master_music_ids"].clone(); + let rates = data["all_user_clear_rate"].clone(); + let mut kept_ids = array![]; + let mut kept_rates = array![]; + for (i, id) in ids.members().enumerate() { + if id.as_i64().unwrap_or(0) >= crate::database::custom_song::FIRST_MUSIC_ID { + continue; + } + kept_ids.push(id.clone()).unwrap(); + kept_rates.push(rates[i].clone()).unwrap(); + } + data["master_music_ids"] = kept_ids; + data["all_user_clear_rate"] = kept_rates; + data +} + pub async fn clearrate(req: HttpRequest) -> impl Responder { - global::api(&req, Some(get_clearrate_json().await)) + let mut data = get_clearrate_json().await; + if !crate::router::custom_song::client_supports_custom_songs(&req) { + data = strip_custom_clear_rates(data); + } + 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 b7345f2..4017301 100644 --- a/src/router/custom_song.rs +++ b/src/router/custom_song.rs @@ -80,6 +80,19 @@ pub fn disabled() -> bool { args.hidden || !args.enable_custom_songs } +// A client that understands custom songs advertises it with this exact header +// on its API requests. Old / official clients don't send it, so we must NOT +// inject custom-song data (custom master_music_ids) into the shared /api/user +// response for them - the unresolvable ids would break the account +const SUPPORT_HEADER: &str = "X-Custom-Songs"; + +pub fn client_supports_custom_songs(req: &HttpRequest) -> bool { + req.headers() + .get(SUPPORT_HEADER) + .and_then(|v| v.to_str().ok()) + == Some("1") +} + // The catalog is filtered per requesting user: private songs only show for // their owner, shared songs for the owner plus their shared-user list async fn list(req: HttpRequest, body: String) -> impl Responder { @@ -1247,4 +1260,43 @@ mod tests { }); jzon::parse(&String::from_utf8_lossy(&body)).unwrap() } + + // With the feature enabled, custom unlock ids are appended to /api/user ONLY + // for clients that send X-Custom-Songs: 1. An old/official client (no + // header, or a different value) gets its official unlock list untouched. + #[test] + fn unlock_ids_gated_on_support_header() { + use actix_web::test::TestRequest; + let _lock = crate::runtime::lock_test_data_path(); + + let owner = 4242; + let music_id = database::next_music_id(); + database::insert_song(music_id, owner, &object!{music_id: music_id}, "public", &array![], false); + // Sanity: the feature is on, so the id is visible to get_music_ids + assert!(get_music_ids(owner).contains(music_id)); + + // Exactly what the /api/user handler appends to master_music_ids + let appended = |req: &HttpRequest| -> JsonValue { + if client_supports_custom_songs(req) { + get_music_ids(owner) + } else { + array![] + } + }; + + // No header -> unsupported -> nothing appended + let without = TestRequest::default().to_http_request(); + assert!(!client_supports_custom_songs(&without)); + assert!(appended(&without).is_empty()); + + // Correct header -> supported -> custom id appended + let with = TestRequest::default().insert_header(("X-Custom-Songs", "1")).to_http_request(); + assert!(client_supports_custom_songs(&with)); + assert!(appended(&with).contains(music_id)); + + // Any other value is treated as an unsupporting client + let wrong = TestRequest::default().insert_header(("X-Custom-Songs", "true")).to_http_request(); + assert!(!client_supports_custom_songs(&wrong)); + assert!(appended(&wrong).is_empty()); + } } diff --git a/src/router/user.rs b/src/router/user.rs index 271ecf6..9047ea9 100644 --- a/src/router/user.rs +++ b/src/router/user.rs @@ -65,9 +65,10 @@ async fn user(req: HttpRequest) -> impl Responder { user["lottery_list"] = array![]; - // Custom songs visible to this user are unlocked - for id in crate::router::custom_song::get_music_ids(user["user"]["id"].as_i64().unwrap()).members() { - user["master_music_ids"].push(id.as_i64().unwrap()).unwrap(); + if crate::router::custom_song::client_supports_custom_songs(&req) { + for id in crate::router::custom_song::get_music_ids(user["user"]["id"].as_i64().unwrap()).members() { + user["master_music_ids"].push(id.as_i64().unwrap()).unwrap(); + } } global::api(&req, Some(user))