use std::{collections::HashMap, sync::OnceLock};
use regex::Regex;
use scraper::{Html, Selector};
use crate::{
client::SteamUser,
endpoint::steam_endpoint,
error::SteamUserError,
services::account::parse_wallet_balance,
types::{AppId, AssetId, BoosterPackEntry, BoosterResult, EconItem, GemResult, GemValue, ItemNameId, ItemOrdersHistogramResponse, MarketHistoryListing, MarketHistoryResponse, MarketListing, MarketRestrictions, SellItemResult, WalletBalance},
};
static LISTING_ID_RE: OnceLock<Regex> = OnceLock::new();
static HOVER_RE: OnceLock<Regex> = OnceLock::new();
static LISTING_ROW_RE: OnceLock<Regex> = OnceLock::new();
static BOOSTER_DATA_RE: OnceLock<Regex> = OnceLock::new();
static ITEM_NAMEID_RE: OnceLock<Regex> = OnceLock::new();
fn get_listing_id_re() -> &'static Regex {
LISTING_ID_RE.get_or_init(|| Regex::new(r"history_row_(.*?)_").expect("Invalid regex"))
}
fn get_hover_re() -> &'static Regex {
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"))
}
fn get_listing_row_re() -> &'static Regex {
LISTING_ROW_RE.get_or_init(|| Regex::new(r"history_row_(\d+)_").expect("Invalid regex"))
}
fn get_booster_data_re() -> &'static Regex {
BOOSTER_DATA_RE.get_or_init(|| Regex::new(r"CBoosterCreatorPage\.Init\(\s*\d+\s*,\s*(\[.+?\])\s*,").expect("Invalid regex"))
}
fn get_item_nameid_re() -> &'static Regex {
ITEM_NAMEID_RE.get_or_init(|| Regex::new(r"Market_LoadOrderSpread\(\s*(\d+)\s*\)").expect("Invalid regex"))
}
fn clean_space(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn parse_style_hex_color(style: &str, property: &str) -> String {
for sep in &[": #", ":#"] {
let needle = format!("{}{}", property, sep);
if let Some(start) = style.find(&needle) {
let rest = &style[start + needle.len()..];
let end = rest.find(|c: char| !c.is_ascii_hexdigit()).unwrap_or(rest.len());
let hex = &rest[..end];
if !hex.is_empty() {
return hex.to_string();
}
}
}
String::new()
}
impl SteamUser {
#[steam_endpoint(GET, host = Community, path = "/auction/ajaxgetgoovalue/", kind = Read)]
pub async fn get_gem_value(&self, appid: AppId, assetid: AssetId) -> Result<GemValue, SteamUserError> {
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?;
Self::check_json_success(&response, "Failed to get gem value")?;
let prompt_title = response.get("strTitle").and_then(|v| v.as_str()).unwrap_or("").to_string();
let gem_value = response.get("goo_value").and_then(|v| v.as_str()).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);
Ok(GemValue { prompt_title, gem_value })
}
#[steam_endpoint(POST, host = Community, path = "/auction/ajaxgrindintogoo/", kind = Write)]
pub async fn turn_item_into_gems(&self, appid: AppId, assetid: AssetId, expected_value: u32) -> Result<GemResult, SteamUserError> {
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?;
Self::check_json_success(&response, "Failed to turn item into gems")?;
let gems_received = response.get("goo_value_received ").and_then(|v| v.as_str()).and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);
let total_gems = u32::try_from(response.get("goo_value_total").and_then(|v| v.as_i64()).unwrap_or(0)).unwrap_or(u32::MAX);
Ok(GemResult { gems_received, total_gems })
}
#[steam_endpoint(POST, host = Community, path = "/auction/ajaxunpackbooster/", kind = Write)]
pub async fn open_booster_pack(&self, appid: AppId, assetid: AssetId) -> Result<Vec<EconItem>, SteamUserError> {
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?;
Self::check_json_success(&response, "Failed to open booster pack")?;
Ok(vec![])
}
#[steam_endpoint(POST, host = Community, path = "/tradingcards/ajaxcreatebooster/", kind = Write)]
pub async fn create_booster_pack(&self, appid: AppId, use_untradable_gems: bool) -> Result<BoosterResult, SteamUserError> {
let pref = if use_untradable_gems { "3" } else { "2" };
let appid_str = appid.to_string();
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?;
Self::check_json_success(&response, "Failed to create booster pack")?;
Ok(BoosterResult {
total_gems: response.get("goo_amount").and_then(|v| v.as_str()).and_then(|s| s.parse().ok()).unwrap_or(0),
tradable_gems: response.get("tradable_goo_amount").and_then(|v| v.as_str()).and_then(|s| s.parse().ok()).unwrap_or(0),
untradable_gems: response.get("untradable_goo_amount").and_then(|v| v.as_str()).and_then(|s| s.parse().ok()).unwrap_or(0),
result_item: response.get("purchase_result").cloned().unwrap_or(serde_json::Value::Null),
})
}
#[steam_endpoint(GET, host = Community, path = "/tradingcards/boostersearch/", kind = Read)]
pub async fn get_booster_pack_catalog(&self) -> Result<Vec<BoosterPackEntry>, SteamUserError> {
let response = self.get_path("/tradingcards/boostersearch/").send().await?.text().await?;
let caps = get_booster_data_re().captures(&response).ok_or_else(|| SteamUserError::MalformedResponse("Could not find booster pack data in response".to_string()))?;
let json_str = caps.get(1).map(|m| m.as_str()).unwrap_or("[]");
let catalog: Vec<BoosterPackEntry> = serde_json::from_str(json_str)?;
Ok(catalog)
}
#[steam_endpoint(GET, host = Community, path = "/market/mylistings/render/", kind = Read)]
pub async fn get_my_listings(&self, start: u32, count: u32) -> Result<(Vec<MarketListing>, Vec<serde_json::Value>, u32), SteamUserError> {
let url = format!("https://steamcommunity.com/market/mylistings/render/?start={}&count={}", start, count);
let text = self.get_with_manual_redirects(&url).await?;
let response = serde_json::from_str::<serde_json::Value>(&text)?;
let success_val = response.get("success");
let is_success = success_val.and_then(|v| v.as_bool()).unwrap_or(false) || success_val.and_then(|v| v.as_i64()) == Some(1);
if !is_success {
return Err(SteamUserError::MalformedResponse("Failed to fetch market listings".to_string()));
}
let total_count = u32::try_from(response.get("total_count").and_then(|v| v.as_u64()).unwrap_or(0)).unwrap_or(u32::MAX);
let html = response.get("results_html").and_then(|v| v.as_str()).unwrap_or("");
let list = parse_market_listings(html)?;
let assets_val = response.get("assets").cloned().unwrap_or_default();
let mut assets = Vec::new();
if let Some(apps) = assets_val.as_object() {
for app_assets in apps.values() {
if let Some(contexts) = app_assets.as_object() {
for context_assets in contexts.values() {
if let Some(items) = context_assets.as_object() {
for item in items.values() {
assets.push(item.clone());
}
}
}
}
}
}
Ok((list, assets, total_count))
}
#[steam_endpoint(GET, host = Community, path = "/market/myhistory/render/", kind = Read)]
pub async fn get_market_history(&self, start: u32, count: u32) -> Result<MarketHistoryResponse, SteamUserError> {
let url = format!("https://steamcommunity.com/market/myhistory/render/?query=&start={}&count={}", start, count);
let text = self.get_with_manual_redirects(&url).await?;
let response = serde_json::from_str::<serde_json::Value>(&text)?;
let success_val = response.get("success");
let is_success = success_val.and_then(|v| v.as_bool()).unwrap_or(false) || success_val.and_then(|v| v.as_i64()) == Some(1);
if !is_success {
return Err(SteamUserError::MalformedResponse("Failed to fetch market history".to_string()));
}
let hovers_html = response.get("hovers").and_then(|v| v.as_str()).unwrap_or("");
let asset_by_listing = extract_asset_items_from_hovers(hovers_html);
let html = response.get("results_html").and_then(|v| v.as_str()).unwrap_or("");
let list = parse_market_history_listings(html, &asset_by_listing)?;
let assets_val = response.get("assets").cloned().unwrap_or_default();
let mut assets = Vec::new();
if let Some(apps) = assets_val.as_object() {
for app_assets in apps.values() {
if let Some(contexts) = app_assets.as_object() {
for context_assets in contexts.values() {
if let Some(items) = context_assets.as_object() {
for item in items.values() {
assets.push(item.clone());
}
}
}
}
}
}
let total_count = u32::try_from(response.get("total_count").and_then(|v| v.as_u64()).unwrap_or(0)).unwrap_or(u32::MAX);
Ok(MarketHistoryResponse { success: true, list, assets, total_count, start, count })
}
#[steam_endpoint(POST, host = Community, path = "/market/sellitem", kind = Write)]
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> {
let my_steam_id = self.session.get_steam_id().steam_id64().to_string();
let appid_str = appid.to_string();
let contextid_str = contextid.to_string();
let assetid_str = assetid.to_string();
let amount_str = amount.to_string();
let price_str = price.to_string();
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?;
let success = response.get("success").and_then(|v| v.as_bool()).unwrap_or(false);
let error = response.get("message").and_then(|v| v.as_str()).map(|s| s.to_string());
Ok(SellItemResult {
success,
requires_confirmation: response.get("requires_confirmation").and_then(|v| v.as_bool()),
needs_mobile_confirmation: response.get("needs_mobile_confirmation").and_then(|v| v.as_bool()),
needs_email_confirmation: response.get("needs_email_confirmation").and_then(|v| v.as_bool()),
email_domain: response.get("email_domain").and_then(|v| v.as_str()).map(|s| s.to_string()),
error,
})
}
#[steam_endpoint(GET, host = Community, path = "/market/", kind = Read)]
pub async fn get_market_apps(&self) -> Result<HashMap<u32, String>, SteamUserError> {
let response = self.get_with_manual_redirects("https://steamcommunity.com/market/").await?;
tokio::task::spawn_blocking(move || parse_market_apps(&response)).await.map_err(|e| SteamUserError::Other(format!("market-apps parse task failed: {e}")))?
}
#[steam_endpoint(POST, host = Community, path = "/market/removelisting/{listing_id}", kind = Write)]
pub async fn remove_listing(&self, listing_id: &str) -> Result<bool, SteamUserError> {
let response = self.post_path(format!("/market/removelisting/{}", listing_id)).form(&([] as [(&str, &str); 0])).header("Referer", "https://steamcommunity.com/market/").send().await?;
Ok(response.status().is_success())
}
#[steam_endpoint(GET, host = Community, path = "/market/", kind = Read)]
pub async fn get_market_restrictions(&self) -> Result<(MarketRestrictions, Option<WalletBalance>), SteamUserError> {
let response = self.get_with_manual_redirects("https://steamcommunity.com/market/").await?;
let document = Html::parse_document(&response);
let wallet = {
let w = parse_wallet_balance(&document);
if w.main_balance.is_some() {
Some(w)
} else {
None
}
};
let login_selector = Selector::parse(".market_login_link_ctn").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
if document.select(&login_selector).next().is_some() {
return Ok((
MarketRestrictions {
success: false,
warning: Some("User not logged in".to_string()),
restrictions: Vec::new(),
restriction_expire: None,
time_can_use: None,
},
wallet,
));
}
let warning_selector = Selector::parse(".market_headertip_container_warning").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let warning_box = document.select(&warning_selector).next();
if let Some(box_elem) = warning_box {
let header_selector = Selector::parse("#market_warning_header").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let warning = box_elem.select(&header_selector).next().map(|e| clean_space(&e.text().collect::<String>()));
let li_selector = Selector::parse("ul.market_restrictions > li").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let restrictions = box_elem.select(&li_selector).map(|li| clean_space(&li.text().collect::<String>())).collect();
let expire_selector = Selector::parse("#market_restriction_expire").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let restriction_expire = box_elem.select(&expire_selector).next().map(|e| clean_space(&e.text().collect::<String>()));
let time_header_selector = Selector::parse("#market_timecanuse_header").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let _time_can_use_text = box_elem.select(&time_header_selector).next().map(|e| clean_space(&e.text().collect::<String>()));
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());
Ok((MarketRestrictions { success: true, warning, restrictions, restriction_expire, time_can_use }, wallet))
} else {
Ok((MarketRestrictions { success: true, warning: None, restrictions: Vec::new(), restriction_expire: None, time_can_use: None }, wallet))
}
}
#[steam_endpoint(GET, host = Community, path = "/market/listings/{app_id}/{market_hash_name}", kind = Read)]
pub async fn get_item_nameid(&self, app_id: AppId, market_hash_name: &str) -> Result<ItemNameId, SteamUserError> {
let url = format!("https://steamcommunity.com/market/listings/{}/{}", app_id, urlencoding::encode(market_hash_name));
let response = self.get_with_manual_redirects(&url).await?;
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()))?;
Ok(ItemNameId::new(item_nameid))
}
#[steam_endpoint(GET, host = Community, path = "/market/itemordershistogram", kind = Read)]
pub async fn get_item_orders_histogram(&self, item_nameid: ItemNameId, country: &str, currency: u32) -> Result<ItemOrdersHistogramResponse, SteamUserError> {
let url = format!("https://steamcommunity.com/market/itemordershistogram?country={}&language=english¤cy={}&item_nameid={}&two_factor=0", country, currency, item_nameid);
let text = self.get_with_manual_redirects(&url).await?;
let response = serde_json::from_str::<ItemOrdersHistogramResponse>(&text)?;
if response.success != 1 {
return Err(SteamUserError::MalformedResponse("Failed to fetch item orders histogram".to_string()));
}
Ok(response)
}
}
fn parse_market_apps(html: &str) -> Result<HashMap<u32, String>, SteamUserError> {
let document = Html::parse_document(html);
let selector = Selector::parse(".market_search_game_button_group a.game_button").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let name_selector = Selector::parse(".game_button_game_name").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let mut apps = HashMap::new();
for element in document.select(&selector) {
if let Some(href) = element.value().attr("href") {
if let Some(idx) = href.find('=') {
if let Ok(appid) = href[idx + 1..].parse::<u32>() {
let name = element.select(&name_selector).next().map(|e| e.text().collect::<String>().trim().to_string()).unwrap_or_default();
apps.insert(appid, name);
}
}
}
}
Ok(apps)
}
fn parse_market_listings(html: &str) -> Result<Vec<MarketListing>, SteamUserError> {
let document = Html::parse_fragment(html);
let row_selector = Selector::parse(".market_listing_row").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let mut list = Vec::new();
for row in document.select(&row_selector) {
let cancel_selector = Selector::parse(".market_listing_cancel_button > a").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
if let Some(cancel_link) = row.select(&cancel_selector).next().and_then(|a| a.value().attr("href")) {
if let Some(start) = cancel_link.find('(') {
if let Some(end) = cancel_link.find(')') {
let params: Vec<&str> = cancel_link[start + 1..end].split(',').map(|s| s.trim().trim_matches('\'').trim_matches('"')).collect();
if params.len() >= 4 {
let offset = if params[0] == "mylisting" { 1 } else { 0 };
if params.len() >= 4 + offset {
let listing_id = params[offset].to_string();
let appid = params[1 + offset].parse().unwrap_or(0);
let contextid = params[2 + offset].parse().unwrap_or(0);
let item_id = params[3 + offset].parse().unwrap_or(0);
let img_selector = Selector::parse(&format!("#mylisting_{}_image", listing_id)).map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let img_el = row.select(&img_selector).next();
let image_url = img_el.and_then(|img| img.value().attr("src")).map(|s| s.to_string());
let img_style = img_el.and_then(|img| img.value().attr("style")).unwrap_or("");
let name_color = parse_style_hex_color(img_style, "border-color");
let background_color = parse_style_hex_color(img_style, "background-color");
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}")))?;
let buyer_pays_price = row.select(&buyer_pays_selector).next().map(|e| clean_space(&e.text().collect::<String>().replace(['(', ')'], ""))).unwrap_or_default();
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}")))?;
let receive_price = row.select(&receive_selector).next().map(|e| clean_space(&e.text().collect::<String>().replace(['(', ')'], ""))).unwrap_or_default();
let name_selector = Selector::parse(".market_listing_item_name_link").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let item_name = row.select(&name_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_else(|| {
String::new()
});
let final_item_name = if !item_name.is_empty() {
item_name
} else {
let alt_name_selector = Selector::parse(".market_listing_item_name").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
row.select(&alt_name_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default()
};
let game_selector = Selector::parse(".market_listing_game_name").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let game_name = row.select(&game_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
let listed_date_selector = Selector::parse(".market_listing_listed_date_combined").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let listed_date = row.select(&listed_date_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
list.push(MarketListing {
listing_id,
appid,
contextid,
item_id,
image_url,
buyer_pays_price,
receive_price,
item_name: final_item_name,
game_name,
listed_date,
name_color,
background_color,
});
}
}
}
}
}
}
Ok(list)
}
fn parse_market_history_listings(html: &str, asset_by_listing: &HashMap<String, u64>) -> Result<Vec<MarketHistoryListing>, SteamUserError> {
let document = Html::parse_fragment(html);
let row_selector = Selector::parse(".market_listing_row").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let mut list = Vec::new();
for row in document.select(&row_selector) {
let id = row.value().attr("id").unwrap_or_default().to_string();
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();
if listing_id.is_empty() {
continue;
}
let gain_loss_selector = Selector::parse(".market_listing_gainorloss").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let gain_or_loss = row.select(&gain_loss_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
let img_selector = Selector::parse(".market_listing_item_img").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let img_el = row.select(&img_selector).next();
let image = img_el.and_then(|img| img.value().attr("src")).map(|s| s.to_string());
let img_style = img_el.and_then(|img| img.value().attr("style")).unwrap_or("");
let name_color = parse_style_hex_color(img_style, "border-color");
let background_color = parse_style_hex_color(img_style, "background-color");
let price_selector = Selector::parse(".market_table_value .market_listing_price").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let price = row.select(&price_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
let name_selector = Selector::parse(".market_listing_item_name").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let item_name = row.select(&name_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
let game_selector = Selector::parse(".market_listing_game_name").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let game_name = row.select(&game_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
let date_selector = Selector::parse(".market_listing_listed_date.can_combine").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let dates: Vec<_> = row.select(&date_selector).collect();
let acted_on = dates.first().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
let listed_on = dates.get(1).map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
let combined_selector = Selector::parse(".market_listing_listed_date_combined").map_err(|e| SteamUserError::MalformedResponse(format!("Invalid selector: {e}")))?;
let status = row.select(&combined_selector).next().map(|e| clean_space(&e.text().collect::<String>())).unwrap_or_default();
let asset_id = asset_by_listing.get(&listing_id).cloned();
list.push(MarketHistoryListing {
id,
listing_id,
price,
item_name,
game_name,
listed_on,
acted_on,
image,
gain_or_loss,
status,
asset_id,
name_color,
background_color,
});
}
Ok(list)
}
fn extract_asset_items_from_hovers(html: &str) -> HashMap<String, u64> {
let mut map = HashMap::new();
for cap in get_hover_re().captures_iter(html) {
let selector = cap.get(1).map_or("", |m| m.as_str());
let assetid = cap.get(4).map_or(0, |m| m.as_str().parse::<u64>().unwrap_or(0));
if let Some(lid_match) = get_listing_row_re().captures(selector) {
if let Some(listing_id) = lid_match.get(1) {
map.insert(listing_id.as_str().to_string(), assetid);
}
}
}
map
}