use anyhow::Result; use base64::engine::general_purpose::STANDARD; use base64::Engine; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use std::fs; use std::path::Path; use tauri_plugin_http::reqwest::blocking::get; use walkdir::WalkDir; use crate::steam; #[derive(Serialize, Deserialize, Debug)] pub struct User { steam_id64: u64, steam_id32: u32, account_name: String, persona_name: String, recent: i32, avatar: String, } #[derive(Serialize, Deserialize, Debug)] pub struct LoginUser { steam_id64: u64, steam_id32: u32, account_name: String, persona_name: String, remember_password: String, wants_offline_mode: String, skip_offline_mode_warning: String, allow_auto_login: String, most_recent: String, timestamp: String, avatar: String, avatar_key: String, } #[derive(Serialize, Deserialize, Debug)] pub struct LocalUser { steam_id32: u32, persona_name: String, avatar_key: String, } pub fn parse_login_users(steam_dir: &str) -> Result> { let t_path = Path::new(steam_dir).join("config/loginusers.vdf"); if !t_path.exists() { return Ok(Vec::new()); } let data = fs::read_to_string(t_path)?; let json_data = super::parse::to_json(&data); let kv: HashMap = serde_json::from_str(&json_data)?; let mut users = Vec::new(); for (k, v) in kv { let props = v.as_object().unwrap(); let avatar = if let Some(img) = read_avatar(&steam_dir, &k) { img } else { String::new() }; let id64 = k.parse::()?; let user = LoginUser { steam_id32: steam::id::id64_to_32(id64), steam_id64: id64, account_name: props .get("AccountName") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), persona_name: props .get("PersonaName") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), remember_password: props .get("RememberPassword") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), wants_offline_mode: props .get("WantsOfflineMode") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), skip_offline_mode_warning: props .get("SkipOfflineModeWarning") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), allow_auto_login: props .get("AllowAutoLogin") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), most_recent: props .get("MostRecent") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), timestamp: props .get("Timestamp") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), avatar, avatar_key: String::new(), }; users.push(user); } Ok(users) } pub fn parse_local_users(steam_dir: &str) -> Result> { let root = Path::new(steam_dir).join("userdata"); if !root.exists() { return Ok(Vec::new()); } let mut local_users = Vec::new(); for entry in WalkDir::new(&root) { let entry = entry?; let path = entry.path(); // 跳过根目录 if path == root { continue; } // 只处理目录 if entry.file_type().is_dir() { let id = path.file_name().unwrap().to_str().unwrap(); // 检查 localconfig.vdf 文件是否存在 let local_config_path = path.join("config/localconfig.vdf"); if !local_config_path.exists() { continue; } // 读取并解析 localconfig.vdf 文件 let data = fs::read_to_string(local_config_path)?; let json_data = super::parse::to_json(&data); let kv: HashMap = serde_json::from_str(&json_data)?; // 获取 friends 节点 let friends = kv.get("friends").and_then(|v| v.as_object()); if friends.is_none() { continue; } let friends = friends.unwrap(); // 获取 PersonaName let persona_name = friends .get("PersonaName") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); // 获取 AvatarKey let avatar_key = friends .get(id) .and_then(|v| v.as_object()) .and_then(|props| props.get("avatar")) .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); // 创建 LocalUser 并加入列表 local_users.push(LocalUser { steam_id32: id.parse::().unwrap(), persona_name, avatar_key, }); // 跳过子目录 WalkDir::new(path).max_depth(1).into_iter().next(); } } Ok(local_users) } pub fn merge_users(login: Vec, local: Vec) -> Vec { let mut users = Vec::new(); for i in login { let avatar: String; let mut avatar_key = String::new(); // 匹配获取 avatar_key 在线获取头像使用 let t_usr: Vec<&LocalUser> = local .iter() .filter(|j| i.steam_id32 == j.steam_id32) .collect(); if let Some(usr) = t_usr.first() { avatar_key = usr.avatar_key.clone(); } // 获取头像的base64 本地头像文件不存在时使用在线api获取 if i.avatar.is_empty() && !avatar_key.is_empty() { avatar = download_avatar(&avatar_key).unwrap_or_default(); } else { avatar = i.avatar; } users.push(User { steam_id64: i.steam_id64, steam_id32: i.steam_id32, account_name: i.account_name, persona_name: i.persona_name, recent: i.most_recent.parse().unwrap_or(0), avatar, }); } // 把第一个recent=1的放在头部 for (id, usr) in users.iter_mut().enumerate() { if usr.recent == 1 { let tmp = users.remove(id); users.insert(0, tmp); break; } } users } pub fn get_users(steam_dir: &str) -> Result> { let login_users = parse_login_users(steam_dir)?; let local_users = parse_local_users(steam_dir)?; let users = merge_users(login_users, local_users); Ok(users) } fn download_avatar(avatar_key: &str) -> Result { // 下载并转换成Base64 "https://avatars.cloudflare.steamstatic.com/" + avatarKey + "_full.jpg" let url = format!( "https://avatars.cloudflare.steamstatic.com/{}_full.jpg", avatar_key ); if let Ok(resp) = get(&url) { if let Ok(bytes) = resp.bytes() { return Ok(STANDARD.encode(bytes)); } } return Err(anyhow::anyhow!("Failed to download avatar")); } fn read_avatar(steam_dir: &str, steam_id64: &str) -> Option { let t_path = Path::new(steam_dir).join(format!("config\\avatarcache\\{}.png", steam_id64)); if !t_path.exists() { return None; } if let Ok(img) = fs::read(t_path) { Some(STANDARD.encode(img)) } else { None } } #[cfg(test)] mod tests { use super::*; #[test] fn test_get_users() { let users = get_users("D:\\Programs\\Steam").unwrap(); println!("{:?}", users); } }