From e74043bb1767481d59adf2c7327cd0727eb6ead7 Mon Sep 17 00:00:00 2001 From: abuztrade <58076271+makarworld@users.noreply.github.com> Date: Thu, 14 Sep 2023 18:41:03 +0300 Subject: [PATCH] Client - Add proxy support #278 and fix documentation --- README.md | 370 ++++++++++++++++++++++++++---------------- steampy/client.py | 91 +++++++---- steampy/exceptions.py | 3 + steampy/market.py | 21 +-- steampy/utils.py | 24 ++- 5 files changed, 322 insertions(+), 187 deletions(-) diff --git a/README.md b/README.md index d6d9b96..452fc79 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ Donate bitcoin: 3PRzESHsTVkCFK7osjwFGQLZjSf7qXP1Ta `steampy` is a library for Python, inspired by node-steam-tradeoffers, node-steam and other libraries for Node.js. It was designed as a simple lightweight library, combining features of many steam libraries from Node.js into a single python module. `steampy` is capable of logging into steam, fetching trade offers and handling them in simple manner, using steam user credentials -and SteamGuard file(no need to extract and pass sessionID and webCookie). +and SteamGuard file, or by passing sessionID and webCookie cookies. +'steampy' is also capable of using proxies. `steampy` is developed with Python 3 using type hints and many other features its supported for Windows, Linux and MacOs. Table of Content @@ -50,6 +51,13 @@ Usage [Obtaining SteamGuard using Android emulation]( https://github.com/codepath/android_guides/wiki/Genymotion-2.0-Emulators-with-Google-Play-support) +** __init__(self, api_key: str, username: str = None, password: str = None, steam_guard: str = None, + login_cookies: dict = None, proxies: dict = None) -> None:** + + +SteamClient needs at least api_key to provide some functionalities. User can also provide username, password +and SteamGuard file to be able to log in and use more methods. Proxies are also supported. + ```python from steampy.client import SteamClient @@ -57,6 +65,32 @@ steam_client = SteamClient('MY_API_KEY') steam_client.login('MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') ``` +User can also provide login_cookies from browser to log in by cookies. + +```python +from steampy.client import SteamClient + +login_cookies = {} # provide dict with cookies +steam_client = SteamClient('MY_API_KEY',login_cookies=login_cookies) +assert steam_client.was_login_executed +``` + +`proxies` dict can be provided for using proxy for internal SteamClient session. + +```python +from steampy.client import SteamClient + +proxies = { + "http": "http://login:password@host:port", + "https": "http://login:password@host:port" +} + +steam_client = SteamClient('MY_API_KEY', proxies=proxies) + +``` + + + If you have `steamid`, `shared_secret` and `identity_secret` you can place it in file `Steamguard.txt` instead of fetching SteamGuard file from device. ```python { @@ -91,6 +125,33 @@ SteamClient methods Unless specified in documentation, the method does not require login to work(it uses API Key from constructor instead) +**def set_proxy(self, proxy: dict) -> dict** + +Set proxy for steampy session, example: + +```python +from steampy.client import SteamClient + +steam_client = SteamClient('MY_API_KEY') +proxies = { + "http": "http://login:password@host:port", + "https": "http://login:password@host:port" +} +steam_client.set_proxies(proxies) + +``` + +**def set_login_cookies(self, cookies: dict) -> None** + +Set login cookies, can be used instead of normal `login` method. + +```python +from steampy.client import SteamClient + +login_cookies = {} # provide dict with cookies +steam_client = SteamClient('MY_API_KEY',login_cookies=login_cookies) +assert steam_client.was_login_executed +``` **login(username: str, password: str, steam_guard: str) -> requests.Response** @@ -106,7 +167,9 @@ steam_client.login('MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') You can also use `with` statement to automatically login and logout. ```python -with SteamClient(api_key, login, password, steam_guard_file) as client: +from steampy.client import SteamClient + +with SteamClient('MY_API_KEY', 'MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') as client: client.some_method1(...) client.some_method2(...) ... @@ -128,7 +191,9 @@ steam_client.logout() You can also use `with` statement to automatically login and logout. ```python -with SteamClient(api_key, login, password, steam_guard_file) as client: +from steampy.client import SteamClient + +with SteamClient('MY_API_KEY', 'MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') as client: client.some_method1(...) client.some_method2(...) ... @@ -254,115 +319,118 @@ and descriptions merged with data are value. `Count` parameter is default max number of items, that can be fetched. Inventory entries looks like this: + ```python -{'7146788981': {'actions': [{'link': 'steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20S%owner_steamid%A%assetid%D316070896107169653', - 'name': 'Inspect in Game...'}], - 'amount': '1', - 'appid': '730', - 'background_color': '', - 'classid': '1304827205', - 'commodity': 0, - 'contextid': '2', - 'descriptions': [{'type': 'html', - 'value': 'Exterior: Field-Tested'}, - {'type': 'html', 'value': ' '}, - {'type': 'html', - 'value': 'Powerful and reliable, the AK-47 ' - 'is one of the most popular assault ' - 'rifles in the world. It is most ' - 'deadly in short, controlled bursts ' - 'of fire. It has been painted using ' - 'a carbon fiber hydrographic and a ' - 'dry-transfer decal of a red ' - 'pinstripe.\n' - '\n' - 'Never be afraid to push it to ' - 'the limit'}, - {'type': 'html', 'value': ' '}, - {'app_data': {'def_index': '65535', - 'is_itemset_name': 1}, - 'color': '9da1a9', - 'type': 'html', - 'value': 'The Phoenix Collection'}, - {'type': 'html', 'value': ' '}, - {'app_data': {'def_index': '65535'}, - 'type': 'html', - 'value': '

