from __future__ import annotations import json from pathlib import Path from typing import TYPE_CHECKING import aiohttp from aiohttp import ClientSession, ClientTimeout from loguru import logger from app.core.exceptions import SteamAuthError from app.core.proxy_manager import proxy_manager from pysteamauth.base.request import BaseRequestStrategy, DEFAULT_REQUEST_TIMEOUT class ProxyRequestStrategy(BaseRequestStrategy): """Request strategy that routes traffic through a proxy connector.""" def __init__(self, connector: aiohttp.BaseConnector): super().__init__() self._proxy_connector = connector def _create_session(self) -> ClientSession: return ClientSession( connector=self._proxy_connector, timeout=DEFAULT_REQUEST_TIMEOUT, ) def read_mafile_data(account: dict) -> dict | None: """Read and parse the account's mafile JSON from disk.""" mafile_path = account.get("mafile_path") if not mafile_path: return None try: return json.loads(Path(mafile_path).read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError, ValueError): return None async def _resolve_proxy(account: dict) -> dict | None: """Determine proxy dict {address, protocol} for the account.""" proxy_address = account.get("proxy") if proxy_address: from app.database import get_db db = await get_db() cursor = await db.execute( "SELECT protocol FROM proxies WHERE address = ?", (proxy_address,) ) row = await cursor.fetchone() protocol = row["protocol"] if row else "http" return {"address": proxy_address, "protocol": protocol} if proxy_manager.count > 0: return proxy_manager.get_next() return None async def create_steam_session(account: dict): """Create an authenticated Steam session for an account.""" try: from pysteamauth.auth import Steam except ImportError: raise SteamAuthError("pysteamauth not installed: pip install pysteamauth") proxy = await _resolve_proxy(account) request_strategy = None if proxy: connector = proxy_manager.get_connector(proxy) request_strategy = ProxyRequestStrategy(connector) logger.debug(f"Using proxy {proxy['protocol']}://{proxy['address']} for {account['login']}") mafile = read_mafile_data(account) shared_secret = account.get("shared_secret", "") identity_secret = account.get("identity_secret", "") device_id = None steamid = None if mafile: shared_secret = mafile.get("shared_secret", shared_secret) identity_secret = mafile.get("identity_secret", identity_secret) device_id = mafile.get("device_id") steamid = int(mafile.get("Session", {}).get("SteamID", 0)) or None steam = Steam( login=account["login"], password=account["password"], shared_secret=shared_secret, identity_secret=identity_secret, device_id=device_id, steamid=steamid, request_strategy=request_strategy, ) try: await steam.login_to_steam() except Exception as exc: await close_steam(steam) exc_str = str(exc) # Pydantic ValidationError from pysteamauth = Steam rejected login if "validation error" in exc_str.lower() and "FinalizeLoginStatus" in exc_str: # Extract the raw response dict from the error m = __import__("re").search(r"input_value=(\{[^}]+\})", exc_str) raw = m.group(1) if m else "" logger.error(f"Auth failed for {account['login']}: Steam rejected login: {raw}") raise SteamAuthError(f"Login failed: Steam rejected credentials ({raw})") logger.error(f"Auth failed for {account['login']}: {exc}") raise SteamAuthError(f"Login failed: {exc}") return steam async def close_steam(steam) -> None: """Close the underlying aiohttp session and proxy connector of a pysteamauth Steam object.""" requests = getattr(steam, "_requests", None) if requests is None: return session = getattr(requests, "_session", None) if session is not None and not getattr(session, "closed", True): try: await session.close() except Exception: pass requests._session = None connector = getattr(requests, "_proxy_connector", None) if connector is not None and not getattr(connector, "closed", True): try: await connector.close() except Exception: pass def extract_session_cookies(steam) -> str | None: """Extract session cookies from a pysteamauth Steam object as JSON string. Returns JSON string of cookies list, or None if extraction fails. """ try: _req = getattr(steam, "_requests", None) _sess = getattr(_req, "_session", None) if _req else None if not _sess or not hasattr(_sess, "cookie_jar"): return None all_cookies = [] for c in _sess.cookie_jar: all_cookies.append({ "name": c.key, "value": c.value, "domain": c.get("domain", ""), "path": c.get("path", "/"), "secure": str(c.get("secure", "")).lower() == "true" or c.get("secure") is True, "httpOnly": str(c.get("httponly", "")).lower() == "true" or c.get("httponly") is True, }) return json.dumps(all_cookies) except Exception: return None async def check_cookies_alive(cookies_json: str, proxy: dict | None = None) -> bool: """Check if Steam session cookies are still valid. Makes a lightweight request to Steam's clientjstoken endpoint. Returns True if logged in, False otherwise. """ try: cookies = json.loads(cookies_json) jar = aiohttp.CookieJar(unsafe=True) for c in cookies: jar.update_cookies( {c["name"]: c["value"]}, response_url=__import__("yarl").URL(f"https://{c.get('domain', '.steamcommunity.com').lstrip('.')}"), ) connector = proxy_manager.get_connector(proxy) if proxy else None async with aiohttp.ClientSession(cookie_jar=jar, connector=connector) as session: async with session.get( "https://steamcommunity.com/chat/clientjstoken", timeout=aiohttp.ClientTimeout(total=10), ) as resp: if resp.status != 200: return False data = await resp.json(content_type=None) return bool(data.get("logged_in")) except Exception: return False async def raw_request(steam, url: str, method: str = "GET", **kwargs) -> aiohttp.ClientResponse: """Low-level request returning raw aiohttp.ClientResponse (for redirects, etc.).""" from yarl import URL host = URL(url).host or "" cookies = await steam.cookies(host) return await steam._requests.request(url=url, method=method, cookies=cookies, **kwargs)