steam-user 0.1.0

Steam User web client for Rust - HTTP-based Steam Community interactions
Documentation
//! Confirmation types for mobile trade/market confirmations.

use serde::{Deserialize, Serialize};

/// Type of confirmation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
#[repr(i32)]
pub enum ConfirmationType {
    /// Generic confirmation.
    Generic = 1,
    /// Trade offer confirmation.
    Trade = 2,
    /// Market listing confirmation.
    MarketSell = 3,
    /// Unknown type.
    #[default]
    Unknown = 0,
}

impl From<i32> for ConfirmationType {
    fn from(value: i32) -> Self {
        match value {
            1 => Self::Generic,
            2 => Self::Trade,
            3 => Self::MarketSell,
            _ => Self::Unknown,
        }
    }
}

/// A mobile confirmation requiring user action.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Confirmation {
    /// Unique confirmation ID.
    pub id: String,
    /// Confirmation type.
    #[serde(alias = "type", default)]
    pub conf_type: ConfirmationType,
    /// Creator ID (trade offer ID for trades, listing ID for market).
    #[serde(alias = "creator_id", default)]
    pub creator: String,
    /// Confirmation key (nonce).
    #[serde(alias = "nonce", default)]
    pub key: String,
    /// Title/headline.
    #[serde(alias = "headline", default)]
    pub title: String,
    /// What items/value you are receiving.
    #[serde(default)]
    pub receiving: String,
    /// What items/value you are sending.
    #[serde(default)]
    pub sending: String,
    /// Summary lines (GAS format: `["sending", "receiving"]`).
    #[serde(default)]
    pub summary: Vec<String>,
    /// Creation time as ISO string.
    #[serde(default)]
    pub time: String,
    /// Creation timestamp.
    #[serde(alias = "creation_time", default)]
    pub timestamp: u64,
    /// Icon URL.
    #[serde(default)]
    pub icon: String,
    /// Type name string (from GAS/API).
    #[serde(default)]
    pub type_name: Option<String>,
}

impl Confirmation {
    /// Create a new Confirmation from API response data.
    ///
    /// Returns `Err(SteamUserError::MalformedResponse)` if a required field
    /// (`id`, `type`, `creator_id`, `nonce`, `creation_time`) is missing or
    /// has the wrong shape. Previously this returned `Option<Self>` and
    /// silently masked typed parse failures behind `None`.
    pub fn from_api(data: &serde_json::Value) -> Result<Self, crate::error::SteamUserError> {
        use crate::error::SteamUserError;
        let id = data.get("id").ok_or_else(|| SteamUserError::MalformedResponse("Confirmation: missing 'id'".into()))?.to_string().trim_matches('"').to_string();
        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()))?;
        let type_i32 = i32::try_from(type_i64).map_err(|_| SteamUserError::MalformedResponse(format!("Confirmation: 'type' {} out of i32 range", type_i64)))?;
        let conf_type = ConfirmationType::from(type_i32);
        let creator = data.get("creator_id").ok_or_else(|| SteamUserError::MalformedResponse("Confirmation: missing 'creator_id'".into()))?.to_string().trim_matches('"').to_string();
        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();

        let type_name = data.get("type_name").and_then(|v| v.as_str()).unwrap_or("Confirm");
        let headline = data.get("headline").and_then(|v| v.as_str()).unwrap_or("");
        let title = format!("{} - {}", type_name, headline);

        let summary = data.get("summary").and_then(|v| v.as_array());
        let sending = summary.and_then(|arr| arr.first()).and_then(|v| v.as_str()).unwrap_or("").to_string();
        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() };

        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()))?;
        let time = chrono_timestamp_to_iso(creation_time);

        let icon = data.get("icon").and_then(|v| v.as_str()).unwrap_or("").to_string();

        let type_name_str = Some(type_name.to_string());
        let summary_arr = summary.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect()).unwrap_or_default();

        Ok(Self { id, conf_type, creator, key, title, receiving, sending, summary: summary_arr, time, timestamp: creation_time, icon, type_name: type_name_str })
    }

    /// Get the trade offer ID if this is a trade confirmation.
    pub fn offer_id(&self) -> Option<&str> {
        if self.conf_type == ConfirmationType::Trade {
            Some(&self.creator)
        } else {
            None
        }
    }
}

/// Convert Unix timestamp to ISO 8601 string.
fn chrono_timestamp_to_iso(timestamp: u64) -> String {
    use std::time::{Duration, UNIX_EPOCH};
    let datetime = UNIX_EPOCH + Duration::from_secs(timestamp);
    // Simple ISO format without chrono dependency
    format!("{:?}", datetime)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_confirmation_type_from() {
        assert_eq!(ConfirmationType::from(1), ConfirmationType::Generic);
        assert_eq!(ConfirmationType::from(2), ConfirmationType::Trade);
        assert_eq!(ConfirmationType::from(3), ConfirmationType::MarketSell);
        assert_eq!(ConfirmationType::from(99), ConfirmationType::Unknown);
    }

    #[test]
    fn test_confirmation_from_api() {
        let json = serde_json::json!({
            "id": "12345",
            "type": 2,
            "creator_id": "67890",
            "nonce": "abc123",
            "type_name": "Trade",
            "headline": "Test Trade",
            "summary": ["1 Key", "2 Ref"],
            "creation_time": 1609459200,
            "icon": "https://example.com/icon.png"
        });

        let conf = Confirmation::from_api(&json).unwrap();
        assert_eq!(conf.id, "12345");
        assert_eq!(conf.conf_type, ConfirmationType::Trade);
        assert_eq!(conf.creator, "67890");
        assert_eq!(conf.sending, "1 Key");
        assert_eq!(conf.receiving, "2 Ref");
        assert_eq!(conf.offer_id(), Some("67890"));
    }
}