Sticker: ' - 'PENTA Sports | Katowice 2015, ' - 'PENTA Sports | Katowice 2015, ' - 'PENTA Sports | Katowice 2015, ' - 'mousesports | Cologne ' - '2015
'}], - 'icon_drag_url': '', - 'icon_url': '-9a81dlWLwJ2UUGcVs_nsVtzdOEdtWwKGZZLQHTxDZ7I56KU0Zwwo4NUX4oFJZEHLbXH5ApeO4YmlhxYQknCRvCo04DEVlxkKgpot7HxfDhjxszJemkV09-5lpKKqPrxN7LEmyVQ7MEpiLuSrYmnjQO3-UdsZGHyd4_Bd1RvNQ7T_FDrw-_ng5Pu75iY1zI97bhLsvQz', - 'icon_url_large': '-9a81dlWLwJ2UUGcVs_nsVtzdOEdtWwKGZZLQHTxDZ7I56KU0Zwwo4NUX4oFJZEHLbXH5ApeO4YmlhxYQknCRvCo04DEVlxkKgpot7HxfDhjxszJemkV09-5lpKKqPrxN7LEm1Rd6dd2j6eQ9N2t2wK3-ENsZ23wcIKRdQE2NwyD_FK_kLq9gJDu7p_KyyRr7nNw-z-DyIFJbNUz', - 'id': '7146788981', - 'instanceid': '480085569', - 'market_actions': [{'link': 'steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20M%listingid%A%assetid%D316070896107169653', - 'name': 'Inspect in Game...'}], - 'market_hash_name': 'AK-47 | Redline (Field-Tested)', - 'market_name': 'AK-47 | Redline (Field-Tested)', - 'market_tradable_restriction': '7', - 'marketable': 1, - 'name': 'AK-47 | Redline', - 'name_color': 'D2D2D2', - 'owner_descriptions': '', - 'tags': [{'category': 'Type', - 'category_name': 'Type', - 'internal_name': 'CSGO_Type_Rifle', - 'name': 'Rifle'}, - {'category': 'Weapon', - 'category_name': 'Weapon', - 'internal_name': 'weapon_ak47', - 'name': 'AK-47'}, - {'category': 'ItemSet', - 'category_name': 'Collection', - 'internal_name': 'set_community_2', - 'name': 'The Phoenix Collection'}, - {'category': 'Quality', - 'category_name': 'Category', - 'internal_name': 'normal', - 'name': 'Normal'}, - {'category': 'Rarity', - 'category_name': 'Quality', - 'color': 'd32ce6', - 'internal_name': 'Rarity_Legendary_Weapon', - 'name': 'Classified'}, - {'category': 'Exterior', - 'category_name': 'Exterior', - 'internal_name': 'WearCategory2', - 'name': 'Field-Tested'}, - {'category': 'Tournament', - 'category_name': 'Tournament', - 'internal_name': 'Tournament6', - 'name': '2015 ESL One Katowice'}, - {'category': 'Tournament', - 'category_name': 'Tournament', - 'internal_name': 'Tournament7', - 'name': '2015 ESL One Cologne'}, - {'category': 'TournamentTeam', - 'category_name': 'Team', - 'internal_name': 'Team39', - 'name': 'PENTA Sports'}, - {'category': 'TournamentTeam', - 'category_name': 'Team', - 'internal_name': 'Team29', - 'name': 'mousesports'}], - 'tradable': 1, - 'type': 'Classified Rifle'}} +inventory_entry = {'7146788981': {'actions': [{ + 'link': 'steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20S%owner_steamid%A%assetid%D316070896107169653', + 'name': 'Inspect in Game...'}], + 'amount': '1', + 'appid': '730', + 'background_color': '', + 'classid': '1304827205', + 'commodity': 0, + 'contextid': '2', + 'descriptions': [{'type': 'html', + 'value': 'Exterior: Field-Tested'}, + {'type': 'html', 'value': ' '}, + {'type': 'html', + 'value': 'Powerful and reliable, the AK-47 ' + 'is one of the most popular assault ' + 'rifles in the world. It is most ' + 'deadly in short, controlled bursts ' + 'of fire. It has been painted using ' + 'a carbon fiber hydrographic and a ' + 'dry-transfer decal of a red ' + 'pinstripe.\n' + '\n' + 'Never be afraid to push it to ' + 'the limit'}, + {'type': 'html', 'value': ' '}, + {'app_data': {'def_index': '65535', + 'is_itemset_name': 1}, + 'color': '9da1a9', + 'type': 'html', + 'value': 'The Phoenix Collection'}, + {'type': 'html', 'value': ' '}, + {'app_data': {'def_index': '65535'}, + 'type': 'html', + 'value': '

