Skip to main content

steam_user/
fallback.rs

1use std::collections::HashMap;
2
3use steamid::SteamID;
4
5#[cfg(feature = "gas")]
6use crate::gas::GasSteamUser;
7#[cfg(feature = "remote")]
8use crate::remote::RemoteSteamUser;
9use crate::{
10    action::{ActionContext, ApiAction},
11    client::SteamUser,
12    error::SteamUserError,
13    steam_user_api::SteamUserApi,
14    types::{
15        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,
16        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,
17    },
18};
19
20/// A composite client that attempts requests across local, remote, and GAS
21/// clients sequentially.
22///
23/// Note: To keep things simple and unified across error types, the
24/// `SteamUserApi` implementation for `FallbackSteamUser` returns
25/// `SteamUserError`. If `remote` or `gas` fail, their errors are mapped to a
26/// `SteamUserError::Other`.
27pub struct FallbackSteamUser {
28    pub local: SteamUser,
29    #[cfg(feature = "remote")]
30    pub remote: Option<RemoteSteamUser>,
31    #[cfg(feature = "gas")]
32    pub gas: Option<GasSteamUser>,
33}
34
35impl FallbackSteamUser {
36    pub fn new(local: SteamUser) -> Self {
37        Self {
38            local,
39            #[cfg(feature = "remote")]
40            remote: None,
41            #[cfg(feature = "gas")]
42            gas: None,
43        }
44    }
45
46    #[cfg(feature = "remote")]
47    pub fn with_remote(mut self, remote: RemoteSteamUser) -> Self {
48        self.remote = Some(remote);
49        self
50    }
51
52    #[cfg(feature = "gas")]
53    pub fn with_gas(mut self, gas: GasSteamUser) -> Self {
54        self.gas = Some(gas);
55        self
56    }
57}
58
59macro_rules! fallback_methods {
60    // Internal rules: redact sensitive arguments
61    (@record_arg identity_secret, $val:expr) => { "[REDACTED]".to_string() };
62    (@record_arg shared_secret, $val:expr) => { "[REDACTED]".to_string() };
63    (@record_arg pin, $val:expr) => { "[REDACTED]".to_string() };
64    (@record_arg code, $val:expr) => { "[REDACTED]".to_string() };
65    (@record_arg activation_code, $val:expr) => { "[REDACTED]".to_string() };
66    (@record_arg revocation_code, $val:expr) => { "[REDACTED]".to_string() };
67    (@record_arg api_key, $val:expr) => { "[REDACTED]".to_string() };
68    (@record_arg $name:ident, $val:expr) => { format!("{:?}", $val) };
69
70    ($( $method:ident( $($arg:ident : $argty:ty),* ) -> $ret:ty => $action:ident; )*) => {
71        #[allow(deprecated)]
72        #[async_trait::async_trait]
73        impl SteamUserApi for FallbackSteamUser {
74            type Error = SteamUserError;
75
76            $(
77                #[allow(unused_assignments)]
78                #[tracing::instrument(target = "steam_user", skip_all, fields(http_method, url, raw_response, response_type, content_type))]
79                async fn $method(&self $(, $arg: $argty)*) -> Result<$ret, Self::Error> {
80                    let _start = std::time::Instant::now();
81                    let _steam_id = self.local.steam_id().and_then(|s| i64::try_from(s.steam_id64()).ok()).unwrap_or(0);
82                    let _input = if tracing::enabled!(target: "steam_user", tracing::Level::WARN) {
83                        #[allow(unused_mut)]
84                        let mut parts: Vec<String> = Vec::new();
85                        $(
86                            parts.push(format!("{}: {}", stringify!($arg), fallback_methods!(@record_arg $arg, &$arg)));
87                        )*
88                        if parts.is_empty() {
89                            String::new()
90                        } else {
91                            format!("{{ {} }}", parts.join(", "))
92                        }
93                    } else {
94                        String::new()
95                    };
96
97                    let mut _source = "local";
98
99                    #[allow(unused_mut)]
100                    let mut last_error = match SteamUserApi::$method(&self.local, $($arg.clone()),*).await {
101                        Ok(v) => {
102                            let duration = i64::try_from(_start.elapsed().as_millis()).unwrap_or(i64::MAX);
103                            let output_str = if tracing::enabled!(target: "steam_user", tracing::Level::DEBUG) {
104                                let s = format!("{:?}", &v);
105                                if s.len() > 256 { format!("{}…[truncated, {} bytes]", &s[..256], s.len()) } else { s }
106                            } else {
107                                String::new()
108                            };
109                            tracing::info!(
110                                target: "steam_user",
111                                steam_id = _steam_id,
112                                function = stringify!($method),
113                                action = stringify!($action),
114                                status = "ok",
115                                source = _source,
116                                duration_ms = duration,
117                                request = _input.as_str(),
118                                response = output_str.as_str(),
119                                "API call completed"
120                            );
121                            return Ok(v);
122                        }
123                        Err(e) => e,
124                    };
125
126                    let is_network_or_rate_limit = last_error.is_retryable();
127                    let is_safe_action = ApiAction::$action.is_read_only();
128
129                    if is_network_or_rate_limit && is_safe_action {
130                        #[cfg(feature = "remote")]
131                        if let Some(remote) = &self.remote {
132                            _source = "remote";
133                            match SteamUserApi::$method(remote, $($arg.clone()),*).await {
134                                Ok(v) => {
135                                    let duration = i64::try_from(_start.elapsed().as_millis()).unwrap_or(i64::MAX);
136                                    let output_str = format!("{:?}", &v);
137                                    tracing::info!(
138                                        target: "steam_user",
139                                        steam_id = _steam_id,
140                                        function = stringify!($method),
141                                        action = stringify!($action),
142                                        status = "ok",
143                                        source = _source,
144                                        duration_ms = duration,
145                                        request = _input.as_str(),
146                                        response = output_str.as_str(),
147                                        "API call completed"
148                                    );
149                                    return Ok(v);
150                                }
151                                Err(e) => {
152                                    last_error = SteamUserError::RemoteFailed(Box::new(e));
153                                }
154                            }
155                        }
156
157                        #[cfg(feature = "gas")]
158                        if let Some(gas) = &self.gas {
159                            _source = "gas";
160                            match SteamUserApi::$method(gas, $($arg),*).await {
161                                Ok(v) => {
162                                    let duration = i64::try_from(_start.elapsed().as_millis()).unwrap_or(i64::MAX);
163                                    let output_str = format!("{:?}", &v);
164                                    tracing::info!(
165                                        target: "steam_user",
166                                        steam_id = _steam_id,
167                                        function = stringify!($method),
168                                        action = stringify!($action),
169                                        status = "ok",
170                                        source = _source,
171                                        duration_ms = duration,
172                                        request = _input.as_str(),
173                                        response = output_str.as_str(),
174                                        "API call completed"
175                                    );
176                                    return Ok(v);
177                                }
178                                Err(e) => {
179                                    last_error = SteamUserError::GasFailed(Box::new(e));
180                                }
181                            }
182                        }
183                    }
184
185                    let duration = i64::try_from(_start.elapsed().as_millis()).unwrap_or(i64::MAX);
186                    let err_str = format!("{}", last_error);
187                    tracing::warn!(
188                        target: "steam_user",
189                        steam_id = _steam_id,
190                        function = stringify!($method),
191                        action = stringify!($action),
192                        status = "error",
193                        source = _source,
194                        duration_ms = duration,
195                        request = _input.as_str(),
196                        error = err_str.as_str(),
197                        "API call failed"
198                    );
199
200                    Err(last_error).with_action(ApiAction::$action)
201                }
202            )*
203        }
204    };
205}
206
207fallback_methods!(
208            get_account_details() -> AccountDetails => GetAccountDetails;
209            get_steam_wallet_balance() -> WalletBalance => GetSteamWalletBalance;
210            get_amount_spent_on_steam() -> String => GetAmountSpentOnSteam;
211            get_purchase_history() -> Vec<PurchaseHistoryItem> => GetPurchaseHistory;
212            redeem_wallet_code(code: &str) -> RedeemWalletCodeResponse => RedeemWalletCode;
213            parental_unlock(pin: &str) -> () => ParentalUnlock;
214            get_friend_activity(start: Option<u64>) -> FriendActivityResponse => GetFriendActivity;
215            get_friend_activity_full() -> Vec<FriendActivity> => GetFriendActivityFull;
216            comment_user_received_new_game(steam_id: SteamID, thread_id: u64, comment: &str) -> ActivityCommentResponse => CommentUserReceivedNewGame;
217            rate_up_user_received_new_game(steam_id: SteamID, thread_id: u64) -> ActivityCommentResponse => RateUpUserReceivedNewGame;
218            delete_comment_user_received_new_game(steam_id: SteamID, thread_id: u64, comment_id: &str) -> ActivityCommentResponse => DeleteCommentUserReceivedNewGame;
219            get_owned_apps() -> Vec<OwnedApp> => GetOwnedApps;
220            get_owned_apps_id() -> Vec<u32> => GetOwnedAppsId;
221            get_owned_apps_detail() -> Vec<OwnedAppDetail> => GetOwnedAppsDetail;
222            get_app_detail(app_ids: &[u32]) -> HashMap<u32, AppDetail> => GetAppDetail;
223            fetch_csgo_account_stats() -> CsgoAccountStats => FetchCsgoAccountStats;
224            get_app_list() -> SimpleSteamAppList => GetAppList;
225            suggest_app_list(term: &str) -> Vec<AppListItem> => SuggestAppList;
226            query_app_list(term: &str) -> Vec<AppListItem> => QueryAppList;
227            get_steam_app_version_info(app_id: u32) -> SteamAppVersionInfo => GetSteamAppVersionInfo;
228            get_dynamic_store_user_data() -> DynamicStoreUserData => GetDynamicStoreUserData;
229            fetch_batched_loyalty_reward_items(app_ids: &[u32]) -> Vec<steam_protos::messages::CLoyaltyRewardsBatchedQueryRewardItemsResponseResponse> => FetchBatchedLoyaltyRewardItems;
230            get_my_comments() -> Vec<UserComment> => GetMyComments;
231            get_user_comments(steam_id: SteamID) -> Vec<UserComment> => GetUserComments;
232            post_comment(steam_id: SteamID, message: &str) -> Option<UserComment> => PostComment;
233            delete_comment(steam_id: SteamID, gidcomment: &str) -> () => DeleteComment;
234            get_confirmations(identity_secret: &str, tag: Option<&str>) -> Vec<Confirmation> => GetConfirmations;
235            accept_confirmation_for_object(identity_secret: &str, object_id: u64) -> () => AcceptConfirmationForObject;
236            deny_confirmation_for_object(identity_secret: &str, object_id: u64) -> () => DenyConfirmationForObject;
237            get_account_email() -> String => GetAccountEmail;
238            get_current_steam_login() -> String => GetCurrentSteamLogin;
239            add_friend(steam_id: SteamID) -> () => AddFriend;
240            remove_friend(steam_id: SteamID) -> () => RemoveFriend;
241            accept_friend_request(steam_id: SteamID) -> () => AcceptFriendRequest;
242            ignore_friend_request(steam_id: SteamID) -> () => IgnoreFriendRequest;
243            block_user(steam_id: SteamID) -> () => BlockUser;
244            unblock_user(steam_id: SteamID) -> () => UnblockUser;
245            get_friends_list() -> HashMap<SteamID, i32> => GetFriendsList;
246            get_friends_details() -> FriendListPage => GetFriendsDetails;
247            get_friends_details_of_user(steam_id: SteamID) -> FriendListPage => GetFriendsDetailsOfUser;
248            search_users(query: &str, page: u32) -> CommunitySearchResult => SearchUsers;
249            create_instant_invite() -> String => CreateInstantInvite;
250            follow_user(steam_id: SteamID) -> () => FollowUser;
251            unfollow_user(steam_id: SteamID) -> () => UnfollowUser;
252            get_following_list() -> FriendListPage => GetFollowingList;
253            get_following_list_of_user(steam_id: SteamID) -> FriendListPage => GetFollowingListOfUser;
254            get_my_friends_id_list() -> Vec<SteamID> => GetMyFriendsIdList;
255            get_pending_friend_list() -> PendingFriendList => GetPendingFriendList;
256            remove_friends(steam_ids: &[SteamID]) -> () => RemoveFriends;
257            unfollow_users(steam_ids: &[SteamID]) -> () => UnfollowUsers;
258            cancel_friend_request(steam_id: SteamID) -> () => CancelFriendRequest;
259            get_friends_in_common(steam_id: SteamID) -> Vec<FriendDetails> => GetFriendsInCommon;
260            join_group(group_id: SteamID) -> () => JoinGroup;
261            leave_group(group_id: SteamID) -> () => LeaveGroup;
262            get_group_members(group_id: SteamID) -> Vec<SteamID> => GetGroupMembers;
263            post_group_announcement(group_id: SteamID, headline: &str, content: &str) -> () => PostGroupAnnouncement;
264            kick_group_member(group_id: SteamID, member_id: SteamID) -> () => KickGroupMember;
265            invite_user_to_group(user_id: SteamID, group_id: SteamID) -> () => InviteUserToGroup;
266            invite_users_to_group(user_ids: &[SteamID], group_id: SteamID) -> () => InviteUsersToGroup;
267            accept_group_invite(group_id: SteamID) -> () => AcceptGroupInvite;
268            ignore_group_invite(group_id: SteamID) -> () => IgnoreGroupInvite;
269            get_group_overview(gid: Option<SteamID>, group_url: Option<&str>, page: Option<i32>, search_key: Option<&str>) -> GroupOverview => GetGroupOverview;
270            get_group_steam_id_from_vanity_url(vanity_url: &str) -> String => GetGroupSteamIdFromVanityUrl;
271            get_group_info_xml(gid: Option<SteamID>, group_url: Option<&str>, page: Option<u32>) -> GroupInfoXml => GetGroupInfoXml;
272            get_group_info_xml_full(gid: Option<SteamID>, group_url: Option<&str>) -> GroupInfoXml => GetGroupInfoXmlFull;
273            get_invitable_groups(user_steam_id: SteamID) -> Vec<InvitableGroup> => GetInvitableGroups;
274            invite_all_friends_to_group(group_id: SteamID) -> () => InviteAllFriendsToGroup;
275            get_inventory(appid: AppId, context_id: ContextId) -> Vec<EconItem> => GetInventory;
276            get_user_inventory_contents(steam_id: SteamID, appid: AppId, context_id: ContextId) -> Vec<EconItem> => GetUserInventoryContents;
277            get_inventory_history() -> InventoryHistoryResult => GetInventoryHistory;
278            get_price_overview(appid: AppId, market_hash_name: &str) -> PriceOverview => GetPriceOverview;
279            get_active_inventories() -> Vec<ActiveInventory> => GetActiveInventories;
280            get_inventory_trading(appid: AppId, context_id: ContextId) -> serde_json::Value => GetInventoryTrading;
281            get_inventory_trading_partner(appid: AppId, partner: SteamID, context_id: ContextId) -> serde_json::Value => GetInventoryTradingPartner;
282            get_full_inventory_history() -> Vec<InventoryHistoryItem> => GetFullInventoryHistory;
283            get_my_listings() -> MyListingsResult => GetMyListings;
284            get_market_history(start: u32, count: u32) -> MarketHistoryResponse => GetMarketHistory;
285            sell_item(appid: AppId, contextid: ContextId, assetid: AssetId, amount: Amount, price: PriceCents) -> SellItemResult => SellItem;
286            remove_listing(listing_id: &str) -> bool => RemoveListing;
287            get_gem_value(appid: AppId, assetid: AssetId) -> GemValue => GetGemValue;
288            turn_item_into_gems(appid: AppId, assetid: AssetId, expected_value: u32) -> GemResult => TurnItemIntoGems;
289            get_booster_pack_catalog() -> Vec<BoosterPackEntry> => GetBoosterPackCatalog;
290            create_booster_pack(appid: AppId, use_untradable_gems: bool) -> BoosterResult => CreateBoosterPack;
291            open_booster_pack(appid: AppId, assetid: AssetId) -> Vec<EconItem> => OpenBoosterPack;
292            get_market_restrictions() -> (MarketRestrictions, Option<WalletBalance>) => GetMarketRestrictions;
293            get_market_apps() -> HashMap<u32, String> => GetMarketApps;
294            get_item_nameid(app_id: AppId, market_hash_name: &str) -> ItemNameId => GetItemNameid;
295            get_item_orders_histogram(item_nameid: ItemNameId, country: &str, currency: u32) -> ItemOrdersHistogramResponse => GetItemOrdersHistogram;
296            get_phone_number_status() -> Option<String> => GetPhoneNumberStatus;
297            add_phone_number(phone: &str) -> AddPhoneNumberResponse => AddPhoneNumber;
298            confirm_phone_code_for_add(code: &str) -> ConfirmPhoneCodeResponse => ConfirmPhoneCodeForAdd;
299            resend_phone_verification_code() -> serde_json::Value => ResendPhoneVerificationCode;
300            get_remove_phone_number_type() -> Option<RemovePhoneResult> => GetRemovePhoneNumberType;
301            send_account_recovery_code(wizard_param: serde_json::Value, method: i32) -> serde_json::Value => SendAccountRecoveryCode;
302            confirm_remove_phone_number_code(wizard_param: serde_json::Value, code: &str) -> serde_json::Value => ConfirmRemovePhoneNumberCode;
303            send_confirmation_2_steam_mobile_app(wizard_param: serde_json::Value) -> serde_json::Value => SendConfirmation2SteamMobileApp;
304            send_confirmation_2_steam_mobile_app_final(wizard_param: serde_json::Value) -> serde_json::Value => SendConfirmation2SteamMobileAppFinal;
305            get_privacy_settings() -> PrivacySettings => GetPrivacySettings;
306            set_privacy_settings(settings: PrivacySettings) -> PrivacySettings => SetPrivacySettings;
307            set_all_privacy(level: &str) -> PrivacySettings => SetAllPrivacy;
308            get_profile(steam_id: Option<SteamID>) -> SteamProfile => GetProfile;
309            edit_profile(settings: serde_json::Value) -> () => EditProfile;
310            set_persona_name(name: &str) -> () => SetPersonaName;
311            get_alias_history(steam_id: SteamID) -> Vec<AliasEntry> => GetAliasHistory;
312            clear_previous_aliases() -> () => ClearPreviousAliases;
313            set_nickname(steam_id: SteamID, nickname: &str) -> () => SetNickname;
314            remove_nickname(steam_id: SteamID) -> () => RemoveNickname;
315            post_profile_status(text: &str, app_id: Option<u32>) -> u64 => PostProfileStatus;
316            select_previous_avatar(avatar_hash: &str) -> () => SelectPreviousAvatar;
317            setup_profile() -> bool => SetupProfile;
318            get_user_summary_from_xml(steam_id: SteamID) -> UserSummaryXml => GetUserSummaryFromXml;
319            get_user_summary_from_profile(steam_id: Option<SteamID>) -> UserSummaryProfile => GetUserSummaryFromProfile;
320            fetch_full_profile(steam_id: SteamID) -> SteamProfile => FetchFullProfile;
321            resolve_user(steam_id: SteamID) -> Option<SteamUserProfile> => ResolveUser;
322            get_avatar_history() -> Vec<AvatarHistoryEntry> => GetAvatarHistory;
323            upload_avatar_from_url(url: &str) -> AvatarUploadResponse => UploadAvatarFromUrl;
324            enumerate_tokens() -> steam_protos::CAuthenticationRefreshTokenEnumerateResponse => EnumerateTokens;
325            check_token_exists(token_id: &str) -> bool => CheckTokenExists;
326            revoke_tokens(token_ids: &[&str], shared_secret: Option<&str>) -> crate::services::tokens::RevokeTokensResult => RevokeTokens;
327            get_trade_url() -> Option<String> => GetTradeUrl;
328            get_trade_offer() -> TradeOffersResponse => GetTradeOffer;
329            accept_trade_offer(trade_offer_id: u64, partner_steam_id: Option<String>) -> String => AcceptTradeOffer;
330            decline_trade_offer(trade_offer_id: u64) -> () => DeclineTradeOffer;
331            send_trade_offer(trade_url: &str, my_assets: Vec<TradeOfferAsset>, their_assets: Vec<TradeOfferAsset>, message: &str) -> TradeOfferResult => SendTradeOffer;
332            get_steam_guard_status() -> SteamGuardStatus => GetSteamGuardStatus;
333            enable_two_factor() -> TwoFactorResponse => EnableTwoFactor;
334            finalize_two_factor(shared_secret: &str, activation_code: &str) -> () => FinalizeTwoFactor;
335            disable_two_factor(revocation_code: &str) -> () => DisableTwoFactor;
336            deauthorize_devices() -> () => DeauthorizeDevices;
337            add_authenticator() -> TwoFactorResponse => AddAuthenticator;
338            finalize_authenticator(activation_code: &str) -> () => FinalizeAuthenticator;
339            remove_authenticator(revocation_code: &str) -> () => RemoveAuthenticator;
340            enable_steam_guard_email() -> bool => EnableSteamGuardEmail;
341            disable_steam_guard_email() -> bool => DisableSteamGuardEmail;
342            get_player_reports() -> Vec<PlayerReport> => GetPlayerReports;
343            add_free_license(package_id: u32) -> bool => AddFreeLicense;
344            add_sub_free_license(sub_id: u32) -> bool => AddSubFreeLicense;
345            redeem_points(definition_id: u32) -> steam_protos::messages::loyalty_rewards::CLoyaltyRewardsRedeemPointsResponse => RedeemPoints;
346            get_help_requests() -> Vec<HelpRequest> => GetHelpRequests;
347            get_help_request_detail(id: &str) -> String => GetHelpRequestDetail;
348            get_match_history(match_type: &str, token: Option<&str>) -> MatchHistoryResponse => GetMatchHistory;
349            logged_in() -> LoggedInResult => LoggedIn;
350            get_notifications() -> Notifications => GetNotifications;
351            get_web_api_key(domain: &str) -> String => GetWebApiKey;
352            resolve_vanity_url(api_key: &str, vanity_name: &str) -> SteamID => ResolveVanityUrl;
353            revoke_web_api_key() -> () => RevokeWebApiKey;
354);