//! Response shaper: transforms OpenFUT Core responses into the envelope format //! FIFA 23 expects, based on the Core path that was called. //! //! These are best-guess wrappers derived from typical EA FUT API patterns. //! They will be refined as real traffic captures come in. use serde_json::{json, Value}; use uuid::Uuid; /// Shape a Core JSON response into the format FIFA 23 expects for this path. /// Returns the shaped response or the original unchanged if no shaping is defined. pub fn shape_response(core_path: &str, core_response: Value) -> Value { // Exact matches match core_path { "/auth/local" => return shape_auth_response(core_response), "/profile" => return shape_profile_response(core_response), "/club" => return shape_club_response(core_response), "/squad" => return shape_squad_response(core_response), "/market" => return shape_market_response(core_response), "/market/my-listings" => return shape_my_listings_response(core_response), "/packs" => return shape_packs_response(core_response), "/collection" => return shape_collection_response(core_response), "/cards" => return shape_cards_response(core_response), "/objectives" => return shape_objectives_response(core_response), "/notifications" => return shape_notifications_response(core_response), "/division" => return shape_division_response(core_response), "/statistics" => return shape_statistics_response(core_response), "/sbc" => return shape_sbc_response(core_response), "/fut-champs" => return shape_fut_champs_response(core_response), "/chemistry-styles" => return shape_chemistry_styles_response(core_response), "/achievements" => return shape_achievements_response(core_response), "/squads" => return shape_squads_list_response(core_response), "/packs/store" => return shape_pack_store_response(core_response), _ => {} } // Prefix matches for parameterised paths if core_path.starts_with("/packs/open/") { return shape_pack_open_response(core_response); } if core_path.starts_with("/draft/sessions/") { return shape_draft_response(core_response); } if core_path.starts_with("/fut-champs/") { return shape_fut_champs_response(core_response); } if core_path.starts_with("/squads/") { return shape_squads_list_response(core_response); } // Unknown — pass through unchanged core_response } /// Wraps Core auth response in the FUT auth envelope format. /// /// EA's `/ut/auth` returns a top-level object with a SID, PID, and persona. /// The `sid` is used as a session token in subsequent `X-UT-SID` headers. fn shape_auth_response(core: Value) -> Value { let profile_id = core .get("profile") .and_then(|p| p.get("id")) .and_then(|v| v.as_str()) .unwrap_or("unknown"); let display_name = core .get("profile") .and_then(|p| p.get("username")) .and_then(|v| v.as_str()) .unwrap_or("FUT Player"); let fake_sid = format!("ofut-sid-{}", Uuid::new_v4().simple()); let fake_pid = format!("1{}", &Uuid::new_v4().simple().to_string()[..9]); let phishing_token = format!("ptkn-{}", Uuid::new_v4().simple()); json!({ "sid": fake_sid, "pid": { "pidId": fake_pid, "email": format!("{}@openfut.local", display_name.to_lowercase().replace(' ', "_")), "emailStatus": "VERIFIED", "strength": "STRONG", "dob": "1990-01-01", "country": "US", }, "phishingToken": phishing_token, "persona": { "personaId": profile_id, "displayName": display_name, "isEmailVerified": true, }, "userAccountInfo": { "userAccountInfoSummary": [{ "personaId": profile_id, "displayName": display_name, "clubs": [{ "clubType": "FUT", "supported": true }], }], }, "openfut": true, }) } /// Wraps Core profile response in the FUT user settings envelope. fn shape_profile_response(core: Value) -> Value { let profile = core.get("profile").cloned().unwrap_or(core.clone()); json!({ "settings": { "personaId": profile.get("id").cloned().unwrap_or(json!("unknown")), "displayName": profile.get("username").cloned().unwrap_or(json!("FUT Player")), "level": profile.get("level").cloned().unwrap_or(json!(1)), "xp": profile.get("xp").cloned().unwrap_or(json!(0)), }, "openfut": true, "_raw": profile, }) } /// Wraps Core club response in the FUT usermassinfo envelope. fn shape_club_response(core: Value) -> Value { let club = core.get("club").cloned().unwrap_or(core.clone()); json!({ "pilluserinfo": { "clubId": club.get("id").cloned().unwrap_or(json!("unknown")), "credits": club.get("coins").cloned().unwrap_or(json!(0)), "purchased": 0, "sold": 0, }, "userInfo": { "duplicationInfo": { "isDuplicate": false }, "purchasedPacksInfo": { "premium": 0, "standard": 0 }, }, "openfut": true, "_raw": club, }) } /// Wraps Core squad response in the FUT squad envelope. fn shape_squad_response(core: Value) -> Value { let squad = core.get("squad").cloned().unwrap_or(core.clone()); let players = core.get("players").cloned().unwrap_or(json!([])); json!({ "squad": { "squadId": squad.get("id").cloned().unwrap_or(json!("unknown")), "name": squad.get("name").cloned().unwrap_or(json!("My Squad")), "formation": squad.get("formation").cloned().unwrap_or(json!("4-4-2")), "players": players, "rating": 0, "chemistry": 0, }, "openfut": true, }) } /// Wraps Core market response in the FUT transfer market envelope. fn shape_market_response(core: Value) -> Value { let listings = core.get("listings").cloned().unwrap_or(json!([])); let total = core .get("total") .and_then(|v| v.as_u64()) .unwrap_or(0); json!({ "auctionInfo": listings, "bidTokens": {}, "credits": 0, "currencies": [ { "name": "COINS", "funds": 0, "finalFunds": 0 }, ], "duplicateItemIdList": [], "itemData": [], "total": total, "openfut": true, }) } /// Wraps Core packs response in the FUT pack store envelope. fn shape_packs_response(core: Value) -> Value { let packs = core.get("packs").cloned().unwrap_or(json!([])); json!({ "purchasedPacks": { "packs": packs, }, "openfut": true, }) } fn shape_my_listings_response(core: Value) -> Value { let listings = core.get("listings").cloned().unwrap_or(json!([])); json!({ "tradepile": listings, "credits": 0, "openfut": true, }) } fn shape_collection_response(core: Value) -> Value { let items = core.get("collection").cloned().unwrap_or(json!([])); let total = core.get("total").cloned().unwrap_or(json!(0)); json!({ "itemData": items, "total": total, "openfut": true, }) } fn shape_cards_response(core: Value) -> Value { let cards = core.get("cards").cloned().unwrap_or(json!([])); let total = core.get("total").cloned().unwrap_or(json!(0)); json!({ "items": cards, "total": total, "openfut": true, }) } fn shape_objectives_response(core: Value) -> Value { let objectives = core.get("objectives").cloned().unwrap_or(json!([])); json!({ "objectives": objectives, "openfut": true, }) } fn shape_notifications_response(core: Value) -> Value { let notifications = core.get("notifications").cloned().unwrap_or(json!([])); let count = core.get("count").cloned().unwrap_or(json!(0)); json!({ "notifications": notifications, "count": count, "openfut": true, }) } fn shape_division_response(core: Value) -> Value { json!({ "divisionRivalsInfo": { "division": core.get("division").cloned().unwrap_or(json!(5)), "seasonNumber": core.get("season_number").cloned().unwrap_or(json!(1)), "points": core.get("season_points").cloned().unwrap_or(json!(0)), "matchesPlayed": core.get("matches_played").cloned().unwrap_or(json!(0)), "matchesRemaining": core.get("matches_remaining").cloned().unwrap_or(json!(10)), "record": core.get("record").cloned().unwrap_or(json!({})), }, "openfut": true, "_raw": core, }) } fn shape_statistics_response(core: Value) -> Value { json!({ "stats": core, "openfut": true, }) } fn shape_sbc_response(core: Value) -> Value { let sbcs = core.get("sbcs").cloned().unwrap_or(json!([])); json!({ "challenges": sbcs, "openfut": true, }) } fn shape_pack_open_response(core: Value) -> Value { let cards = core.get("cards").cloned().unwrap_or(json!([])); json!({ "itemData": cards, "duplicateItemIdList": [], "openfut": true, }) } fn shape_draft_response(core: Value) -> Value { json!({ "draft": core, "openfut": true, }) } fn shape_fut_champs_response(core: Value) -> Value { json!({ "futChampions": core, "openfut": true, }) } fn shape_chemistry_styles_response(core: Value) -> Value { let styles = core.get("chemistry_styles").cloned().unwrap_or(json!([])); json!({ "chemistryStyles": styles, "openfut": true, }) } fn shape_achievements_response(core: Value) -> Value { // FIFA 23 uses a "trophies" envelope with "trophyData" array let achievements = core.get("achievements").cloned().unwrap_or(json!([])); let earned = core.get("earned").and_then(|v| v.as_i64()).unwrap_or(0); let total = core.get("total").and_then(|v| v.as_i64()).unwrap_or(0); json!({ "trophyData": achievements, "totalEarned": earned, "totalAvailable": total, "openfut": true, }) } fn shape_squads_list_response(core: Value) -> Value { // Wrap squads in EA's squad list envelope let squads = if core.is_array() { core } else { core.get("squads").cloned().unwrap_or(json!([])) }; json!({ "squads": squads, "openfut": true, }) } fn shape_pack_store_response(core: Value) -> Value { let packs = core.get("packs").cloned().unwrap_or(json!([])); json!({ "purchasablePacks": packs, "openfut": true, }) } #[cfg(test)] mod tests { use super::*; #[test] fn test_shape_auth_response_has_required_fields() { let core = json!({ "profile": { "id": "prof_001", "username": "TestPlayer" } }); let shaped = shape_auth_response(core); assert!(shaped.get("sid").is_some()); assert!(shaped.get("phishingToken").is_some()); assert!(shaped.get("persona").is_some()); assert_eq!(shaped["persona"]["displayName"], "TestPlayer"); assert_eq!(shaped["openfut"], true); } #[test] fn test_shape_response_dispatches_correctly() { let core = json!({ "profile": { "id": "x", "username": "u" } }); let shaped = shape_response("/auth/local", core); assert!(shaped.get("sid").is_some()); } #[test] fn test_shape_response_passthrough_for_unknown_path() { let core = json!({ "foo": "bar" }); let shaped = shape_response("/some/unknown/path", core.clone()); assert_eq!(shaped, core); } #[test] fn test_shape_market_response_has_auction_info() { let core = json!({ "listings": [{ "id": "l1" }], "total": 1 }); let shaped = shape_market_response(core); assert!(shaped["auctionInfo"].as_array().is_some()); assert_eq!(shaped["total"], 1); } #[test] fn test_shape_achievements_response() { let core = json!({ "achievements": [{ "id": "first_match" }], "earned": 1, "total": 18 }); let shaped = shape_response("/achievements", core); assert!(shaped["trophyData"].is_array()); assert_eq!(shaped["totalEarned"], 1); assert_eq!(shaped["totalAvailable"], 18); } #[test] fn test_shape_squads_list_response() { let core = json!({ "squads": [{ "id": "sq-1", "name": "My Squad" }] }); let shaped = shape_response("/squads", core); assert!(shaped["squads"].is_array()); assert_eq!(shaped["squads"].as_array().unwrap().len(), 1); } #[test] fn test_shape_pack_store_response() { let core = json!({ "packs": [{ "id": "gold-pack" }] }); let shaped = shape_response("/packs/store", core); assert!(shaped["purchasablePacks"].is_array()); } }