Render clearrates for private songs for that user

This commit is contained in:
Ethan O'Brien
2026-07-22 00:41:05 -05:00
parent b6cbfd0713
commit 2cd11c8467
4 changed files with 91 additions and 6 deletions

View File

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

View File

@@ -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 {

View File

@@ -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<i64> {
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));
}
}
}

View File

@@ -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);