late-java-core 2.2.9

A Rust library for launching Minecraft Java Edition
use super::*;
use crate::error::{Result, LateJavaCoreError};
use reqwest::Client;
use serde_json::json;
use uuid::Uuid;
use base64::{Engine as _, engine::general_purpose};

/// Autenticador Microsoft
pub struct MicrosoftAuth {
    client_id: String,
    client: Client,
}

impl MicrosoftAuth {
    /// Crear un nuevo autenticador Microsoft
    pub fn new(client_id: String) -> Self {
        Self {
            client_id,
            client: Client::new(),
        }
    }

    /// Autenticar usando el flujo de código de dispositivo
    pub async fn authenticate(&self) -> Result<AuthResponse> {
        // Paso 1: Obtener código de dispositivo
        let device_code = self.get_device_code().await?;
        
        // Paso 2: Mostrar código al usuario
        println!("Por favor, ve a: {}", device_code.verification_uri);
        println!("E ingresa el código: {}", device_code.user_code);
        println!("Presiona Enter cuando hayas completado la autenticación...");
        
        // En una aplicación real, esto sería manejado por la UI
        let mut input = String::new();
        std::io::stdin().read_line(&mut input)?;
        
        // Paso 3: Intercambiar código por token
        let token_response = self.exchange_device_code(&device_code).await?;
        
        // Paso 4: Obtener cuenta completa
        self.get_account(&token_response).await
    }

    /// Refrescar token de autenticación
    pub async fn refresh(&self, auth: AuthResponse) -> Result<AuthResponse> {
        let refresh_token = auth.refresh_token
            .ok_or_else(|| LateJavaCoreError::Auth("No refresh token available".to_string()))?;

        let response = self.client
            .post("https://login.live.com/oauth20_token.srf")
            .form(&[
                ("grant_type", "refresh_token"),
                ("client_id", &self.client_id),
                ("refresh_token", &refresh_token),
            ])
            .send()
            .await?;

        let token_response: serde_json::Value = response.json().await?;
        
        if let Some(error) = token_response.get("error") {
            return Err(LateJavaCoreError::Auth(format!("OAuth error: {}", error)));
        }

        self.get_account(&token_response).await
    }

    async fn get_device_code(&self) -> Result<DeviceCodeResponse> {
        let response = self.client
            .post("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")
            .form(&[
                ("client_id", &self.client_id),
                ("scope", &"XboxLive.signin offline_access".to_string()),
            ])
            .send()
            .await?;

        let device_code: DeviceCodeResponse = response.json().await?;
        Ok(device_code)
    }

