steam-user 0.1.0

Steam User web client for Rust - HTTP-based Steam Community interactions
Documentation
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;
);