Skip to main content

steam_user/types/
confirmation.rs

1//! Confirmation types for mobile trade/market confirmations.
2
3use serde::{Deserialize, Serialize};
4
5/// Type of confirmation.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
7#[non_exhaustive]
8#[repr(i32)]
9pub enum ConfirmationType {
10    /// Generic confirmation.
11    Generic = 1,
12    /// Trade offer confirmation.
13    Trade = 2,
14    /// Market listing confirmation.
15    MarketSell = 3,
16    /// Unknown type.
17    #[default]
18    Unknown = 0,
19}
20
21impl From<i32> for ConfirmationType {
22    fn from(value: i32) -> Self {
23        match value {
24            1 => Self::Generic,
25            2 => Self::Trade,
26            3 => Self::MarketSell,
27            _ => Self::Unknown,
28        }
29    }
30}
31
32/// A mobile confirmation requiring user action.
33#[derive(Debug, Clone, Serialize, Deserialize, Default)]
34pub struct Confirmation {
35    /// Unique confirmation ID.
36    pub id: String,
37    /// Confirmation type.
38    #[serde(alias = "type", default)]
39    pub conf_type: ConfirmationType,
40    /// Creator ID (trade offer ID for trades, listing ID for market).
41    #[serde(alias = "creator_id", default)]
42    pub creator: String,
43    /// Confirmation key (nonce).
44    #[serde(alias = "nonce", default)]
45    pub key: String,
46    /// Title/headline.
47    #[serde(alias = "headline", default)]
48    pub title: String,
49    /// What items/value you are receiving.
50    #[serde(default)]
51    pub receiving: String,
52    /// What items/value you are sending.
53    #[serde(default)]
54    pub sending: String,
55    /// Summary lines (GAS format: `["sending", "receiving"]`).
56    #[serde(default)]
57    pub summary: Vec<String>,
58    /// Creation time as ISO string.
59    #[serde(default)]
60    pub time: String,
61    /// Creation timestamp.
62    #[serde(alias = "creation_time", default)]
63    pub timestamp: u64,
64    /// Icon URL.
65    #[serde(default)]
66    pub icon: String,
67    /// Type name string (from GAS/API).
68    #[serde(default)]
69    pub type_name: Option<String>,
70}
71
72impl Confirmation {
73    /// Create a new Confirmation from API response data.
74    ///
75    /// Returns `Err(SteamUserError::MalformedResponse)` if a required field
76    /// (`id`, `type`, `creator_id`, `nonce`, `creation_time`) is missing or
77    /// has the wrong shape. Previously this returned `Option<Self>` and
78    /// silently masked typed parse failures behind `None`.
79    pub fn from_api(data: &serde_json::Value) -> Result<Self, crate::error::SteamUserError> {
80        use crate::error::SteamUserError;
81        let id = data.get("id").ok_or_else(|| SteamUserError::MalformedResponse("Confirmation: missing 'id'".into()))?.to_string().trim_matches('"').to_string();
82        let type_i64 = data.get("type").ok_or_else(|| SteamUserError::MalformedResponse("Confirmation: missing 'type'".into()))?.as_i64().ok_or_else(|| SteamUserError::MalformedResponse("Confirmation: 'type' is not an integer".into()))?;
83        let type_i32 = i32::try_from(type_i64).map_err(|_| SteamUserError::MalformedResponse(format!("Confirmation: 'type' {} out of i32 range", type_i64)))?;
84        let conf_type = ConfirmationType::from(type_i32);
85        let creator = data.get("creator_id").ok_or_else(|| SteamUserError::MalformedResponse("Confirmation: missing 'creator_id'".into()))?.to_string().trim_matches('"').to_string();
86        let key = data.get("nonce").and_then(|v| v.as_str()).ok_or_else(|| SteamUserError::MalformedResponse("Confirmation: missing or non-string 'nonce'".into()))?.to_string();
87
88        let type_name = data.get("type_name").and_then(|v| v.as_str()).unwrap_or("Confirm");
89        let headline = data.get("headline").and_then(|v| v.as_str()).unwrap_or("");
90        let title = format!("{} - {}", type_name, headline);
91
92        let summary = data.get("summary").and_then(|v| v.as_array());
93        let sending = summary.and_then(|arr| arr.first()).and_then(|v| v.as_str()).unwrap_or("").to_string();
94        let receiving = if conf_type == ConfirmationType::Trade { summary.and_then(|arr| arr.get(1)).and_then(|v| v.as_str()).unwrap_or("").to_string() } else { String::new() };
95
96        let creation_time = data.get("creation_time").ok_or_else(|| SteamUserError::MalformedResponse("Confirmation: missing 'creation_time'".into()))?.as_u64().ok_or_else(|| SteamUserError::MalformedResponse("Confirmation: 'creation_time' is not a u64".into()))?;
97        let time = chrono_timestamp_to_iso(creation_time);
98
99        let icon = data.get("icon").and_then(|v| v.as_str()).unwrap_or("").to_string();
100
101        let type_name_str = Some(type_name.to_string());
102        let summary_arr = summary.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect()).unwrap_or_default();
103
104        Ok(Self { id, conf_type, creator, key, title, receiving, sending, summary: summary_arr, time, timestamp: creation_time, icon, type_name: type_name_str })
105    }
106
107    /// Get the trade offer ID if this is a trade confirmation.
108    pub fn offer_id(&self) -> Option<&str> {
109        if self.conf_type == ConfirmationType::Trade {
110            Some(&self.creator)
111        } else {
112            None
113        }
114    }
115}
116
117/// Convert Unix timestamp to ISO 8601 string.
118fn chrono_timestamp_to_iso(timestamp: u64) -> String {
119    use std::time::{Duration, UNIX_EPOCH};
120    let datetime = UNIX_EPOCH + Duration::from_secs(timestamp);
121    // Simple ISO format without chrono dependency
122    format!("{:?}", datetime)
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_confirmation_type_from() {
131        assert_eq!(ConfirmationType::from(1), ConfirmationType::Generic);
132        assert_eq!(ConfirmationType::from(2), ConfirmationType::Trade);
133        assert_eq!(ConfirmationType::from(3), ConfirmationType::MarketSell);
134        assert_eq!(ConfirmationType::from(99), ConfirmationType::Unknown);
135    }
136
137    #[test]
138    fn test_confirmation_from_api() {
139        let json = serde_json::json!({
140            "id": "12345",
141            "type": 2,
142            "creator_id": "67890",
143            "nonce": "abc123",
144            "type_name": "Trade",
145            "headline": "Test Trade",
146            "summary": ["1 Key", "2 Ref"],
147            "creation_time": 1609459200,
148            "icon": "https://example.com/icon.png"
149        });
150
151        let conf = Confirmation::from_api(&json).unwrap();
152        assert_eq!(conf.id, "12345");
153        assert_eq!(conf.conf_type, ConfirmationType::Trade);
154        assert_eq!(conf.creator, "67890");
155        assert_eq!(conf.sending, "1 Key");
156        assert_eq!(conf.receiving, "2 Ref");
157        assert_eq!(conf.offer_id(), Some("67890"));
158    }
159}