Skip to main content

steam_user/utils/
avatar.rs

1//! Avatar URL and hash utilities for Steam profiles.
2
3/// Avatar size options.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum AvatarSize {
6    /// Small size (default, no suffix).
7    #[default]
8    Small,
9    /// Medium size (suffix: `_medium`).
10    Medium,
11    /// Full size (suffix: `_full`).
12    Full,
13}
14
15/// Constructs a Steam avatar URL from a hash and optional size.
16///
17/// # Arguments
18/// * `hash` - The avatar hash string (e.g.,
19///   `fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb`).
20/// * `size` - The desired avatar size.
21///
22/// # Returns
23/// The full avatar URL, or `None` if the hash is empty.
24///
25/// # Examples
26/// ```
27/// use steam_user::utils::avatar::{get_avatar_url_from_hash, AvatarSize};
28///
29/// let url = get_avatar_url_from_hash("abc123", AvatarSize::Full);
30/// assert_eq!(
31///     url,
32///     Some("https://avatars.akamai.steamstatic.com/abc123_full.jpg".to_string())
33/// );
34///
35/// let url_small = get_avatar_url_from_hash("abc123", AvatarSize::Small);
36/// assert_eq!(
37///     url_small,
38///     Some("https://avatars.akamai.steamstatic.com/abc123.jpg".to_string())
39/// );
40/// ```
41pub fn get_avatar_url_from_hash(hash: &str, size: AvatarSize) -> Option<String> {
42    if hash.is_empty() {
43        return None;
44    }
45
46    let base_url = "https://avatars.akamai.steamstatic.com/";
47    let suffix = match size {
48        AvatarSize::Full => "_full.jpg",
49        AvatarSize::Medium => "_medium.jpg",
50        AvatarSize::Small => ".jpg",
51    };
52
53    Some(format!("{}{}{}", base_url, hash, suffix))
54}
55
56/// Extracts the avatar hash from a Steam avatar URL.
57///
58/// # Arguments
59/// * `url` - The full avatar URL.
60///
61/// # Returns
62/// The extracted hash, or `None` if the URL doesn't contain a valid avatar
63/// hash.
64///
65/// # Examples
66/// ```
67/// use steam_user::utils::avatar::get_avatar_hash_from_url;
68///
69/// let hash = get_avatar_hash_from_url("https://avatars.akamai.steamstatic.com/abc123_full.jpg");
70/// assert_eq!(hash, Some("abc123".to_string()));
71///
72/// let hash_medium =
73///     get_avatar_hash_from_url("https://avatars.akamai.steamstatic.com/xyz789_medium.jpg");
74/// assert_eq!(hash_medium, Some("xyz789".to_string()));
75/// ```
76pub fn get_avatar_hash_from_url(url: &str) -> Option<String> {
77    if url.is_empty() {
78        return None;
79    }
80
81    const SUFFIXES: [&str; 3] = ["_full.jpg", "_medium.jpg", ".jpg"];
82
83    for suffix in SUFFIXES {
84        if let Some(before_suffix) = url.strip_suffix(suffix) {
85            // Get the last path segment (the hash)
86            if let Some(hash) = before_suffix.rsplit('/').next() {
87                if !hash.is_empty() {
88                    return Some(hash.to_string());
89                }
90            }
91        }
92    }
93
94    None
95}
96
97/// Extracts an avatar hash from multiple URLs, returning the first valid hash
98/// found.
99///
100/// # Arguments
101/// * `urls` - A slice of avatar URLs to check.
102///
103/// # Returns
104/// The first valid hash found, or `None` if no valid hash is found in any URL.
105///
106/// # Examples
107/// ```
108/// use steam_user::utils::avatar::get_avatar_hash_from_multiple_urls;
109///
110/// let urls = vec![
111///     "",
112///     "invalid_url",
113///     "https://avatars.akamai.steamstatic.com/abc123_full.jpg",
114///     "https://avatars.akamai.steamstatic.com/xyz789_medium.jpg",
115/// ];
116/// let hash = get_avatar_hash_from_multiple_urls(&urls);
117/// assert_eq!(hash, Some("abc123".to_string()));
118/// ```
119pub fn get_avatar_hash_from_multiple_urls(urls: &[&str]) -> Option<String> {
120    for url in urls {
121        if let Some(hash) = get_avatar_hash_from_url(url) {
122            return Some(hash);
123        }
124    }
125    None
126}
127
128/// Extracts the custom vanity URL segment from a Steam Community profile URL.
129///
130/// # Arguments
131/// * `url` - A Steam Community profile URL (e.g. `https://steamcommunity.com/id/gaben/`).
132///
133/// # Returns
134/// The custom URL slug (e.g. `"gaben"`), or `None` if the URL uses a numeric
135/// `/profiles/` path.
136pub(crate) fn extract_custom_url(url: &str) -> Option<String> {
137    url.find("/id/").map(|start| url[start + 4..].trim_matches('/').to_string())
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_get_avatar_url_from_hash() {
146        // Test full size
147        assert_eq!(get_avatar_url_from_hash("fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb", AvatarSize::Full), Some("https://avatars.akamai.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg".to_string()));
148
149        // Test medium size
150        assert_eq!(get_avatar_url_from_hash("abc123", AvatarSize::Medium), Some("https://avatars.akamai.steamstatic.com/abc123_medium.jpg".to_string()));
151
152        // Test small size (default)
153        assert_eq!(get_avatar_url_from_hash("abc123", AvatarSize::Small), Some("https://avatars.akamai.steamstatic.com/abc123.jpg".to_string()));
154
155        // Test empty hash
156        assert_eq!(get_avatar_url_from_hash("", AvatarSize::Full), None);
157    }
158
159    #[test]
160    fn test_get_avatar_hash_from_url() {
161        // Test full size URL
162        assert_eq!(get_avatar_hash_from_url("https://avatars.akamai.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg"), Some("fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb".to_string()));
163
164        // Test medium size URL
165        assert_eq!(get_avatar_hash_from_url("https://avatars.akamai.steamstatic.com/abc123_medium.jpg"), Some("abc123".to_string()));
166
167        // Test small size URL
168        assert_eq!(get_avatar_hash_from_url("https://avatars.akamai.steamstatic.com/xyz789.jpg"), Some("xyz789".to_string()));
169
170        // Test empty URL
171        assert_eq!(get_avatar_hash_from_url(""), None);
172
173        // Test invalid URL (no suffix)
174        assert_eq!(get_avatar_hash_from_url("https://example.com/image.png"), None);
175    }
176
177    #[test]
178    fn test_get_avatar_hash_from_multiple_urls() {
179        // Test with valid URLs
180        let urls = vec!["", "invalid_url", "https://avatars.akamai.steamstatic.com/abc123_full.jpg", "https://avatars.akamai.steamstatic.com/xyz789_medium.jpg"];
181        assert_eq!(get_avatar_hash_from_multiple_urls(&urls), Some("abc123".to_string()));
182
183        // Test with only invalid URLs
184        let invalid_urls = vec!["", "invalid", "https://example.com/image.png"];
185        assert_eq!(get_avatar_hash_from_multiple_urls(&invalid_urls), None);
186
187        // Test with empty slice
188        let empty: Vec<&str> = vec![];
189        assert_eq!(get_avatar_hash_from_multiple_urls(&empty), None);
190    }
191}