893ee369c2
- mapper.rs: 22 new exact routes (squad list, trophies, rivals, objectives
sub-groups, catalogue, consumables, loan items, active messages, price
tiers, fitness, customization) + 5 new prefix routes (squad GET/DELETE by
ID, item chemistry PUT, SBC set GET); total exact routes now ≥55
- shaper.rs: shape /achievements → trophyData envelope; /squads → squads
list envelope; /packs/store → purchasablePacks envelope; also shapes
parameterised /squads/{id} responses
- dashboard: card detail modal (click any collection card → full stat view,
chemistry/training/loan info, quick-sell action); loan-only checkbox in
collection filters; position + min/max price filters in transfer market
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
396 lines
13 KiB
Rust
396 lines
13 KiB
Rust
//! 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());
|
|
}
|
|
}
|