diff --git a/steampy/client.py b/steampy/client.py index ece1057..47e9ec9 100644 --- a/steampy/client.py +++ b/steampy/client.py @@ -4,6 +4,7 @@ import urllib.parse as urlparse from typing import List, Union from decimal import Decimal +from urllib.parse import unquote import requests @@ -61,8 +62,8 @@ def __init__( def set_proxies(self, proxies: dict) -> dict: if not isinstance(proxies, dict): raise TypeError( - 'Proxy must be a dict. Example: ' - '\{"http": "http://login:password@host:port"\, "https": "http://login:password@host:port"\}' + r'Proxy must be a dict. Example: ' + r'\{"http": "http://login:password@host:port"\, "https": "http://login:password@host:port"\}' ) if ping_proxy(proxies): @@ -79,6 +80,17 @@ def set_login_cookies(self, cookies: dict) -> None: self.market._set_login_executed(self.steam_guard, self._get_session_id()) + steam_login_secure_cookies = [cookie for cookie in self._session.cookies if cookie.name == 'steamLoginSecure'] + cookie_value = steam_login_secure_cookies[0].value + decoded_cookie_value = unquote(cookie_value) + access_token_parts = decoded_cookie_value.split('||') + if len(access_token_parts) < 2: + print(decoded_cookie_value) + raise ValueError('Access token not found in steamLoginSecure cookie') + + access_token = access_token_parts[1] + self._access_token = access_token + @login_required def get_steam_id(self) -> int: url = SteamUrl.COMMUNITY_URL @@ -152,18 +164,18 @@ def is_invalid_api_key(response: requests.Response) -> bool: return msg in response.text @login_required - def get_my_inventory(self, game: GameOptions, merge: bool = True, count: int = 5000) -> dict: + def get_my_inventory(self, game: GameOptions, merge: bool = True, count: int = 1000) -> dict: steam_id = self.steam_guard['steamid'] return self.get_partner_inventory(steam_id, game, merge, count) @login_required def get_partner_inventory( - self, partner_steam_id: str, game: GameOptions, merge: bool = True, count: int = 5000 + self, partner_steam_id: str, game: GameOptions, merge: bool = True, count: int = 1000 ) -> dict: url = '/'.join((SteamUrl.COMMUNITY_URL, 'inventory', partner_steam_id, game.app_id, game.context_id)) params = {'l': 'english', 'count': count} - - response_dict = self._session.get(url, params=params).json() + response = (self._session.get(url, params=params)) + response_dict = response.json() if response_dict is None or response_dict.get('success') != 1: raise ApiException('Success value should be 1.') @@ -176,16 +188,23 @@ def get_trade_offers_summary(self) -> dict: params = {'key': self._api_key} return self.api_call('GET', 'IEconService', 'GetTradeOffersSummary', 'v1', params).json() - def get_trade_offers(self, merge: bool = True) -> dict: + def get_trade_offers( + self, + merge: bool = True, + use_webtoken: bool = True, + active_only: bool = True, + historical_only: bool = False, + time_historical_cutoff: str = '' + ) -> dict: params = { - 'key': self._api_key, + 'key'if not use_webtoken else 'access_token': self._api_key if not use_webtoken else self._access_token, 'get_sent_offers': 1, 'get_received_offers': 1, 'get_descriptions': 1, 'language': 'english', - 'active_only': 1, - 'historical_only': 0, - 'time_historical_cutoff': '', + 'active_only': 1 if active_only else 0, + 'historical_only': 1 if historical_only else 0, + 'time_historical_cutoff': time_historical_cutoff, } response = self.api_call('GET', 'IEconService', 'GetTradeOffers', 'v1', params).json() response = self._filter_non_active_offers(response) @@ -206,8 +225,11 @@ def _filter_non_active_offers(offers_response): return offers_response - def get_trade_offer(self, trade_offer_id: str, merge: bool = True) -> dict: - params = {'key': self._api_key, 'tradeofferid': trade_offer_id, 'language': 'english'} + def get_trade_offer(self, trade_offer_id: str, merge: bool = True, use_webtoken: bool = True) -> dict: + params = { + 'key'if not use_webtoken else 'access_token': self._api_key if not use_webtoken else self._access_token, + 'tradeofferid': trade_offer_id, 'language': 'english' + } response = self.api_call('GET', 'IEconService', 'GetTradeOffer', 'v1', params).json() if merge and 'descriptions' in response['response']: @@ -264,7 +286,6 @@ def accept_trade_offer(self, trade_offer_id: str) -> dict: 'captcha': '', } headers = {'Referer': self._get_trade_offer_url(trade_offer_id)} - response = self._session.post(accept_url, data=params, headers=headers).json() if response.get('needs_mobile_confirmation', False): return self._confirm_transaction(trade_offer_id) @@ -367,6 +388,7 @@ def make_offer_with_url( trade_offer_url: str, message: str = '', case_sensitive: bool = True, + confirm_trade: bool = True, ) -> dict: token = get_key_value_from_url(trade_offer_url, 'token', case_sensitive) partner_account_id = get_key_value_from_url(trade_offer_url, 'partner', case_sensitive) @@ -390,9 +412,9 @@ def make_offer_with_url( 'Referer': f'{SteamUrl.COMMUNITY_URL}{urlparse.urlparse(trade_offer_url).path}', 'Origin': SteamUrl.COMMUNITY_URL, } - response = self._session.post(url, data=params, headers=headers).json() - if response.get('needs_mobile_confirmation'): + + if confirm_trade and response.get('needs_mobile_confirmation'): response.update(self._confirm_transaction(response['tradeofferid'])) return response @@ -403,7 +425,12 @@ def _get_trade_offer_url(trade_offer_id: str) -> str: @login_required # If convert_to_decimal = False, the price will be returned WITHOUT a decimal point. - def get_wallet_balance(self, convert_to_decimal: bool = True, on_hold: bool = False) -> Union[str, Decimal]: + def get_wallet_balance( + self, + convert_to_decimal: bool = True, + on_hold: bool = False, + return_full : bool = False + ) -> Union[str, Decimal, dict]: response = self._session.get(f'{SteamUrl.COMMUNITY_URL}/market') wallet_info_match = re.search(r'var g_rgWalletInfo = (.*?);', response.text) if wallet_info_match: @@ -412,6 +439,8 @@ def get_wallet_balance(self, convert_to_decimal: bool = True, on_hold: bool = Fa else: raise Exception('Unable to get wallet balance string match') balance_dict_key = 'wallet_delayed_balance' if on_hold else 'wallet_balance' + if return_full: + return balance_dict if convert_to_decimal: return Decimal(balance_dict[balance_dict_key]) / 100 else: diff --git a/steampy/confirmation.py b/steampy/confirmation.py index a9836cb..3b86ff7 100644 --- a/steampy/confirmation.py +++ b/steampy/confirmation.py @@ -13,9 +13,10 @@ class Confirmation: - def __init__(self, data_confid, nonce): + def __init__(self, data_confid, nonce, creator_id): self.data_confid = data_confid self.nonce = nonce + self.creator_id = creator_id class Tag(enum.Enum): @@ -43,6 +44,16 @@ def confirm_sell_listing(self, asset_id: str) -> dict: confirmation = self._select_sell_listing_confirmation(confirmations, asset_id) return self._send_confirmation(confirmation) + def confirm_by_id(self, confirmation_id: str) -> bool: + # Confirm a trade/order based on confirmation_id + confirmations = self._get_confirmations() + for conf in confirmations: + if str(conf.creator_id) == str(confirmation_id): + result = self._send_confirmation(conf) + print(f'confirm_by_id result:{result}') + return result.get("success", False) + return False + def _send_confirmation(self, confirmation: Confirmation) -> dict: tag = Tag.ALLOW params = self._create_confirmation_params(tag.value) @@ -60,7 +71,8 @@ def _get_confirmations(self) -> List[Confirmation]: for conf in confirmations_json['conf']: data_confid = conf['id'] nonce = conf['nonce'] - confirmations.append(Confirmation(data_confid, nonce)) + creator_id = conf['creator_id'] + confirmations.append(Confirmation(data_confid, nonce, creator_id)) return confirmations else: raise ConfirmationExpected diff --git a/steampy/login.py b/steampy/login.py index ec94b8b..bd57662 100644 --- a/steampy/login.py +++ b/steampy/login.py @@ -3,6 +3,7 @@ from rsa import encrypt, PublicKey from requests import Session, Response +from urllib.parse import unquote from steampy import guard from steampy.models import SteamUrl @@ -41,6 +42,17 @@ def login(self) -> Session: self.set_sessionid_cookies() return self.session + steam_login_secure_cookies = [cookie for cookie in self._session.cookies if cookie.name == 'steamLoginSecure'] + cookie_value = steam_login_secure_cookies[0].value + decoded_cookie_value = unquote(cookie_value) + access_token_parts = decoded_cookie_value.split('||') + if len(access_token_parts) < 2: + print(decoded_cookie_value) + raise ValueError('Access token not found in steamLoginSecure cookie') + + access_token = access_token_parts[1] + self._access_token = access_token + def _send_login_request(self) -> Response: rsa_params = self._fetch_rsa_params() encrypted_password = self._encrypt_password(rsa_params) @@ -137,5 +149,6 @@ def _finalize_login(self) -> Response: sessionid = self.session.cookies['sessionid'] redir = f'{SteamUrl.COMMUNITY_URL}/login/home/?goto=' finalized_data = {'nonce': self.refresh_token, 'sessionid': sessionid, 'redir': redir} - response = self.session.post(SteamUrl.LOGIN_URL + '/jwt/finalizelogin', data=finalized_data) + headers = {'Referer': f'{SteamUrl.COMMUNITY_URL}/', 'Origin': SteamUrl.COMMUNITY_URL} + response = self.session.post(SteamUrl.LOGIN_URL + '/jwt/finalizelogin', data=finalized_data, headers=headers) return response diff --git a/steampy/market.py b/steampy/market.py index a44178c..f7051fb 100644 --- a/steampy/market.py +++ b/steampy/market.py @@ -1,4 +1,6 @@ import json +import time +import random import urllib.parse from decimal import Decimal from http import HTTPStatus @@ -41,7 +43,7 @@ def fetch_price( 'market_hash_name': item_hash_name, } - response = self._session.get(url, params=params) + response = self._session.get(url, params=params, timeout=60) if response.status_code == HTTPStatus.TOO_MANY_REQUESTS: raise TooManyRequests('You can fetch maximum 20 prices in 60s period') @@ -52,7 +54,7 @@ def fetch_price_history(self, item_hash_name: str, game: GameOptions) -> dict: url = f'{SteamUrl.COMMUNITY_URL}/market/pricehistory/' params = {'country': 'PL', 'appid': game.app_id, 'market_hash_name': item_hash_name} - response = self._session.get(url, params=params) + response = self._session.get(url, params=params, timeout=60) if response.status_code == HTTPStatus.TOO_MANY_REQUESTS: raise TooManyRequests('You can fetch maximum 20 prices in 60s period') @@ -60,11 +62,10 @@ def fetch_price_history(self, item_hash_name: str, game: GameOptions) -> dict: @login_required def get_my_market_listings(self) -> dict: - response = self._session.get(f'{SteamUrl.COMMUNITY_URL}/market') + response = self._session.get(f'{SteamUrl.COMMUNITY_URL}/market/?count=100', timeout=60) if response.status_code != HTTPStatus.OK: raise ApiException(f'There was a problem getting the listings. HTTP code: {response.status_code}') - - assets_descriptions = json.loads(text_between(response.text, 'var g_rgAssets = ', ';\r\n')) + assets_descriptions = json.loads(text_between(response.text, "var g_rgAssets = ", ";\n")) listing_id_to_assets_address = get_listing_id_to_assets_address_from_html(response.text) listings = get_market_listings_from_html(response.text) listings = merge_items_with_descriptions_from_listing( @@ -80,8 +81,8 @@ def get_my_market_listings(self) -> dict: ) if n_showing < n_total < 1000: - url = f'{SteamUrl.COMMUNITY_URL}/market/mylistings/render/?query=&start={n_showing}&count={-1}' - response = self._session.get(url) + url = f'{SteamUrl.COMMUNITY_URL}/market/mylistings/render/?query=&start={0}&count={-1}' + response = self._session.get(url, timeout=60) if response.status_code != HTTPStatus.OK: raise ApiException(f'There was a problem getting the listings. HTTP code: {response.status_code}') @@ -95,7 +96,7 @@ def get_my_market_listings(self) -> dict: else: for i in range(0, n_total, 100): url = f'{SteamUrl.COMMUNITY_URL}/market/mylistings/?query=&start={n_showing + i}&count={100}' - response = self._session.get(url) + response = self._session.get(url, timeout=60) if response.status_code != HTTPStatus.OK: raise ApiException( f'There was a problem getting the listings. HTTP code: {response.status_code}' @@ -111,7 +112,7 @@ def get_my_market_listings(self) -> dict: return listings @login_required - def create_sell_order(self, assetid: str, game: GameOptions, money_to_receive: str) -> dict: + def create_sell_order(self, assetid: str, game: GameOptions, money_to_receive: str, confirm_trade: bool = True) -> dict: data = { 'assetid': assetid, 'sessionid': self._session_id, @@ -122,9 +123,10 @@ def create_sell_order(self, assetid: str, game: GameOptions, money_to_receive: s } headers = {'Referer': f'{SteamUrl.COMMUNITY_URL}/profiles/{self._steam_guard["steamid"]}/inventory'} - response = self._session.post(f'{SteamUrl.COMMUNITY_URL}/market/sellitem/', data, headers=headers).json() + response = self._session.post(f'{SteamUrl.COMMUNITY_URL}/market/sellitem/', data, headers=headers, timeout=60).json() has_pending_confirmation = 'pending confirmation' in response.get('message', '') - if response.get('needs_mobile_confirmation') or (not response.get('success') and has_pending_confirmation): + if confirm_trade and response.get('needs_mobile_confirmation') or ( + not response.get('success') and has_pending_confirmation): return self._confirm_sell_listing(assetid) return response @@ -150,14 +152,54 @@ def create_buy_order( 'Referer': f'{SteamUrl.COMMUNITY_URL}/market/listings/{game.app_id}/{urllib.parse.quote(market_name)}' } - response = self._session.post(f'{SteamUrl.COMMUNITY_URL}/market/createbuyorder/', data, headers=headers).json() + response = self._session.post(f'{SteamUrl.COMMUNITY_URL}/market/createbuyorder/', data, headers=headers, timeout=30) + response = response.json() - if (success := response.get('success')) != 1: - raise ApiException( - f'There was a problem creating the order. Are you using the right currency? success: {success}' + # If the order is successful, return immediately + if response.get("success") == 1: + return response + + # If mobile confirmation is required + if response.get("need_confirmation"): + if not self._steam_guard: + raise ApiException("Order requires mobile confirmation, but steam_guard info is not provided") + + confirmation_id = response["confirmation"]["confirmation_id"] + # print("Confirmation required, ID:", confirmation_id) + + # Execute mobile confirmation + confirmation_executor = ConfirmationExecutor( + self._steam_guard['identity_secret'], + self._steam_guard['steamid'], + self._session ) + time.sleep(random.uniform(2, 4)) + success = confirmation_executor.confirm_by_id(confirmation_id) + if not success: + raise ApiException("Mobile confirmation failed") + + # print("Mobile confirmation succeeded, resending request with confirmation ID") + + # Second request, update confirmation to the confirmed ID + data["confirmation"] = confirmation_id + time.sleep(random.uniform(2, 4)) + response = self._session.post( + SteamUrl.COMMUNITY_URL + "/market/createbuyorder/", + data, + headers=headers, + timeout=60 + ).json() + # print("Second order response:", response) + + if response.get("success") == 1: + # print("Buy order effective") + return response + else: + raise ApiException(f"Order failed after confirmation: {response}") + + # Other exceptions + raise ApiException(f"Buy order failed: {response}") - return response @login_required def buy_item( @@ -181,7 +223,7 @@ def buy_item( 'Referer': f'{SteamUrl.COMMUNITY_URL}/market/listings/{game.app_id}/{urllib.parse.quote(market_name)}' } response = self._session.post( - f'{SteamUrl.COMMUNITY_URL}/market/buylisting/{market_id}', data, headers=headers + f'{SteamUrl.COMMUNITY_URL}/market/buylisting/{market_id}', data, headers=headers, timeout=60 ).json() try: @@ -200,7 +242,7 @@ def cancel_sell_order(self, sell_listing_id: str) -> None: headers = {'Referer': f'{SteamUrl.COMMUNITY_URL}/market/'} url = f'{SteamUrl.COMMUNITY_URL}/market/removelisting/{sell_listing_id}' - response = self._session.post(url, data=data, headers=headers) + response = self._session.post(url, data=data, headers=headers, timeout=60) if response.status_code != HTTPStatus.OK: raise ApiException(f'There was a problem removing the listing. HTTP code: {response.status_code}') @@ -208,7 +250,7 @@ def cancel_sell_order(self, sell_listing_id: str) -> None: def cancel_buy_order(self, buy_order_id) -> dict: data = {'sessionid': self._session_id, 'buy_orderid': buy_order_id} headers = {'Referer': f'{SteamUrl.COMMUNITY_URL}/market'} - response = self._session.post(f'{SteamUrl.COMMUNITY_URL}/market/cancelbuyorder/', data, headers=headers).json() + response = self._session.post(f'{SteamUrl.COMMUNITY_URL}/market/cancelbuyorder/', data, headers=headers, timeout=60).json() if (success := response.get('success')) != 1: raise ApiException(f'There was a problem canceling the order. success: {success}') @@ -217,6 +259,8 @@ def cancel_buy_order(self, buy_order_id) -> dict: def _confirm_sell_listing(self, asset_id: str) -> dict: con_executor = ConfirmationExecutor( - self._steam_guard['identity_secret'], self._steam_guard['steamid'], self._session + self._steam_guard['identity_secret'], + self._steam_guard['steamid'], + self._session ) return con_executor.confirm_sell_listing(asset_id) diff --git a/steampy/utils.py b/steampy/utils.py index fb4f3bc..4df7102 100644 --- a/steampy/utils.py +++ b/steampy/utils.py @@ -168,7 +168,8 @@ def get_market_listings_from_html(html: str) -> dict: for node in nodes: if 'My sell listings' in node.text: - sell_listings_dict = get_sell_listings_from_node(node) + sell_listings_active = get_sell_listings_from_node(node) + sell_listings_dict.update(sell_listings_active) elif 'My listings awaiting confirmation' in node.text: sell_listings_awaiting_conf = get_sell_listings_from_node(node) for listing in sell_listings_awaiting_conf.values():