    async fn exchange_device_code(&self, device_code: &DeviceCodeResponse) -> Result<serde_json::Value> {
        let response = self.client
            .post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
            .form(&[
                ("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
                ("client_id", &self.client_id),
                ("device_code", &device_code.device_code),
            ])
            .send()
            .await?;

        let token_response: serde_json::Value = response.json().await?;
        
        if let Some(error) = token_response.get("error") {
            return Err(LateJavaCoreError::Auth(format!("OAuth error: {}", error)));
        }

        Ok(token_response)
    }

    async fn get_account(&self, oauth2: &serde_json::Value) -> Result<AuthResponse> {
        let access_token = oauth2["access_token"]
            .as_str()
            .ok_or_else(|| LateJavaCoreError::Auth("No access token in response".to_string()))?;

        // Xbox Live authentication
        let xbl_response = self.authenticate_xbox_live(access_token).await?;
        let xbl_token = xbl_response["Token"]
            .as_str()
            .ok_or_else(|| LateJavaCoreError::Auth("No XBL token".to_string()))?;

        // XSTS authorization
        let xsts_response = self.authorize_xsts(xbl_token).await?;
        let xsts_token = xsts_response["Token"]
            .as_str()
            .ok_or_else(|| LateJavaCoreError::Auth("No XSTS token".to_string()))?;

        // Minecraft login
        let mc_login = self.login_minecraft(&xbl_response, xsts_token).await?;
        let mc_access_token = mc_login["access_token"]
            .as_str()
            .ok_or_else(|| LateJavaCoreError::Auth("No Minecraft access token".to_string()))?;

        // Verificar entitlements
        self.verify_minecraft_entitlements(mc_access_token).await?;

        // Obtener perfil
        let profile = self.get_minecraft_profile(mc_access_token).await?;

        // Obtener información de Xbox
        let xbox_account = self.get_xbox_account_info(&xbl_response).await?;

        Ok(AuthResponse {
            access_token: mc_access_token.to_string(),
            client_token: Uuid::new_v4().to_string(),
            uuid: profile["id"].as_str().unwrap_or("").to_string(),
            name: profile["name"].as_str().unwrap_or("").to_string(),
            refresh_token: oauth2["refresh_token"].as_str().map(|s| s.to_string()),
            user_properties: "{}".to_string(),
            meta: AuthMeta {
                auth_type: "Xbox".to_string(),
                access_token_expires_in: chrono::Utc::now().timestamp() as u64 + 
                    oauth2["expires_in"].as_u64().unwrap_or(3600),
                demo: false,
            },
            xbox_account: Some(xbox_account),
            profile: Some(MinecraftProfile {
                skins: profile["skins"].as_array()
                    .map(|arr| arr.iter().filter_map(|s| serde_json::from_value(s.clone()).ok()).collect())
                    .unwrap_or_default(),
                capes: profile["capes"].as_array()
                    .map(|arr| arr.iter().filter_map(|s| serde_json::from_value(s.clone()).ok()).collect())
                    .unwrap_or_default(),
            }),
        })
    }

    async fn authenticate_xbox_live(&self, access_token: &str) -> Result<serde_json::Value> {
        let response = self.client
            .post("https://user.auth.xboxlive.com/user/authenticate")
            .json(&json!({
                "Properties": {
                    "AuthMethod": "RPS",
                    "SiteName": "user.auth.xboxlive.com",
                    "RpsTicket": format!("d={}", access_token)
                },
                "RelyingParty": "http://auth.xboxlive.com",
                "TokenType": "JWT"
            }))
            .send()
            .await?;

        let xbl: serde_json::Value = response.json().await?;
        
        if let Some(error) = xbl.get("error") {
            return Err(LateJavaCoreError::Auth(format!("XBL error: {}", error)));
        }

        Ok(xbl)
    }

    async fn authorize_xsts(&self, xbl_token: &str) -> Result<serde_json::Value> {
        let response = self.client
            .post("https://xsts.auth.xboxlive.com/xsts/authorize")
            .json(&json!({
                "Properties": {
                    "SandboxId": "RETAIL",
                    "UserTokens": [xbl_token]
                },
                "RelyingParty": "rp://api.minecraftservices.com/",
                "TokenType": "JWT"
            }))
            .send()
            .await?;

        let xsts: serde_json::Value = response.json().await?;
        
        if let Some(error) = xsts.get("error") {
            return Err(LateJavaCoreError::Auth(format!("XSTS error: {}", error)));
        }

        Ok(xsts)
    }

    async fn login_minecraft(&self, xbl: &serde_json::Value, xsts_token: &str) -> Result<serde_json::Value> {
        let uhs = xbl["DisplayClaims"]["xui"][0]["uhs"]
            .as_str()
            .ok_or_else(|| LateJavaCoreError::Auth("No UHS in XBL response".to_string()))?;

        let response = self.client
            .post("https://api.minecraftservices.com/authentication/login_with_xbox")
            .json(&json!({
                "identityToken": format!("XBL3.0 x={};{}", uhs, xsts_token)
            }))
            .send()
            .await?;

        let mc_login: serde_json::Value = response.json().await?;
        
        if let Some(error) = mc_login.get("error") {
            return Err(LateJavaCoreError::Auth(format!("Minecraft login error: {}", error)));
        }

        if mc_login["username"].is_null() {
            return Err(LateJavaCoreError::Auth("No Minecraft account found".to_string()));
        }

        Ok(mc_login)
    }

    async fn verify_minecraft_entitlements(&self, access_token: &str) -> Result<()> {
        let response = self.client
            .get("https://api.minecraftservices.com/entitlements/mcstore")
            .header("Authorization", format!("Bearer {}", access_token))
            .send()
            .await?;

        let mcstore: serde_json::Value = response.json().await?;
        
        if let Some(error) = mcstore.get("error") {
            return Err(LateJavaCoreError::Auth(format!("Minecraft store error: {}", error)));
        }

        let items = mcstore["items"].as_array()
            .ok_or_else(|| LateJavaCoreError::Auth("No items in store response".to_string()))?;

        let has_minecraft = items.iter().any(|item| {
            item["name"].as_str().map(|name| 
                name == "game_minecraft" || name == "product_minecraft"

            ).unwrap_or(false)
        });

        if !has_minecraft {
            return Err(LateJavaCoreError::Auth("No Minecraft entitlements found".to_string()));
        }

        Ok(())
    }

    async fn get_minecraft_profile(&self, access_token: &str) -> Result<serde_json::Value> {
        let response = self.client
            .get("https://api.minecraftservices.com/minecraft/profile")
            .header("Authorization", format!("Bearer {}", access_token))
            .send()
            .await?;

        let profile: serde_json::Value = response.json().await?;
        
        if let Some(error) = profile.get("error") {
            return Err(LateJavaCoreError::Auth(format!("Profile error: {}", error)));
        }

        Ok(profile)
    }

    async fn get_xbox_account_info(&self, xbl: &serde_json::Value) -> Result<XboxAccount> {
        let xui = &xbl["DisplayClaims"]["xui"][0];
        
        Ok(XboxAccount {
            xuid: xui["xid"].as_str().unwrap_or("").to_string(),
            gamertag: xui["gt"].as_str().unwrap_or("").to_string(),
            age_group: xui["ag"].as_str().unwrap_or("").to_string(),
        })
    }
}

#[derive(Debug, serde::Deserialize)]
struct DeviceCodeResponse {
    device_code: String,
    user_code: String,
    verification_uri: String,
    expires_in: u64,
    interval: u64,
    message: String,
}