[feat] basic steam user parse

todo: steamid32 + avatar
This commit is contained in:
Purp1e
2025-03-23 21:55:17 +08:00
parent 03e5704a6e
commit 45e4ab1c6a
23 changed files with 6158 additions and 50 deletions

2
src-tauri/Cargo.lock generated
View File

@@ -7,6 +7,7 @@ name = "CS工具箱"
version = "0.0.1"
dependencies = [
"anyhow",
"base64 0.22.1",
"log",
"regex",
"serde",
@@ -28,6 +29,7 @@ dependencies = [
"tauri-plugin-system-info",
"tauri-plugin-theme",
"tauri-plugin-valtio",
"walkdir",
"window-vibrancy",
"winreg 0.55.0",
]

View File

@@ -26,6 +26,8 @@ tauri-build = { version = "2.1.0", features = [] }
[dependencies]
log = "0.4.26"
base64 = "0.22.1"
walkdir = "2.5.0"
serde_json = "1.0.140"
serde = { version = "1.0.219", features = ["derive"] }
regex = "1.11.1"

View File

@@ -1,5 +1,6 @@
use crate::steam;
use crate::tool::*;
use crate::vdf::preset;
use crate::wrap_err;
use anyhow::Result;
@@ -64,11 +65,10 @@ pub fn set_powerplan(plan: i32) -> Result<(), String> {
Ok(())
}
// TODO watch_steam_users
// TODO watch_video_settings
// TODO watch + cancel
// TODO fs_list_dir
// TODO fs_watch_dir
#[tauri::command]
pub fn get_steam_users(steam_dir: &str) -> Result<Vec<preset::User>, String> {
wrap_err!(preset::get_users(steam_dir))
}
#[tauri::command]
pub fn set_auto_login_user(user: &str) -> Result<String, String> {

View File

@@ -21,6 +21,7 @@ mod tray;
mod cmds;
mod steam;
mod tool;
mod vdf;
#[tauri::command]
fn on_button_clicked() -> String {
@@ -96,6 +97,7 @@ fn main() {
cmds::open_path,
cmds::get_powerplan,
cmds::set_powerplan,
cmds::get_steam_users,
cmds::set_auto_login_user,
cmds::check_path,
on_button_clicked

View File

@@ -2,6 +2,7 @@
pub mod id;
pub mod path;
pub mod reg;
pub mod user;
// common steam utils
use anyhow::Result;

View File

@@ -0,0 +1,25 @@
use crate::vdf::parse::to_json;
pub fn get_steam_users() -> Result<String, String> {
let vdf_data = r#"
{
"key1"\t\t"value1"
"key2"\t\t"value2"
"subkey" {
"key3"\t\t"value3"
}
}"#;
let json_data = to_json(vdf_data);
Ok(json_data)
}
mod tests {
use super::*;
#[test]
fn test_get_steam_users() {
let result = get_steam_users();
assert!(result.is_ok() || result.is_err());
println!("{}", result.unwrap());
}
}

2
src-tauri/src/vdf/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod parse;
pub mod preset;

View File

@@ -0,0 +1,84 @@
pub fn to_json(vdf_data: &str) -> String {
let linebreak = match std::env::consts::OS {
"macos" => "\r",
"windows" => "\n",
"linux" => "\n",
_ => "\n",
};
let startpoint = vdf_data.find('{').unwrap_or(0);
let vdf_data = &vdf_data[startpoint..];
let lines: Vec<&str> = vdf_data.split(linebreak).collect();
let mut json_data = String::new();
for line in lines {
let mut line = line.trim_end_matches('\r').trim().to_string();
if line.contains("\"\t\t\"") {
line = line.replace("\"\t\t\"", "\": \"");
line.push(',');
} else if line.contains("\" \"") {
line = line.replace("\" \"", "\": \"");
line.push(',');
}
line = line
.replace('{', ": {")
.replace('\t', "")
.replace('}', "},");
json_data.push_str(&line);
}
json_data = json_data
.replace(",}", "}")
.trim_start_matches(": ")
.trim_end_matches(',')
.to_string();
json_data
}
mod tests {
use super::*;
#[test]
fn test_to_json() {
let vdf_data = "\"users\"
{
\"76561198315078806\"
{
\"AccountName\" \"_jerry_dota2\"
\"PersonaName\" \"Rop紫已黑化\"
\"RememberPassword\" \"1\"
\"WantsOfflineMode\" \"0\"
\"SkipOfflineModeWarning\" \"0\"
\"AllowAutoLogin\" \"1\"
\"MostRecent\" \"1\"
\"Timestamp\" \"1742706884\"
}
\"76561198107125441\"
{
\"AccountName\" \"_im_ai_\"
\"PersonaName\" \"Buongiorno\"
\"RememberPassword\" \"1\"
\"WantsOfflineMode\" \"0\"
\"SkipOfflineModeWarning\" \"0\"
\"AllowAutoLogin\" \"1\"
\"MostRecent\" \"0\"
\"Timestamp\" \"1739093763\"
}
}
";
// let expected_json = r#"{"key1": "value1","key2": "value2","subkey": {"key3": "value3"}}"#;
let json_data = to_json(vdf_data);
// 解析json
let json_value: serde_json::Value = serde_json::from_str(&json_data).unwrap();
println!("{}", json_value)
// assert_eq!(to_json(vdf_data), expected_json);
}
}

259
src-tauri/src/vdf/preset.rs Normal file
View File

@@ -0,0 +1,259 @@
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 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<Vec<LoginUser>> {
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<String, Value> = 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::<u64>()?;
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<Vec<LocalUser>> {
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<String, Value> = 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::<u32>().unwrap(),
persona_name,
avatar_key,
});
// 跳过子目录
WalkDir::new(path).max_depth(1).into_iter().next();
}
}
Ok(local_users)
}
fn read_avatar(steam_dir: &str, steam_id64: &str) -> Option<String> {
let t_path = Path::new(steam_dir).join(format!("avatarcache/{}.png", steam_id64));
if !t_path.exists() {
return None;
}
if let Ok(img) = fs::read(t_path) {
Some(STANDARD.encode(img))
} else {
None
}
}
pub fn get_users(steam_dir: &str) -> Result<Vec<User>> {
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)
}
pub fn merge_users(login: Vec<LoginUser>, local: Vec<LocalUser>) -> Vec<User> {
let mut users = Vec::new();
for i in login {
let mut id32: u32 = 0;
let mut avatar = i.avatar;
let mut avatar_key = String::new();
let t_usr: Vec<&LocalUser> = local
.iter()
.filter(|j| i.persona_name == j.persona_name)
.collect();
if t_usr.len() > 1 {
id32 = steam::id::id64_to_32(i.steam_id64);
} else if t_usr.len() == 1 {
avatar_key = t_usr[0].avatar_key.clone();
}
if avatar.is_empty() && !avatar_key.is_empty() {
avatar = download_avatar(&avatar_key).unwrap_or_default();
}
users.push(User {
steam_id64: i.steam_id64,
steam_id32: 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
}
fn download_avatar(avatar_key: &str) -> Result<String> {
// Implement avatar download logic here
Ok(String::new())
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,35 @@
"video.cfg"
{
"Version" "15"
"VendorID" "4318"
"DeviceID" "9861"
"setting.cpu_level" "3"
"setting.gpu_mem_level" "3"
"setting.gpu_level" "3"
"setting.knowndevice" "0"
"setting.defaultres" "2880"
"setting.defaultresheight" "2160"
"setting.refreshrate_numerator" "0"
"setting.refreshrate_denominator" "0"
"setting.fullscreen" "1"
"setting.coop_fullscreen" "0"
"setting.nowindowborder" "1"
"setting.mat_vsync" "0"
"setting.fullscreen_min_on_focus_loss" "1"
"setting.high_dpi" "0"
"AutoConfig" "2"
"setting.shaderquality" "0"
"setting.r_texturefilteringquality" "3"
"setting.msaa_samples" "2"
"setting.r_csgo_cmaa_enable" "0"
"setting.videocfg_shadow_quality" "0"
"setting.videocfg_dynamic_shadows" "1"
"setting.videocfg_texture_detail" "1"
"setting.videocfg_particle_detail" "0"
"setting.videocfg_ao_detail" "0"
"setting.videocfg_hdr_detail" "3"
"setting.videocfg_fsr_detail" "0"
"setting.monitor_index" "0"
"setting.r_low_latency" "1"
"setting.aspectratiomode" "1"
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
"users"
{
"76561198315078806"
{
"AccountName" "_jerry_dota2"
"PersonaName" "Rop紫已黑化"
"RememberPassword" "1"
"WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0"
"AllowAutoLogin" "1"
"MostRecent" "1"
"Timestamp" "1742706884"
}
"76561198107125441"
{
"AccountName" "_im_ai_"
"PersonaName" "Buongiorno"
"RememberPassword" "1"
"WantsOfflineMode" "0"
"SkipOfflineModeWarning" "0"
"AllowAutoLogin" "1"
"MostRecent" "0"
"Timestamp" "1739093763"
}
}