mirror of
https://git.ethanthesleepy.one/ethanaobrien/ew
synced 2026-10-11 00:54:32 +08:00
Add macos route and some other tests I guess
This commit is contained in:
@@ -350,7 +350,10 @@ pub fn get_browse_cards() -> JsonValue {
|
||||
// considered, and ids are never reused, so official (or imported) cards can't
|
||||
// come back from this and a wipe is final. A card that's merely unpublished
|
||||
// still has its row - only genuinely deleted ids are returned
|
||||
pub fn dead_card_ids(candidates: &JsonValue) -> JsonValue {
|
||||
// None = the catalog could not be read (or is entirely empty while players still hold
|
||||
// custom ids): the caller must prune NOTHING then. An unreadable catalog looks exactly
|
||||
// like one where every id is dead, and get_acc saves the pruned userdata.
|
||||
pub fn dead_card_ids(candidates: &JsonValue) -> Option<JsonValue> {
|
||||
let mut ids: Vec<i64> = Vec::new();
|
||||
for id in candidates.members() {
|
||||
let Some(id) = id.as_i64() else { continue; };
|
||||
@@ -359,17 +362,23 @@ pub fn dead_card_ids(candidates: &JsonValue) -> JsonValue {
|
||||
}
|
||||
}
|
||||
if ids.is_empty() {
|
||||
return array![];
|
||||
return Some(array![]);
|
||||
}
|
||||
let list = ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(",");
|
||||
let alive = DATABASE.lock_and_select_all(&format!("SELECT master_card_id FROM cards WHERE master_card_id IN ({})", list), params!()).unwrap_or(array![]);
|
||||
let alive = DATABASE.lock_and_select_all(&format!("SELECT master_card_id FROM cards WHERE master_card_id IN ({})", list), params!()).ok()?;
|
||||
if alive.is_empty() {
|
||||
let total: i64 = DATABASE.lock_and_select_type("SELECT COUNT(*) FROM cards", params!()).ok()?;
|
||||
if total == 0 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let mut rv = array![];
|
||||
for id in ids {
|
||||
if !alive.contains(id) {
|
||||
rv.push(id).unwrap();
|
||||
}
|
||||
}
|
||||
rv
|
||||
Some(rv)
|
||||
}
|
||||
|
||||
// The published + obtainable pool the custom gacha banner draws from, per
|
||||
@@ -596,10 +605,33 @@ mod tests {
|
||||
insert_card(dead, 1001, 3008, &card_blob(dead, 1), true, false).unwrap();
|
||||
delete_card(dead);
|
||||
|
||||
let dead_ids = dead_card_ids(&array![alive, dead, 10010001, 100010001, dead]);
|
||||
let dead_ids = dead_card_ids(&array![alive, dead, 10010001, 100010001, dead]).unwrap();
|
||||
assert_eq!(dead_ids.len(), 1);
|
||||
assert_eq!(dead_ids[0].as_i64(), Some(dead));
|
||||
|
||||
wipe(3008);
|
||||
}
|
||||
|
||||
// An unreadable or blank catalog must never read as "every custom card is
|
||||
// dead": get_acc prunes on that answer and saves the pruned userdata
|
||||
#[test]
|
||||
fn dead_ids_are_unknown_when_the_catalog_cannot_be_read() {
|
||||
let _lock = crate::runtime::lock_test_data_path();
|
||||
wipe(3009);
|
||||
let alive = next_card_id();
|
||||
insert_card(alive, 1001, 3009, &card_blob(alive, 1), false, false).unwrap();
|
||||
|
||||
let conn = rusqlite::Connection::open(DATABASE.get_path()).unwrap();
|
||||
conn.execute("ALTER TABLE cards RENAME TO cards_hidden", ()).unwrap();
|
||||
let unreadable = dead_card_ids(&array![alive]);
|
||||
conn.execute("CREATE TABLE cards (master_card_id BIGINT NOT NULL PRIMARY KEY, master_character_id BIGINT NOT NULL, owner_id BIGINT NOT NULL, card TEXT NOT NULL, rarity INT NOT NULL DEFAULT 1, published INT NOT NULL DEFAULT 0, obtainable INT NOT NULL DEFAULT 0)", ()).unwrap();
|
||||
let blank = dead_card_ids(&array![alive]);
|
||||
conn.execute("DROP TABLE cards", ()).unwrap();
|
||||
conn.execute("ALTER TABLE cards_hidden RENAME TO cards", ()).unwrap();
|
||||
|
||||
assert!(unreadable.is_none(), "an unreadable catalog reported dead cards");
|
||||
assert!(blank.is_none(), "a blank catalog reported every card dead");
|
||||
assert_eq!(dead_card_ids(&array![alive]).unwrap().len(), 0);
|
||||
wipe(3009);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,7 +365,10 @@ pub fn non_public_music_ids_for(user_id: i64) -> JsonValue {
|
||||
// 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
|
||||
// (which are never reused) are returned
|
||||
pub fn dead_music_ids(candidates: &JsonValue) -> JsonValue {
|
||||
// None = the catalog could not be read (or is entirely empty while players still hold
|
||||
// custom ids): the caller must prune NOTHING then. An unreadable catalog looks exactly
|
||||
// like one where every id is dead, and get_acc saves the pruned userdata.
|
||||
pub fn dead_music_ids(candidates: &JsonValue) -> Option<JsonValue> {
|
||||
let mut ids: Vec<i64> = Vec::new();
|
||||
for id in candidates.members() {
|
||||
let Some(id) = id.as_i64() else { continue; };
|
||||
@@ -374,17 +377,23 @@ pub fn dead_music_ids(candidates: &JsonValue) -> JsonValue {
|
||||
}
|
||||
}
|
||||
if ids.is_empty() {
|
||||
return array![];
|
||||
return Some(array![]);
|
||||
}
|
||||
let list = ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(",");
|
||||
let alive = DATABASE.lock_and_select_all(&format!("SELECT music_id FROM songs WHERE music_id IN ({})", list), params!()).unwrap_or(array![]);
|
||||
let alive = DATABASE.lock_and_select_all(&format!("SELECT music_id FROM songs WHERE music_id IN ({})", list), params!()).ok()?;
|
||||
if alive.is_empty() {
|
||||
let total: i64 = DATABASE.lock_and_select_type("SELECT COUNT(*) FROM songs", params!()).ok()?;
|
||||
if total == 0 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let mut rv = array![];
|
||||
for id in ids {
|
||||
if !alive.contains(id) {
|
||||
rv.push(id).unwrap();
|
||||
}
|
||||
}
|
||||
rv
|
||||
Some(rv)
|
||||
}
|
||||
|
||||
// Every stored catalog blob, unparsed and unfiltered by visibility. Only the
|
||||
|
||||
@@ -86,6 +86,12 @@ pub struct Args {
|
||||
#[arg(long, default_value = "", help = "Asset hash for windows client.")]
|
||||
pub windows_asset_hash: String,
|
||||
|
||||
#[arg(long, default_value = "", help = "Asset hash for linux client.")]
|
||||
pub linux_asset_hash: String,
|
||||
|
||||
#[arg(long, default_value = "", help = "Asset hash for macOS client.")]
|
||||
pub mac_asset_hash: String,
|
||||
|
||||
#[arg(long, default_value = "", help = "Path to image assets.")]
|
||||
pub image_asset_path: String,
|
||||
|
||||
|
||||
@@ -660,6 +660,9 @@ pub fn purge_song(music_id: i64) {
|
||||
// exists (browse renders an uploader name for every row). Same steps as the
|
||||
// owner's own delete, blob GC included
|
||||
pub fn purge_owner(uid: i64) {
|
||||
if uid <= 0 {
|
||||
return;
|
||||
}
|
||||
if disabled() {
|
||||
return;
|
||||
}
|
||||
@@ -715,6 +718,12 @@ pub fn sweep_blobs() {
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing referenced means the catalog is empty (or gone): a sweep would then delete
|
||||
// every blob on disk, so it is skipped until the catalog has rows again
|
||||
if referenced.is_empty() {
|
||||
println!("Custom 3DMV blob sweep: catalog empty, skipped");
|
||||
return;
|
||||
}
|
||||
// No directory means nothing was ever uploaded
|
||||
let Ok(entries) = fs::read_dir(get_data_path("custom_3dmv/blobs")) else {
|
||||
return;
|
||||
|
||||
@@ -1545,6 +1545,9 @@ pub fn delete_character(uid: i64, master_character_id: i64) -> Result<(), String
|
||||
// serving a dangling master_character_id, which the client throws on. Those stay,
|
||||
// ownerless, which is the same trade delete_character already makes
|
||||
pub fn purge_owner(uid: i64) {
|
||||
if uid <= 0 {
|
||||
return;
|
||||
}
|
||||
if disabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -243,6 +243,12 @@ pub fn sweep_audio() {
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing referenced means the catalog is empty (or gone): a sweep would then delete
|
||||
// every ogg on disk, so it is skipped until the catalog has rows again
|
||||
if referenced.is_empty() {
|
||||
println!("Custom song audio sweep: catalog empty, skipped");
|
||||
return;
|
||||
}
|
||||
// No directory means nothing was ever uploaded
|
||||
let Ok(entries) = fs::read_dir(get_data_path("custom_songs/audio")) else {
|
||||
return;
|
||||
@@ -1334,6 +1340,9 @@ async fn delete(req: HttpRequest, body: String) -> HttpResponse {
|
||||
// the owner's own delete does, including the cross-feature MV cascade and the
|
||||
// content-addressed audio GC
|
||||
pub fn purge_owner(uid: i64) {
|
||||
if uid <= 0 {
|
||||
return;
|
||||
}
|
||||
if disabled() {
|
||||
return;
|
||||
}
|
||||
@@ -2724,6 +2733,123 @@ mod tests {
|
||||
assert!(crate::database::custom_card::get_character(character_id).is_none(), "the character survived the purge");
|
||||
}
|
||||
|
||||
// A purge is scoped to ONE owner: nothing another account uploaded, nor an
|
||||
// ogg two accounts share, may go with it - and a degenerate uid, a broken
|
||||
// catalog or a disabled feature is a no-op, never a wildcard
|
||||
#[test]
|
||||
fn purging_one_account_leaves_other_accounts_uploads_alone() {
|
||||
let _lock = crate::runtime::lock_test_data_path();
|
||||
use crate::database::custom_card as card_db;
|
||||
use crate::database::custom_3dmv as mv_db;
|
||||
|
||||
let victim = userdata::get_acc("custom-content-purge-victim")["user"]["id"].as_i64().unwrap();
|
||||
let bystander = userdata::get_acc("custom-content-purge-bystander")["user"]["id"].as_i64().unwrap();
|
||||
assert_ne!(victim, bystander);
|
||||
for uid in [victim, bystander] {
|
||||
purge_owner(uid);
|
||||
crate::router::custom_card::purge_owner(uid);
|
||||
crate::router::custom_3dmv::purge_owner(uid);
|
||||
}
|
||||
|
||||
// One song each, plus one song each with byte-identical audio so the two
|
||||
// owners share a single content-addressed ogg
|
||||
let v_song = create_song(victim, &song_fields("Purge Victim", 401.0)).unwrap();
|
||||
let b_song = create_song(bystander, &song_fields("Purge Bystander", 409.0)).unwrap();
|
||||
let v_shared = create_song(victim, &song_fields("Purge Victim Shared", 419.0)).unwrap();
|
||||
let b_shared = create_song(bystander, &song_fields("Purge Bystander Shared", 419.0)).unwrap();
|
||||
let b_play = database::get_song(b_song).unwrap()["sound"]["play"]["md5"].to_string();
|
||||
let shared_play = database::get_song(b_shared).unwrap()["sound"]["play"]["md5"].to_string();
|
||||
assert_eq!(database::get_song(v_shared).unwrap()["sound"]["play"]["md5"].to_string(), shared_play);
|
||||
|
||||
// Cards and characters criss-crossed between the two owners
|
||||
let b_char = card_db::next_character_id();
|
||||
card_db::insert_character(b_char, bystander, &object!{ "master_character_id": b_char, "name": "B" }).unwrap();
|
||||
let b_card = card_db::next_card_id();
|
||||
card_db::insert_card(b_card, b_char, bystander, &object!{ "master_card_id": b_card, "rarity": 1 }, true, true).unwrap();
|
||||
let v_card_on_b_char = card_db::next_card_id();
|
||||
card_db::insert_card(v_card_on_b_char, b_char, victim, &object!{ "master_card_id": v_card_on_b_char, "rarity": 1 }, true, true).unwrap();
|
||||
let v_char = card_db::next_character_id();
|
||||
card_db::insert_character(v_char, victim, &object!{ "master_character_id": v_char, "name": "V" }).unwrap();
|
||||
let b_card_on_v_char = card_db::next_card_id();
|
||||
card_db::insert_card(b_card_on_v_char, v_char, bystander, &object!{ "master_card_id": b_card_on_v_char, "rarity": 1 }, true, true).unwrap();
|
||||
let b_mv = mv_db::next_mv_id();
|
||||
mv_db::insert_mv(b_mv, b_song, bystander, &object!{ "mv_id": b_mv, "music_id": b_song, "files": [] }, true).unwrap();
|
||||
|
||||
userdata::delete_account(victim);
|
||||
|
||||
assert!(database::get_song(v_song).is_none(), "the victim's song survived");
|
||||
assert!(database::get_song(v_shared).is_none(), "the victim's shared-audio song survived");
|
||||
assert!(card_db::get_card(v_card_on_b_char).is_none(), "the victim's card survived");
|
||||
|
||||
assert!(database::get_song(b_song).is_some(), "the bystander's song was deleted");
|
||||
assert!(database::get_song(b_shared).is_some(), "the bystander's shared-audio song was deleted");
|
||||
assert!(fs::metadata(get_data_path(&format!("custom_songs/{}", b_song))).is_ok(), "the bystander's song directory was deleted");
|
||||
assert!(fs::read(audio_file_path(&b_play)).is_ok(), "the bystander's ogg was unlinked");
|
||||
assert!(fs::read(audio_file_path(&shared_play)).is_ok(), "the cross-owner shared ogg was unlinked");
|
||||
assert!(card_db::get_card(b_card).is_some(), "the bystander's card was deleted");
|
||||
assert!(card_db::get_character(b_char).is_some(), "the bystander's character was deleted");
|
||||
assert!(card_db::get_card(b_card_on_v_char).is_some(), "the bystander's card on the victim's character was deleted");
|
||||
assert!(card_db::get_character(v_char).is_some(), "a character another account's card still uses was deleted");
|
||||
assert!(mv_db::get_mv(b_mv).is_some(), "the bystander's MV was deleted");
|
||||
|
||||
purge_owner(0);
|
||||
crate::router::custom_card::purge_owner(0);
|
||||
crate::router::custom_3dmv::purge_owner(0);
|
||||
purge_owner(-1);
|
||||
userdata::delete_account(0);
|
||||
with_songs_table_broken(|| purge_owner(bystander));
|
||||
crate::runtime::set_enable_custom_songs(false);
|
||||
crate::runtime::set_enable_custom_cards(false);
|
||||
crate::runtime::set_enable_custom_3dmv(false);
|
||||
purge_owner(bystander);
|
||||
crate::router::custom_card::purge_owner(bystander);
|
||||
crate::router::custom_3dmv::purge_owner(bystander);
|
||||
crate::runtime::set_enable_custom_songs(true);
|
||||
crate::runtime::set_enable_custom_cards(true);
|
||||
crate::runtime::set_enable_custom_3dmv(true);
|
||||
assert!(database::get_song(b_song).is_some(), "a degenerate, broken-catalog or feature-off purge deleted a song");
|
||||
assert!(fs::read(audio_file_path(&b_play)).is_ok(), "a degenerate, broken-catalog or feature-off purge unlinked a live ogg");
|
||||
assert!(card_db::get_card(b_card).is_some(), "a degenerate or feature-off purge deleted a card");
|
||||
assert!(card_db::get_character(b_char).is_some(), "a degenerate or feature-off purge deleted a character");
|
||||
assert!(mv_db::get_mv(b_mv).is_some(), "a degenerate or feature-off purge deleted an MV");
|
||||
|
||||
// Leave nothing behind: other tests assume an empty obtainable-card pool
|
||||
purge_owner(bystander);
|
||||
crate::router::custom_card::purge_owner(bystander);
|
||||
crate::router::custom_3dmv::purge_owner(bystander);
|
||||
}
|
||||
|
||||
// The pruning get_acc runs on every login must read an unreadable or blank
|
||||
// catalog as "unknown", never as "every custom song is dead", and the startup
|
||||
// sweep must never unlink files when the catalog references nothing
|
||||
#[test]
|
||||
fn an_unreadable_or_blank_catalog_never_prunes_or_sweeps() {
|
||||
let _lock = crate::runtime::lock_test_data_path();
|
||||
let uid = userdata::get_acc("custom-content-blank-catalog")["user"]["id"].as_i64().unwrap();
|
||||
purge_owner(uid);
|
||||
let music_id = create_song(uid, &song_fields("Blank Catalog", 383.0)).unwrap();
|
||||
let play = database::get_song(music_id).unwrap()["sound"]["play"]["md5"].to_string();
|
||||
let live_ogg = audio_file_path(&play);
|
||||
|
||||
let unreadable = with_songs_table_broken(|| database::dead_music_ids(&array![music_id]));
|
||||
assert!(unreadable.is_none(), "an unreadable catalog reported dead songs");
|
||||
with_songs_table_broken(|| sweep_audio());
|
||||
assert!(fs::read(&live_ogg).is_ok(), "a sweep over an unreadable catalog unlinked a live ogg");
|
||||
|
||||
let conn = rusqlite::Connection::open(database::test_db_path()).unwrap();
|
||||
conn.execute("ALTER TABLE songs RENAME TO songs_parked", ()).unwrap();
|
||||
conn.execute("CREATE TABLE songs (music_id BIGINT NOT NULL PRIMARY KEY, owner_id BIGINT NOT NULL, song TEXT NOT NULL, visibility TEXT NOT NULL DEFAULT 'public', downloads_disabled INT NOT NULL DEFAULT 0)", ()).unwrap();
|
||||
let blank = database::dead_music_ids(&array![music_id]);
|
||||
sweep_audio();
|
||||
let survived = fs::read(&live_ogg).is_ok();
|
||||
conn.execute("DROP TABLE songs", ()).unwrap();
|
||||
conn.execute("ALTER TABLE songs_parked RENAME TO songs", ()).unwrap();
|
||||
assert!(blank.is_none(), "a blank catalog reported every song dead");
|
||||
assert!(survived, "a sweep over a blank catalog unlinked a live ogg");
|
||||
assert_eq!(database::dead_music_ids(&array![music_id]).unwrap().len(), 0);
|
||||
purge_owner(uid);
|
||||
}
|
||||
|
||||
|
||||
// D1/D6/D7: the whole upload route, end to end - a real multipart body, a real
|
||||
// session, the expansion of a real package, and the create running on the
|
||||
|
||||
+34
-2
@@ -49,6 +49,10 @@ static ASSET_VERSIONS: &[AssetVersion] = &[
|
||||
AssetVersion { region: "JP", platform: "Windows", version: "01a71b00f63e4dba92117ac7e60070a6", hash: "b8d0e7edcb63f5bdd28817a597772ca6", latest: true },
|
||||
AssetVersion { region: "JP", platform: "Android", version: "01a71b00f63e4dba92117ac7e60070a6", hash: "d12cecc5695da7f81f8873a3ff93752e", latest: true },
|
||||
AssetVersion { region: "JP", platform: "iOS", version: "01a71b00f63e4dba92117ac7e60070a6", hash: "7c1f61ee68ac84c82dd397a162629142", latest: true },
|
||||
// Linux / macOS players (StandaloneLinux64 / StandaloneOSX lanes): no baked hash, the
|
||||
// running build's hash is supplied with --linux-asset-hash / --mac-asset-hash
|
||||
AssetVersion { region: "JP", platform: "Linux", version: "01a71b00f63e4dba92117ac7e60070a6", hash: "", latest: true },
|
||||
AssetVersion { region: "JP", platform: "Mac", version: "01a71b00f63e4dba92117ac7e60070a6", hash: "", latest: true },
|
||||
|
||||
//AssetVersion { region: "JP", platform: "WebGL", version: "4c921d2443335e574a82e04ec9ea243c", hash: "e1ff7c74b20c8d216507972b6f24b9df", latest: true },
|
||||
];
|
||||
@@ -68,6 +72,8 @@ impl AssetVersion {
|
||||
let ov = args.asset_version.as_str();
|
||||
let oh = match (self.region, self.platform) {
|
||||
("JP", "Windows") => args.windows_asset_hash.as_str(),
|
||||
("JP", "Linux") => args.linux_asset_hash.as_str(),
|
||||
("JP", "Mac") => args.mac_asset_hash.as_str(),
|
||||
("JP", "Android") => args.jp_android_asset_hash.as_str(),
|
||||
("JP", "iOS") => args.jp_ios_asset_hash.as_str(),
|
||||
("GL", "Android") => args.en_android_asset_hash.as_str(),
|
||||
@@ -89,7 +95,7 @@ fn valid_hashes(asset_version: &str, platform: &str) -> Vec<String> {
|
||||
if entry.platform != platform {
|
||||
continue;
|
||||
}
|
||||
if entry.version == asset_version {
|
||||
if entry.version == asset_version && !entry.hash.is_empty() {
|
||||
out.push(entry.stock_hash());
|
||||
}
|
||||
if entry.latest {
|
||||
@@ -116,7 +122,7 @@ fn preferred_hash(asset_version: &str, platform: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if entry.version == asset_version && stock.is_none() {
|
||||
if entry.version == asset_version && stock.is_none() && !entry.hash.is_empty() {
|
||||
stock = Some(entry.stock_hash());
|
||||
}
|
||||
}
|
||||
@@ -169,6 +175,8 @@ pub fn parse_platform(header: &str) -> &str {
|
||||
"iphone" => "iOS",
|
||||
"windows" => "Windows",
|
||||
"windowsplayer" => "Windows",
|
||||
"linuxplayer" => "Linux",
|
||||
"osxplayer" => "Mac",
|
||||
"webglplayer" => "WebGL",
|
||||
"editor" => "Editor",
|
||||
"windowseditor" => "Editor",
|
||||
@@ -471,3 +479,27 @@ pub(crate) fn get_cards(arr: JsonValue, user: &JsonValue) -> JsonValue {
|
||||
}
|
||||
rv
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod platform_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn standalone_player_platforms_are_known() {
|
||||
assert_eq!(parse_platform("WindowsPlayer"), "Windows");
|
||||
assert_eq!(parse_platform("LinuxPlayer"), "Linux");
|
||||
assert_eq!(parse_platform("OSXPlayer"), "Mac");
|
||||
assert_eq!(parse_platform("OSXEditor"), "Editor");
|
||||
}
|
||||
|
||||
// Linux / macOS have no baked hash: without the CLI override the platform has no valid
|
||||
// hash at all (never an empty string presented as one)
|
||||
#[test]
|
||||
fn unhashed_platforms_answer_nothing_without_an_override() {
|
||||
let latest = ASSET_VERSIONS.iter().find(|e| e.latest && e.platform == "Windows").unwrap().version;
|
||||
assert_eq!(preferred_hash(latest, "Mac"), None);
|
||||
assert_eq!(preferred_hash(latest, "Linux"), None);
|
||||
assert!(valid_hashes(latest, "Mac").is_empty());
|
||||
assert!(preferred_hash(latest, "Windows").is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +268,9 @@ fn remove_deleted_custom_songs(user: &mut JsonValue) -> bool {
|
||||
if candidates.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let dead = custom_song::dead_music_ids(&candidates);
|
||||
let Some(dead) = custom_song::dead_music_ids(&candidates) else {
|
||||
return false;
|
||||
};
|
||||
if dead.is_empty() {
|
||||
return false;
|
||||
}
|
||||
@@ -301,7 +303,9 @@ fn remove_deleted_custom_cards(user: &mut JsonValue) -> bool {
|
||||
if candidates.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let dead = custom_card::dead_card_ids(&candidates);
|
||||
let Some(dead) = custom_card::dead_card_ids(&candidates) else {
|
||||
return false;
|
||||
};
|
||||
if dead.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ fn platform_guard(ctx: &guard::GuardContext) -> bool {
|
||||
.split('/')
|
||||
.nth(1)
|
||||
.unwrap_or("");
|
||||
matches!(platform, "Android" | "StandaloneWindows64" | "StandaloneLinux64" | "WebGL" | "iOS")
|
||||
matches!(platform, "Android" | "StandaloneWindows64" | "StandaloneLinux64" | "StandaloneOSX" | "WebGL" | "iOS")
|
||||
}
|
||||
|
||||
pub fn routes(cfg: &mut web::ServiceConfig) {
|
||||
|
||||
Reference in New Issue
Block a user