steam-user 0.1.0

Steam User web client for Rust - HTTP-based Steam Community interactions
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
//! Profile settings types.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use steamid::SteamID;

/// Privacy state for profile elements.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[repr(i32)]
#[derive(Default)]
pub enum PrivacyState {
    /// Only visible to the user.
    Private = 1,
    /// Visible to friends only.
    FriendsOnly = 2,
    /// Visible to everyone.
    #[default]
    Public = 3,
}

impl From<i32> for PrivacyState {
    fn from(value: i32) -> Self {
        match value {
            1 => Self::Private,
            2 => Self::FriendsOnly,
            3 => Self::Public,
            _ => Self::Public,
        }
    }
}

/// Profile editing settings.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProfileSettings {
    /// Display name.
    pub name: Option<String>,
    /// Real name.
    pub real_name: Option<String>,
    /// Profile summary/bio.
    pub summary: Option<String>,
    /// Country code.
    pub country: Option<String>,
    /// State/province code.
    pub state: Option<String>,
    /// City code.
    pub city: Option<String>,
    /// Custom URL (vanity URL).
    pub custom_url: Option<String>,
    /// Primary group SteamID.
    pub primary_group: Option<u64>,
}

/// Privacy settings for profile elements.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PrivacySettings {
    /// Profile visibility.
    pub profile: Option<PrivacyState>,
    /// Comment permissions (uses different values: 0=friends, 1=anyone,
    /// 2=private).
    pub comments: Option<i32>,
    /// Inventory visibility.
    pub inventory: Option<PrivacyState>,
    /// Gift inventory visibility.
    pub inventory_gifts: Option<PrivacyState>,
    /// Game details visibility.
    pub game_details: Option<PrivacyState>,
    /// Playtime visibility.
    pub playtime: Option<PrivacyState>,
    /// Friends list visibility.
    pub friends_list: Option<PrivacyState>,
}

impl PrivacySettings {
    /// Convert to the JSON format Steam expects.
    pub fn to_steam_format(&self) -> serde_json::Value {
        serde_json::json!({
            "PrivacyProfile": self.profile.unwrap_or_default() as i32,
            "PrivacyInventory": self.inventory.unwrap_or_default() as i32,
            "PrivacyInventoryGifts": self.inventory_gifts.map(|s| s as i32).unwrap_or(3),
            "PrivacyOwnedGames": self.game_details.unwrap_or_default() as i32,
            "PrivacyPlaytime": self.playtime.map(|s| s as i32).unwrap_or(3),
            "PrivacyFriendsList": self.friends_list.unwrap_or_default() as i32,
        })
    }

    /// Parse from Steam's internal response format.
    pub fn from_steam_json(json: &serde_json::Value) -> Option<Self> {
        let privacy = json.get("PrivacySettings").or_else(|| json.get("Privacy")?.get("PrivacySettings"))?;

        let comments = json.get("eCommentPermission").or_else(|| json.get("Privacy")?.get("eCommentPermission"))?.as_i64().map(|v| v as i32);

        Some(Self {
            profile: privacy.get("PrivacyProfile").and_then(|v| v.as_i64()).map(|v| PrivacyState::from(v as i32)),
            comments,
            inventory: privacy.get("PrivacyInventory").and_then(|v| v.as_i64()).map(|v| PrivacyState::from(v as i32)),
            inventory_gifts: privacy.get("PrivacyInventoryGifts").and_then(|v| v.as_i64()).map(|v| PrivacyState::from(v as i32)),
            game_details: privacy.get("PrivacyOwnedGames").and_then(|v| v.as_i64()).map(|v| PrivacyState::from(v as i32)),
            playtime: privacy.get("PrivacyPlaytime").and_then(|v| v.as_i64()).map(|v| PrivacyState::from(v as i32)),
            friends_list: privacy.get("PrivacyFriendsList").and_then(|v| v.as_i64()).map(|v| PrivacyState::from(v as i32)),
        })
    }

    /// Merge settings from another instance, keeping Some values.
    pub fn merge(&mut self, other: PrivacySettings) {
        if other.profile.is_some() {
            self.profile = other.profile;
        }
        if other.comments.is_some() {
            self.comments = other.comments;
        }
        if other.inventory.is_some() {
            self.inventory = other.inventory;
        }
        if other.inventory_gifts.is_some() {
            self.inventory_gifts = other.inventory_gifts;
        }
        if other.game_details.is_some() {
            self.game_details = other.game_details;
        }
        if other.playtime.is_some() {
            self.playtime = other.playtime;
        }
        if other.friends_list.is_some() {
            self.friends_list = other.friends_list;
        }
    }
}

