Files
OpenFUT-Bridge/src/shaper.rs
T
funman300 893ee369c2 Phase 22: mapper round 2, response shaper, card detail modal, market filters
- 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>
2026-06-25 18:28:43 -07:00

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());
}
}