steam-user 0.1.0

Steam User web client for Rust - HTTP-based Steam Community interactions
Documentation
//! Parse a locally-saved authorized-devices HTML file and print all extracted
//! fields.
//!
//! Run with:
//!   cargo run --example authorized_devices --
//! "C:\Users\Admin\Downloads\view-source_https___store.steampowered.
//! com_account_authorizeddevices.html"
//!
//! If no path is given it defaults to the path above.

use steam_user::services::account::parse_account_details_html;
use tracing::{error, info};

/// Firefox saves view-source pages as an HTML syntax-highlighter wrapper
/// where the original source is entity-encoded inside `<span>` elements.
/// This strips the wrapper and decodes the entities to recover the real HTML.
fn unwrap_viewsource(raw: &str) -> String {
    // Strip every HTML tag
    let stripped = regex::Regex::new(r"<[^>]+>").expect("regex compilation failed").replace_all(raw, "");
    // Decode HTML entities — &amp; must be last
    stripped.replace("&lt;", "<").replace("&gt;", ">").replace("&quot;", "\"").replace("&#39;", "'").replace("&amp;", "&")
}

fn main() {
    let path = std::env::args().nth(1).unwrap_or_else(|| r"C:\Users\Admin\Downloads\view-source_https___store.steampowered.com_account_authorizeddevices.html".to_string());

    let raw = std::fs::read_to_string(&path).unwrap_or_else(|e| {
        error!("Failed to read '{}': {}", path, e);
        std::process::exit(1);
    });

    // Auto-detect Firefox view-source wrapper
    let html = if raw.contains(r#"class="line-content""#) {
        error!("[info] Detected Firefox view-source file — unwrapping...");
        unwrap_viewsource(&raw)
    } else {
        raw
    };

    let page = parse_account_details_html(&html);

    // ── Account ────────────────────────────────────────────────────────────────
    info!("═══════════════════════  ACCOUNT  ═══════════════════════");
    info!("  account_name          : {:?}", page.account_name);
    info!("  email                 : {:?}", page.email);
    info!("  phone_hint            : {:?}", page.phone_hint);

    info!("  avatar_hash           : {:?}", page.avatar_hash);
    info!("  requesting_token_id   : {:?}", page.requesting_token_id);
    info!("  latest_android_ver    : {:?}", page.latest_android_app_version);
    info!("  country               : {:?}", page.country);
    info!("  account_security      : {:?}", page.account_security());

    // ── Wallet ─────────────────────────────────────────────────────────────────
    info!("\n═══════════════════════  WALLET  ════════════════════════");
    if let Some(w) = &page.wallet_balance {
        info!("  main_balance   : {:?}", w.main_balance);
        info!("  currency       : {:?}", w.currency);
        info!("  pending        : {:?}", w.pending);
        info!("  parsed amount  : {:?}", w.parse_main_balance());
    } else {
        info!("  (not found)");
    }

    // ── User info ──────────────────────────────────────────────────────────────
    info!("\n═══════════════════════  USER INFO  ═════════════════════");
    if let Some(u) = &page.user_info {
        info!("  logged_in        : {}", u.logged_in);
        info!("  steamid          : {:?}", u.steamid);
        info!("  accountid        : {:?}", u.accountid);
        info!("  account_name     : {:?}", u.account_name);
        info!("  country_code     : {:?}", u.country_code);
        info!("  is_limited       : {:?}", u.is_limited);
        info!("  is_support       : {:?}", u.is_support);
        info!("  is_partner_member: {:?}", u.is_partner_member);
        info!("  is_valve_email   : {:?}", u.is_valve_email);
        info!("  excluded_content : {:?}", u.excluded_content_descriptors);
    } else {
        info!("  (not found)");
    }

    // ── HW info ────────────────────────────────────────────────────────────────
    info!("\n═══════════════════════  HW INFO  ═══════════════════════");
    if let Some(h) = &page.hw_info {
        info!("  steam_os   : {}", h.steam_os);
        info!("  steam_deck : {}", h.steam_deck);
    } else {
        info!("  (not found)");
    }

    // ── Two-factor status ──────────────────────────────────────────────────────
    info!("\n══════════════════  TWO-FACTOR STATUS  ══════════════════");
    if let Some(tf) = &page.two_factor_status {
        info!("  state                        : {}", tf.state);
        info!("  authenticator_type           : {:?}", tf.authenticator_type);
        info!("  authenticator_allowed        : {:?}", tf.authenticator_allowed);
        info!("  steamguard_scheme            : {:?}", tf.steamguard_scheme);
        info!("  email_validated              : {:?}", tf.email_validated);
        info!("  token_gid                    : {:?}", tf.token_gid);
        info!("  time_created                 : {:?}", tf.time_created);
        info!("  revocation_attempts_remaining: {:?}", tf.revocation_attempts_remaining);
        info!("  allow_external_authenticator : {:?}", tf.allow_external_authenticator);
        info!("  version                      : {:?}", tf.version);
        info!("  success                      : {:?}", tf.success);
        info!("  rwgrsn                       : {:?}", tf.rwgrsn);
        if let Some(usages) = &tf.usages {
            info!("  usages ({}):", usages.len());
            for u in usages {
                info!("    time={} type={} conf_type={} conf_action={}", u.time, u.usage_type, u.confirmation_type, u.confirmation_action);
            }
        }
    } else {
        info!("  (not found)");
    }

    // ── Active devices ─────────────────────────────────────────────────────────
    info!("\n═══════════════════════  ACTIVE DEVICES ({})  ═══════════════════════", page.active_devices.len());
    for (i, d) in page.active_devices.iter().enumerate() {
        print_device(i + 1, d);
    }

    // ── Revoked devices ────────────────────────────────────────────────────────
    info!("\n══════════════════════  REVOKED DEVICES ({})  ══════════════════════", page.revoked_devices.len());
    for (i, d) in page.revoked_devices.iter().enumerate() {
        print_device(i + 1, d);
    }

    // ── Notifications ──────────────────────────────────────────────────────────
    info!("\n══════════════════════  NOTIFICATIONS  ══════════════════");
    if let Some(n) = &page.notifications {
        info!("  pending_gift_count  : {:?}", n.pending_gift_count);
        info!("  pending_friend_count: {:?}", n.pending_friend_count);
        info!("  notifications ({}):", n.notifications.len());
        for notif in &n.notifications {
            info!("    id={} type={} read={} ts={}", notif.notification_id, notif.notification_type, notif.read, notif.timestamp);
        }
    } else {
        info!("  (not found)");
    }

    // ── Broadcast ──────────────────────────────────────────────────────────────
    info!("\n════════════════════════  BROADCAST  ════════════════════");
    if let Some(b) = &page.broadcast_user {
        info!("  success               : {}", b.success);
        info!("  hide_store_broadcast  : {}", b.hide_store_broadcast);
    } else {
        info!("  (not found)");
    }

    // ── Store user config ──────────────────────────────────────────────────────
    info!("\n═══════════════════  STORE USER CONFIG  ═════════════════");
    if let Some(c) = &page.store_user_config {
        let token_preview = c.webapi_token.as_deref().map(|t| if t.len() > 40 { format!("{}", &t[..40]) } else { t.to_string() });
        info!("  webapi_token (truncated): {:?}", token_preview);
        info!("  extra keys: {:?}", c.extra.keys().collect::<Vec<_>>());
    } else {
        info!("  (not found)");
    }

    // ── Page config ────────────────────────────────────────────────────────────
    info!("\n════════════════════════  PAGE CONFIG  ══════════════════");
    if let Some(c) = &page.page_config {
        info!("  language        : {:?}", c.language);
        info!("  country         : {:?}", c.country);
        info!("  platform        : {:?}", c.platform);
        info!("  website_id      : {:?}", c.website_id);
        info!("  euniverse       : {:?}", c.euniverse);
        info!("  erealm          : {:?}", c.erealm);
        info!("  build_timestamp : {:?}", c.build_timestamp);
        info!("  page_timestamp  : {:?}", c.page_timestamp);
        info!("  now             : {:?}", c.now);
        info!("  in_client       : {:?}", c.in_client);
        info!("  from_web        : {:?}", c.from_web);
        info!("  snr             : {:?}", c.snr);
        info!("  store_base_url  : {:?}", c.store_base_url);
        info!("  webapi_base_url : {:?}", c.webapi_base_url);
        info!("  avatar_base_url : {:?}", c.avatar_base_url);
    } else {
        info!("  (not found)");
    }
}

fn print_device(n: usize, d: &steam_user::types::AuthorizedDevice) {
    info!("  [{n}] token_id    : {}", d.token_id);
    info!("       description : {}", d.token_description);
    info!("       platform    : {}  os_platform: {}  auth_type: {}  state: {}", d.platform_type, d.os_platform, d.auth_type, d.effective_token_state);
    info!("       logged_in   : {}  os_type: {}  auth_type2: {}", d.logged_in, d.os_type, d.authentication_type);
    info!("       updated     : {}", d.time_updated);
    if let Some(ls) = &d.last_seen {
        let ip = ls
            .ip
            .as_ref()
            .and_then(|i| i.v4)
            .map(|v| {
                let b = (v as u32).to_be_bytes();
                format!("{}.{}.{}.{}", b[0], b[1], b[2], b[3])
            })
            .unwrap_or_else(|| "".into());
        info!("       last_seen   : time={} ip={} {}, {}", ls.time.unwrap_or(0), ip, ls.country.as_deref().unwrap_or("?"), ls.city.as_deref().unwrap_or("?"));
    }
}