Sticker: ' + 'PENTA Sports | Katowice 2015, ' + 'PENTA Sports | Katowice 2015, ' + 'PENTA Sports | Katowice 2015, ' + 'mousesports | Cologne ' + '2015
'}], + 'icon_drag_url': '', + 'icon_url': '-9a81dlWLwJ2UUGcVs_nsVtzdOEdtWwKGZZLQHTxDZ7I56KU0Zwwo4NUX4oFJZEHLbXH5ApeO4YmlhxYQknCRvCo04DEVlxkKgpot7HxfDhjxszJemkV09-5lpKKqPrxN7LEmyVQ7MEpiLuSrYmnjQO3-UdsZGHyd4_Bd1RvNQ7T_FDrw-_ng5Pu75iY1zI97bhLsvQz', + 'icon_url_large': '-9a81dlWLwJ2UUGcVs_nsVtzdOEdtWwKGZZLQHTxDZ7I56KU0Zwwo4NUX4oFJZEHLbXH5ApeO4YmlhxYQknCRvCo04DEVlxkKgpot7HxfDhjxszJemkV09-5lpKKqPrxN7LEm1Rd6dd2j6eQ9N2t2wK3-ENsZ23wcIKRdQE2NwyD_FK_kLq9gJDu7p_KyyRr7nNw-z-DyIFJbNUz', + 'id': '7146788981', + 'instanceid': '480085569', + 'market_actions': [{ + 'link': 'steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20M%listingid%A%assetid%D316070896107169653', + 'name': 'Inspect in Game...'}], + 'market_hash_name': 'AK-47 | Redline (Field-Tested)', + 'market_name': 'AK-47 | Redline (Field-Tested)', + 'market_tradable_restriction': '7', + 'marketable': 1, + 'name': 'AK-47 | Redline', + 'name_color': 'D2D2D2', + 'owner_descriptions': '', + 'tags': [{'category': 'Type', + 'category_name': 'Type', + 'internal_name': 'CSGO_Type_Rifle', + 'name': 'Rifle'}, + {'category': 'Weapon', + 'category_name': 'Weapon', + 'internal_name': 'weapon_ak47', + 'name': 'AK-47'}, + {'category': 'ItemSet', + 'category_name': 'Collection', + 'internal_name': 'set_community_2', + 'name': 'The Phoenix Collection'}, + {'category': 'Quality', + 'category_name': 'Category', + 'internal_name': 'normal', + 'name': 'Normal'}, + {'category': 'Rarity', + 'category_name': 'Quality', + 'color': 'd32ce6', + 'internal_name': 'Rarity_Legendary_Weapon', + 'name': 'Classified'}, + {'category': 'Exterior', + 'category_name': 'Exterior', + 'internal_name': 'WearCategory2', + 'name': 'Field-Tested'}, + {'category': 'Tournament', + 'category_name': 'Tournament', + 'internal_name': 'Tournament6', + 'name': '2015 ESL One Katowice'}, + {'category': 'Tournament', + 'category_name': 'Tournament', + 'internal_name': 'Tournament7', + 'name': '2015 ESL One Cologne'}, + {'category': 'TournamentTeam', + 'category_name': 'Team', + 'internal_name': 'Team39', + 'name': 'PENTA Sports'}, + {'category': 'TournamentTeam', + 'category_name': 'Team', + 'internal_name': 'Team29', + 'name': 'mousesports'}], + 'tradable': 1, + 'type': 'Classified Rifle'}} ``` **get_partner_inventory(partner_steam_id: str, game: GameOptions, merge: bool = True, count: int = 5000) -> dict** @@ -380,9 +448,11 @@ to covnert money string to Decimal if `convert_to_decimal` is set to `True`. Example: ```python - with SteamClient(api_key, login, password, steam_guard_file) as client: +from steampy.client import SteamClient +from decimal import Decimal +with SteamClient('MY_API_KEY', 'MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') as client: wallet_balance = client.get_wallet_balance() - self.assertTrue(type(wallet_balance), decimal.Decimal) + assert type(wallet_balance) == Decimal ``` market methods @@ -400,10 +470,13 @@ Default currency is USD May rise `TooManyRequests` exception if used more than 20 times in 60 seconds. ```python -steam_client = SteamClient(self.credentials.api_key) +from steampy.client import SteamClient +from steampy.models import GameOptions + +steam_client = SteamClient('API_KEY') item = 'M4A1-S | Cyrex (Factory New)' -steam_client.market.fetch_price(item, game=GameOptions.CS) -{'volume': '208', 'lowest_price': '$11.30 USD', 'median_price': '$11.33 USD', 'success': True} +price = steam_client.market.fetch_price(item, game=GameOptions.CS) +# price == {'volume': '208', 'lowest_price': '$11.30 USD', 'median_price': '$11.33 USD', 'success': True} ``` **fetch_price_history(item_hash_name: str, game: GameOptions) -> dict** @@ -412,8 +485,10 @@ Using `SteamClient.login` method is required before usage Returns list of price history of and item. ```python -with SteamClient(self.credentials.api_key, self.credentials.login, - self.credentials.password, self.steam_guard_file) as client: +from steampy.client import SteamClient +from steampy.models import GameOptions + +with SteamClient('MY_API_KEY', 'MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') as client: item = 'M4A1-S | Cyrex (Factory New)' response = client.market.fetch_price_history(item, GameOptions.CS) response['prices'][0] @@ -429,9 +504,10 @@ Using `SteamClient.login` method is required before usage Returns market listings posted by user ```python -steam_client = SteamClient(self.credentials.api_key) -steam_client.login('MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') -listings = steam_client.market.get_my_market_listings() +from steampy.client import SteamClient + +with SteamClient('MY_API_KEY', 'MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') as client: + listings = client.market.get_my_market_listings() ``` @@ -442,11 +518,13 @@ Using `SteamClient.login` method is required before usage Create sell order of the asset on the steam market. ```python -steam_client = SteamClient(self.credentials.api_key) -steam_client.login('MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') -asset_id_to_sell = 'some_asset_id' -game = GameOptions.DOTA2 -sell_response = steam_client.market.create_sell_order(asset_id_to_sell, game, "10000") +from steampy.client import SteamClient +from steampy.models import GameOptions + +with SteamClient('MY_API_KEY', 'MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') as client: + asset_id_to_sell = 'some_asset_id' + game = GameOptions.DOTA2 + sell_response = client.market.create_sell_order(asset_id_to_sell, game, "10000") ``` ⚠️ `money_to_receive` has to be in cents, so "100.00" should be passed has "10000" @@ -458,10 +536,12 @@ Using `SteamClient.login` method is required before usage Create buy order of the assets on the steam market. ```python -steam_client = SteamClient(self.credentials.api_key) -steam_client.login('MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') -response = steam_client.market.create_buy_order("AK-47 | Redline (Field-Tested)", "1034", 2, GameOptions.CS, Currency.EURO) -buy_order_id = response["buy_orderid"] +from steampy.client import SteamClient +from steampy.models import GameOptions, Currency + +with SteamClient('MY_API_KEY', 'MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') as client: + response = client.market.create_buy_order("AK-47 | Redline (Field-Tested)", "1034", 2, GameOptions.CS, Currency.EURO) + buy_order_id = response["buy_orderid"] ``` ⚠️ `price_single_item` has to be in cents, so "10.34" should be passed has "1034" @@ -472,11 +552,13 @@ Using `SteamClient.login` method is required before usage Buy a certain item from market listing. ```python -steam_client = SteamClient(self.credentials.api_key) -steam_client.login('MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') -response = steam_client.market.buy_item('AK-47 | Redline (Field-Tested)', '1942659007774983251', 81, 10, +from steampy.client import SteamClient +from steampy.models import Currency, GameOptions + +with SteamClient('MY_API_KEY', 'MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') as client: + response = client.market.buy_item('AK-47 | Redline (Field-Tested)', '1942659007774983251', 81, 10, GameOptions.CS, Currency.RUB) -wallet_balance = response["wallet_info"]["wallet_balance"] + wallet_balance = response["wallet_info"]["wallet_balance"] ``` **cancel_sell_order(sell_listing_id: str) -> None** @@ -486,10 +568,11 @@ Using `SteamClient.login` method is required before usage Cancel previously requested sell order on steam market. ```python -steam_client = SteamClient(self.credentials.api_key) -steam_client.login('MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') -sell_order_id = "some_sell_order_id" -response = steam_client.market.cancel_sell_order(sell_order_id) +from steampy.client import SteamClient + +with SteamClient('MY_API_KEY', 'MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') as client: + sell_order_id = "some_sell_order_id" + response = client.market.cancel_sell_order(sell_order_id) ``` **cancel_buy_order(buy_order_id) -> dict** @@ -499,10 +582,11 @@ Using `SteamClient.login` method is required before usage Cancel previously requested buy order on steam market. ```python -steam_client = SteamClient(self.credentials.api_key) -steam_client.login('MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') -buy_order_id = "some_buy_order_id" -response = steam_client.market.cancel_buy_order(buy_order_id) +from steampy.client import SteamClient + +with SteamClient('MY_API_KEY', 'MY_USERNAME', 'MY_PASSWORD', 'PATH_TO_STEAMGUARD_FILE') as client: + buy_order_id = "some_buy_order_id" + response = client.market.cancel_buy_order(buy_order_id) ``` Currencies @@ -654,7 +738,7 @@ In some tests you also have to obtain `transaction_id`. You can do it by `SteamClient.get_trade_offers` or by logging manually into steam account in browser and get it from url In some tests you also have to obtain partner steam id. -You can do it by by logging manually into steam account in browser and get it from url +You can do it by logging manually into steam account in browser and get it from url License ======= diff --git a/steampy/client.py b/steampy/client.py index 6337910..dc99246 100644 --- a/steampy/client.py +++ b/steampy/client.py @@ -1,5 +1,5 @@ import decimal - +import re import bs4 import urllib.parse as urlparse from typing import List, Union @@ -15,35 +15,72 @@ from steampy.models import Asset, TradeOfferState, SteamUrl, GameOptions from steampy.utils import text_between, texts_between, merge_items_with_descriptions_from_inventory, \ steam_id_to_account_id, merge_items_with_descriptions_from_offers, get_description_key, \ - merge_items_with_descriptions_from_offer, account_id_to_steam_id, get_key_value_from_url, parse_price - - -def login_required(func): - def func_wrapper(self, *args, **kwargs): - if not self.was_login_executed: - raise LoginRequired('Use login method first') - else: - return func(self, *args, **kwargs) - - return func_wrapper + merge_items_with_descriptions_from_offer, account_id_to_steam_id, get_key_value_from_url, parse_price, \ + ping_proxy, login_required class SteamClient: - def __init__(self, api_key: str, username: str = None, password: str = None, steam_guard:str = None) -> None: + def __init__(self, api_key: str, username: str = None, password: str = None, steam_guard: str = None, + login_cookies: dict = None, proxies: dict = None) -> None: self._api_key = api_key self._session = requests.Session() - self.steam_guard = steam_guard + if proxies: + self.set_proxies(proxies) + self.steam_guard_string = steam_guard + if self.steam_guard_string is not None: + self.steam_guard = guard.load_steam_guard(self.steam_guard) + else: + self.steam_guard = None self.was_login_executed = False self.username = username self._password = password self.market = SteamMarket(self._session) self.chat = SteamChat(self._session) + if login_cookies: + self.set_login_cookies(login_cookies) + + 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"\}') + proxy_status = ping_proxy(proxies) + if proxy_status is True: + self._session.proxies.update(proxies) + return proxies + + def set_login_cookies(self, cookies: dict) -> None: + self._session.cookies.update(cookies) + self.was_login_executed = True + if self.steam_guard is None: + self.steam_guard = {"steam_id": self.get_steam_id()} + self.market._set_login_executed(self.steam_guard, self._get_session_id()) - def login(self, username: str, password: str, steam_guard: str) -> None: - self.steam_guard = guard.load_steam_guard(steam_guard) - self.username = username - self._password = password - LoginExecutor(username, password, self.steam_guard['shared_secret'], self._session).login() + @login_required + def get_steam_id(self) -> int: + url = SteamUrl.COMMUNITY_URL + response = self._session.get(url) + steam_id = re.match(r'g_steamID = "(\d+)";', response.text) + if steam_id: + return int(steam_id.group(1)) + else: + raise ValueError('Invalid steam_id: {}'.format(steam_id)) + + def login(self, username: str = None, password: str = None, steam_guard: str = None) -> None: + if self.was_login_executed and self.is_session_alive(): + # session is alive, no need login again + return + + if None in [self.username, self._password, self.steam_guard_string] and None in [username, password, steam_guard]: + raise InvalidCredentials('You have to pass username, password and steam_guard' + 'parameters when using "login" method') + + if None in [self.username, self._password, self.steam_guard_string]: + self.steam_guard_string = steam_guard + self.steam_guard = guard.load_steam_guard(steam_guard) + self.username = username + self._password = password + + LoginExecutor(self.username, self._password, self.steam_guard['shared_secret'], self._session).login() self.was_login_executed = True self.market._set_login_executed(self.steam_guard, self._get_session_id()) @@ -57,10 +94,7 @@ def logout(self) -> None: self.was_login_executed = False def __enter__(self): - if None in [self.username, self._password, self.steam_guard]: - raise InvalidCredentials('You have to pass username, password and steam_guard' - 'parameters when using "with" statement') - self.login(self.username, self._password, self.steam_guard) + self.login(self.username, self._password, self.steam_guard_string) return self def __exit__(self, exc_type, exc_val, exc_tb): @@ -76,9 +110,9 @@ def api_call(self, request_method: str, interface: str, api_method: str, version params: dict = None) -> requests.Response: url = '/'.join([SteamUrl.API_URL, interface, api_method, version]) if request_method == 'GET': - response = requests.get(url, params=params, verify=self._session.verify, auth=self._session.auth) + response = self._session.get(url, params=params) else: - response = requests.post(url, data=params, verify=self._session.verify, auth=self._session.auth) + response = self._session.post(url, data=params) if self.is_invalid_api_key(response): raise InvalidCredentials('Invalid API key') return response @@ -94,7 +128,8 @@ def get_my_inventory(self, game: GameOptions, merge: bool = True, count: int = 5 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) -> dict: + def get_partner_inventory(self, partner_steam_id: str, game: GameOptions, merge: bool = True, + count: int = 5000) -> dict: url = '/'.join([SteamUrl.COMMUNITY_URL, 'inventory', partner_steam_id, game.app_id, game.context_id]) params = {'l': 'english', 'count': count} @@ -250,7 +285,7 @@ def get_profile(self, steam_id: str) -> dict: data = response.json() return data['response']['players'][0] - def get_friend_list(self, steam_id: str, relationship_filter: str="all") -> dict: + def get_friend_list(self, steam_id: str, relationship_filter: str = "all") -> dict: params = { 'key': self._api_key, 'steamid': steam_id, @@ -288,7 +323,7 @@ def get_escrow_duration(self, trade_offer_url: str) -> int: @login_required def make_offer_with_url(self, items_from_me: List[Asset], items_from_them: List[Asset], - trade_offer_url: str, message: str = '', case_sensitive: bool=True) -> dict: + trade_offer_url: str, message: str = '', case_sensitive: 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) partner_steam_id = account_id_to_steam_id(partner_account_id) diff --git a/steampy/exceptions.py b/steampy/exceptions.py index c40f9f7..0a55123 100644 --- a/steampy/exceptions.py +++ b/steampy/exceptions.py @@ -24,3 +24,6 @@ class CaptchaRequired(Exception): class ConfirmationExpected(Exception): pass + +class ProxyConnectionError(Exception): + pass \ No newline at end of file diff --git a/steampy/market.py b/steampy/market.py index b95fb07..6a156ab 100644 --- a/steampy/market.py +++ b/steampy/market.py @@ -7,17 +7,7 @@ from steampy.exceptions import ApiException, TooManyRequests, LoginRequired from steampy.models import Currency, SteamUrl, GameOptions from steampy.utils import text_between, get_listing_id_to_assets_address_from_html, get_market_listings_from_html, \ - merge_items_with_descriptions_from_listing, get_market_sell_listings_from_api - - -def login_required(func): - def func_wrapper(self, *args, **kwargs): - if not self.was_login_executed: - raise LoginRequired('Use login method first on SteamClient') - else: - return func(self, *args, **kwargs) - - return func_wrapper + merge_items_with_descriptions_from_listing, get_market_sell_listings_from_api, login_required class SteamMarket: @@ -32,9 +22,9 @@ def _set_login_executed(self, steamguard: dict, session_id: str): self._session_id = session_id self.was_login_executed = True - def fetch_price(self, item_hash_name: str, game: GameOptions, currency: str = Currency.USD) -> dict: + def fetch_price(self, item_hash_name: str, game: GameOptions, currency: str = Currency.USD, country='PL') -> dict: url = SteamUrl.COMMUNITY_URL + '/market/priceoverview/' - params = {'country': 'PL', + params = {'country': country, 'currency': currency.value, 'appid': game.app_id, 'market_hash_name': item_hash_name} @@ -104,7 +94,8 @@ def create_sell_order(self, assetid: str, game: GameOptions, money_to_receive: s } headers = {'Referer': "%s/profiles/%s/inventory" % (SteamUrl.COMMUNITY_URL, self._steam_guard['steamid'])} response = self._session.post(SteamUrl.COMMUNITY_URL + "/market/sellitem/", data, headers=headers).json() - if response.get("needs_mobile_confirmation"): + has_pending_confirmation = 'pending confirmation' in response.get('message', '') + if response.get("needs_mobile_confirmation") or (response.get('success') is False and has_pending_confirmation): return self._confirm_sell_listing(assetid) return response @@ -119,7 +110,7 @@ def create_buy_order(self, market_name: str, price_single_item: str, quantity: i "price_total": str(Decimal(price_single_item) * Decimal(quantity)), "quantity": quantity } - headers = {'Referer': "%s/market/listings/%s/%s" % (SteamUrl.COMMUNITY_URL, game.app_id, + headers = {'Referer': "%s/market/listings/%s/%s" % (SteamUrl.COMMUNITY_URL, game.app_id, urllib.parse.quote(market_name))} response = self._session.post(SteamUrl.COMMUNITY_URL + "/market/createbuyorder/", data, headers=headers).json() diff --git a/steampy/utils.py b/steampy/utils.py index a44c0fe..269f0af 100644 --- a/steampy/utils.py +++ b/steampy/utils.py @@ -1,5 +1,7 @@ import os import re +import requests +import re import copy import math import struct @@ -11,6 +13,17 @@ from requests.structures import CaseInsensitiveDict from steampy.models import GameOptions +from steampy.exceptions import ProxyConnectionError, LoginRequired + + +def login_required(func): + def func_wrapper(self, *args, **kwargs): + if not self.was_login_executed: + raise LoginRequired('Use login method first') + else: + return func(self, *args, **kwargs) + + return func_wrapper def text_between(text: str, begin: str, end: str) -> str: @@ -200,7 +213,9 @@ def get_buy_orders_from_node(node: Tag) -> dict: "order_id": order.attrs["id"].replace("mybuyorder_", ""), "quantity": int(qnt_price_raw[0].strip()), "price": qnt_price_raw[1].strip(), - "item_name": order.a.text + "item_name": order.a.text, + "icon_url": order.select(f"img[class=market_listing_item_img]")[0].attrs["src"].rsplit('/', 2)[-2], + "game_name": order.select("span[class=market_listing_game_name]")[0].text } buy_orders_dict[order["order_id"]] = order return buy_orders_dict @@ -237,3 +252,10 @@ def __init__(self, login: str, password: str, api_key: str): self.login = login self.password = password self.api_key = api_key + +def ping_proxy(proxies: dict): + try: + requests.get('https://steamcommunity.com/', proxies = proxies) + return True + except Exception as e: + raise ProxyConnectionError("Proxy not working for steamcommunity.com")