Skip to main content

steam_user/types/
account.rs

1//! Account related types.
2
3use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8/// Transaction ID wrapper
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct TransactionId(pub String);
11
12/// Response from adding a phone number.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct AddPhoneNumberResponse {
15    pub success: bool,
16    #[serde(rename = "showResend")]
17    pub show_resend: bool,
18    pub state: String,
19    #[serde(rename = "errorText")]
20    pub error_text: String,
21    pub token: String,
22    #[serde(rename = "phoneNumber")]
23    pub phone_number: Option<String>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ConfirmPhoneCodeResponse {
28    pub success: bool,
29    #[serde(rename = "showResend")]
30    pub show_resend: bool,
31    pub state: serde_json::Value, // Can be string or bool in JS example
32    #[serde(rename = "errorText")]
33    pub error_text: String,
34    pub token: String,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct RemovePhoneResult {
39    pub success: bool,
40    pub method: Option<i32>,
41    #[serde(rename = "type")]
42    pub confirm_type: Option<String>,
43    pub link: Option<String>,
44    pub wizard_param: Option<serde_json::Value>,
45}
46
47/// Wallet balance information.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct WalletBalance {
50    #[serde(rename = "mainBalance")]
51    pub main_balance: Option<String>,
52    pub pending: Option<String>,
53    pub currency: Option<String>,
54}
55
56impl WalletBalance {
57    /// Parse [`main_balance`](Self::main_balance) into a float.
58    ///
59    /// Expects VNĐ format (`"35.016,48₫"` or `"132.500₫"`).
60    /// Returns `None` if the field is absent or unparseable.
61    pub fn parse_main_balance(&self) -> Option<f64> {
62        crate::utils::parse_steam_balance(self.main_balance.as_deref()?)
63    }
64
65    /// Parse [`pending`](Self::pending) into a float.
66    pub fn parse_pending(&self) -> Option<f64> {
67        crate::utils::parse_steam_balance(self.pending.as_deref()?)
68    }
69}
70
71/// A single purchase history item from the Steam account history.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct PurchaseHistoryItem {
74    /// The date of the purchase (e.g., "Jan 10, 2024"). Stored as a
75    /// `DateTime<Utc>` at midnight UTC (Steam reports day granularity only).
76    pub date: DateTime<Utc>,
77    /// The type of transaction (e.g., "Purchase", "Refund", "Gift Purchase")
78    #[serde(rename = "type")]
79    pub transaction_type: String,
80    /// List of items involved in the transaction
81    pub items: Vec<String>,
82    /// The total amount of the transaction
83    pub total: String,
84    /// The base price before taxes/discounts
85    pub base_price: Option<String>,
86    /// Tax applied
87    pub tax: Option<String>,
88    /// Shipping cost
89    pub shipping: Option<String>,
90    /// Amount wallet balance changed
91    pub wallet_change: Option<String>,
92    /// The payment method used (e.g., "Visa", "Wallet")
93    pub payment_method: Option<String>,
94    /// The current wallet balance after this transaction
95    pub wallet_balance: Option<String>,
96    /// A unique transaction ID if available
97    pub transaction_id: Option<TransactionId>,
98}
99
100/// Response from redeeming a Steam wallet code.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct RedeemWalletCodeResponse {
103    /// Whether the redemption was successful (1 = success)
104    pub success: i32,
105    /// Detail code for errors (0 = no error)
106    pub detail: i32,
107    /// The wallet balance after redemption
108    #[serde(rename = "formattednewwalletbalance")]
109    pub formatted_new_wallet_balance: Option<String>,
110}
111
112// ── Authorized Devices Page ────────────────────────────────────────────────
113
114/// IPv4/v6 address for a device location entry.
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct DeviceIp {
117    pub v4: Option<u64>,
118    pub v6: Option<serde_json::Value>,
119}
120
121/// Geographic + time info for a device first/last-seen event.
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct DeviceLocation {
124    pub time: Option<u64>,
125    pub ip: Option<DeviceIp>,
126    pub locale: Option<String>,
127    pub country: Option<String>,
128    pub state: Option<String>,
129    pub city: Option<String>,
130}
131
132/// A single authorized Steam device / active session.
133///
134/// Returned in both `active_devices` and `revoked_devices` lists from
135/// `https://store.steampowered.com/account/authorizeddevices`.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct AuthorizedDevice {
138    /// Unique token identifier (u64 serialized as string by Steam).
139    pub token_id: String,
140    /// Human-readable description (e.g. user-agent string or device name).
141    pub token_description: String,
142    /// Unix timestamp of the last token update.
143    pub time_updated: u64,
144    /// Steam platform type (2 = browser/web, 3 = mobile, 7 = Steam client).
145    pub platform_type: i32,
146    /// Whether the session is currently logged in (1) or not (0).
147    pub logged_in: i32,
148    /// OS platform code (2 = Windows, 8 = Android, etc.).
149    pub os_platform: i32,
150    /// Auth type code (2 = credentials, 3 = QR, 4 = token refresh, …).
151    pub auth_type: i32,
152    /// Gaming device subtype (e.g. 528 = Samsung Galaxy; null for non-gaming).
153    pub gaming_device_type: Option<i32>,
154    /// Where and when the session was first established.
155    pub first_seen: Option<DeviceLocation>,
156    /// Where and when the session was last active.
157    pub last_seen: Option<DeviceLocation>,
158    /// Detailed OS type integer returned by Steam.
159    pub os_type: i32,
160    /// How authentication was performed (1 = new login, 2 = token refresh, …).
161    pub authentication_type: i32,
162    /// Token state: 3 = active, 99 = revoked/expired.
163    pub effective_token_state: i32,
164}
165
166/// A single two-factor authenticator usage record.
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct TwoFactorUsage {
169    pub time: u64,
170    pub usage_type: i32,
171    pub confirmation_type: i32,
172    pub confirmation_action: i32,
173}
174
175/// Two-factor / Steam Guard status for the account.
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct TwoFactorStatus {
178    /// Guard state: 0 = email guard active, 1 = authenticator active.
179    pub state: i32,
180    pub inactivation_reason: Option<serde_json::Value>,
181    pub authenticator_type: Option<i32>,
182    pub authenticator_allowed: Option<i32>,
183    pub steamguard_scheme: Option<i32>,
184    pub token_gid: Option<String>,
185    pub email_validated: Option<i32>,
186    pub device_identifier: Option<String>,
187    pub time_created: Option<u64>,
188    pub revocation_attempts_remaining: Option<i32>,
189    pub classified_agent: Option<String>,
190    pub allow_external_authenticator: Option<bool>,
191    pub time_transferred: Option<u64>,
192    pub version: Option<i32>,
193    pub last_seen_auth_token_id: Option<String>,
194    pub usages: Option<Vec<TwoFactorUsage>>,
195    /// EResult from the underlying Steam API call.
196    pub success: Option<i32>,
197    /// Internal result code (`rwgrsn`).
198    pub rwgrsn: Option<i32>,
199}
200
201/// Basic account / session info embedded in every Steam Store page.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct PageUserInfo {
204    pub logged_in: bool,
205    pub country_code: Option<String>,
206    #[serde(default)]
207    pub excluded_content_descriptors: Vec<i32>,
208    pub steamid: Option<String>,
209    pub accountid: Option<u64>,
210    pub account_name: Option<String>,
211    pub is_support: Option<bool>,
212    pub is_limited: Option<bool>,
213    pub is_partner_member: Option<bool>,
214    pub is_valve_email: Option<bool>,
215}
216
217/// Hardware / client-type flags embedded in the page.
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct PageHwInfo {
220    #[serde(rename = "bSteamOS")]
221    pub steam_os: bool,
222    #[serde(rename = "bSteamDeck")]
223    pub steam_deck: bool,
224}
225
226/// Store page configuration block (`data-config`).
227///
228/// Contains CDN base URLs, build metadata, and runtime flags.
229#[derive(Debug, Clone, Serialize, Deserialize, Default)]
230pub struct PageConfig {
231    #[serde(rename = "EUNIVERSE")]
232    pub euniverse: Option<i32>,
233    #[serde(rename = "WEB_UNIVERSE")]
234    pub web_universe: Option<String>,
235    #[serde(rename = "LANGUAGE")]
236    pub language: Option<String>,
237    #[serde(rename = "COUNTRY")]
238    pub country: Option<String>,
239    #[serde(rename = "PLATFORM")]
240    pub platform: Option<String>,
241    #[serde(rename = "WEBSITE_ID")]
242    pub website_id: Option<String>,
243    #[serde(rename = "BUILD_TIMESTAMP")]
244    pub build_timestamp: Option<u64>,
245    #[serde(rename = "PAGE_TIMESTAMP")]
246    pub page_timestamp: Option<u64>,
247    #[serde(rename = "NOW")]
248    pub now: Option<u64>,
249    #[serde(rename = "EREALM")]
250    pub erealm: Option<i32>,
251    #[serde(rename = "SNR")]
252    pub snr: Option<String>,
253    #[serde(rename = "IN_CLIENT")]
254    pub in_client: Option<bool>,
255    #[serde(rename = "IN_TENFOOT")]
256    pub in_tenfoot: Option<bool>,
257    #[serde(rename = "IN_GAMEPADUI")]
258    pub in_gamepadui: Option<bool>,
259    #[serde(rename = "IN_CHROMEOS")]
260    pub in_chromeos: Option<bool>,
261    #[serde(rename = "IN_MOBILE_WEBVIEW")]
262    pub in_mobile_webview: Option<bool>,
263    #[serde(rename = "FROM_WEB")]
264    pub from_web: Option<bool>,
265    #[serde(rename = "USE_POPUPS")]
266    pub use_popups: Option<bool>,
267    #[serde(rename = "COMMUNITY_BASE_URL")]
268    pub community_base_url: Option<String>,
269    #[serde(rename = "STORE_BASE_URL")]
270    pub store_base_url: Option<String>,
271    #[serde(rename = "STORE_CHECKOUT_BASE_URL")]
272    pub store_checkout_base_url: Option<String>,
273    #[serde(rename = "HELP_BASE_URL")]
274    pub help_base_url: Option<String>,
275    #[serde(rename = "WEBAPI_BASE_URL")]
276    pub webapi_base_url: Option<String>,
277    #[serde(rename = "AVATAR_BASE_URL")]
278    pub avatar_base_url: Option<String>,
279    #[serde(rename = "LOGIN_BASE_URL")]
280    pub login_base_url: Option<String>,
281    #[serde(rename = "MEDIA_CDN_URL")]
282    pub media_cdn_url: Option<String>,
283    #[serde(rename = "MEDIA_CDN_COMMUNITY_URL")]
284    pub media_cdn_community_url: Option<String>,
285    #[serde(rename = "COMMUNITY_CDN_URL")]
286    pub community_cdn_url: Option<String>,
287    #[serde(rename = "COMMUNITY_CDN_ASSET_URL")]
288    pub community_cdn_asset_url: Option<String>,
289    #[serde(rename = "STORE_CDN_URL")]
290    pub store_cdn_url: Option<String>,
291    #[serde(rename = "STORE_ICON_BASE_URL")]
292    pub store_icon_base_url: Option<String>,
293    #[serde(rename = "STORE_ITEM_BASE_URL")]
294    pub store_item_base_url: Option<String>,
295    #[serde(rename = "VIDEO_CDN_URL")]
296    pub video_cdn_url: Option<String>,
297    #[serde(rename = "TOKEN_URL")]
298    pub token_url: Option<String>,
299    #[serde(rename = "STEAMTV_BASE_URL")]
300    pub steamtv_base_url: Option<String>,
301    #[serde(rename = "PARTNER_BASE_URL")]
302    pub partner_base_url: Option<String>,
303    #[serde(rename = "SUPPORT_BASE_URL")]
304    pub support_base_url: Option<String>,
305    #[serde(rename = "CHAT_BASE_URL")]
306    pub chat_base_url: Option<String>,
307    #[serde(rename = "PUBLIC_SHARED_URL")]
308    pub public_shared_url: Option<String>,
309    #[serde(rename = "BASE_URL_SHARED_CDN")]
310    pub base_url_shared_cdn: Option<String>,
311    #[serde(rename = "BASE_URL_STORE_CDN_ASSETS")]
312    pub base_url_store_cdn_assets: Option<String>,
313    #[serde(rename = "COMMUNITY_ASSETS_BASE_URL")]
314    pub community_assets_base_url: Option<String>,
315    #[serde(rename = "CLAN_CDN_ASSET_URL")]
316    pub clan_cdn_asset_url: Option<String>,
317}
318
319/// Store-side user config block (`data-store_user_config`).
320///
321/// Contains the short-lived WebAPI JWT token for the current browser session.
322#[derive(Debug, Clone, Serialize, Deserialize, Default)]
323pub struct StoreUserConfig {
324    /// Short-lived WebAPI JWT (`webapi_token`). Valid for ~2 hours.
325    pub webapi_token: Option<String>,
326    /// Any remaining fields returned by Steam.
327    #[serde(flatten)]
328    pub extra: HashMap<String, serde_json::Value>,
329}
330
331/// A single Steam notification embedded in the page.
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct PageNotification {
334    pub notification_id: String,
335    /// Bitmask of targets (store / community / …).
336    pub notification_targets: i32,
337    /// Notification type code (3 = forum reply, etc.).
338    pub notification_type: i32,
339    /// Inner JSON payload; content depends on `notification_type`.
340    pub body_data: String,
341    /// 0 = unread, 1 = read.
342    pub read: i32,
343    pub timestamp: u64,
344    pub hidden: i32,
345    pub expiry: u64,
346    pub viewed: Option<u64>,
347}
348
349/// Notification list and pending-count data from `data-steam_notifications`.
350#[derive(Debug, Clone, Serialize, Deserialize, Default)]
351pub struct PageNotifications {
352    #[serde(default)]
353    pub notifications: Vec<PageNotification>,
354    pub pending_gift_count: Option<i32>,
355    pub pending_friend_count: Option<i32>,
356}
357
358/// Broadcast status for the logged-in user (`data-broadcastuser`).
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct BroadcastUser {
361    pub success: i32,
362    #[serde(rename = "bHideStoreBroadcast")]
363    pub hide_store_broadcast: i32,
364}
365
366/// Full account details parsed from `https://store.steampowered.com/account/authorizeddevices`.
367#[derive(Debug, Clone, Serialize, Deserialize, Default)]
368pub struct AccountDetails {
369    /// Steam account name (`data-accountName`).
370    pub account_name: Option<String>,
371    /// Account email address (`data-email`).
372    pub email: Option<String>,
373    /// Masked phone number hint, empty string if none (`data-phone_hint`).
374    pub phone_hint: Option<String>,
375    /// Latest Steam Android app version string
376    /// (`data-latest_android_app_version`).
377    pub latest_android_app_version: Option<String>,
378    /// Token ID of the session currently browsing the page
379    /// (`data-requesting_token_id`).
380    pub requesting_token_id: Option<String>,
381    /// Sessions that are currently active / not revoked.
382    pub active_devices: Vec<AuthorizedDevice>,
383    /// Sessions that were previously revoked.
384    pub revoked_devices: Vec<AuthorizedDevice>,
385    /// Steam Guard / two-factor authenticator status.
386    pub two_factor_status: Option<TwoFactorStatus>,
387    /// Basic account info embedded in every Store page.
388    pub user_info: Option<PageUserInfo>,
389    /// Hardware / platform flags.
390    pub hw_info: Option<PageHwInfo>,
391    /// Page-level configuration (CDN URLs, build info, etc.).
392    pub page_config: Option<PageConfig>,
393    /// Store user config including the short-lived WebAPI token.
394    pub store_user_config: Option<StoreUserConfig>,
395    /// Steam notifications for this account.
396    pub notifications: Option<PageNotifications>,
397    /// Broadcast status for this account.
398    pub broadcast_user: Option<BroadcastUser>,
399    /// Wallet balance parsed from the page header.
400    pub wallet_balance: Option<WalletBalance>,
401    /// SHA-1 avatar hash (e.g. `"834966fea6a0a8a3b7011db7f96d38b51ee0ba64"`).
402    pub avatar_hash: Option<String>,
403    /// Country code from `data-userinfo` (e.g. `"VN"`, `"US"`).
404    pub country: Option<String>,
405}
406
407impl AccountDetails {
408    /// Human-readable Steam Guard status derived from
409    /// [`two_factor_status`](Self::two_factor_status).
410    ///
411    /// - `state == 1` → `"Steam Guard Mobile Authenticator"`
412    /// - `state == 0, email_validated == 1` → `"Steam Guard (Email)"`
413    /// - otherwise → `None`
414    pub fn account_security(&self) -> Option<String> {
415        let tf = self.two_factor_status.as_ref()?;
416        match tf.state {
417            1 => Some("Steam Guard Mobile Authenticator".to_string()),
418            0 if tf.email_validated == Some(1) => Some("Steam Guard (Email)".to_string()),
419            _ => None,
420        }
421    }
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    #[test]
429    fn test_deserialize_redeem_wallet_response() {
430        let json = r#"{"success":1,"detail":0,"formattednewwalletbalance":"132.500\u20ab"}"#;
431        let response: RedeemWalletCodeResponse = serde_json::from_str(json).unwrap();
432        assert_eq!(response.success, 1);
433        assert_eq!(response.detail, 0);
434        assert_eq!(response.formatted_new_wallet_balance.as_deref(), Some("132.500₫"));
435    }
436}