use std::collections::HashMap;
use steamid::SteamID;
#[cfg(feature = "gas")]
use crate::gas::GasSteamUser;
#[cfg(feature = "remote")]
use crate::remote::RemoteSteamUser;
use crate::{
action::{ActionContext, ApiAction},
client::SteamUser,
error::SteamUserError,
steam_user_api::SteamUserApi,
types::{
AccountDetails, ActiveInventory, ActivityCommentResponse, AddPhoneNumberResponse, AliasEntry, Amount, AppDetail, AppId, AppListItem, AssetId, AvatarHistoryEntry, AvatarUploadResponse, BoosterPackEntry, BoosterResult, CommunitySearchResult, ConfirmPhoneCodeResponse, Confirmation, ContextId, CsgoAccountStats, DynamicStoreUserData, EconItem, FriendActivity, FriendActivityResponse, FriendDetails, FriendListPage, GemResult, GemValue, GroupInfoXml, GroupOverview, HelpRequest, InventoryHistoryItem, InventoryHistoryResult, InvitableGroup, ItemNameId, ItemOrdersHistogramResponse, LoggedInResult, MarketHistoryResponse, MarketRestrictions,
MatchHistoryResponse, MyListingsResult, Notifications, OwnedApp, OwnedAppDetail, PendingFriendList, PlayerReport, PriceCents, PriceOverview, PrivacySettings, PurchaseHistoryItem, RedeemWalletCodeResponse, RemovePhoneResult, SellItemResult, SimpleSteamAppList, SteamAppVersionInfo, SteamGuardStatus, SteamProfile, SteamUserProfile, TradeOfferAsset, TradeOfferResult, TradeOffersResponse, TwoFactorResponse, UserComment, UserSummaryProfile, UserSummaryXml, WalletBalance,
},
};
/// A composite client that attempts requests across local, remote, and GAS
/// clients sequentially.
///
/// Note: To keep things simple and unified across error types, the
/// `SteamUserApi` implementation for `FallbackSteamUser` returns
/// `SteamUserError`. If `remote` or `gas` fail, their errors are mapped to a
/// `SteamUserError::Other`.
pub struct FallbackSteamUser {
pub local: SteamUser,
#[cfg(feature = "remote")]
pub remote: Option<RemoteSteamUser>,
#[cfg(feature = "gas")]
pub gas: Option<GasSteamUser>,
}
impl FallbackSteamUser {
pub fn new(local: SteamUser) -> Self {
Self {
local,
#[cfg(feature = "remote")]
remote: None,
#[cfg(feature = "gas")]
gas: None,
}
}
#[cfg(feature = "remote")]
pub fn with_remote(mut self, remote: RemoteSteamUser) -> Self {
self.remote = Some(remote);
self
}
#[cfg(feature = "gas")]
pub fn with_gas(mut self, gas: GasSteamUser) -> Self {
self.gas = Some(gas);
self
}
}
macro_rules! fallback_methods {
// Internal rules: redact sensitive arguments
(@record_arg identity_secret, $val:expr) => { "[REDACTED]".to_string() };
(@record_arg shared_secret, $val:expr) => { "[REDACTED]".to_string() };
(@record_arg pin, $val:expr) => { "[REDACTED]".to_string() };
(@record_arg code, $val:expr) => { "[REDACTED]".to_string() };
(@record_arg activation_code, $val:expr) => { "[REDACTED]".to_string() };
(@record_arg revocation_code, $val:expr) => { "[REDACTED]".to_string() };
(@record_arg api_key, $val:expr) => { "[REDACTED]".to_string() };
(@record_arg $name:ident, $val:expr) => { format!("{:?}", $val) };
($( $method:ident( $($arg:ident : $argty:ty),* ) -> $ret:ty => $action:ident; )*) => {
#[allow(deprecated)]
#[async_trait::async_trait]
impl SteamUserApi for FallbackSteamUser {
type Error = SteamUserError;
$(
#[allow(unused_assignments)]
#[tracing::instrument(target = "steam_user", skip_all, fields(http_method, url, raw_response, response_type, content_type))]
async fn $method(&self $(, $arg: $argty)*) -> Result<$ret, Self::Error> {
let _start = std::time::Instant::now();
let _steam_id = self.local.steam_id().and_then(|s| i64::try_from(s.steam_id64()).ok()).unwrap_or(0);
let _input = if tracing::enabled!(target: "steam_user", tracing::Level::WARN) {
#[allow(unused_mut)]
let mut parts: Vec<String> = Vec::new();
$(
parts.push(format!("{}: {}", stringify!($arg), fallback_methods!(@record_arg $arg, &$arg)));
)*
if parts.is_empty() {
String::new()
} else {
format!("{{ {} }}", parts.join(", "))
}
} else {
String::new()
};
let mut _source = "local";
#[allow(unused_mut)]
let mut last_error = match SteamUserApi::$method(&self.local, $($arg.clone()),*).await {
Ok(v) => {
let duration = i64::try_from(_start.elapsed().as_millis()).unwrap_or(i64::MAX);
let output_str = if tracing::enabled!(target: "steam_user", tracing::Level::DEBUG) {
let s = format!("{:?}", &v);
if s.len() > 256 { format!("{}…[truncated, {} bytes]", &s[..256], s.len()) } else { s }
} else {
String::new()
};
tracing::info!(
target: "steam_user",
steam_id = _steam_id,
function = stringify!($method),
action = stringify!($action),
status = "ok",
source = _source,
duration_ms = duration,
request = _input.as_str(),
response = output_str.as_str(),
"API call completed"
);
return Ok(v);
}
Err(e) => e,
};
let is_network_or_rate_limit = last_error.is_retryable();
let is_safe_action = ApiAction::$action.is_read_only();
if is_network_or_rate_limit && is_safe_action {
#[cfg(feature = "remote")]
if let Some(remote) = &self.remote {
_source = "remote";
match SteamUserApi::$method(remote, $($arg.clone()),*).await {
Ok(v) => {
let duration = i64::try_from(_start.elapsed().as_millis()).unwrap_or(i64::MAX);
let output_str = format!("{:?}", &v);
tracing::info!(
target: "steam_user",
steam_id = _steam_id,
function = stringify!($method),
action = stringify!($action),
status = "ok",
source = _source,
duration_ms = duration,
request = _input.as_str(),
response = output_str.as_str(),
"API call completed"
);
return Ok(v);
}
Err(e) => {
last_error = SteamUserError::RemoteFailed(Box::new(e));
}
}
}
#[cfg(feature = "gas")]
if let Some(gas) = &self.gas {
_source = "gas";
match SteamUserApi::$method(gas, $($arg),*).await {
Ok(v) => {
let duration = i64::try_from(_start.elapsed().as_millis()).unwrap_or(i64::MAX);
let output_str = format!("{:?}", &v);
tracing::info!(
target: "steam_user",
steam_id = _steam_id,
function = stringify!($method),
action = stringify!($action),
status = "ok",
source = _source,
duration_ms = duration,
request = _input.as_str(),
response = output_str.as_str(),
"API call completed"
);
return Ok(v);
}
Err(e) => {
last_error = SteamUserError::GasFailed(Box::new(e));
}
}
}
}
let duration = i64::try_from(_start.elapsed().as_millis()).unwrap_or(i64::MAX);
let err_str = format!("{}", last_error);
tracing::warn!(
target: "steam_user",
steam_id = _steam_id,
function = stringify!($method),
action = stringify!($action),
status = "error",
source = _source,
duration_ms = duration,
request = _input.as_str(),
error = err_str.as_str(),
"API call failed"
);
Err(last_error).with_action(ApiAction::$action)
}
)*
}
};
}
fallback_methods!(
get_account_details() -> AccountDetails => GetAccountDetails;
get_steam_wallet_balance() -> WalletBalance => GetSteamWalletBalance;
get_amount_spent_on_steam() -> String => GetAmountSpentOnSteam;
get_purchase_history() -> Vec<PurchaseHistoryItem> => GetPurchaseHistory;
redeem_wallet_code(code: &str) -> RedeemWalletCodeResponse => RedeemWalletCode;
parental_unlock(pin: &str) -> () => ParentalUnlock;
get_friend_activity(start: Option<u64>) -> FriendActivityResponse => GetFriendActivity;
get_friend_activity_full() -> Vec<FriendActivity> => GetFriendActivityFull;
comment_user_received_new_game(steam_id: SteamID, thread_id: u64, comment: &str) -> ActivityCommentResponse => CommentUserReceivedNewGame;
rate_up_user_received_new_game(steam_id: SteamID, thread_id: u64) -> ActivityCommentResponse => RateUpUserReceivedNewGame;
delete_comment_user_received_new_game(steam_id: SteamID, thread_id: u64, comment_id: &str) -> ActivityCommentResponse => DeleteCommentUserReceivedNewGame;
get_owned_apps() -> Vec<OwnedApp> => GetOwnedApps;
get_owned_apps_id() -> Vec<u32> => GetOwnedAppsId;
get_owned_apps_detail() -> Vec<OwnedAppDetail> => GetOwnedAppsDetail;
get_app_detail(app_ids: &[u32]) -> HashMap<u32, AppDetail> => GetAppDetail;
fetch_csgo_account_stats() -> CsgoAccountStats => FetchCsgoAccountStats;
get_app_list() -> SimpleSteamAppList => GetAppList;
suggest_app_list(term: &str) -> Vec<AppListItem> => SuggestAppList;
query_app_list(term: &str) -> Vec<AppListItem> => QueryAppList;
get_steam_app_version_info(app_id: u32) -> SteamAppVersionInfo => GetSteamAppVersionInfo;
get_dynamic_store_user_data() -> DynamicStoreUserData => GetDynamicStoreUserData;
fetch_batched_loyalty_reward_items(app_ids: &[u32]) -> Vec<steam_protos::messages::CLoyaltyRewardsBatchedQueryRewardItemsResponseResponse> => FetchBatchedLoyaltyRewardItems;
get_my_comments() -> Vec<UserComment> => GetMyComments;
get_user_comments(steam_id: SteamID) -> Vec<UserComment> => GetUserComments;
post_comment(steam_id: SteamID, message: &str) -> Option<UserComment> => PostComment;
delete_comment(steam_id: SteamID, gidcomment: &str) -> () => DeleteComment;
get_confirmations(identity_secret: &str, tag: Option<&str>) -> Vec<Confirmation> => GetConfirmations;
accept_confirmation_for_object(identity_secret: &str, object_id: u64) -> () => AcceptConfirmationForObject;
deny_confirmation_for_object(identity_secret: &str, object_id: u64) -> () => DenyConfirmationForObject;
get_account_email() -> String => GetAccountEmail;
get_current_steam_login() -> String => GetCurrentSteamLogin;
add_friend(steam_id: SteamID) -> () => AddFriend;
remove_friend(steam_id: SteamID) -> () => RemoveFriend;
accept_friend_request(steam_id: SteamID) -> () => AcceptFriendRequest;
ignore_friend_request(steam_id: SteamID) -> () => IgnoreFriendRequest;
block_user(steam_id: SteamID) -> () => BlockUser;
unblock_user(steam_id: SteamID) -> () => UnblockUser;
get_friends_list() -> HashMap<SteamID, i32> => GetFriendsList;
get_friends_details() -> FriendListPage => GetFriendsDetails;
get_friends_details_of_user(steam_id: SteamID) -> FriendListPage => GetFriendsDetailsOfUser;
search_users(query: &str, page: u32) -> CommunitySearchResult => SearchUsers;
create_instant_invite() -> String => CreateInstantInvite;
follow_user(steam_id: SteamID) -> () => FollowUser;
unfollow_user(steam_id: SteamID) -> () => UnfollowUser;
get_following_list() -> FriendListPage => GetFollowingList;
get_following_list_of_user(steam_id: SteamID) -> FriendListPage => GetFollowingListOfUser;
get_my_friends_id_list() -> Vec<SteamID> => GetMyFriendsIdList;
get_pending_friend_list() -> PendingFriendList => GetPendingFriendList;
remove_friends(steam_ids: &[SteamID]) -> () => RemoveFriends;
unfollow_users(steam_ids: &[SteamID]) -> () => UnfollowUsers;
cancel_friend_request(steam_id: SteamID) -> () => CancelFriendRequest;
get_friends_in_common(steam_id: SteamID) -> Vec<FriendDetails> => GetFriendsInCommon;
join_group(group_id: SteamID) -> () => JoinGroup;
leave_group(group_id: SteamID) -> () => LeaveGroup;
get_group_members(group_id: SteamID) -> Vec<SteamID> => GetGroupMembers;
post_group_announcement(group_id: SteamID, headline: &str, content: &str) -> () => PostGroupAnnouncement;
kick_group_member(group_id: SteamID, member_id: SteamID) -> () => KickGroupMember;
invite_user_to_group(user_id: SteamID, group_id: SteamID) -> () => InviteUserToGroup;
invite_users_to_group(user_ids: &[SteamID], group_id: SteamID) -> () => InviteUsersToGroup;
accept_group_invite(group_id: SteamID) -> () => AcceptGroupInvite;
ignore_group_invite(group_id: SteamID) -> () => IgnoreGroupInvite;
get_group_overview(gid: Option<SteamID>, group_url: Option<&str>, page: Option<i32>, search_key: Option<&str>) -> GroupOverview => GetGroupOverview;
get_group_steam_id_from_vanity_url(vanity_url: &str) -> String => GetGroupSteamIdFromVanityUrl;
get_group_info_xml(gid: Option<SteamID>, group_url: Option<&str>, page: Option<u32>) -> GroupInfoXml => GetGroupInfoXml;
get_group_info_xml_full(gid: Option<SteamID>, group_url: Option<&str>) -> GroupInfoXml => GetGroupInfoXmlFull;
get_invitable_groups(user_steam_id: SteamID) -> Vec<InvitableGroup> => GetInvitableGroups;
invite_all_friends_to_group(group_id: SteamID) -> () => InviteAllFriendsToGroup;
get_inventory(appid: AppId, context_id: ContextId) -> Vec<EconItem> => GetInventory;
get_user_inventory_contents(steam_id: SteamID, appid: AppId, context_id: ContextId) -> Vec<EconItem> => GetUserInventoryContents;
get_inventory_history() -> InventoryHistoryResult => GetInventoryHistory;
get_price_overview(appid: AppId, market_hash_name: &str) -> PriceOverview => GetPriceOverview;
get_active_inventories() -> Vec<ActiveInventory> => GetActiveInventories;
get_inventory_trading(appid: AppId, context_id: ContextId) -> serde_json::Value => GetInventoryTrading;
get_inventory_trading_partner(appid: AppId, partner: SteamID, context_id: ContextId) -> serde_json::Value => GetInventoryTradingPartner;
get_full_inventory_history() -> Vec<InventoryHistoryItem> => GetFullInventoryHistory;
get_my_listings() -> MyListingsResult => GetMyListings;
get_market_history(start: u32, count: u32) -> MarketHistoryResponse => GetMarketHistory;
sell_item(appid: AppId, contextid: ContextId, assetid: AssetId, amount: Amount, price: PriceCents) -> SellItemResult => SellItem;
remove_listing(listing_id: &str) -> bool => RemoveListing;
get_gem_value(appid: AppId, assetid: AssetId) -> GemValue => GetGemValue;
turn_item_into_gems(appid: AppId, assetid: AssetId, expected_value: u32) -> GemResult => TurnItemIntoGems;
get_booster_pack_catalog() -> Vec<BoosterPackEntry> => GetBoosterPackCatalog;
create_booster_pack(appid: AppId, use_untradable_gems: bool) -> BoosterResult => CreateBoosterPack;
open_booster_pack(appid: AppId, assetid: AssetId) -> Vec<EconItem> => OpenBoosterPack;
get_market_restrictions() -> (MarketRestrictions, Option<WalletBalance>) => GetMarketRestrictions;
get_market_apps() -> HashMap<u32, String> => GetMarketApps;
get_item_nameid(app_id: AppId, market_hash_name: &str) -> ItemNameId => GetItemNameid;
get_item_orders_histogram(item_nameid: ItemNameId, country: &str, currency: u32) -> ItemOrdersHistogramResponse => GetItemOrdersHistogram;
get_phone_number_status() -> Option<String> => GetPhoneNumberStatus;
add_phone_number(phone: &str) -> AddPhoneNumberResponse => AddPhoneNumber;
confirm_phone_code_for_add(code: &str) -> ConfirmPhoneCodeResponse => ConfirmPhoneCodeForAdd;
resend_phone_verification_code() -> serde_json::Value => ResendPhoneVerificationCode;
get_remove_phone_number_type() -> Option<RemovePhoneResult> => GetRemovePhoneNumberType;
send_account_recovery_code(wizard_param: serde_json::Value, method: i32) -> serde_json::Value => SendAccountRecoveryCode;
confirm_remove_phone_number_code(wizard_param: serde_json::Value, code: &str) -> serde_json::Value => ConfirmRemovePhoneNumberCode;
send_confirmation_2_steam_mobile_app(wizard_param: serde_json::Value) -> serde_json::Value => SendConfirmation2SteamMobileApp;
send_confirmation_2_steam_mobile_app_final(wizard_param: serde_json::Value) -> serde_json::Value => SendConfirmation2SteamMobileAppFinal;
get_privacy_settings() -> PrivacySettings => GetPrivacySettings;
set_privacy_settings(settings: PrivacySettings) -> PrivacySettings => SetPrivacySettings;
set_all_privacy(level: &str) -> PrivacySettings => SetAllPrivacy;
get_profile(steam_id: Option<SteamID>) -> SteamProfile => GetProfile;
edit_profile(settings: serde_json::Value) -> () => EditProfile;
set_persona_name(name: &str) -> () => SetPersonaName;
get_alias_history(steam_id: SteamID) -> Vec<AliasEntry> => GetAliasHistory;
clear_previous_aliases() -> () => ClearPreviousAliases;
set_nickname(steam_id: SteamID, nickname: &str) -> () => SetNickname;
remove_nickname(steam_id: SteamID) -> () => RemoveNickname;
post_profile_status(text: &str, app_id: Option<u32>) -> u64 => PostProfileStatus;
select_previous_avatar(avatar_hash: &str) -> () => SelectPreviousAvatar;
setup_profile() -> bool => SetupProfile;
get_user_summary_from_xml(steam_id: SteamID) -> UserSummaryXml => GetUserSummaryFromXml;
get_user_summary_from_profile(steam_id: Option<SteamID>) -> UserSummaryProfile => GetUserSummaryFromProfile;
fetch_full_profile(steam_id: SteamID) -> SteamProfile => FetchFullProfile;
resolve_user(steam_id: SteamID) -> Option<SteamUserProfile> => ResolveUser;
get_avatar_history() -> Vec<AvatarHistoryEntry> => GetAvatarHistory;
upload_avatar_from_url(url: &str) -> AvatarUploadResponse => UploadAvatarFromUrl;
enumerate_tokens() -> steam_protos::CAuthenticationRefreshTokenEnumerateResponse => EnumerateTokens;
check_token_exists(token_id: &str) -> bool => CheckTokenExists;
revoke_tokens(token_ids: &[&str], shared_secret: Option<&str>) -> crate::services::tokens::RevokeTokensResult => RevokeTokens;
get_trade_url() -> Option<String> => GetTradeUrl;
get_trade_offer() -> TradeOffersResponse => GetTradeOffer;
accept_trade_offer(trade_offer_id: u64, partner_steam_id: Option<String>) -> String => AcceptTradeOffer;
decline_trade_offer(trade_offer_id: u64) -> () => DeclineTradeOffer;
send_trade_offer(trade_url: &str, my_assets: Vec<TradeOfferAsset>, their_assets: Vec<TradeOfferAsset>, message: &str) -> TradeOfferResult => SendTradeOffer;
get_steam_guard_status() -> SteamGuardStatus => GetSteamGuardStatus;
enable_two_factor() -> TwoFactorResponse => EnableTwoFactor;
finalize_two_factor(shared_secret: &str, activation_code: &str) -> () => FinalizeTwoFactor;
disable_two_factor(revocation_code: &str) -> () => DisableTwoFactor;
deauthorize_devices() -> () => DeauthorizeDevices;
add_authenticator() -> TwoFactorResponse => AddAuthenticator;
finalize_authenticator(activation_code: &str) -> () => FinalizeAuthenticator;
remove_authenticator(revocation_code: &str) -> () => RemoveAuthenticator;
enable_steam_guard_email() -> bool => EnableSteamGuardEmail;
disable_steam_guard_email() -> bool => DisableSteamGuardEmail;
get_player_reports() -> Vec<PlayerReport> => GetPlayerReports;
add_free_license(package_id: u32) -> bool => AddFreeLicense;
add_sub_free_license(sub_id: u32) -> bool => AddSubFreeLicense;
redeem_points(definition_id: u32) -> steam_protos::messages::loyalty_rewards::CLoyaltyRewardsRedeemPointsResponse => RedeemPoints;
get_help_requests() -> Vec<HelpRequest> => GetHelpRequests;
get_help_request_detail(id: &str) -> String => GetHelpRequestDetail;
get_match_history(match_type: &str, token: Option<&str>) -> MatchHistoryResponse => GetMatchHistory;
logged_in() -> LoggedInResult => LoggedIn;
get_notifications() -> Notifications => GetNotifications;
get_web_api_key(domain: &str) -> String => GetWebApiKey;
resolve_vanity_url(api_key: &str, vanity_name: &str) -> SteamID => ResolveVanityUrl;
revoke_web_api_key() -> () => RevokeWebApiKey;
);