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}