/// Parsed profile information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SteamProfile {
    pub name: String,
    pub real_name: String,
    pub online_state: String,
    pub steam_id: SteamID,
    pub avatar_hash: String,
    pub avatar_frame: Option<String>,
    pub custom_url: String,
    pub location: String,
    pub summary: Option<String>,
    pub not_yet_setup: bool,
    pub profile_private_info: Option<String>,
    pub lobby_link: Option<String>,
    pub add_friend_enable: bool,
    pub is_private: bool,
    pub url: String,
    pub nickname: Option<String>,
    pub level: Option<u32>,
    pub day_last_ban: Option<i64>,
    pub game_ban: Option<GameBanData>,
    pub state_message_game: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GameBanData {
    #[serde(default)]
    pub is_vac_ban: BanStatus,
    #[serde(default)]
    pub is_game_ban: BanStatus,
    #[serde(default)]
    pub is_trade_ban: bool,
    pub days_since_last_ban: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[non_exhaustive]
pub enum BanStatus {
    #[default]
    None,
    Single,
    Multiple,
}

impl From<i32> for BanStatus {
    fn from(v: i32) -> Self {
        match v {
            1 => BanStatus::Single,
            _ => BanStatus::None,
        }
    }
}

/// Response from an avatar upload.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AvatarUploadResponse {
    /// The full URL of the uploaded avatar.
    pub url: String,
    /// The SHA hash of the uploaded avatar.
    pub hash: String,
}

/// Lightweight profile information returned by `resolve_users`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SteamUserProfile {
    #[serde(rename = "steamId")]
    pub steam_id: SteamID,
    #[serde(rename = "accountId")]
    pub account_id: u32,
    #[serde(rename = "persona_name")]
    pub name: String,
    #[serde(rename = "real_name")]
    pub real_name: String,
    #[serde(rename = "avatar_url")]
    pub avatar_hash: String,
    #[serde(rename = "profile_url")]
    pub custom_url: String,
    #[serde(rename = "persona_state")]
    pub persona_state: i32,
    pub city: String,
    pub state: String,
    pub country: String,
    #[serde(rename = "is_friend")]
    pub is_friend: bool,
    #[serde(rename = "friends_in_common")]
    pub friends_in_common: u32,
}

/// Entry in a user's avatar history.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AvatarHistoryEntry {
    /// The SHA1 hash of the avatar.
    pub avatar_sha1: String,
    /// Whether the avatar was uploaded by the user.
    pub user_uploaded: bool,
    /// The timestamp when the avatar was used.
    pub timestamp: u32,
}

/// User summary from XML profile endpoint (`/profiles/{steamid}/?xml=1`).
///
/// This represents the parsed data from Steam's XML profile format.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserSummaryXml {
    /// Display name (persona name).
    pub name: String,
    /// Real name if provided.
    pub real_name: Option<String>,
    /// 64-bit Steam ID.
    pub steam_id: SteamID,
    /// Online state: "offline", "online", "in-game".
    pub online_state: String,
    /// Parsed state message: "offline", "online", "in-game".
    pub state_message: String,
    /// Game name if in-game (Steam game).
    pub state_message_game: Option<String>,
    /// Game name if in non-Steam game.
    pub state_message_non_steam_game: Option<String>,
    /// Privacy state: "public", "private", "friendsonly".
    pub privacy_state: String,
    /// Visibility state: 1 (private), 2 (friends only), 3 (public).
    pub visibility_state: Option<i32>,
    /// Avatar hash (hash portion of avatar URL).
    pub avatar_hash: String,
    /// VAC ban status: 0 = no ban, 1+ = banned.
    pub vac_banned: Option<i32>,
    /// Trade ban state: "None" or ban description.
    pub trade_ban_state: Option<String>,
    /// Whether this is a limited account.
    pub is_limited_account: Option<bool>,
    /// Custom URL (vanity URL path).
    pub custom_url: Option<String>,
    /// Member since timestamp (Unix milliseconds).
    pub member_since: Option<i64>,
    /// Steam rating.
    pub steam_rating: Option<String>,
    /// Location string.
    pub location: Option<String>,
    /// Profile summary/bio.
    pub summary: Option<String>,
    /// Privacy message if profile is private.
    pub privacy_message: Option<String>,
    /// Whether profile has not been set up yet.
    pub not_yet_setup: bool,
}

