diff --git a/CITATION.cff b/CITATION.cff index 3e8502e..e8364b8 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -11,12 +11,11 @@ authors: - given-names: Dmytro family-names: Tkachenko email: itsme@somespecial.one -repository-code: 'https://github.com/somespecialone/aiosteampy' -url: 'https://aiosteampy.somespecial.one/' -repository-artifact: 'https://pypi.org/project/aiosteampy/' +repository-code: "https://github.com/somespecialone/aiosteampy" +url: "https://aiosteampy.somespecial.one/" +repository-artifact: "https://pypi.org/project/aiosteampy/" abstract: >- - Simple library to trade and interact with steam market, - webapi, guard. + Trade and interact with steam market, webapi, guard. keywords: - python - asyncio diff --git a/README.md b/README.md index 72cc015..f96c23d 100644 --- a/README.md +++ b/README.md @@ -43,17 +43,19 @@ pipenv install aiosteampy poetry add aiosteampy ``` -Project have extra [currencies converter](https://aiosteampy.somespecial.one/ext/converter/) with -target dependency `aiosteampy[converter]`. For instance: +Project have some extras [currencies converter](https://aiosteampy.somespecial.one/ext/converter/), +[socks proxies](https://aiosteampy.somespecial.one/proxies). +To install them all, please, use `aiosteampy[all]` install target: ```shell -poetry add aiosteampy[converter] +poetry add aiosteampy[all] ``` > [!TIP] -> [aiohttp docs](https://docs.aiohttp.org/en/stable/#installing-all-speedups-in-one-command) recommends installing speedups (`aiodns`, `cchardet`, ...) +> [aiohttp docs](https://docs.aiohttp.org/en/stable/#installing-all-speedups-in-one-command) recommends installing +> speedups (`aiodns`, `cchardet`, ...) @@ -71,6 +73,7 @@ with modern async/await syntax. - Declarative: there is models almost for every data. - Typed: for editor support most things are typed. - Short: I really tried to fit most important for steam trading methods. +- Connection behind web proxy. ## What can I do with this @@ -107,6 +110,8 @@ I will be very grateful for helping me get the things right. ## Credits - [bukson/steampy](https://github.com/bukson/steampy) +- [aiohttp-socks](https://github.com/romis2012/aiohttp-socks) +- [croniter](https://github.com/kiorky/croniter) - [DoctorMcKay/node-steamcommunity](https://github.com/DoctorMcKay/node-steamcommunity) - [Identifying Steam items](https://dev.doctormckay.com/topic/332-identifying-steam-items/) - [Revadike/InternalSteamWebAPI](https://github.com/Revadike/InternalSteamWebAPI) diff --git a/aiosteampy/__init__.py b/aiosteampy/__init__.py index 6146df6..3fabeff 100644 --- a/aiosteampy/__init__.py +++ b/aiosteampy/__init__.py @@ -1,5 +1,5 @@ """ -Simple library to trade and interact with steam market, webapi, guard. +Trade and interact with steam market, webapi, guard. """ from .exceptions import ApiError, LoginError, ConfirmationError, SessionExpired diff --git a/aiosteampy/client.py b/aiosteampy/client.py index c9847bd..058de6d 100644 --- a/aiosteampy/client.py +++ b/aiosteampy/client.py @@ -70,6 +70,7 @@ def __init__( lang=Language.ENGLISH, tz_offset=0.0, session: ClientSession = None, + proxy: str = None, user_agent: str = None, ): self.username = username @@ -84,7 +85,13 @@ def __init__( self._wallet_country = wallet_country - super().__init__(session=session, user_agent=user_agent, access_token=access_token, refresh_token=refresh_token) + super().__init__( + session=session, + user_agent=user_agent, + access_token=access_token, + refresh_token=refresh_token, + proxy=proxy, + ) self._set_init_cookies(lang, tz_offset) diff --git a/aiosteampy/converter.py b/aiosteampy/converter.py index 8a2d8fb..afb17be 100644 --- a/aiosteampy/converter.py +++ b/aiosteampy/converter.py @@ -1,3 +1,5 @@ +"""Currency converter extension.""" + import asyncio from collections import UserDict from datetime import datetime, timedelta @@ -15,10 +17,7 @@ """ The `aiosteampy.converter` module requires the `croniter` library to be installed to make the rates synchronization with backend service work. - In order You need this functionality, You can use `aiosteampy[converter]` dependency install target: - `pip install aiosteampy[converter]` - or - `poetry add aiosteampy[converter]` + In order You need this functionality, You can use `aiosteampy[converter]` dependency install target. """, category=RuntimeWarning, ) @@ -133,7 +132,7 @@ def convert(self, amount: int, currency: Currency, target=Currency.USD) -> int: # direct conversion # return round(amount * (target_rate / source_rate)) - + # with USD in middle step usd_amount = round(amount * (1 / source_rate)) return round(usd_amount * target_rate) diff --git a/aiosteampy/http.py b/aiosteampy/http.py index 97d53b0..db74827 100644 --- a/aiosteampy/http.py +++ b/aiosteampy/http.py @@ -1,4 +1,12 @@ -from aiohttp import ClientSession +from functools import partial + +from yarl import URL +from aiohttp import ClientSession, InvalidURL + +try: + from aiohttp_socks import ProxyConnector +except ImportError: + ProxyConnector = None __all__ = ("SteamHTTPTransportMixin",) @@ -16,8 +24,35 @@ class SteamHTTPTransportMixin: session: ClientSession - def __init__(self, *args, session: ClientSession = None, user_agent: str = None, **kwargs): - self.session = session or ClientSession(raise_for_status=True) + def __init__(self, *args, session: ClientSession = None, proxy: str = None, user_agent: str = None, **kwargs): + if proxy and session: + raise ValueError("You need to handle proxy connection by yourself with predefined session instance.") + elif proxy: + if "socks" in proxy: + if ProxyConnector is None: + raise TypeError( + """ + To use `socks` type proxies you need `aiohttp_socks` package. + You can do this with `aiosteampy[socks]` dependency install target. + """ + ) + + self.session = ClientSession(connector=ProxyConnector.from_url(proxy), raise_for_status=True) + else: # http/s + self.session = ClientSession(raise_for_status=True) + + try: + proxy = URL(proxy) + except ValueError as e: + raise InvalidURL(proxy) from e + + self.session._request = partial(self.session._request, proxy=proxy) # patch session instance + + elif session: + self.session = session + else: + self.session = ClientSession(raise_for_status=True) + if user_agent: self.user_agent = user_agent diff --git a/aiosteampy/trade.py b/aiosteampy/trade.py index 38c797b..c4c6561 100644 --- a/aiosteampy/trade.py +++ b/aiosteampy/trade.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, overload, Literal, Type, TypeAlias, Callable +from typing import TYPE_CHECKING, overload, Literal, Type, TypeAlias, Callable, Iterable, Sequence from datetime import datetime from json import dumps as jdumps @@ -423,7 +423,7 @@ async def accept_trade_offer(self: "SteamCommunityMixin", offer: int | TradeOffe raise ValueError("You can't accept your offer! Are you trying to cancel outgoing offer?") offer_id = offer.id partner = offer.partner_id64 - to_remove = TradeOffer + to_remove = offer else: # int if not partner: fetched = await self.get_or_fetch_trade_offer(offer) @@ -452,8 +452,8 @@ async def accept_trade_offer(self: "SteamCommunityMixin", offer: int | TradeOffe async def make_trade_offer( self, obj: int, - to_give: list[EconItemType] = ..., - to_receive: list[EconItemType] = ..., + to_give: Sequence[EconItemType] = ..., + to_receive: Sequence[EconItemType] = ..., message: str = ..., *, token: str = ..., @@ -466,8 +466,8 @@ async def make_trade_offer( async def make_trade_offer( self, obj: str, - to_give: list[EconItemType] = ..., - to_receive: list[EconItemType] = ..., + to_give: Sequence[EconItemType] = ..., + to_receive: Sequence[EconItemType] = ..., message: str = ..., *, confirm: bool = ..., @@ -478,8 +478,8 @@ async def make_trade_offer( async def make_trade_offer( self: "SteamCommunityMixin", obj: int | str, - to_give: list[EconItemType] = (), - to_receive: list[EconItemType] = (), + to_give: Sequence[EconItemType] = (), + to_receive: Sequence[EconItemType] = (), message="", *, token: str = None, @@ -493,13 +493,13 @@ async def make_trade_offer( .. note:: Make sure that partner is in friends list if you not pass trade url or trade token. :param obj: partner trade url, partner id(id32 or id64) - :param token: - :param to_give: - :param to_receive: - :param message: - :param confirm: - :param countered_id: - :param kwargs: + :param token: trade token (mandatory if `obj` is partner id) + :param to_give: sequence of items that you want to give + :param to_receive: sequence of items that you want to receive + :param message: message to the partner + :param confirm: auto-confirm offer + :param countered_id: id of offer that you want to counter. Use `counter_trade_offer` method for this + :param kwargs: additional data to send in payload :return: trade offer id :raises ValueError: trade is empty """ @@ -571,8 +571,8 @@ def _parse_make_offer_args(obj: str | int, token: str | None) -> tuple[str | Non async def counter_trade_offer( self, obj: TradeOffer, - to_give: list[EconItemType] = (), - to_receive: list[EconItemType] = (), + to_give: Sequence[EconItemType] = (), + to_receive: Sequence[EconItemType] = (), message="", *, confirm: bool = ..., @@ -583,8 +583,8 @@ async def counter_trade_offer( async def counter_trade_offer( self, obj: int, - to_give: list[EconItemType] = (), - to_receive: list[EconItemType] = (), + to_give: Sequence[EconItemType] = (), + to_receive: Sequence[EconItemType] = (), message="", *, partner_id: int, @@ -595,8 +595,8 @@ async def counter_trade_offer( def counter_trade_offer( self, obj: TradeOffer | int, - to_give: list[EconItemType] = (), - to_receive: list[EconItemType] = (), + to_give: Sequence[EconItemType] = (), + to_receive: Sequence[EconItemType] = (), message="", *, partner_id: int = None, diff --git a/aiosteampy/user_agents.py b/aiosteampy/user_agents.py new file mode 100644 index 0000000..82e11b7 --- /dev/null +++ b/aiosteampy/user_agents.py @@ -0,0 +1,42 @@ +"""User agents service extension.""" + +from collections import UserList +from random import choice + +from yarl import URL +from aiohttp import ClientSession + +API_URL = URL("https://randua.somespecial.one") + + +class UserAgentsService(UserList[str]): + """ + List-like class of user agents responsible for loading and getting random user agents. + + .. seealso:: https://github.com/somespecialone/random-user-agent + """ + + __slots__ = ("_api_url",) + + def __init__(self, *, api_url=API_URL): + """ + :param api_url: url of `random user agent` backend service api + """ + + super().__init__() + + self._api_url = api_url + + @property + def agents(self) -> list[str]: + return self.data + + async def load(self): + async with ClientSession(raise_for_status=True) as sess: + r = await sess.get(self._api_url / "all") + agents: list[str] = await r.json() + + self.data = agents + + def get_random(self) -> str: + return choice(self.agents) diff --git a/aiosteampy/utils.py b/aiosteampy/utils.py index 76388ea..26f6610 100644 --- a/aiosteampy/utils.py +++ b/aiosteampy/utils.py @@ -1,3 +1,5 @@ +"""Useful utils.""" + import asyncio from base64 import b64decode, b64encode from struct import pack, unpack @@ -12,7 +14,7 @@ from re import search as re_search from json import loads as j_loads -from aiohttp import ClientSession +from aiohttp import ClientSession, ClientResponse from yarl import URL from .typed import JWTToken @@ -97,7 +99,7 @@ def extract_openid_payload(page_text: str) -> dict[str, str]: } -async def do_session_steam_auth(session: ClientSession, auth_url: str | URL): +async def do_session_steam_auth(session: ClientSession, auth_url: str | URL) -> ClientResponse: """ Request auth page, find specs of steam openid and log in through steam with passed session. Use it when you need to log in 3rd party site trough Steam using only cookies. @@ -106,6 +108,7 @@ async def do_session_steam_auth(session: ClientSession, auth_url: str | URL): :param session: just session. :param auth_url: url to site, which redirect you to steam login page. + :return: response with history, headers and data """ r = await session.get(auth_url) @@ -113,7 +116,7 @@ async def do_session_steam_auth(session: ClientSession, auth_url: str | URL): data = extract_openid_payload(rt) - await session.post("https://steamcommunity.com/openid/login", data=data, allow_redirects=True) + return await session.post("https://steamcommunity.com/openid/login", data=data, allow_redirects=True) def get_cookie_value_from_session(session: ClientSession, url: URL, field: str) -> str | None: @@ -243,10 +246,11 @@ async def restore_from_cookies( *, init_data=True, **init_kwargs, -): +) -> bool: """ Helper func. Restore client session from cookies. Login if session is not alive. + Return `True` if cookies are valid and not expired. """ prepared = [] @@ -272,9 +276,11 @@ async def restore_from_cookies( client.session.cookie_jar.update_cookies(c) if not (await client.is_session_alive()): await client.login(init_data=init_data, **init_kwargs) + return False else: client._is_logged = True init_data and await client._init_data() + return True def get_jsonable_cookies(session: ClientSession) -> JSONABLE_COOKIE_JAR: @@ -399,3 +405,7 @@ def decode_jwt(token: str) -> JWTToken: raise ValueError("Invalid JWT", parts) return j_loads(b64decode(parts[1] + "==", altchars="-_")) + + +def patch_session_with_proxy(session: ClientSession, proxy: str): + pass diff --git a/docs/client.md b/docs/client.md index 29165ff..2d222df 100644 --- a/docs/client.md +++ b/docs/client.md @@ -57,8 +57,7 @@ histogram = await client.fetch_item_orders_histogram(12345687) ### Proxies -For proxies support you can use [aiohttp-socks](https://github.com/romis2012/aiohttp-socks) as you can create `session` by -yourself. +Read more about proxy support on the [dedicated page](./proxies.md) ### Inheritance diff --git a/docs/ext/converter.md b/docs/ext/converter.md index 970bbc7..22d9a24 100644 --- a/docs/ext/converter.md +++ b/docs/ext/converter.md @@ -1,6 +1,6 @@ # Currencies converter -A dict-like class to handle converting steam currencies. +A `dict-like` class to handle converting steam currencies. !!! note "" Instance is an API consumer of [SERT](https://github.com/somespecialone/sert) and use service endpoints. diff --git a/docs/ext/user_agents.md b/docs/ext/user_agents.md new file mode 100644 index 0000000..6eb808d --- /dev/null +++ b/docs/ext/user_agents.md @@ -0,0 +1,35 @@ +# User Agents Server + +`List-like` class of user agents responsible for loading and getting random user agents. + +!!! note "" + Instance is an API consumer of [Random User Agent](https://github.com/somespecialone/random-user-agent). + +## Creating instance and loading agents + +```python +from aiosteampy.user_agents import UserAgentsService + +user_agents = UserAgentsService() +await user_agents.load() +``` + +## Getting random + +```python +random_user_agent = user_agents.get_random() +``` + +## List behaviour + +Since the class inherits `collections.UserList`, it implements `list` methods, +which means you can do the following: + +```python +for agent in user_agents: # iterate over + print(agent) + +len(user_agents) # know agents count +user_agents.append("user_agent") # append +user_agents.remove("user-agent") # and even remove +``` diff --git a/docs/get_started.md b/docs/get_started.md index e33cc51..37ab51e 100644 --- a/docs/get_started.md +++ b/docs/get_started.md @@ -24,7 +24,8 @@ await client.login() ???+ tip "User-Agent" [Aiohttp](https://docs.aiohttp.org/en/stable/) uses its own `User-Agent` header by default. It is strongly recommended to replace it with your own. - You can easily get one from [randua.somespecial.one](https://randua.somespecial.one). + You can easily get one from [randua.somespecial.one](https://randua.somespecial.one) + or use [User Agent Service](./ext/user_agents.md). ### Do work diff --git a/docs/proxies.md b/docs/proxies.md new file mode 100644 index 0000000..81a8b78 --- /dev/null +++ b/docs/proxies.md @@ -0,0 +1,55 @@ +# Web proxies + +It is possible to hide all interactions with `Steam` servers (including all `session` requests) behind a web proxy. + +!!! warning "HTTPS proxies" + [Aiohttp](https://docs.aiohttp.org/en/stable/) has no support for `HTTPS` proxies at the current moment. + You can read more [here](https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support) + +## First and last steps + +To get `SteamClient` to connect, log in, and make requests to `Steam` through a web proxy, +you can pass `web proxy url string` when creating an instance: + +```python +from aiosteampy import SteamClient + +client = SteamClient(..., proxy="http://my-proxy.com") +``` + +## Authorization + +To pass username, password of the web proxy use `url string` with next format: + +`schema://user:password@host:port` + +For example: + +```python +proxy_url_with_auth = "http://username:password@my-proxy.com" +# or +proxy_url_with_auth = "http://username:password@127.0.0.1:1080" + +client = SteamClient(..., proxy=proxy_url_with_auth) +``` + +## SOCKS proxy type + +!!! note "Extra dependency" + To use `socks` type of the proxies you need [aiohttp_socks](https://github.com/romis2012/aiohttp-socks) package. + +`Aiosteampy` does all the necessary work behind the curtain, all you need to do is install `aiosteampy[socks]` dependency +target. + +```shell +poetry add aiosteampy[socks] +``` + +Then pass web proxy url: + +```python +client = SteamClient(..., proxy="socks5://username:password@127.0.0.1:1080") +``` + +!!! tip "Supported `socks` types" + All supported proxy types you can find in [aiohttp_socks repository page](https://github.com/romis2012/aiohttp-socks) diff --git a/mkdocs.yml b/mkdocs.yml index 7959838..e92502d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,7 @@ site_name: Aiosteampy site_url: https://aiosteampy.somespecial.one site_author: Dmytro Tkachenko -site_description: Simple library to trade and interact with steam market, webapi, guard. +site_description: Trade and interact with steam market, webapi, guard. repo_name: aiosteampy repo_url: https://github.com/somespecialone/aiosteampy @@ -77,9 +77,11 @@ nav: - Market: market.md - Trade: trade.md - Public: public.md + - "Proxies 🌐": proxies.md - States&Cache: states.md - Extensions: - Converter: ext/converter.md + - "User Agents Service": ext/user_agents.md - Examples: - "States mixin": examples/states.md - "Session persistence": examples/session.md diff --git a/poetry.lock b/poetry.lock index c07dec2..8407018 100644 --- a/poetry.lock +++ b/poetry.lock @@ -96,6 +96,21 @@ yarl = ">=1.0,<2.0" [package.extras] speedups = ["Brotli", "aiodns", "brotlicffi"] +[[package]] +name = "aiohttp-socks" +version = "0.8.4" +description = "Proxy connector for aiohttp" +optional = true +python-versions = "*" +files = [ + {file = "aiohttp_socks-0.8.4-py3-none-any.whl", hash = "sha256:74b21105634ed31d56ed6fee43701ca16218b53475e606d56950a4d17e8290ea"}, + {file = "aiohttp_socks-0.8.4.tar.gz", hash = "sha256:6b611d4ce838e9cf2c2fed5e0dba447cc84824a6cba95dc5747606201da46cb4"}, +] + +[package.dependencies] +aiohttp = ">=2.3.2" +python-socks = {version = ">=2.4.3,<3.0.0", extras = ["asyncio"]} + [[package]] name = "aiosignal" version = "1.3.1" @@ -1101,6 +1116,26 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-socks" +version = "2.4.4" +description = "Core proxy (SOCKS4, SOCKS5, HTTP tunneling) functionality for Python" +optional = true +python-versions = "*" +files = [ + {file = "python-socks-2.4.4.tar.gz", hash = "sha256:e5a8e4f78203612c813946feacd87b98943965a04389fe221fa1e9ab263ad22e"}, + {file = "python_socks-2.4.4-py3-none-any.whl", hash = "sha256:fda465d3ef229119ee614eb85f2b7c0ad28be6dd40e0ef8dd317c49e8725e514"}, +] + +[package.dependencies] +async-timeout = {version = ">=3.0.1", optional = true, markers = "extra == \"asyncio\""} + +[package.extras] +anyio = ["anyio (>=3.3.4,<5.0.0)"] +asyncio = ["async-timeout (>=3.0.1)"] +curio = ["curio (>=1.4)"] +trio = ["trio (>=0.16.0)"] + [[package]] name = "pytz" version = "2023.3.post1" @@ -1540,8 +1575,9 @@ multidict = ">=4.0" [extras] converter = ["croniter"] +socks = ["aiohttp-socks"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "00b40b74bc3352a2c1d738f7f394f323d9f246ec3e0c6e805ca02c0666161442" +content-hash = "419e7f392a3b0ac1b19caa3fff2c6f5244424300d5a4acb7c44c4b52073b8bf1" diff --git a/pyproject.toml b/pyproject.toml index 4f7b04b..454dda5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "aiosteampy" -version = "0.4.1" -description = "Simple library to trade and interact with steam market, webapi, guard" +version = "0.5.0" +description = "Trade and interact with steam market, webapi, guard" license = "MIT" authors = ["Dmytro Tkachenko "] readme = "README.md" @@ -29,9 +29,12 @@ python = "^3.10" aiohttp = "^3.9.1" rsa = "^4.9" croniter = { version = "^2.0.1", optional = true } +aiohttp-socks = { version = "^0.8.4", optional = true } [tool.poetry.extras] converter = ["croniter"] +socks = ["aiohttp-socks"] +all = ["croniter", "aiohttp-socks"] [tool.poetry.group.dev.dependencies] black = "^23.12.1"