steam-user 0.1.0

Steam User web client for Rust - HTTP-based Steam Community interactions
Documentation
//! Avatar URL and hash utilities for Steam profiles.

/// Avatar size options.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AvatarSize {
    /// Small size (default, no suffix).
    #[default]
    Small,
    /// Medium size (suffix: `_medium`).
    Medium,
    /// Full size (suffix: `_full`).
    Full,
}

/// Constructs a Steam avatar URL from a hash and optional size.
///
/// # Arguments
/// * `hash` - The avatar hash string (e.g.,
///   `fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb`).
/// * `size` - The desired avatar size.
///
/// # Returns
/// The full avatar URL, or `None` if the hash is empty.
///
/// # Examples
/// ```
/// use steam_user::utils::avatar::{get_avatar_url_from_hash, AvatarSize};
///
/// let url = get_avatar_url_from_hash("abc123", AvatarSize::Full);
/// assert_eq!(
///     url,
///     Some("https://avatars.akamai.steamstatic.com/abc123_full.jpg".to_string())
/// );
///
/// let url_small = get_avatar_url_from_hash("abc123", AvatarSize::Small);
/// assert_eq!(
///     url_small,
///     Some("https://avatars.akamai.steamstatic.com/abc123.jpg".to_string())
/// );
/// ```
pub fn get_avatar_url_from_hash(hash: &str, size: AvatarSize) -> Option<String> {
    if hash.is_empty() {
        return None;
    }

    let base_url = "https://avatars.akamai.steamstatic.com/";
    let suffix = match size {
        AvatarSize::Full => "_full.jpg",
        AvatarSize::Medium => "_medium.jpg",
        AvatarSize::Small => ".jpg",
    };

    Some(format!("{}{}{}", base_url, hash, suffix))
}

/// Extracts the avatar hash from a Steam avatar URL.
///
/// # Arguments
/// * `url` - The full avatar URL.
///
/// # Returns
/// The extracted hash, or `None` if the URL doesn't contain a valid avatar
/// hash.
///
/// # Examples
/// ```
/// use steam_user::utils::avatar::get_avatar_hash_from_url;
///
/// let hash = get_avatar_hash_from_url("https://avatars.akamai.steamstatic.com/abc123_full.jpg");
/// assert_eq!(hash, Some("abc123".to_string()));
///
/// let hash_medium =
///     get_avatar_hash_from_url("https://avatars.akamai.steamstatic.com/xyz789_medium.jpg");
/// assert_eq!(hash_medium, Some("xyz789".to_string()));
/// ```
pub fn get_avatar_hash_from_url(url: &str) -> Option<String> {
    if url.is_empty() {
        return None;
    }

    const SUFFIXES: [&str; 3] = ["_full.jpg", "_medium.jpg", ".jpg"];

    for suffix in SUFFIXES {
        if let Some(before_suffix) = url.strip_suffix(suffix) {
            // Get the last path segment (the hash)
            if let Some(hash) = before_suffix.rsplit('/').next() {
                if !hash.is_empty() {
                    return Some(hash.to_string());
                }
            }
        }
    }

    None
}

/// Extracts an avatar hash from multiple URLs, returning the first valid hash
/// found.
///
/// # Arguments
/// * `urls` - A slice of avatar URLs to check.
///
/// # Returns
/// The first valid hash found, or `None` if no valid hash is found in any URL.
///
/// # Examples
/// ```
/// use steam_user::utils::avatar::get_avatar_hash_from_multiple_urls;
///
/// let urls = vec![
///     "",
///     "invalid_url",
///     "https://avatars.akamai.steamstatic.com/abc123_full.jpg",
///     "https://avatars.akamai.steamstatic.com/xyz789_medium.jpg",
/// ];
/// let hash = get_avatar_hash_from_multiple_urls(&urls);
/// assert_eq!(hash, Some("abc123".to_string()));
/// ```
pub fn get_avatar_hash_from_multiple_urls(urls: &[&str]) -> Option<String> {
    for url in urls {
        if let Some(hash) = get_avatar_hash_from_url(url) {
            return Some(hash);
        }
    }
    None
}

/// Extracts the custom vanity URL segment from a Steam Community profile URL.
///
/// # Arguments
/// * `url` - A Steam Community profile URL (e.g. `https://steamcommunity.com/id/gaben/`).
///
/// # Returns
/// The custom URL slug (e.g. `"gaben"`), or `None` if the URL uses a numeric
/// `/profiles/` path.
pub(crate) fn extract_custom_url(url: &str) -> Option<String> {
    url.find("/id/").map(|start| url[start + 4..].trim_matches('/').to_string())
}

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

    #[test]
    fn test_get_avatar_url_from_hash() {
        // Test full size
        assert_eq!(get_avatar_url_from_hash("fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb", AvatarSize::Full), Some("https://avatars.akamai.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg".to_string()));

        // Test medium size
        assert_eq!(get_avatar_url_from_hash("abc123", AvatarSize::Medium), Some("https://avatars.akamai.steamstatic.com/abc123_medium.jpg".to_string()));

        // Test small size (default)
        assert_eq!(get_avatar_url_from_hash("abc123", AvatarSize::Small), Some("https://avatars.akamai.steamstatic.com/abc123.jpg".to_string()));

        // Test empty hash
        assert_eq!(get_avatar_url_from_hash("", AvatarSize::Full), None);
    }

    #[test]
    fn test_get_avatar_hash_from_url() {
        // Test full size URL
        assert_eq!(get_avatar_hash_from_url("https://avatars.akamai.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg"), Some("fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb".to_string()));

        // Test medium size URL
        assert_eq!(get_avatar_hash_from_url("https://avatars.akamai.steamstatic.com/abc123_medium.jpg"), Some("abc123".to_string()));

        // Test small size URL
        assert_eq!(get_avatar_hash_from_url("https://avatars.akamai.steamstatic.com/xyz789.jpg"), Some("xyz789".to_string()));

        // Test empty URL
        assert_eq!(get_avatar_hash_from_url(""), None);

        // Test invalid URL (no suffix)
        assert_eq!(get_avatar_hash_from_url("https://example.com/image.png"), None);
    }

    #[test]
    fn test_get_avatar_hash_from_multiple_urls() {
        // Test with valid URLs
        let urls = vec!["", "invalid_url", "https://avatars.akamai.steamstatic.com/abc123_full.jpg", "https://avatars.akamai.steamstatic.com/xyz789_medium.jpg"];
        assert_eq!(get_avatar_hash_from_multiple_urls(&urls), Some("abc123".to_string()));

        // Test with only invalid URLs
        let invalid_urls = vec!["", "invalid", "https://example.com/image.png"];
        assert_eq!(get_avatar_hash_from_multiple_urls(&invalid_urls), None);

        // Test with empty slice
        let empty: Vec<&str> = vec![];
        assert_eq!(get_avatar_hash_from_multiple_urls(&empty), None);
    }
}