1use std::collections::HashMap;
10
11use steamid::SteamID;
12
13use crate::{error::SteamError, SteamClient};
14
15#[derive(Debug, Clone)]
17pub struct AssetClassInfo {
18 pub appid: i32,
20 pub classid: u64,
22 pub instanceid: u64,
24 pub name: String,
26 pub market_hash_name: Option<String>,
28 pub market_name: Option<String>,
30 pub name_color: Option<String>,
32 pub background_color: Option<String>,
34 pub item_type: Option<String>,
36 pub icon_url: Option<String>,
38 pub icon_url_large: Option<String>,
40 pub tradable: bool,
42 pub marketable: bool,
44 pub commodity: bool,
46}
47
48#[derive(Debug, Clone)]
50pub struct AssetClass {
51 pub classid: u64,
53 pub instanceid: Option<u64>,
55}
56
57#[derive(Debug, Clone)]
59pub struct TradeUrl {
60 pub token: String,
62 pub url: String,
64}
65
66#[derive(Debug, Clone)]
68pub struct Emoticon {
69 pub name: String,
71 pub use_count: u32,
73 pub time_last_used: Option<u32>,
75 pub time_received: Option<u32>,
77 pub appid: Option<u32>,
79}
80
81#[derive(Debug, Clone)]
83pub struct ProfileItem {
84 pub communityitemid: u64,
86 pub image_large: Option<String>,
88 pub image_small: Option<String>,
90 pub name: Option<String>,
92 pub item_title: Option<String>,
94 pub item_description: Option<String>,
96 pub appid: Option<u32>,
98 pub item_type: Option<u32>,
100 pub item_class: Option<u32>,
102 pub movie_webm: Option<String>,
104 pub movie_mp4: Option<String>,
106}
107
108#[derive(Debug, Clone, Default)]
110pub struct OwnedProfileItems {
111 pub profile_backgrounds: Vec<ProfileItem>,
113 pub mini_profile_backgrounds: Vec<ProfileItem>,
115 pub avatar_frames: Vec<ProfileItem>,
117 pub animated_avatars: Vec<ProfileItem>,
119 pub profile_modifiers: Vec<ProfileItem>,
121}
122
123#[derive(Debug, Clone, Default)]
125pub struct EquippedProfileItems {
126 pub profile_background: Option<ProfileItem>,
128 pub mini_profile_background: Option<ProfileItem>,
130 pub avatar_frame: Option<ProfileItem>,
132 pub animated_avatar: Option<ProfileItem>,
134 pub profile_modifier: Option<ProfileItem>,
136}
137
138const STEAM_CDN_BASE: &str = "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/";
139
140impl SteamClient {
141 pub async fn get_asset_class_info(&mut self, language: &str, appid: u32, classes: Vec<AssetClass>) -> Result<HashMap<u64, AssetClassInfo>, SteamError> {
165 if !self.is_logged_in() {
166 return Err(SteamError::NotLoggedOn);
167 }
168
169 let request_classes: Vec<_> = classes.iter().map(|c| steam_protos::c_econ_get_asset_class_info_request::Class { classid: Some(c.classid), instanceid: c.instanceid }).collect();
170
171 let request = steam_protos::CEconGetAssetClassInfoRequest { language: Some(language.to_string()), appid: Some(appid), classes: request_classes };
172
173 let response: steam_protos::CEconGetAssetClassInfoResponse = self.send_unified_request_and_wait("Econ.GetAssetClassInfo#1", &request).await?;
174
175 let mut result = HashMap::new();
176 for description in response.descriptions {
177 if let Some(classid) = description.classid {
178 result.insert(
179 classid,
180 AssetClassInfo {
181 appid: description.appid.unwrap_or(0),
182 classid,
183 instanceid: description.instanceid.unwrap_or(0),
184 name: description.name.unwrap_or_default(),
185 market_hash_name: description.market_hash_name,
186 market_name: description.market_name,
187 name_color: description.name_color,
188 background_color: description.background_color,
189 item_type: description.r#type,
190 icon_url: description.icon_url.map(|s| format!("{}{}", STEAM_CDN_BASE, s)),
191 icon_url_large: description.icon_url_large.map(|s| format!("{}{}", STEAM_CDN_BASE, s)),
192 tradable: description.tradable.unwrap_or(false),
193 marketable: description.marketable.unwrap_or(false),
194 commodity: description.commodity.unwrap_or(false),
195 },
196 );
197 }
198 }
199
200 Ok(result)
201 }
202
203 pub async fn get_trade_url(&mut self) -> Result<TradeUrl, SteamError> {
215 if !self.is_logged_in() {
216 return Err(SteamError::NotLoggedOn);
217 }
218
219 let request = steam_protos::CEconGetTradeOfferAccessTokenRequest { generate_new_token: Some(false) };
220
221 let response: steam_protos::CEconGetTradeOfferAccessTokenResponse = self.send_unified_request_and_wait("Econ.GetTradeOfferAccessToken#1", &request).await?;
223
224 let token = response.trade_offer_access_token.unwrap_or_default();
225 let account_id = self.steam_id.map(|id| id.account_id).unwrap_or(0);
226
227 Ok(TradeUrl {
228 token: token.clone(),
229 url: format!("https://steamcommunity.com/tradeoffer/new/?partner={}&token={}", account_id, token),
230 })
231 }
232
233 pub async fn change_trade_url(&mut self) -> Result<TradeUrl, SteamError> {
244 if !self.is_logged_in() {
245 return Err(SteamError::NotLoggedOn);
246 }
247
248 let request = steam_protos::CEconGetTradeOfferAccessTokenRequest { generate_new_token: Some(true) };
249
250 let response: steam_protos::CEconGetTradeOfferAccessTokenResponse = self.send_unified_request_and_wait("Econ.GetTradeOfferAccessToken#1", &request).await?;
252
253 let token = response.trade_offer_access_token.unwrap_or_default();
254 let account_id = self.steam_id.map(|id| id.account_id).unwrap_or(0);
255
256 Ok(TradeUrl {
257 token: token.clone(),
258 url: format!("https://steamcommunity.com/tradeoffer/new/?partner={}&token={}", account_id, token),
259 })
260 }
261
262 pub async fn get_emoticon_list(&mut self) -> Result<HashMap<String, Emoticon>, SteamError> {
273 if !self.is_logged_in() {
274 return Err(SteamError::NotLoggedOn);
275 }
276
277 let request = steam_protos::CPlayerGetEmoticonListRequest {};
278
279 let response: steam_protos::CPlayerGetEmoticonListResponse = self.send_unified_request_and_wait("Player.GetEmoticonList#1", &request).await?;
281
282 let mut emoticons = HashMap::new();
283 for emoticon in response.emoticons {
284 if let Some(name) = emoticon.name {
285 let name_clone = name.clone();
286 emoticons.insert(
287 name_clone.clone(),
288 Emoticon {
289 name: name_clone,
290 use_count: emoticon.use_count.unwrap_or(0),
291 time_last_used: emoticon.time_last_used,
292 time_received: emoticon.time_received,
293 appid: emoticon.appid,
294 },
295 );
296 }
297 }
298
299 Ok(emoticons)
300 }
301
302 pub async fn get_owned_profile_items(&mut self, language: Option<&str>) -> Result<OwnedProfileItems, SteamError> {
320 if !self.is_logged_in() {
321 return Err(SteamError::NotLoggedOn);
322 }
323
324 let request = steam_protos::CPlayerGetProfileItemsOwnedRequest { language: Some(language.unwrap_or("english").to_string()) };
325
326 let response: steam_protos::CPlayerGetProfileItemsOwnedResponse = self.send_unified_request_and_wait("Player.GetProfileItemsOwned#1", &request).await?;
328
329 Ok(OwnedProfileItems {
330 profile_backgrounds: response.profile_backgrounds.iter().filter_map(process_profile_item).collect(),
331 mini_profile_backgrounds: response.mini_profile_backgrounds.iter().filter_map(process_profile_item).collect(),
332 avatar_frames: response.avatar_frames.iter().filter_map(process_profile_item).collect(),
333 animated_avatars: response.animated_avatars.iter().filter_map(process_profile_item).collect(),
334 profile_modifiers: response.profile_modifiers.iter().filter_map(process_profile_item).collect(),
335 })
336 }
337
338 pub async fn get_equipped_profile_items(&mut self, steam_id: SteamID, language: Option<&str>) -> Result<EquippedProfileItems, SteamError> {
358 if !self.is_logged_in() {
359 return Err(SteamError::NotLoggedOn);
360 }
361
362 let request = steam_protos::CPlayerGetProfileItemsEquippedRequest { steamid: Some(steam_id.steam_id64()), language: Some(language.unwrap_or("english").to_string()) };
363
364 let response: steam_protos::CPlayerGetProfileItemsEquippedResponse = self.send_unified_request_and_wait("Player.GetProfileItemsEquipped#1", &request).await?;
366
367 Ok(EquippedProfileItems {
368 profile_background: response.profile_background.as_ref().and_then(process_profile_item),
369 mini_profile_background: response.mini_profile_background.as_ref().and_then(process_profile_item),
370 avatar_frame: response.avatar_frame.as_ref().and_then(process_profile_item),
371 animated_avatar: response.animated_avatar.as_ref().and_then(process_profile_item),
372 profile_modifier: response.profile_modifier.as_ref().and_then(process_profile_item),
373 })
374 }
375
376 pub async fn set_profile_background(&mut self, background_asset_id: u64) -> Result<(), SteamError> {
392 if !self.is_logged_in() {
393 return Err(SteamError::NotLoggedOn);
394 }
395
396 let request = steam_protos::CPlayerSetProfileBackgroundRequest { communityitemid: Some(background_asset_id) };
397
398 self.send_service_method("Player.SetProfileBackground#1", &request).await
399 }
400}
401
402fn process_profile_item(item: &steam_protos::CPlayerProfileItem) -> Option<ProfileItem> {
407 item.communityitemid?;
409
410 Some(ProfileItem {
411 communityitemid: item.communityitemid.unwrap_or(0),
412 image_large: item.image_large.as_ref().map(|s| format!("{}{}", STEAM_CDN_BASE, s)),
413 image_small: item.image_small.as_ref().map(|s| format!("{}{}", STEAM_CDN_BASE, s)),
414 name: item.name.clone(),
415 item_title: item.item_title.clone(),
416 item_description: item.item_description.clone(),
417 appid: item.appid,
418 item_type: item.item_type,
419 item_class: item.item_class,
420 movie_webm: item.movie_webm.as_ref().map(|s| format!("{}{}", STEAM_CDN_BASE, s)),
421 movie_mp4: item.movie_mp4.as_ref().map(|s| format!("{}{}", STEAM_CDN_BASE, s)),
422 })
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 #[test]
430 fn test_trade_url_format() {
431 let url = TradeUrl {
432 token: "abc123".to_string(),
433 url: "https://steamcommunity.com/tradeoffer/new/?partner=12345&token=abc123".to_string(),
434 };
435 assert!(url.url.contains("partner=12345"));
436 assert!(url.url.contains("token=abc123"));
437 }
438}