use std::{collections::HashMap, sync::OnceLock, time::Duration};
use scraper::{Html, Selector};
use serde_json::Value;
use crate::{
client::SteamUser,
endpoint::steam_endpoint,
error::SteamUserError,
types::apps::{AppDetail, CsgoAccountStats, OwnedApp},
};
const ADHOC_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
const ADHOC_TIMEOUT: Duration = Duration::from_secs(60);
fn build_adhoc_client() -> Result<reqwest::Client, SteamUserError> {
reqwest::Client::builder().connect_timeout(ADHOC_CONNECT_TIMEOUT).timeout(ADHOC_TIMEOUT).build().map_err(SteamUserError::from)
}
static SEL_KV_LINE: OnceLock<Selector> = OnceLock::new();
fn sel_kv_line() -> &'static Selector {
SEL_KV_LINE.get_or_init(|| Selector::parse(".generic_kv_table .generic_kv_line").expect("valid CSS selector"))
}
static SEL_KV_TABLE: OnceLock<Selector> = OnceLock::new();
fn sel_kv_table() -> &'static Selector {
SEL_KV_TABLE.get_or_init(|| Selector::parse("table.generic_kv_table").expect("valid CSS selector"))
}
static SEL_TR: OnceLock<Selector> = OnceLock::new();
fn sel_tr() -> &'static Selector {
SEL_TR.get_or_init(|| Selector::parse("tr").expect("valid CSS selector"))
}
static SEL_TH: OnceLock<Selector> = OnceLock::new();
fn sel_th() -> &'static Selector {
SEL_TH.get_or_init(|| Selector::parse("th").expect("valid CSS selector"))
}
static SEL_TD: OnceLock<Selector> = OnceLock::new();
fn sel_td() -> &'static Selector {
SEL_TD.get_or_init(|| Selector::parse("td").expect("valid CSS selector"))
}
static SEL_MATCH_ANCHOR: OnceLock<Selector> = OnceLock::new();
fn sel_match_anchor() -> &'static Selector {
SEL_MATCH_ANCHOR.get_or_init(|| Selector::parse("a.match").expect("valid CSS selector"))
}
static SEL_MATCH_NAME: OnceLock<Selector> = OnceLock::new();
fn sel_match_name() -> &'static Selector {
SEL_MATCH_NAME.get_or_init(|| Selector::parse(".match_name").expect("valid CSS selector"))
}
static SEL_MATCH_IMG: OnceLock<Selector> = OnceLock::new();
fn sel_match_img() -> &'static Selector {
SEL_MATCH_IMG.get_or_init(|| Selector::parse(".match_img img").expect("valid CSS selector"))
}
static SEL_MATCH_PRICE: OnceLock<Selector> = OnceLock::new();
fn sel_match_price() -> &'static Selector {
SEL_MATCH_PRICE.get_or_init(|| Selector::parse(".match_price").expect("valid CSS selector"))
}
static SEL_SEARCH_ROW: OnceLock<Selector> = OnceLock::new();
fn sel_search_row() -> &'static Selector {
SEL_SEARCH_ROW.get_or_init(|| Selector::parse("a.search_result_row").expect("valid CSS selector"))
}
static SEL_SEARCH_NAME: OnceLock<Selector> = OnceLock::new();
fn sel_search_name() -> &'static Selector {
SEL_SEARCH_NAME.get_or_init(|| Selector::parse(".search_name .title").expect("valid CSS selector"))
}
static SEL_SEARCH_CAPSULE_IMG: OnceLock<Selector> = OnceLock::new();
fn sel_search_capsule_img() -> &'static Selector {
SEL_SEARCH_CAPSULE_IMG.get_or_init(|| Selector::parse(".search_capsule img").expect("valid CSS selector"))
}
static SEL_SEARCH_PRICE: OnceLock<Selector> = OnceLock::new();
fn sel_search_price() -> &'static Selector {
SEL_SEARCH_PRICE.get_or_init(|| Selector::parse(".search_price").expect("valid CSS selector"))
}
static SEL_APP_CONFIG: OnceLock<Selector> = OnceLock::new();
fn sel_app_config() -> &'static Selector {
SEL_APP_CONFIG.get_or_init(|| Selector::parse("#application_config").expect("valid CSS selector"))
}
impl SteamUser {
#[steam_endpoint(GET, host = Community, path = "/actions/GetOwnedApps/", kind = Read)]
pub async fn get_owned_apps(&self) -> Result<Vec<OwnedApp>, SteamUserError> {
let response: Value = self.get_path("/actions/GetOwnedApps/").send().await?.json().await?;
let apps = response.as_array().ok_or_else(|| SteamUserError::MalformedResponse("Expected an array of apps".into()))?;
let mut owned_apps = Vec::new();
for app in apps {
if let Ok(owned_app) = serde_json::from_value::<OwnedApp>(app.clone()) {
owned_apps.push(owned_app);
}
}
Ok(owned_apps)
}
#[steam_endpoint(GET, host = Store, path = "/api/appdetails", kind = Read)]
pub async fn get_app_detail(&self, app_ids: &[u32]) -> Result<HashMap<u32, AppDetail>, SteamUserError> {
if app_ids.is_empty() {
return Ok(HashMap::new());
}
let ids = app_ids.iter().map(|id| id.to_string()).collect::<Vec<String>>().join(",");
let response: Value = self.get_path("/api/appdetails").query(&[("appids", ids.as_str()), ("hl", "en")]).send().await?.json().await?;
let mut details = HashMap::new();
if let Some(obj) = response.as_object() {
for (key, val) in obj {
if let Ok(app_id) = key.parse::<u32>() {
if val["success"].as_bool().unwrap_or(false) {
if let Ok(detail) = serde_json::from_value::<AppDetail>(val["data"].clone()) {
details.insert(app_id, detail);
}
}
}
}
}
Ok(details)
}
#[steam_endpoint(GET, host = Community, path = "/my/gcpd/730/", kind = Read)]
pub async fn fetch_csgo_account_stats(&self) -> Result<CsgoAccountStats, SteamUserError> {
let html = self.my_profile_get("gcpd/730/?tab=accountmain").await?;
let document = Html::parse_document(&html);
let mut stats = CsgoAccountStats {
last_logout_csgo: None,
last_launch_steam_client: None,
start_play_csgo: None,
first_played_cs_franchise: None,
last_known_ip: None,
earned_service_medal: None,
profile_rank: None,
xp_to_next_rank: None,
anti_addiction_online_time: None,
};
for element in document.select(sel_kv_line()) {
let text = element.text().collect::<String>().trim().to_string();
let mut parts = text.splitn(2, ": ");
if let (Some(raw_key), Some(raw_value)) = (parts.next(), parts.next()) {
let key = raw_key.trim();
let value = raw_value.trim().to_string();
match key {
"Logged out of CS:GO" => stats.last_logout_csgo = Some(value),
"Launched CS:GO using Steam Client" | "Launched CSGO using Steam Client" => stats.last_launch_steam_client = Some(value),
"Started playing CS:GO" | "Started playing CSGO" => stats.start_play_csgo = Some(value),
"First Counter-Strike franchise game" => stats.first_played_cs_franchise = Some(value),
"Last known IP address" => stats.last_known_ip = Some(value),
"Earned a Service Medal" => stats.earned_service_medal = Some(value),
"CS:GO Profile Rank" | "CSGO Profile Rank" => stats.profile_rank = value.parse().ok(),
"Experience points earned towards next rank" => stats.xp_to_next_rank = value.parse().ok(),
"Anti-addiction online time" => stats.anti_addiction_online_time = Some(value),
_ => {}
}
}
}
for table in document.select(sel_kv_table()) {
let mut rows = table.select(sel_tr());
if let Some(header_row) = rows.next() {
let headers: Vec<String> = header_row.select(sel_th()).map(|h| h.text().collect::<String>().trim().to_lowercase()).collect();
if headers.contains(&"recorded activity".to_string()) && headers.contains(&"activity time".to_string()) {
for tr in rows {
let cells: Vec<String> = tr.select(sel_td()).map(|c| c.text().collect::<String>().trim().to_string()).collect();
if cells.len() >= 2 {
let key = &cells[0];
let value = cells[1].clone();
match key.as_str() {
"Logged out of CS:GO" => stats.last_logout_csgo = Some(value),
"Launched CS:GO using Steam Client" => stats.last_launch_steam_client = Some(value),
"Started playing CS:GO" => stats.start_play_csgo = Some(value),
"First Counter-Strike franchise game" => stats.first_played_cs_franchise = Some(value),
_ => {}
}
}
}
}
}
}
Ok(stats)
}
#[steam_endpoint(POST, host = Api, path = "/ILoyaltyRewardsService/BatchedQueryRewardItems/v1", kind = Read)]
pub async fn fetch_batched_loyalty_reward_items(&self, app_ids: &[u32]) -> Result<Vec<steam_protos::messages::CLoyaltyRewardsBatchedQueryRewardItemsResponseResponse>, SteamUserError> {
use prost::Message;
use steam_protos::messages::{CLoyaltyRewardsBatchedQueryRewardItemsRequest, CLoyaltyRewardsBatchedQueryRewardItemsResponse, CLoyaltyRewardsQueryRewardItemsRequest};
if app_ids.is_empty() {
return Ok(Vec::new());
}
let request = CLoyaltyRewardsBatchedQueryRewardItemsRequest {
requests: app_ids
.iter()
.map(|&app_id| CLoyaltyRewardsQueryRewardItemsRequest {
appids: vec![app_id],
time_available: None,
community_item_classes: Vec::new(),
language: Some("english".to_string()),
count: Some(10),
cursor: None,
sort: Some(1),
sort_descending: Some(true),
reward_types: Vec::new(),
excluded_community_item_classes: Vec::new(),
definitionids: Vec::new(),
filters: Vec::new(),
filter_match_all_category_tags: Vec::new(),
filter_match_any_category_tags: Vec::new(),
contains_definitionids: Vec::new(),
include_direct_purchase_disabled: None,
excluded_content_descriptors: vec![3, 4],
excluded_appids: Vec::new(),
excluded_store_tagids: Vec::new(),
store_tagids: Vec::new(),
search_term: None,
})
.collect(),
};
let mut body = Vec::new();
request.encode(&mut body)?;
let params = [("origin", "https://store.steampowered.com"), ("input_protobuf_encoded", &base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &body))];
let response = self.get_path("/ILoyaltyRewardsService/BatchedQueryRewardItems/v1").query(¶ms).send().await?;
if !response.status().is_success() {
return Err(SteamUserError::HttpStatus {
status: response.status().as_u16(),
url: response.url().to_string(),
});
}
let bytes = response.bytes().await?;
let response_proto = CLoyaltyRewardsBatchedQueryRewardItemsResponse::decode(bytes)?;
Ok(response_proto.responses)
}
#[steam_endpoint(GET, host = Community, path = "/my/games/", kind = Read)]
pub async fn get_owned_apps_detail(&self) -> Result<Vec<crate::types::apps::OwnedAppDetail>, SteamUserError> {
let html = self.my_profile_get("games/?tab=all").await?;
let start_marker = "var rgGames = ";
let end_marker = "var rgChangingGames = []";
let start = html.find(start_marker).ok_or_else(|| SteamUserError::MalformedResponse("rgGames not found".into()))?;
let rest = &html[start + start_marker.len()..];
let end = rest.find(end_marker).ok_or_else(|| SteamUserError::MalformedResponse("rgChangingGames not found".into()))?;
let json_str = rest[..end].trim().trim_end_matches(';').trim();
let apps: Vec<crate::types::apps::OwnedAppDetail> = serde_json::from_str(json_str).map_err(|e| SteamUserError::MalformedResponse(format!("Failed to parse rgGames: {}", e)))?;
Ok(apps)
}
#[steam_endpoint(GET, host = Store, path = "/dynamicstore/userdata/", kind = Read)]
pub async fn get_dynamic_store_user_data(&self) -> Result<crate::types::apps::DynamicStoreUserData, SteamUserError> {
let response = self.get_path("/dynamicstore/userdata/").send().await?;
if !response.status().is_success() {
return Err(SteamUserError::HttpStatus {
status: response.status().as_u16(),
url: response.url().to_string(),
});
}
let data: crate::types::apps::DynamicStoreUserData = response.json().await?;
Ok(data)
}
#[tracing::instrument(skip(self))]
pub async fn get_owned_apps_id(&self) -> Result<Vec<u32>, SteamUserError> {
let data = self.get_dynamic_store_user_data().await?;
Ok(data.owned_apps)
}
#[steam_endpoint(GET, host = Api, path = "/ISteamApps/UpToDateCheck/v1/", kind = Read)]
pub async fn get_steam_app_version_info(app_id: u32) -> Result<crate::types::apps::SteamAppVersionInfo, SteamUserError> {
let client = build_adhoc_client()?;
let response = client.get("https://api.steampowered.com/ISteamApps/UpToDateCheck/v1/").query(&[("format", "json"), ("appid", &app_id.to_string()), ("version", "0")]).send().await?;
if !response.status().is_success() {
return Err(SteamUserError::HttpStatus {
status: response.status().as_u16(),
url: response.url().to_string(),
});
}
let info: crate::types::apps::SteamAppVersionInfo = response.json().await?;
Ok(info)
}
#[steam_endpoint(GET, host = Store, path = "/search/suggest", kind = Read)]
pub async fn suggest_app_list(term: &str) -> Result<Vec<crate::types::apps::AppListItem>, SteamUserError> {
let client = build_adhoc_client()?;
let response = client.get("https://store.steampowered.com/search/suggest").query(&[("term", term), ("f", "games"), ("cc", "VN"), ("realm", "1"), ("l", "english"), ("use_store_query", "1")]).send().await?;
if !response.status().is_success() {
return Err(SteamUserError::HttpStatus {
status: response.status().as_u16(),
url: response.url().to_string(),
});
}
let html = response.text().await?;
let document = Html::parse_document(&html);
let mut items = Vec::new();
for element in document.select(sel_match_anchor()) {
let appid = element.value().attr("data-ds-appid").and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);
let name = element.select(sel_match_name()).next().map(|e| e.text().collect::<String>().trim().to_string()).unwrap_or_default();
let img = element.select(sel_match_img()).next().and_then(|e| e.value().attr("src")).unwrap_or("").to_string();
let price = element.select(sel_match_price()).next().map(|e| e.text().collect::<String>().trim().to_string()).unwrap_or_default();
if appid > 0 {
items.push(crate::types::apps::AppListItem { appid, name, img, price });
}
}
Ok(items)
}
#[steam_endpoint(GET, host = Store, path = "/search/results/", kind = Read)]
pub async fn query_app_list(term: &str) -> Result<Vec<crate::types::apps::AppListItem>, SteamUserError> {
let client = build_adhoc_client()?;
let response = client.get("https://store.steampowered.com/search/results/").query(&[("query", ""), ("start", "0"), ("count", "50"), ("dynamic_data", ""), ("sort_by", "_ASC"), ("term", term), ("infinite", "1")]).send().await?;
if !response.status().is_success() {
return Err(SteamUserError::HttpStatus {
status: response.status().as_u16(),
url: response.url().to_string(),
});
}
#[derive(serde::Deserialize)]
struct SearchResponse {
results_html: Option<String>,
}
let data: SearchResponse = response.json().await?;
let results_html = data.results_html.unwrap_or_default();
let document = Html::parse_document(&results_html);
let mut items = Vec::new();
for element in document.select(sel_search_row()) {
let appid = element.value().attr("data-ds-appid").and_then(|s| s.parse::<u32>().ok()).unwrap_or(0);
let name = element.select(sel_search_name()).next().map(|e| e.text().collect::<String>().trim().to_string()).unwrap_or_default();
let img = element.select(sel_search_capsule_img()).next().and_then(|e| e.value().attr("src")).unwrap_or("").to_string();
let price = element.select(sel_search_price()).next().map(|e| e.text().collect::<String>().trim().to_string()).unwrap_or_default();
if appid > 0 {
items.push(crate::types::apps::AppListItem { appid, name, img, price });
}
}
Ok(items)
}
#[steam_endpoint(GET, host = Api, path = "/ISteamApps/GetAppList/v0002/", kind = Read)]
pub async fn get_app_list() -> Result<crate::types::apps::SimpleSteamAppList, SteamUserError> {
let client = build_adhoc_client()?;
let response = client.get("https://api.steampowered.com/ISteamApps/GetAppList/v0002/?format=json").send().await?;
if !response.status().is_success() {
return Err(SteamUserError::HttpStatus {
status: response.status().as_u16(),
url: response.url().to_string(),
});
}
let list: crate::types::apps::SimpleSteamAppList = response.json().await?;
Ok(list)
}
#[steam_endpoint(GET, host = Community, path = "/my/gcpd/730/", kind = Read)]
pub async fn fetch_matchmaking_stats(&self) -> Result<crate::types::apps::MatchmakingStats, SteamUserError> {
let html = self.my_profile_get("gcpd/730/?tab=matchmaking").await?;
Ok(parse_matchmaking_html(&html))
}
}
pub fn parse_matchmaking_html(html: &str) -> crate::types::apps::MatchmakingStats {
let document = Html::parse_document(html);
let mut matchmaking_cooldown = None;
let mut matchmaking_summary = Vec::new();
let mut matchmaking_per_map = Vec::new();
let mut last_played_modes = None;
for table in document.select(sel_kv_table()) {
let mut rows = table.select(sel_tr());
if let Some(header_row) = rows.next() {
let headers: Vec<String> = header_row.select(sel_th()).map(|h| h.text().collect::<String>().trim().to_lowercase()).collect();
if headers == ["competitive cooldown expiration", "competitive cooldown level", "acknowledged"] {
let mut list = Vec::new();
for tr in rows {
let cells: Vec<String> = tr.select(sel_td()).map(|c| c.text().collect::<String>().trim().to_string()).collect();
if cells.len() >= 3 {
list.push(crate::types::apps::CooldownInfo {
competitive_cooldown_expiration: cells.first().map(|s| {
if s.eq_ignore_ascii_case("never") || s.is_empty() {
crate::types::apps::CooldownExpiration::Never
} else {
let clean = s.replace("GMT", "");
chrono::NaiveDateTime::parse_from_str(clean.trim(), "%Y-%m-%d %H:%M:%S").map(|dt| crate::types::apps::CooldownExpiration::At(dt.and_utc())).unwrap_or(crate::types::apps::CooldownExpiration::Never)
}
}),
competitive_cooldown_level: cells.get(1).and_then(|s| s.parse().ok()),
acknowledged: cells.get(2).is_some_and(|s| s.eq_ignore_ascii_case("yes")),
});
}
}
if !list.is_empty() {
matchmaking_cooldown = Some(list);
}
} else if headers == ["matchmaking mode", "map", "wins", "ties", "losses", "skill group", "last match", "region"] {
for tr in rows {
let cells: Vec<String> = tr.select(sel_td()).map(|c| c.text().collect::<String>().trim().to_string()).collect();
if cells.len() >= 2 {
matchmaking_per_map.push(crate::types::apps::MatchmakingPerMap {
matchmaking_mode: cells.first().cloned(),
map: cells.get(1).cloned(),
wins: cells.get(2).and_then(|s| s.parse().ok()),
ties: cells.get(3).and_then(|s| s.parse().ok()),
losses: cells.get(4).and_then(|s| s.parse().ok()),
skill_group: cells.get(5).cloned(),
last_match: cells.get(6).cloned(),
region: cells.get(7).and_then(|s| s.parse().ok()),
});
}
}
} else if headers == ["matchmaking mode", "wins", "ties", "losses", "skill group", "last match", "region"] {
for tr in rows {
let cells: Vec<String> = tr.select(sel_td()).map(|c| c.text().collect::<String>().trim().to_string()).collect();
if cells.len() >= 2 {
matchmaking_summary.push(crate::types::apps::MatchmakingSummary {
matchmaking_mode: cells.first().cloned(),
wins: cells.get(1).and_then(|s| s.parse().ok()),
ties: cells.get(2).and_then(|s| s.parse().ok()),
losses: cells.get(3).and_then(|s| s.parse().ok()),
skill_group: cells.get(4).cloned(),
last_match: cells.get(5).cloned(),
region: cells.get(6).and_then(|s| s.parse().ok()),
});
}
}
} else if headers == ["matchmaking mode", "last match"] {
let mut list = Vec::new();
for tr in rows {
let cells: Vec<String> = tr.select(sel_td()).map(|c| c.text().collect::<String>().trim().to_string()).collect();
if cells.len() >= 2 {
list.push(crate::types::apps::LastPlayedMode { matchmaking_mode: cells.first().cloned(), last_match: cells.get(1).cloned() });
}
}
if !list.is_empty() {
last_played_modes = Some(list);
}
}
}
}
crate::types::apps::MatchmakingStats { matchmaking_cooldown, matchmaking_summary, matchmaking_per_map, last_played_modes }
}
impl SteamUser {
#[steam_endpoint(GET, host = Store, path = "/points/shop/c/events", kind = Read)]
pub async fn get_eligible_event_apps() -> Result<Vec<crate::types::apps::EligibleEventApp>, SteamUserError> {
let client = build_adhoc_client()?;
let response = client.get("https://store.steampowered.com/points/shop/c/events").send().await?;
if !response.status().is_success() {
return Err(SteamUserError::HttpStatus {
status: response.status().as_u16(),
url: response.url().to_string(),
});
}
let html = response.text().await?;
let document = Html::parse_document(&html);
let loyalty_str = document.select(sel_app_config()).next().and_then(|e| e.value().attr("data-loyaltystore")).ok_or_else(|| SteamUserError::MalformedResponse("data-loyaltystore not found".into()))?;
#[derive(serde::Deserialize)]
struct LoyaltyStore {
eligible_apps: Option<EligibleApps>,
}
#[derive(serde::Deserialize)]
struct EligibleApps {
apps: Vec<crate::types::apps::EligibleEventApp>,
}
let loyalty_obj: LoyaltyStore = serde_json::from_str(loyalty_str).map_err(|e| SteamUserError::MalformedResponse(format!("Failed to parse loyalty store: {}", e)))?;
let apps = loyalty_obj.eligible_apps.map(|ea| ea.apps.into_iter().filter(|a| a.event_app).collect()).unwrap_or_default();
Ok(apps)
}
#[steam_endpoint(POST, host = Api, path = "/ICommunityService/GetApps/v1", kind = Read)]
pub async fn get_community_apps(&self, app_ids: &[u32]) -> Result<steam_protos::messages::community::CCommunityGetAppsResponse, SteamUserError> {
use prost::Message;
use steam_protos::messages::community::CCommunityGetAppsRequest;
if app_ids.is_empty() {
return Ok(steam_protos::messages::community::CCommunityGetAppsResponse { apps: Vec::new() });
}
let request = CCommunityGetAppsRequest { appids: app_ids.to_vec(), language: Some(0) };
let mut body = Vec::new();
request.encode(&mut body)?;
let params = [("origin", "https://store.steampowered.com"), ("input_protobuf_encoded", &base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &body))];
let response = self.get_path("/ICommunityService/GetApps/v1").query(¶ms).send().await?;
if !response.status().is_success() {
return Err(SteamUserError::HttpStatus {
status: response.status().as_u16(),
url: response.url().to_string(),
});
}
let bytes = response.bytes().await?;
let response_proto = steam_protos::messages::community::CCommunityGetAppsResponse::decode(bytes)?;
Ok(response_proto)
}
#[steam_endpoint(POST, host = Api, path = "/IStoreBrowseService/GetItems/v1", kind = Read)]
pub async fn get_steam_store_items(&self, app_ids: &[u32]) -> Result<steam_protos::messages::store::CStoreBrowseGetItemsResponse, SteamUserError> {
use prost::Message;
use steam_protos::messages::store::{
c_store_browse_get_items_request::{StoreBrowseContext, StoreBrowseItemDataRequest, StoreItemId},
CStoreBrowseGetItemsRequest,
};
if app_ids.is_empty() {
return Ok(steam_protos::messages::store::CStoreBrowseGetItemsResponse { store_items: Vec::new() });
}
let request = CStoreBrowseGetItemsRequest {
ids: app_ids.iter().map(|&appid| StoreItemId { appid: Some(appid), packageid: None, bundleid: None, tagid: None, creatorid: None, hubcategoryid: None }).collect(),
context: Some(StoreBrowseContext {
language: Some("english".to_string()),
elanguage: None,
country_code: Some("VN".to_string()),
steam_realm: Some(1),
}),
data_request: Some(StoreBrowseItemDataRequest {
include_assets: Some(true),
include_release: None,
include_platforms: None,
include_all_purchase_options: None,
include_screenshots: None,
include_trailers: None,
include_ratings: None,
include_tag_count: None,
include_reviews: None,
include_basic_info: None,
include_supported_languages: None,
include_full_description: None,
include_included_items: None,
included_item_data_request: None,
include_assets_without_overrides: None,
apply_user_filters: None,
include_links: None,
}),
};
let mut body = Vec::new();
request.encode(&mut body)?;
let params = [("origin", "https://store.steampowered.com"), ("input_protobuf_encoded", &base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &body))];
let response = self.get_path("/IStoreBrowseService/GetItems/v1").query(¶ms).send().await?;
if !response.status().is_success() {
return Err(SteamUserError::HttpStatus {
status: response.status().as_u16(),
url: response.url().to_string(),
});
}
let bytes = response.bytes().await?;
let response_proto = steam_protos::messages::store::CStoreBrowseGetItemsResponse::decode(bytes)?;
Ok(response_proto)
}
#[steam_endpoint(POST, host = Api, path = "/ICheckoutService/GetFriendOwnershipForGifting/v1", kind = Read)]
pub async fn get_friend_ownership_for_gifting(&self, access_token: &str, package_id: u32) -> Result<crate::types::apps::FriendOwnershipResponse, SteamUserError> {
use prost::Message;
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct OwnershipItemIdProto {
#[prost(uint32, optional, tag = "1")]
pub appid: Option<u32>,
#[prost(uint32, optional, tag = "2")]
pub packageid: Option<u32>,
#[prost(uint32, optional, tag = "3")]
pub bundleid: Option<u32>,
#[prost(uint32, optional, tag = "4")]
pub tagid: Option<u32>,
#[prost(uint32, optional, tag = "5")]
pub creatorid: Option<u32>,
#[prost(uint32, optional, tag = "6")]
pub hubcategoryid: Option<u32>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct CCheckoutGetFriendOwnershipForGiftingRequest {
#[prost(message, repeated, tag = "1")]
pub item_ids: ::prost::alloc::vec::Vec<OwnershipItemIdProto>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct FriendOwnershipProto {
#[prost(uint32, repeated, tag = "1")]
pub partial_owns_appids: ::prost::alloc::vec::Vec<u32>,
#[prost(uint32, repeated, tag = "2")]
pub partial_wishes_for: ::prost::alloc::vec::Vec<u32>,
#[prost(uint32, tag = "3")]
pub accountid: u32,
#[prost(bool, tag = "4")]
pub already_owns: bool,
#[prost(bool, tag = "5")]
pub wishes_for: bool,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct FriendOwnershipInfoProto {
#[prost(message, repeated, tag = "1")]
pub friend_ownership: ::prost::alloc::vec::Vec<FriendOwnershipProto>,
#[prost(message, optional, tag = "2")]
pub item_id: Option<OwnershipItemIdProto>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct CCheckoutGetFriendOwnershipForGiftingResponse {
#[prost(message, repeated, tag = "1")]
pub ownership_info: ::prost::alloc::vec::Vec<FriendOwnershipInfoProto>,
}
let item_id = OwnershipItemIdProto { packageid: Some(package_id), appid: None, bundleid: None, tagid: None, creatorid: None, hubcategoryid: None };
let request = CCheckoutGetFriendOwnershipForGiftingRequest { item_ids: vec![item_id] };
let mut body = Vec::new();
request.encode(&mut body)?;
let params = [("access_token", access_token), ("spoof_steamid", ""), ("origin", "https://store.steampowered.com"), ("input_protobuf_encoded", &base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &body))];
let response = self.get_path("/ICheckoutService/GetFriendOwnershipForGifting/v1").query(¶ms).send().await?;
if !response.status().is_success() {
return Err(SteamUserError::HttpStatus {
status: response.status().as_u16(),
url: response.url().to_string(),
});
}
let bytes = response.bytes().await?;
let response_proto = CCheckoutGetFriendOwnershipForGiftingResponse::decode(bytes)?;
let ownership_info = response_proto
.ownership_info
.into_iter()
.map(|info| crate::types::apps::FriendOwnershipInfo {
friend_ownership: info
.friend_ownership
.into_iter()
.map(|fo| crate::types::apps::FriendOwnership {
partial_owns_appids: fo.partial_owns_appids,
partial_wishes_for: fo.partial_wishes_for,
accountid: fo.accountid,
already_owns: fo.already_owns,
wishes_for: fo.wishes_for,
})
.collect(),
item_id: info.item_id.map(|id| crate::types::apps::OwnershipItemId {
appid: id.appid,
packageid: id.packageid,
bundleid: id.bundleid,
tagid: id.tagid,
creatorid: id.creatorid,
hubcategoryid: id.hubcategoryid,
}),
})
.collect();
Ok(crate::types::apps::FriendOwnershipResponse { ownership_info })
}
}