1use std::{collections::HashMap, sync::OnceLock};
4
5use regex::Regex;
6use scraper::{Html, Selector};
7
8use crate::{
9 client::SteamUser,
10 endpoint::steam_endpoint,
11 error::SteamUserError,
12 services::account::parse_wallet_balance,
13 types::{AppId, AssetId, BoosterPackEntry, BoosterResult, EconItem, GemResult, GemValue, ItemNameId, ItemOrdersHistogramResponse, MarketHistoryListing, MarketHistoryResponse, MarketListing, MarketRestrictions, SellItemResult, WalletBalance},
14};
15
16static LISTING_ID_RE: OnceLock<Regex> = OnceLock::new();
17static HOVER_RE: OnceLock<Regex> = OnceLock::new();
18static LISTING_ROW_RE: OnceLock<Regex> = OnceLock::new();
19static BOOSTER_DATA_RE: OnceLock<Regex> = OnceLock::new();
20static ITEM_NAMEID_RE: OnceLock<Regex> = OnceLock::new();
21
22fn get_listing_id_re() -> &'static Regex {
23 LISTING_ID_RE.get_or_init(|| Regex::new(r"history_row_(.*?)_").expect("Invalid regex"))
24}
25
26fn get_hover_re() -> &'static Regex {
27 HOVER_RE.get_or_init(|| Regex::new(r"CreateItemHoverFromContainer\s*\(\s*g_rgAssets\s*,\s*'([^']+)'\s*,\s*(\d+)\s*,\s*'(\d+)'\s*,\s*'(\d+)'\s*,\s*(\d+)\s*\)").expect("Invalid regex"))
28}
29
30fn get_listing_row_re() -> &'static Regex {
31 LISTING_ROW_RE.get_or_init(|| Regex::new(r"history_row_(\d+)_").expect("Invalid regex"))
32}
33
34fn get_booster_data_re() -> &'static Regex {
35 BOOSTER_DATA_RE.get_or_init(|| Regex::new(r"CBoosterCreatorPage\.Init\(\s*\d+\s*,\s*(\[.+?\])\s*,").expect("Invalid regex"))
36}
37
38fn get_item_nameid_re() -> &'static Regex {
39 ITEM_NAMEID_RE.get_or_init(|| Regex::new(r"Market_LoadOrderSpread\(\s*(\d+)\s*\)").expect("Invalid regex"))
40}
41
42fn clean_space(text: &str) -> String {
44 text.split_whitespace().collect::<Vec<_>>().join(" ")
45}
46
47fn parse_style_hex_color(style: &str, property: &str) -> String {
51 for sep in &[": #", ":#"] {
53 let needle = format!("{}{}", property, sep);
54 if let Some(start) = style.find(&needle) {
55 let rest = &style[start + needle.len()..];
56 let end = rest.find(|c: char| !c.is_ascii_hexdigit()).unwrap_or(rest.len());
57 let hex = &rest[..end];
58 if !hex.is_empty() {
59 return hex.to_string();
60 }
61 }
62 }
63 String::new()
64}
65
66impl SteamUser {
67 #[steam_endpoint(GET, host = Community, path = "/auction/ajaxgetgoovalue/", kind = Read)]
74 pub async fn get_gem_value(&self, appid: AppId, assetid: AssetId) -> Result<GemValue, SteamUserError> {
75 let response: serde_json::Value = self.post_path("/auction/ajaxgetgoovalue/").form(&[("appid", appid.to_string().as_str()), ("contextid", "6"), ("assetid", assetid.to_string().as_str())]).send().await?.json().await?;
76
77 Self::check_json_success(&response, "Failed to get gem value")?;
78
79 let prompt_title = response.get("strTitle").and_then(|v| v.as_str()).unwrap_or("").to_string();
80 let gem_value = response.get("goo_value").and_then(|v| v.as_str()).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);
81
82 Ok(GemValue { prompt_title, gem_value })
83 }
84
85 #[steam_endpoint(POST, host = Community, path = "/auction/ajaxgrindintogoo/", kind = Write)]
94 pub async fn turn_item_into_gems(&self, appid: AppId, assetid: AssetId, expected_value: u32) -> Result<GemResult, SteamUserError> {
95 let response: serde_json::Value = self.post_path("/auction/ajaxgrindintogoo/").form(&[("appid", appid.to_string().as_str()), ("contextid", "6"), ("assetid", assetid.to_string().as_str()), ("goo_value_expected", expected_value.to_string().as_str())]).send().await?.json().await?;
96
97 Self::check_json_success(&response, "Failed to turn item into gems")?;
98
99 let gems_received = response.get("goo_value_received ").and_then(|v| v.as_str()).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);
100
101 let total_gems = u32::try_from(response.get("goo_value_total").and_then(|v| v.as_i64()).unwrap_or(0)).unwrap_or(u32::MAX);
102
103 Ok(GemResult { gems_received, total_gems })
104 }
105
106 #[steam_endpoint(POST, host = Community, path = "/auction/ajaxunpackbooster/", kind = Write)]
113 pub async fn open_booster_pack(&self, appid: AppId, assetid: AssetId) -> Result<Vec<EconItem>, SteamUserError> {
114 let response: serde_json::Value = self.post_path("/auction/ajaxunpackbooster/").form(&[("appid", appid.to_string().as_str()), ("communityitemid", assetid.to_string().as_str())]).send().await?.json().await?;
115
116 Self::check_json_success(&response, "Failed to open booster pack")?;
117
118 Ok(vec![])
128 }
129
130 #[steam_endpoint(POST, host = Community, path = "/tradingcards/ajaxcreatebooster/", kind = Write)]
137 pub async fn create_booster_pack(&self, appid: AppId, use_untradable_gems: bool) -> Result<BoosterResult, SteamUserError> {
138 let pref = if use_untradable_gems { "3" } else { "2" };
139 let appid_str = appid.to_string();
140 let response: serde_json::Value = self.post_path("/tradingcards/ajaxcreatebooster/").form(&[("appid", appid_str.as_str()), ("series", "1"), ("tradability_preference", pref)]).send().await?.json().await?;
141
142 Self::check_json_success(&response, "Failed to create booster pack")?;
143
144 Ok(BoosterResult {
145 total_gems: response.get("goo_amount").and_then(|v| v.as_str()).and_then(|s| s.parse().ok()).unwrap_or(0),
146 tradable_gems: response.get("tradable_goo_amount").and_then(|v| v.as_str()).and_then(|s| s.parse().ok()).unwrap_or(0),
147 untradable_gems: response.get("untradable_goo_amount").and_then(|v| v.as_str()).and_then(|s| s.parse().ok()).unwrap_or(0),
148 result_item: response.get("purchase_result").cloned().unwrap_or(serde_json::Value::Null),
149 })
150 }
151
152 #[steam_endpoint(GET, host = Community, path = "/tradingcards/boostersearch/", kind = Read)]
157 pub async fn get_booster_pack_catalog(&self) -> Result<Vec<BoosterPackEntry>, SteamUserError> {
158 let response = self.get_path("/tradingcards/boostersearch/").send().await?.text().await?;
159
160 let caps = get_booster_data_re().captures(&response).ok_or_else(|| SteamUserError::MalformedResponse("Could not find booster pack data in response".to_string()))?;
161
162 let json_str = caps.get(1).map(|m| m.as_str()).unwrap_or("[]");
163 let catalog: Vec<BoosterPackEntry> = serde_json::from_str(json_str)?;
164
165 Ok(catalog)
166 }
167
168 #[steam_endpoint(GET, host = Community, path = "/market/mylistings/render/", kind = Read)]
183 pub async fn get_my_listings(&self, start: u32, count: u32) -> Result<(Vec<MarketListing>, Vec<serde_json::Value>, u32), SteamUserError> {
184 let url = format!("https://steamcommunity.com/market/mylistings/render/?start={}&count={}", start, count);
185 let text = self.get_with_manual_redirects(&url).await?;
186 let response = serde_json::from_str::<serde_json::Value>(&text)?;
187
188 let success_val = response.get("success");
189 let is_success = success_val.and_then(|v| v.as_bool()).unwrap_or(false) || success_val.and_then(|v| v.as_i64()) == Some(1);
190 if !is_success {
191 return Err(SteamUserError::MalformedResponse("Failed to fetch market listings".to_string()));
192 }
193
194 let total_count = u32::try_from(response.get("total_count").and_then(|v| v.as_u64()).unwrap_or(0)).unwrap_or(u32::MAX);
195
196 let html = response.get("results_html").and_then(|v| v.as_str()).unwrap_or("");
197 let list = parse_market_listings(html)?;
198
199 let assets_val = response.get("assets").cloned().unwrap_or_default();
200 let mut assets = Vec::new();
201 if let Some(apps) = assets_val.as_object() {
202 for app_assets in apps.values() {
203 if let Some(contexts) = app_assets.as_object() {
204 for context_assets in contexts.values() {
205 if let Some(items) = context_assets.as_object() {
206 for item in items.values() {
207 assets.push(item.clone());
208 }
209 }
210 }
211 }
212 }
213 }
214
215 Ok((list, assets, total_count))
216 }
217
218 #[steam_endpoint(GET, host = Community, path = "/market/myhistory/render/", kind = Read)]
225 pub async fn get_market_history(&self, start: u32, count: u32) -> Result<MarketHistoryResponse, SteamUserError> {
226 let url = format!("https://steamcommunity.com/market/myhistory/render/?query=&start={}&count={}", start, count);
227 let text = self.get_with_manual_redirects(&url).await?;
228 let response = serde_json::from_str::<serde_json::Value>(&text)?;
229
230 let success_val = response.get("success");
231 let is_success = success_val.and_then(|v| v.as_bool()).unwrap_or(false) || success_val.and_then(|v| v.as_i64()) == Some(1);
232 if !is_success {
233 return Err(SteamUserError::MalformedResponse("Failed to fetch market history".to_string()));
234 }
235
236 let hovers_html = response.get("hovers").and_then(|v| v.as_str()).unwrap_or("");
237 let asset_by_listing = extract_asset_items_from_hovers(hovers_html);
238
239 let html = response.get("results_html").and_then(|v| v.as_str()).unwrap_or("");
240 let list = parse_market_history_listings(html, &asset_by_listing)?;
241
242 let assets_val = response.get("assets").cloned().unwrap_or_default();
243 let mut assets = Vec::new();
244 if let Some(apps) = assets_val.as_object() {
245 for app_assets in apps.values() {
246 if let Some(contexts) = app_assets.as_object() {
247 for context_assets in contexts.values() {
248 if let Some(items) = context_assets.as_object() {
249 for item in items.values() {
250 assets.push(item.clone());
251 }
252 }
253 }
254 }
255 }
256 }
257
258 let total_count = u32::try_from(response.get("total_count").and_then(|v| v.as_u64()).unwrap_or(0)).unwrap_or(u32::MAX);
259
260 Ok(MarketHistoryResponse { success: true, list, assets, total_count, start, count })
261 }
262
263 #[steam_endpoint(POST, host = Community, path = "/market/sellitem", kind = Write)]
270 pub async fn sell_item(&self, appid: crate::types::AppId, contextid: crate::types::ContextId, assetid: crate::types::AssetId, amount: crate::types::Amount, price: crate::types::PriceCents) -> Result<SellItemResult, SteamUserError> {
271 let my_steam_id = self.session.get_steam_id().steam_id64().to_string();
272 let appid_str = appid.to_string();
273 let contextid_str = contextid.to_string();
274 let assetid_str = assetid.to_string();
275 let amount_str = amount.to_string();
276 let price_str = price.to_string();
277
278 let response: serde_json::Value = self.post_path("/market/sellitem").form(&[("appid", &appid_str), ("contextid", &contextid_str), ("assetid", &assetid_str), ("amount", &amount_str), ("price", &price_str)]).header("Referer", format!("https://steamcommunity.com/profiles/{}/inventory/", my_steam_id)).send().await?.json().await?;
279
280 let success = response.get("success").and_then(|v| v.as_bool()).unwrap_or(false);
281 let error = response.get("message").and_then(|v| v.as_str()).map(|s| s.to_string());
282
283 Ok(SellItemResult {
284 success,
285 requires_confirmation: response.get("requires_confirmation").and_then(|v| v.as_bool()),
286 needs_mobile_confirmation: response.get("needs_mobile_confirmation").and_then(|v| v.as_bool()),
287 needs_email_confirmation: response.get("needs_email_confirmation").and_then(|v| v.as_bool()),
288 email_domain: response.get("email_domain").and_then(|v| v.as_str()).map(|s| s.to_string()),
289 error,
290 })
291 }
292
293 #[steam_endpoint(GET, host = Community, path = "/market/", kind = Read)]
301 pub async fn get_market_apps(&self) -> Result<HashMap<u32, String>, SteamUserError> {
302 let response = self.get_with_manual_redirects("https://steamcommunity.com/market/").await?;
303
304 tokio::task::spawn_blocking(move || parse_market_apps(&response)).await.map_err(|e| SteamUserError::Other(format!("market-apps parse task failed: {e}")))?
307 }
308
309 #[steam_endpoint(POST, host = Community, path = "/market/removelisting/{listing_id}", kind = Write)]
311 pub async fn remove_listing(&self, listing_id: &str) -> Result<bool, SteamUserError> {
312 let response = self.post_path(format!("/market/removelisting/{}", listing_id)).form(&([] as [(&str, &str); 0])).header("Referer", "https://steamcommunity.com/market/").send().await?;
313
314 Ok(response.status().is_success())
315 }
316
317 #[steam_endpoint(GET, host = Community, path = "/market/", kind = Read)]
320 pub async fn get_market_restrictions(&self) -> Result<(MarketRestrictions, Option<WalletBalance>), SteamUserError> {
321 let response = self.get_with_manual_redirects("https://steamcommunity.com/market/").await?;
322
323 let document = Html::parse_document(&response);
324
325 let wallet = {
326 let w = parse_wallet_balance(&document);
327 if w.main_balance.is_some() {
328 Some(w)
329 } else {
330 None
331 }
332 };
333
334 let login_selector = Selector::parse(".market_login_link_ctn").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
336 if document.select(&login_selector).next().is_some() {
337 return Ok((
338 MarketRestrictions {
339 success: false,
340 warning: Some("User not logged in".to_string()),
341 restrictions: Vec::new(),
342 restriction_expire: None,
343 time_can_use: None,
344 },
345 wallet,
346 ));
347 }
348
349 let warning_selector = Selector::parse(".market_headertip_container_warning").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
350 let warning_box = document.select(&warning_selector).next();
351
352 if let Some(box_elem) = warning_box {
353 let header_selector = Selector::parse("#market_warning_header").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
354 let warning = box_elem.select(&header_selector).next().map(|e| clean_space(&e.text().collect::<String>()));
355
356 let li_selector = Selector::parse("ul.market_restrictions > li").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
357 let restrictions = box_elem.select(&li_selector).map(|li| clean_space(&li.text().collect::<String>())).collect();
358
359 let expire_selector = Selector::parse("#market_restriction_expire").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
360 let restriction_expire = box_elem.select(&expire_selector).next().map(|e| clean_space(&e.text().collect::<String>()));
361
362 let time_header_selector = Selector::parse("#market_timecanuse_header").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
363 let _time_can_use_text = box_elem.select(&time_header_selector).next().map(|e| clean_space(&e.text().collect::<String>()));
364
365 let time_can_use = _time_can_use_text.and_then(|text| chrono::DateTime::parse_from_str(&text, "%a, %d %b %Y %H:%M:%S %z").map(|dt| dt.timestamp_millis()).ok());
366
367 Ok((MarketRestrictions { success: true, warning, restrictions, restriction_expire, time_can_use }, wallet))
368 } else {
369 Ok((MarketRestrictions { success: true, warning: None, restrictions: Vec::new(), restriction_expire: None, time_can_use: None }, wallet))
370 }
371 }
372
373 #[steam_endpoint(GET, host = Community, path = "/market/listings/{app_id}/{market_hash_name}", kind = Read)]
402 pub async fn get_item_nameid(&self, app_id: AppId, market_hash_name: &str) -> Result<ItemNameId, SteamUserError> {
403 let url = format!("https://steamcommunity.com/market/listings/{}/{}", app_id, urlencoding::encode(market_hash_name));
404
405 let response = self.get_with_manual_redirects(&url).await?;
406
407 let item_nameid = get_item_nameid_re().captures(&response).and_then(|c| c.get(1)).and_then(|m| m.as_str().parse::<u64>().ok()).ok_or_else(|| SteamUserError::MalformedResponse("Could not find item_nameid in market listing page".into()))?;
408
409 Ok(ItemNameId::new(item_nameid))
410 }
411
412 #[steam_endpoint(GET, host = Community, path = "/market/itemordershistogram", kind = Read)]
465 pub async fn get_item_orders_histogram(&self, item_nameid: ItemNameId, country: &str, currency: u32) -> Result<ItemOrdersHistogramResponse, SteamUserError> {
466 let url = format!("https://steamcommunity.com/market/itemordershistogram?country={}&language=english¤cy={}&item_nameid={}&two_factor=0", country, currency, item_nameid);
467 let text = self.get_with_manual_redirects(&url).await?;
468 let response = serde_json::from_str::<ItemOrdersHistogramResponse>(&text)?;
469
470 if response.success != 1 {
471 return Err(SteamUserError::MalformedResponse("Failed to fetch item orders histogram".to_string()));
472 }
473
474 Ok(response)
475 }
476}
477
478fn parse_market_apps(html: &str) -> Result<HashMap<u32, String>, SteamUserError> {
483 let document = Html::parse_document(html);
484 let selector = Selector::parse(".market_search_game_button_group a.game_button").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
485 let name_selector = Selector::parse(".game_button_game_name").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
486
487 let mut apps = HashMap::new();
488
489 for element in document.select(&selector) {
490 if let Some(href) = element.value().attr("href") {
491 if let Some(idx) = href.find('=') {
492 if let Ok(appid) = href[idx + 1..].parse::<u32>() {
493 let name = element.select(&name_selector).next().map(|e| e.text().collect::<String>().trim().to_string()).unwrap_or_default();
494
495 apps.insert(appid, name);
496 }
497 }
498 }
499 }
500
501 Ok(apps)
502}
503
504fn parse_market_listings(html: &str) -> Result<Vec<MarketListing>, SteamUserError> {
505 let document = Html::parse_fragment(html);
506 let row_selector = Selector::parse(".market_listing_row").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
507 let mut list = Vec::new();
508
509 for row in document.select(&row_selector) {
510 let cancel_selector = Selector::parse(".market_listing_cancel_button > a").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
511 if let Some(cancel_link) = row.select(&cancel_selector).next().and_then(|a| a.value().attr("href")) {
512 if let Some(start) = cancel_link.find('(') {
513 if let Some(end) = cancel_link.find(')') {
514 let params: Vec<&str> = cancel_link[start + 1..end].split(',').map(|s| s.trim().trim_matches('\'').trim_matches('"')).collect();
515
516 if params.len() >= 4 {
517 let offset = if params[0] == "mylisting" { 1 } else { 0 };
520 if params.len() >= 4 + offset {
521 let listing_id = params[offset].to_string();
522 let appid = params[1 + offset].parse().unwrap_or(0);
523 let contextid = params[2 + offset].parse().unwrap_or(0);
524 let item_id = params[3 + offset].parse().unwrap_or(0);
525
526 let img_selector = Selector::parse(&format!("#mylisting_{}_image", listing_id)).map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
527 let img_el = row.select(&img_selector).next();
528 let image_url = img_el.and_then(|img| img.value().attr("src")).map(|s| s.to_string());
529 let img_style = img_el.and_then(|img| img.value().attr("style")).unwrap_or("");
530 let name_color = parse_style_hex_color(img_style, "border-color");
531 let background_color = parse_style_hex_color(img_style, "background-color");
532
533 let buyer_pays_selector = Selector::parse(".market_listing_price span[title=\"This is the price the buyer pays.\"]").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
534 let buyer_pays_price = row.select(&buyer_pays_selector).next().map(|e| clean_space(&e.text().collect::<String>().replace(['(', ')'], ""))).unwrap_or_default();
535
536 let receive_selector = Selector::parse(".market_listing_price span[title=\"This is how much you will receive.\"]").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
537 let receive_price = row.select(&receive_selector).next().map(|e| clean_space(&e.text().collect::<String>().replace(['(', ')'], ""))).unwrap_or_default();
538
539 let name_selector = Selector::parse(".market_listing_item_name_link").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
540 let item_name = row.select(&name_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_else(|| {
541 String::new()
545 });
546
547 let final_item_name = if !item_name.is_empty() {
553 item_name
554 } else {
555 let alt_name_selector = Selector::parse(".market_listing_item_name").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
556 row.select(&alt_name_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default()
557 };
558
559 let game_selector = Selector::parse(".market_listing_game_name").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
560 let game_name = row.select(&game_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
561
562 let listed_date_selector = Selector::parse(".market_listing_listed_date_combined").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
563 let listed_date = row.select(&listed_date_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
564
565 list.push(MarketListing {
566 listing_id,
567 appid,
568 contextid,
569 item_id,
570 image_url,
571 buyer_pays_price,
572 receive_price,
573 item_name: final_item_name,
574 game_name,
575 listed_date,
576 name_color,
577 background_color,
578 });
579 }
580 }
581 }
582 }
583 }
584 }
585 Ok(list)
586}
587
588fn parse_market_history_listings(html: &str, asset_by_listing: &HashMap<String, u64>) -> Result<Vec<MarketHistoryListing>, SteamUserError> {
589 let document = Html::parse_fragment(html);
590 let row_selector = Selector::parse(".market_listing_row").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
591 let mut list = Vec::new();
592
593 for row in document.select(&row_selector) {
594 let id = row.value().attr("id").unwrap_or_default().to_string();
595 let listing_id = get_listing_id_re().captures(&id).and_then(|c| c.get(1)).map(|m| m.as_str().to_string()).unwrap_or_default();
596
597 if listing_id.is_empty() {
598 continue;
599 }
600
601 let gain_loss_selector = Selector::parse(".market_listing_gainorloss").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
602 let gain_or_loss = row.select(&gain_loss_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
603
604 let img_selector = Selector::parse(".market_listing_item_img").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
605 let img_el = row.select(&img_selector).next();
606 let image = img_el.and_then(|img| img.value().attr("src")).map(|s| s.to_string());
607 let img_style = img_el.and_then(|img| img.value().attr("style")).unwrap_or("");
608 let name_color = parse_style_hex_color(img_style, "border-color");
609 let background_color = parse_style_hex_color(img_style, "background-color");
610
611 let price_selector = Selector::parse(".market_table_value .market_listing_price").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
612 let price = row.select(&price_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
613
614 let name_selector = Selector::parse(".market_listing_item_name").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
615 let item_name = row.select(&name_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
616
617 let game_selector = Selector::parse(".market_listing_game_name").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
618 let game_name = row.select(&game_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
619
620 let date_selector = Selector::parse(".market_listing_listed_date.can_combine").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
623 let dates: Vec<_> = row.select(&date_selector).collect();
624 let acted_on = dates.first().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
625 let listed_on = dates.get(1).map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
626
627 let combined_selector = Selector::parse(".market_listing_listed_date_combined").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
629 let status = row.select(&combined_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
630
631 let asset_id = asset_by_listing.get(&listing_id).cloned();
632
633 list.push(MarketHistoryListing {
634 id,
635 listing_id,
636 price,
637 item_name,
638 game_name,
639 listed_on,
640 acted_on,
641 image,
642 gain_or_loss,
643 status,
644 asset_id,
645 name_color,
646 background_color,
647 });
648 }
649 Ok(list)
650}
651
652fn extract_asset_items_from_hovers(html: &str) -> HashMap<String, u64> {
653 let mut map = HashMap::new();
654
655 for cap in get_hover_re().captures_iter(html) {
656 let selector = cap.get(1).map_or("", |m| m.as_str());
657 let assetid = cap.get(4).map_or(0, |m| m.as_str().parse::<u64>().unwrap_or(0));
658
659 if let Some(lid_match) = get_listing_row_re().captures(selector) {
660 if let Some(listing_id) = lid_match.get(1) {
661 map.insert(listing_id.as_str().to_string(), assetid);
662 }
663 }
664 }
665 map
666}