/// User summary from HTML profile page (parsed).
///
/// This represents the parsed data from Steam's HTML profile page.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserSummaryProfile {
    /// Display name (persona name).
    pub name: String,
    /// Real name if provided.
    pub real_name: String,
    /// Online state: "offline", "online", "in-game".
    pub online_state: String,
    /// 64-bit Steam ID.
    pub steam_id: SteamID,
    /// Avatar hash.
    pub avatar_hash: String,
    /// Avatar frame URL if equipped.
    pub avatar_frame: Option<String>,
    /// Custom URL (vanity path).
    pub custom_url: String,
    /// Country location code.
    pub location: String,
    /// Profile summary/bio.
    pub summary: Option<String>,
    /// Whether profile has not been set up yet.
    pub not_yet_setup: bool,
    /// Private profile info message.
    pub profile_private_info: Option<String>,
    /// Steam lobby link if in a joinable lobby.
    pub lobby_link: Option<String>,
    /// Whether the "Add Friend" button is visible.
    pub add_friend_enable: bool,
    /// Whether the profile is private.
    pub is_private: bool,
    /// Full profile URL.
    pub url: String,
    /// Nickname set by the viewer for this user.
    pub nickname: Option<String>,
    /// Steam level.
    pub level: Option<u32>,
    /// Timestamp of last ban (Unix milliseconds).
    pub day_last_ban: Option<i64>,
    /// Game ban information.
    pub game_ban: Option<GameBanData>,
    /// Current game name if in-game.
    pub state_message_game: Option<String>,
}

/// Steam Community online state, as reported by the public profile XML.
///
/// `Other` covers any state Valve adds in the future without breaking
/// downstream code (Snooze, Looking to Trade, etc.).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OnlineState {
    Online,
    Offline,
    InGame,
    Snooze,
    Busy,
    Away,
    LookingToTrade,
    LookingToPlay,
    Other(String),
}

impl OnlineState {
    pub(crate) fn from_xml(s: &str) -> Self {
        match s {
            "online" => Self::Online,
            "offline" => Self::Offline,
            "in-game" => Self::InGame,
            "snooze" => Self::Snooze,
            "busy" => Self::Busy,
            "away" => Self::Away,
            "looking to trade" => Self::LookingToTrade,
            "looking to play" => Self::LookingToPlay,
            other => Self::Other(other.to_owned()),
        }
    }
}

/// Trade-ban status reported by Steam.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TradeBanState {
    None,
    Probation,
    Banned,
    Other(String),
}

impl TradeBanState {
    pub(crate) fn from_xml(s: &str) -> Self {
        match s {
            "None" => Self::None,
            "Probation" => Self::Probation,
            "Banned" => Self::Banned,
            other => Self::Other(other.to_owned()),
        }
    }
}

/// Snapshot of a Steam profile as returned by the public
/// `steamcommunity.com/id/{vanity}/?xml=1` (or `/profiles/{id}/?xml=1`) feed.
///
/// Anonymous; no API key required. Field availability depends on the
/// profile's privacy setting — fields wrapped in `Option` are populated only
/// for profiles where the corresponding section is public.
#[derive(Debug, Clone)]
pub struct PublicProfileSummary {
    /// 64-bit Steam ID.
    pub steam_id: SteamID,
    /// Display name (`<steamID>` in XML — confusingly named by Valve).
    pub persona_name: String,
    /// Online status (online / offline / in-game / …).
    pub online_state: OnlineState,
    /// Free-form status text (e.g. `"Last Online 3 days ago"`).
    pub state_message: String,
    /// Profile visibility — collapses Steam's redundant `<privacyState>`
    /// (string) and `<visibilityState>` (1/2/3) tags into one value.
    pub privacy_state: PrivacyState,
    /// Small (32x32) avatar URL.
    pub avatar_icon: String,
    /// Medium (64x64) avatar URL.
    pub avatar_medium: String,
    /// Full (184x184) avatar URL.
    pub avatar_full: String,
    /// VAC banned at least once.
    pub vac_banned: bool,
    /// Trade-ban state.
    pub trade_ban_state: TradeBanState,
    /// Limited account (no purchase activation, etc.).
    pub is_limited_account: bool,
    /// Custom URL slug (only for profiles that set one).
    pub custom_url: Option<String>,
    /// Account creation date. Steam serves this localized; the client pins
    /// `l=english`, so parsing as `"%B %d, %Y"` is stable. Stored as a
    /// `DateTime<Utc>` at midnight UTC (Steam reports day granularity only).
    /// `None` if absent or unparseable.
    pub member_since: Option<DateTime<Utc>>,
    /// Profile headline.
    pub headline: Option<String>,
    /// User-supplied location.
    pub location: Option<String>,
    /// Real name.
    pub real_name: Option<String>,
    /// Profile summary (HTML).
    pub summary: Option<String>,
    /// Hours played in last 2 weeks across all games.
    pub hours_played_2wk: Option<f32>,
}