Skip to main content

steam_client/services/
gifts.rs

1//! Steam Gift redemption features.
2//!
3//! This module provides functionality for redeeming gifts from the Steam
4//! inventory:
5//! - Get gift details (validate before redeeming)
6//! - Redeem a gift to your library
7
8use serde::Deserialize;
9
10use crate::{error::SteamError, SteamClient};
11
12/// Gift details from validation.
13#[derive(Debug, Clone)]
14pub struct GiftDetails {
15    /// Name of the gift/game
16    pub gift_name: String,
17    /// Package ID
18    pub package_id: u32,
19    /// Whether the gift is already owned
20    pub owned: bool,
21}
22
23/// Internal response structure for gift validation.
24#[derive(Debug, Deserialize)]
25struct GiftValidateResponse {
26    success: Option<i32>,
27    gift_name: Option<String>,
28    #[serde(deserialize_with = "deserialize_id")]
29    packageid: Option<String>,
30    owned: Option<bool>,
31    message: Option<String>,
32}
33
34/// Helper to deserialize fields that could be strings or numbers.
35fn deserialize_id<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
36where
37    D: serde::Deserializer<'de>,
38{
39    use serde::Deserialize;
40    let v: serde_json::Value = Deserialize::deserialize(deserializer)?;
41    match v {
42        serde_json::Value::String(s) => Ok(Some(s)),
43        serde_json::Value::Number(n) => Ok(Some(n.to_string())),
44        serde_json::Value::Null => Ok(None),
45        _ => Err(serde::de::Error::custom("expected string or number")),
46    }
47}
48
49/// Internal response structure for gift redemption.
50#[derive(Debug, Deserialize)]
51struct GiftRedeemResponse {
52    success: Option<i32>,
53    message: Option<String>,
54}
55
56/// Extract session ID from cookies string.
57///
58/// The sessionid is typically stored as `sessionid=<value>` in the cookie
59/// string.
60fn extract_session_id(cookies: &str) -> Option<String> {
61    for part in cookies.split(';') {
62        let part = part.trim();
63        if let Some(value) = part.strip_prefix("sessionid=") {
64            return Some(value.to_string());
65        }
66    }
67    None
68}
69
70impl SteamClient {
71    /// Get details about a gift in your inventory.
72    ///
73    /// This validates the gift and returns information about it before
74    /// redemption.
75    ///
76    /// # Arguments
77    ///
78    /// * `gift_id` - The gift ID from the inventory
79    /// * `cookies` - Web session cookies (e.g.,
80    ///   "sessionid=xxx;steamLoginSecure=yyy")
81    ///
82    /// # Example
83    ///
84    /// ```rust,ignore
85    /// let details = client.get_gift_details("1234567890", &cookies).await?;
86    /// tracing::info!("Gift: {} (Package {})", details.gift_name, details.package_id);
87    /// if details.owned {
88    ///     tracing::info!("Warning: You already own this game!");
89    /// }
90    /// ```
91    pub async fn get_gift_details(&self, gift_id: &str, cookies: &str) -> Result<GiftDetails, SteamError> {
92        let session_id = extract_session_id(cookies).ok_or_else(|| SteamError::Other("No sessionid found in cookies".to_string()))?;
93
94        let url = format!("https://steamcommunity.com/gifts/{}/validateunpack", gift_id);
95
96        let response = self.http_client.post_form_with_cookies(&url, &[("sessionid", session_id.as_str())], cookies).await?;
97
98        if !response.is_success() {
99            return Err(SteamError::Other(format!("HTTP error: {}", response.status)));
100        }
101
102        let body: GiftValidateResponse = response.json().map_err(|e| SteamError::ProtocolError(format!("Failed to parse response: {}", e)))?;
103
104        // Check success (1 = OK in Steam's EResult)
105        if body.success != Some(1) {
106            return Err(SteamError::Other(body.message.unwrap_or_else(|| "Unknown error validating gift".to_string())));
107        }
108
109        let gift_name = body.gift_name.ok_or_else(|| SteamError::ProtocolError("Missing gift_name in response".to_string()))?;
110        let package_id: u32 = body.packageid.ok_or_else(|| SteamError::ProtocolError("Missing packageid in response".to_string()))?.parse().map_err(|_| SteamError::ProtocolError("Invalid packageid".to_string()))?;
111
112        Ok(GiftDetails { gift_name, package_id, owned: body.owned.unwrap_or(false) })
113    }
114
115    /// Redeem a gift from your inventory to your library.
116    ///
117    /// This unpacks the gift and adds the game to your Steam library.
118    ///
119    /// # Arguments
120    ///
121    /// * `gift_id` - The gift ID from the inventory
122    /// * `cookies` - Web session cookies (e.g.,
123    ///   "sessionid=xxx;steamLoginSecure=yyy")
124    ///
125    /// # Example
126    ///
127    /// ```rust,ignore
128    /// // First validate the gift
129    /// let details = client.get_gift_details("1234567890", &cookies).await?;
130    /// if !details.owned {
131    ///     client.redeem_gift("1234567890", &cookies).await?;
132    ///     tracing::info!("Redeemed: {}", details.gift_name);
133    /// }
134    /// ```
135    pub async fn redeem_gift(&self, gift_id: &str, cookies: &str) -> Result<(), SteamError> {
136        let session_id = extract_session_id(cookies).ok_or_else(|| SteamError::Other("No sessionid found in cookies".to_string()))?;
137
138        let url = format!("https://steamcommunity.com/gifts/{}/unpack", gift_id);
139
140        let response = self.http_client.post_form_with_cookies(&url, &[("sessionid", session_id.as_str())], cookies).await?;
141
142        if !response.is_success() {
143            return Err(SteamError::Other(format!("HTTP error: {}", response.status)));
144        }
145
146        let body: GiftRedeemResponse = response.json().map_err(|e| SteamError::ProtocolError(format!("Failed to parse response: {}", e)))?;
147
148        // Check success (1 = OK in Steam's EResult)
149        if body.success != Some(1) {
150            return Err(SteamError::Other(body.message.unwrap_or_else(|| "Unknown error redeeming gift".to_string())));
151        }
152
153        Ok(())
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_extract_session_id() {
163        let cookies = "sessionid=abc123; steamLoginSecure=xyz789";
164        assert_eq!(extract_session_id(cookies), Some("abc123".to_string()));
165
166        let cookies_no_session = "steamLoginSecure=xyz789";
167        assert_eq!(extract_session_id(cookies_no_session), None);
168
169        let cookies_with_spaces = " sessionid=def456 ; other=value";
170        assert_eq!(extract_session_id(cookies_with_spaces), Some("def456".to_string()));
171    }
172
173    #[test]
174    fn test_gift_details_struct() {
175        let details = GiftDetails { gift_name: "Test Game".to_string(), package_id: 12345, owned: false };
176        assert_eq!(details.gift_name, "Test Game");
177        assert_eq!(details.package_id, 12345);
178        assert!(!details.owned);
179    }
180}