This commit is contained in:
Manchik
2026-04-17 22:22:40 +03:00
commit c82866b58a
135 changed files with 18493 additions and 0 deletions
View File
+96
View File
@@ -0,0 +1,96 @@
import asyncio
import json
from loguru import logger
_active_browsers: list = []
STEAM_DOMAINS = [
".steamcommunity.com",
".steampowered.com",
".store.steampowered.com",
".help.steampowered.com",
".login.steampowered.com",
]
async def open_browser_with_cookies(account: dict) -> None:
"""Open a Chrome browser with Steam session cookies using nodriver."""
import nodriver as uc
cookies_json = account.get("session_cookies")
if not cookies_json:
raise ValueError("No session cookies saved. Validate the account first.")
cookies = json.loads(cookies_json)
browser = await uc.start()
_active_browsers.append(browser)
tab = await browser.get("about:blank")
for c in cookies:
domain = c.get("domain") or ".steamcommunity.com"
domains_to_set = [domain]
if "steam" in domain:
domains_to_set = list(set([domain] + STEAM_DOMAINS))
for d in domains_to_set:
try:
await tab.send(uc.cdp.network.set_cookie(
name=c["name"],
value=c["value"],
domain=d,
path=c.get("path") or "/",
secure=c.get("secure", True),
http_only=c.get("httpOnly", False),
))
except Exception:
pass
steam_id = account.get("steam_id") or ""
if steam_id:
start_url = f"https://steamcommunity.com/profiles/{steam_id}/"
else:
start_url = "https://steamcommunity.com/"
await browser.get(start_url)
logger.info(f"Browser opened for {account['login']}")
# Wait until browser is closed by user, suppress connection errors
try:
while browser:
await asyncio.sleep(1)
try:
if not browser.connection or browser.connection.closed:
break
except Exception:
break
except Exception:
pass
finally:
if browser in _active_browsers:
_active_browsers.remove(browser)
# Temporarily suppress nodriver's internal task errors during cleanup
# (Browser.update_targets() raises ConnectionRefusedError after close)
loop = asyncio.get_running_loop()
_original_handler = loop.get_exception_handler()
def _suppress_nodriver_close_errors(loop, context):
exc = context.get("exception")
if isinstance(exc, (ConnectionRefusedError, ConnectionResetError, OSError)):
return
if _original_handler:
_original_handler(loop, context)
else:
loop.default_exception_handler(context)
loop.set_exception_handler(_suppress_nodriver_close_errors)
try:
browser.stop()
except Exception:
pass
await asyncio.sleep(0.5)
loop.set_exception_handler(_original_handler)
logger.debug(f"Browser closed for {account['login']}")
+20
View File
@@ -0,0 +1,20 @@
from app.core.task_manager import task_manager
from app.services.steam_guard import remove_guard, generate_2fa
from app.services.steam_password import change_password, random_password
from app.services.steam_email import change_email, validate_account
from app.services.steam_phone import change_phone
from app.services.steam_checker import check_logpass_account, full_parse_logpass_account
from app.services.steam_token_checker import check_token_account
def register_all_handlers() -> None:
task_manager.register_handler("change_password", change_password)
task_manager.register_handler("random_password", random_password)
task_manager.register_handler("change_email", change_email)
task_manager.register_handler("change_phone", change_phone)
task_manager.register_handler("remove_guard", remove_guard)
task_manager.register_handler("generate_2fa", generate_2fa)
task_manager.register_handler("validate", validate_account)
task_manager.register_handler("logpass_validate", check_logpass_account)
task_manager.register_handler("logpass_full_parse", full_parse_logpass_account)
task_manager.register_handler("token_validate", check_token_account)
+192
View File
@@ -0,0 +1,192 @@
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)
+220
View File
@@ -0,0 +1,220 @@
"""Steam auto-accept login confirmations — ported from legacy/steam_auto_accept_logins.py."""
import asyncio
import base64
import hashlib
import hmac
import struct
import aiohttp
import rsa
from loguru import logger
from pysteamauth.pb2.steammessages_auth.steamclient_pb2 import (
CAuthentication_GetPasswordRSAPublicKey_Request,
CAuthentication_GetPasswordRSAPublicKey_Response,
CAuthentication_BeginAuthSessionViaCredentials_Request,
CAuthentication_BeginAuthSessionViaCredentials_Response,
CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request,
CAuthentication_PollAuthSessionStatus_Request,
CAuthentication_PollAuthSessionStatus_Response,
CAuthentication_GetAuthSessionsForAccount_Response,
CAuthentication_GetAuthSessionInfo_Request,
CAuthentication_GetAuthSessionInfo_Response,
CAuthentication_UpdateAuthSessionWithMobileConfirmation_Request,
CAuthentication_UpdateAuthSessionWithMobileConfirmation_Response,
EAuthTokenPlatformType,
EAuthSessionGuardType,
)
BASE_URL = "https://api.steampowered.com"
HEADERS = {"User-Agent": "okhttp/4.9.2", "Cookie": "Steam_Language=english"}
def _generate_2fa_code(shared_secret: str, server_time: int | None = None) -> str:
"""Generate a Steam Guard TOTP code from shared_secret."""
import time as _time
if server_time is None:
server_time = int(_time.time())
key = base64.b64decode(shared_secret)
msg = struct.pack(">Q", server_time // 30)
mac = hmac.new(key, msg, hashlib.sha1).digest()
offset = mac[-1] & 0x0F
code_int = struct.unpack(">I", mac[offset : offset + 4])[0] & 0x7FFFFFFF
chars = "23456789BCDFGHJKMNPQRTVWXY"
code = ""
for _ in range(5):
code += chars[code_int % len(chars)]
code_int //= len(chars)
return code
def _confirmation_signature(shared_secret: str, client_id: int, steamid: int) -> bytes:
"""HMAC-SHA256 signature for login confirmation (version=1 + client_id + steamid)."""
key = base64.b64decode(shared_secret)
data = struct.pack("<HQQ", 1, client_id, steamid)
return hmac.new(key, data, hashlib.sha256).digest()
async def mobile_login(session: aiohttp.ClientSession, account: dict) -> str | None:
"""Perform mobile login flow, return access_token or None."""
login = account["login"]
password = account["password"]
shared_secret = account.get("shared_secret", "")
try:
# GetPasswordRSAPublicKey
req = CAuthentication_GetPasswordRSAPublicKey_Request()
req.account_name = login
encoded = base64.b64encode(req.SerializeToString()).decode()
async with session.get(
f"{BASE_URL}/IAuthenticationService/GetPasswordRSAPublicKey/v1",
params={"origin": "SteamMobile", "input_protobuf_encoded": encoded},
headers=HEADERS,
) as resp:
if resp.status != 200:
logger.error(f"[auto-accept] RSA key failed for {login}: HTTP {resp.status}")
return None
rsa_resp = CAuthentication_GetPasswordRSAPublicKey_Response.FromString(await resp.read())
pub_key = rsa.PublicKey(int(rsa_resp.publickey_mod, 16), int(rsa_resp.publickey_exp, 16))
encrypted_pw = base64.b64encode(rsa.encrypt(password.encode(), pub_key)).decode()
# BeginAuthSession
begin = CAuthentication_BeginAuthSessionViaCredentials_Request()
begin.account_name = login
begin.encrypted_password = encrypted_pw
begin.encryption_timestamp = rsa_resp.timestamp
begin.platform_type = EAuthTokenPlatformType.k_EAuthTokenPlatformType_MobileApp
begin.device_friendly_name = "Android Device"
begin.persistence = 1
data = aiohttp.FormData()
data.add_field("input_protobuf_encoded", base64.b64encode(begin.SerializeToString()).decode())
async with session.post(
f"{BASE_URL}/IAuthenticationService/BeginAuthSessionViaCredentials/v1",
data=data,
headers=HEADERS,
) as resp:
if resp.status != 200:
logger.error(f"[auto-accept] BeginAuth failed for {login}: HTTP {resp.status}")
return None
begin_resp = CAuthentication_BeginAuthSessionViaCredentials_Response.FromString(await resp.read())
# Send 2FA code
if shared_secret:
code = _generate_2fa_code(shared_secret)
upd = CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request()
upd.client_id = begin_resp.client_id
upd.steamid = begin_resp.steamid
upd.code = code
upd.code_type = EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode
data = aiohttp.FormData()
data.add_field("input_protobuf_encoded", base64.b64encode(upd.SerializeToString()).decode())
async with session.post(
f"{BASE_URL}/IAuthenticationService/UpdateAuthSessionWithSteamGuardCode/v1",
data=data,
headers=HEADERS,
) as resp:
if resp.status != 200:
logger.error(f"[auto-accept] 2FA submit failed for {login}: HTTP {resp.status}")
return None
# PollAuthSessionStatus
poll = CAuthentication_PollAuthSessionStatus_Request()
poll.client_id = begin_resp.client_id
poll.request_id = begin_resp.request_id
data = aiohttp.FormData()
data.add_field("input_protobuf_encoded", base64.b64encode(poll.SerializeToString()).decode())
async with session.post(
f"{BASE_URL}/IAuthenticationService/PollAuthSessionStatus/v1",
data=data,
headers=HEADERS,
) as resp:
if resp.status != 200:
logger.error(f"[auto-accept] Poll failed for {login}: HTTP {resp.status}")
return None
poll_resp = CAuthentication_PollAuthSessionStatus_Response.FromString(await resp.read())
if not poll_resp.access_token:
logger.error(f"[auto-accept] No access_token for {login}")
return None
logger.success(f"[auto-accept] Mobile login OK for {login}")
return poll_resp.access_token
except Exception as exc:
logger.error(f"[auto-accept] Login error for {login}: {exc}")
return None
async def get_pending_sessions(session: aiohttp.ClientSession, access_token: str) -> list[int] | None:
"""Get pending auth session client_ids. Returns None on 401 (token expired)."""
try:
async with session.get(
f"{BASE_URL}/IAuthenticationService/GetAuthSessionsForAccount/v1",
params={
"access_token": access_token,
"origin": "SteamMobile",
"input_protobuf_encoded": "",
},
headers=HEADERS,
) as resp:
if resp.status == 401:
return None
if resp.status != 200:
return []
data = CAuthentication_GetAuthSessionsForAccount_Response.FromString(await resp.read())
return list(data.client_ids)
except Exception as exc:
logger.warning(f"[auto-accept] get_pending_sessions error: {exc}")
return []
async def confirm_session(
session: aiohttp.ClientSession,
access_token: str,
account: dict,
client_id: int,
) -> bool:
"""Confirm a pending login session."""
steam_id = int(account.get("steam_id", 0))
shared_secret = account.get("shared_secret", "")
if not steam_id or not shared_secret:
return False
try:
signature = _confirmation_signature(shared_secret, client_id, steam_id)
msg = CAuthentication_UpdateAuthSessionWithMobileConfirmation_Request()
msg.version = 1
msg.client_id = client_id
msg.steamid = steam_id
msg.signature = signature
msg.confirm = True
msg.persistence = 1
data = aiohttp.FormData()
data.add_field("input_protobuf_encoded", base64.b64encode(msg.SerializeToString()).decode())
async with session.post(
f"{BASE_URL}/IAuthenticationService/UpdateAuthSessionWithMobileConfirmation/v1",
params={"access_token": access_token},
data=data,
headers=HEADERS,
) as resp:
if resp.status == 200:
logger.success(f"[auto-accept] Confirmed login for {account['login']} (client_id={client_id})")
return True
logger.warning(f"[auto-accept] Confirm failed for {account['login']}: HTTP {resp.status}")
return False
except Exception as exc:
logger.error(f"[auto-accept] Confirm error for {account['login']}: {exc}")
return False
+29
View File
@@ -0,0 +1,29 @@
"""Steam account ban/alert checking via supportmessages page."""
from loguru import logger
async def check_ban(steam) -> str:
"""Check if account has active bans/alerts.
Returns: 'BANNED', 'NO BAN', or '' on error.
"""
try:
response_html = await steam.request(
"https://store.steampowered.com/supportmessages/",
method="GET",
)
if isinstance(response_html, bytes):
response_html = response_html.decode("utf-8", errors="replace")
if "support_message_page" in response_html or "This account has been locked" in response_html:
return "BANNED"
elif "It doesn't appear that you have any active account alerts" in response_html:
return "NO BAN"
else:
return "NO BAN"
except Exception as exc:
logger.warning(f"Ban check failed: {exc}")
return ""
+297
View File
@@ -0,0 +1,297 @@
"""Validation service for log:pass accounts (no mafile)."""
from pathlib import Path
from bs4 import BeautifulSoup
from loguru import logger
from app.services.steam_auth import _resolve_proxy, ProxyRequestStrategy, close_steam, extract_session_cookies
from app.services.steam_ban import check_ban
from app.services.steam_profile import fetch_profile
from app.core.proxy_manager import proxy_manager
from app.core.task_manager import task_manager
async def check_logpass_account(account: dict, params: dict, *, task_id: str) -> None:
"""Login with login:pass only, get steamid/nickname, check ban, update DB row."""
try:
from pysteamauth.auth import Steam
except ImportError:
raise RuntimeError("pysteamauth not installed")
acc_id = account["id"]
proxy = await _resolve_proxy(account)
request_strategy = None
if proxy:
connector = proxy_manager.get_connector(proxy)
request_strategy = ProxyRequestStrategy(connector)
logger.debug(f"[checker] Using proxy for {account['login']}")
steam = Steam(
login=account["login"],
password=account["password"],
request_strategy=request_strategy,
)
try:
await task_manager.set_step(task_id, 1, 3, "Авторизация", acc_id)
# Pre-validate password before attempting RSA encryption (Steam RSA-2048 max 245 bytes)
password = account.get("password") or ""
if len(password.encode("utf-8")) > 245:
raise ValueError(
f"Password is too long ({len(password)} chars) — possible mafile data imported as password. "
"Re-import the account in login:password format."
)
await steam.login_to_steam()
# Save session cookies right after login
try:
session_cookies_json = extract_session_cookies(steam)
if session_cookies_json:
from app.database import get_db as _get_db
_db = await _get_db()
await _db.execute(
"UPDATE logpass_accounts SET session_cookies = ? WHERE id = ?",
(session_cookies_json, acc_id),
)
await _db.commit()
logger.debug(f"[checker] {account['login']}: saved session cookies")
except Exception as cookie_err:
logger.warning(f"[checker] {account['login']}: failed to save cookies — {cookie_err}")
from app.config import read_validation_settings
val_settings = read_validation_settings()
steam_id = str(steam.steamid)
ban_status = ""
if val_settings.get("check_ban"):
await task_manager.set_step(task_id, 2, 3, "Проверка бана", acc_id)
ban_status = await check_ban(steam)
nickname = None
steam_level = None
avatar_url = None
last_online = None
if steam_id and steam_id != "0" and val_settings.get("fetch_profile"):
await task_manager.set_step(task_id, 3, 3, "Получение профиля", acc_id)
profile = await fetch_profile(steam_id, steam=steam)
if profile:
nickname = profile.get("nickname")
steam_level = profile.get("steam_level")
avatar_url = profile.get("avatar_url")
last_online = profile.get("last_online")
from app.database import get_db
db = await get_db()
await db.execute(
"""UPDATE logpass_accounts
SET steam_id = ?, nickname = ?, steam_level = ?, avatar_url = ?, last_online = ?,
ban_status = ?, status = 'valid', updated_at = datetime('now')
WHERE id = ?""",
(steam_id, nickname, steam_level, avatar_url, last_online, ban_status, acc_id),
)
await db.commit()
logger.success(f"[checker] {account['login']} → valid, ban={ban_status}")
except Exception as exc:
from app.database import get_db
db = await get_db()
await db.execute(
"UPDATE logpass_accounts SET status = 'invalid', updated_at = datetime('now') WHERE id = ?",
(acc_id,),
)
await db.commit()
logger.error(f"[checker] {account['login']}{exc}")
raise
finally:
await close_steam(steam)
async def _check_cs2_prime(steam, steam_id: str) -> str:
"""Check CS2 Prime status."""
try:
url = f"https://steamcommunity.com/profiles/{steam_id}/gcpd/730/?tab=primeaccount"
resp = await steam.request(url, method="GET")
html = resp if isinstance(resp, str) else resp.decode("utf-8", errors="ignore")
if "no_personal_data_stored_message" in html:
return "Disabled"
if "personaldata_elements_container" in html and "Enabled" in html:
return "Enabled"
return "Disabled"
except Exception as e:
logger.debug(f"[full_parse] prime check error: {e}")
return "Disabled"
async def _check_dota_trophy(steam, steam_id: str) -> str | None:
"""Check Dota 2 Trophy Score."""
try:
url = f"https://steamcommunity.com/profiles/{steam_id}/gcpd/570/?category=Stats&tab=Trophy"
resp = await steam.request(url, method="GET")
html = resp if isinstance(resp, str) else resp.decode("utf-8", errors="ignore")
if "no_personal_data_stored_message" in html:
return None
if "personaldata_elements_container" in html:
soup = BeautifulSoup(html, "html.parser")
for row in soup.find_all("tr"):
cells = row.find_all("td")
if len(cells) >= 3:
try:
int(cells[0].get_text(strip=True))
score = cells[1].get_text(strip=True)
if score.isdigit():
return score
except (ValueError, AttributeError):
continue
return None
except Exception as e:
logger.debug(f"[full_parse] trophy check error: {e}")
return None
async def _check_dota_behavior(steam, steam_id: str) -> str | None:
"""Check Dota 2 Behavior Score."""
try:
url = f"https://steamcommunity.com/profiles/{steam_id}/gcpd/570/?category=Account&tab=MatchPlayerReportIncoming"
resp = await steam.request(url, method="GET")
html = resp if isinstance(resp, str) else resp.decode("utf-8", errors="ignore")
if "no_personal_data_stored_message" in html:
return None
if "personaldata_elements_container" in html:
soup = BeautifulSoup(html, "html.parser")
for row in soup.find_all("tr"):
cells = row.find_all("td")
if cells:
last_cell = cells[-1].get_text(strip=True)
if last_cell.isdigit():
return last_cell
return None
except Exception as e:
logger.debug(f"[full_parse] behavior check error: {e}")
return None
async def _check_licenses(steam) -> str:
"""Get list of licenses (games) from Steam account."""
try:
resp = await steam.request("https://store.steampowered.com/account/licenses/", method="GET")
html = resp if isinstance(resp, str) else resp.decode("utf-8", errors="ignore")
soup = BeautifulSoup(html, "html.parser")
table = soup.find("table", class_="account_table")
if not table:
return ""
licenses = []
rows = table.find_all("tr")
for row in rows[1:]:
cells = row.find_all("td")
if len(cells) >= 2:
game_cell = cells[1]
remove_div = game_cell.find("div", class_="free_license_remove_link")
if remove_div:
remove_div.decompose()
name = game_cell.get_text(strip=True)
if name:
licenses.append(name)
return ", ".join(licenses)
except Exception as e:
logger.debug(f"[full_parse] licenses check error: {e}")
return ""
async def full_parse_logpass_account(account: dict, params: dict, *, task_id: str) -> None:
"""Full parse: login, ban, profile, prime, trophy, behavior, licenses."""
try:
from pysteamauth.auth import Steam
except ImportError:
raise RuntimeError("pysteamauth not installed")
acc_id = account["id"]
proxy = await _resolve_proxy(account)
request_strategy = None
if proxy:
connector = proxy_manager.get_connector(proxy)
request_strategy = ProxyRequestStrategy(connector)
steam = Steam(
login=account["login"],
password=account["password"],
request_strategy=request_strategy,
)
try:
await task_manager.set_step(task_id, 1, 6, "Авторизация", acc_id)
await steam.login_to_steam()
# Save session cookies
try:
session_cookies_json = extract_session_cookies(steam)
if session_cookies_json:
from app.database import get_db as _get_db
_db = await _get_db()
await _db.execute(
"UPDATE logpass_accounts SET session_cookies = ? WHERE id = ?",
(session_cookies_json, acc_id),
)
await _db.commit()
except Exception as cookie_err:
logger.warning(f"[full_parse] {account['login']}: failed to save cookies — {cookie_err}")
steam_id = str(steam.steamid)
await task_manager.set_step(task_id, 2, 6, "Проверка бана", acc_id)
ban_status = await check_ban(steam)
nickname = avatar_url = last_online = None
steam_level = None
if steam_id and steam_id != "0":
await task_manager.set_step(task_id, 3, 6, "Профиль", acc_id)
profile = await fetch_profile(steam_id, steam=steam)
if profile:
nickname = profile.get("nickname")
steam_level = profile.get("steam_level")
avatar_url = profile.get("avatar_url")
last_online = profile.get("last_online")
prime = "Disabled"
trophy = None
behavior = None
if steam_id and steam_id != "0":
await task_manager.set_step(task_id, 4, 6, "Prime / Trophy", acc_id)
prime = await _check_cs2_prime(steam, steam_id)
trophy = await _check_dota_trophy(steam, steam_id)
behavior = await _check_dota_behavior(steam, steam_id)
await task_manager.set_step(task_id, 5, 6, "Лицензии", acc_id)
license_str = await _check_licenses(steam)
await task_manager.set_step(task_id, 6, 6, "Сохранение", acc_id)
from app.database import get_db
db = await get_db()
await db.execute(
"""UPDATE logpass_accounts
SET steam_id = ?, nickname = ?, steam_level = ?, avatar_url = ?, last_online = ?,
ban_status = ?, prime = ?, trophy = ?, behavior = ?, license = ?,
status = 'valid', updated_at = datetime('now')
WHERE id = ?""",
(steam_id, nickname, steam_level, avatar_url, last_online,
ban_status, prime, trophy, behavior, license_str, acc_id),
)
await db.commit()
logger.success(f"[full_parse] {account['login']} → valid, ban={ban_status}, prime={prime}, trophy={trophy}, behavior={behavior}, licenses={len(license_str.split(', ')) if license_str else 0}")
except Exception as exc:
from app.database import get_db
db = await get_db()
await db.execute(
"UPDATE logpass_accounts SET status = 'invalid', updated_at = datetime('now') WHERE id = ?",
(acc_id,),
)
await db.commit()
logger.error(f"[full_parse] {account['login']}{exc}")
raise
finally:
await close_steam(steam)
+201
View File
@@ -0,0 +1,201 @@
import asyncio
import json
from loguru import logger
from app.services.steam_auth import create_steam_session, close_steam, extract_session_cookies
from app.services.steam_wizard import AJAX_HEADERS, run_common_wizard
def _get_fresh_sessionid(steam) -> str:
"""Get sessionid from the live aiohttp cookie jar."""
return steam._requests.cookies("help.steampowered.com").get("sessionid", "")
async def change_email(account: dict, params: dict, task_id: str = "") -> None:
"""Change account email via Steam recovery wizard + interactive prompts."""
from app.core.task_manager import task_manager
login = account["login"]
new_email = params.get("new_email", "")
await task_manager.set_step(task_id, 1, 5, "Авторизация", acc_id=account["id"])
logger.info(f"[change_email] {login}: logging in to Steam...")
steam = await create_steam_session(account)
try:
password = account["password"]
await task_manager.set_step(task_id, 2, 5, "Wizard", acc_id=account["id"])
logger.info(f"[change_email] {login}: auth ok, starting email change wizard...")
wizard = await run_common_wizard(
steam,
entry_url="https://help.steampowered.com/wizard/HelpChangeEmail?redir=store/account/",
login=login,
password=password,
)
logger.info(f"[change_email] {login}: wizard done")
if not new_email:
new_email = await task_manager.prompt_user(task_id, f"Введите новый email для {login}", login=login)
if not new_email:
raise ValueError("new_email is required")
await task_manager.set_step(task_id, 3, 5, "Запрос смены", acc_id=account["id"])
logger.info(f"[change_email] {login}: requesting email change -> {new_email}...")
for attempt in range(2):
fresh_sid = _get_fresh_sessionid(steam)
text = await steam.request(
url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryChangeEmail/",
method="POST",
data={
"s": wizard.s,
"account": wizard.account,
"sessionid": fresh_sid,
"wizard_ajax": 1,
"gamepad": 0,
"email": new_email,
},
headers=AJAX_HEADERS,
)
data = json.loads(text) if isinstance(text, str) else text
logger.debug(f"[change_email] {login}: change request attempt {attempt+1} response: {data}")
if data.get("errorMsg"):
error_code = data.get("success", 0)
if error_code == 24:
raise PermissionError(f"Insufficient privileges for email change: {data['errorMsg']}")
raise RuntimeError(f"Email change request failed: {data['errorMsg']}")
if attempt == 0:
await asyncio.sleep(3)
await task_manager.set_step(task_id, 4, 5, "Код подтверждения", acc_id=account["id"])
email_code = await task_manager.prompt_user(task_id, f"Введите код подтверждения с email ({new_email})", login=login)
if not email_code:
raise ValueError("email_code is required")
email_code = email_code.strip()
logger.info(f"[change_email] {login}: confirming email change...")
fresh_sid = _get_fresh_sessionid(steam)
confirm_text = await steam.request(
url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryConfirmChangeEmail/",
method="POST",
data={
"s": wizard.s,
"account": wizard.account,
"sessionid": fresh_sid,
"wizard_ajax": 1,
"gamepad": 0,
"email": new_email,
"email_change_code": email_code,
},
headers=AJAX_HEADERS,
)
confirm_data = json.loads(confirm_text) if isinstance(confirm_text, str) else confirm_text
logger.debug(f"[change_email] {login}: confirm response: success={confirm_data.get('success')}")
error_code = confirm_data.get("success", 0)
if confirm_data.get("errorMsg") and error_code != 29:
raise RuntimeError(f"Email confirmation failed: {confirm_data['errorMsg']}")
await task_manager.set_step(task_id, 5, 5, "Сохранение", acc_id=account["id"])
from app.database import get_db
db = await get_db()
await db.execute(
"UPDATE accounts SET email = ?, updated_at = datetime('now') WHERE id = ?",
(new_email, account["id"]),
)
await db.commit()
logger.success(f"[change_email] {login}: email changed successfully -> {new_email}")
finally:
await close_steam(steam)
async def validate_account(account: dict, params: dict, task_id: str = "") -> None:
"""Validate account credentials by attempting login. Fetches profile and ban status on success."""
from app.database import get_db
from app.services.steam_auth import close_steam
from app.services.steam_profile import fetch_profile
from app.services.steam_ban import check_ban
from app.core.task_manager import task_manager
from app.config import read_validation_settings
steam = None
error_msg = None
ban_status = ""
val_settings = read_validation_settings()
login = account["login"]
profile = None
try:
await task_manager.set_step(task_id, 1, 4, "Авторизация", acc_id=account["id"])
logger.info(f"[validate] {login}: logging in to Steam...")
steam = await create_steam_session(account)
status = "valid"
logger.info(f"[validate] {login}: auth ok")
if status == "valid" and steam is not None:
if val_settings.get("check_ban"):
await task_manager.set_step(task_id, 2, 4, "Проверка бана", acc_id=account["id"])
logger.info(f"[validate] {login}: checking ban status...")
ban_status = await check_ban(steam)
logger.info(f"[validate] {login}: ban = {ban_status or 'none'}")
if account.get("steam_id") and val_settings.get("fetch_profile"):
await task_manager.set_step(task_id, 3, 4, "Профиль", acc_id=account["id"])
logger.info(f"[validate] {login}: fetching profile (id={account['steam_id']})...")
profile = await fetch_profile(account["steam_id"], steam=steam)
if profile:
logger.info(f"[validate] {login}: profile fetched — {profile.get('nickname')}, lvl={profile.get('steam_level')}")
else:
logger.warning(f"[validate] {login}: failed to fetch profile")
await task_manager.set_step(task_id, 4, 4, "Сохранение", acc_id=account["id"])
if steam is not None and status == "valid":
try:
session_cookies = extract_session_cookies(steam)
if session_cookies:
_db = await get_db()
await _db.execute(
"UPDATE accounts SET session_cookies = ?, updated_at = datetime('now') WHERE id = ?",
(session_cookies, account["id"]),
)
await _db.commit()
logger.debug(f"[validate] {login}: saved session cookies")
except Exception as cookie_err:
logger.warning(f"[validate] {login}: failed to save cookies — {cookie_err}")
except Exception as exc:
status = "invalid"
error_msg = str(exc)
logger.warning(f"[validate] {login}: error — {error_msg}")
finally:
if steam is not None:
await close_steam(steam)
db = await get_db()
await db.execute(
"UPDATE accounts SET status = ?, ban_status = ?, updated_at = datetime('now') WHERE id = ?",
(status, ban_status or None, account["id"]),
)
await db.commit()
if profile:
sets = []
vals = []
for col, key in [("nickname", "nickname"), ("avatar_url", "avatar_url"), ("steam_level", "steam_level"), ("last_online", "last_online")]:
if profile.get(key) is not None:
sets.append(f"{col} = ?")
vals.append(profile[key])
if sets:
sets.append("updated_at = datetime('now')")
vals.append(account["id"])
await db.execute(f"UPDATE accounts SET {', '.join(sets)} WHERE id = ?", vals)
await db.commit()
logger.info(f"[validate] {login}: profile saved — {profile.get('nickname')}")
if error_msg:
raise Exception(f"{error_msg}")
else:
ban_info = f" | ban={ban_status}" if ban_status else ""
logger.info(f"[validate] {login}: result — {status}{ban_info}")
+83
View File
@@ -0,0 +1,83 @@
import base64
import hashlib
import hmac
import struct
import time
from loguru import logger
def generate_2fa_code(shared_secret: str) -> str:
"""Generate a Steam Guard 2FA code from shared_secret."""
timestamp = int(time.time()) // 30
msg = struct.pack(">Q", timestamp)
key = base64.b64decode(shared_secret)
auth = hmac.new(key, msg, hashlib.sha1).digest()
offset = auth[19] & 0xF
code = struct.unpack(">I", auth[offset : offset + 4])[0] & 0x7FFFFFFF
chars = "23456789BCDFGHJKMNPQRTVWXY"
result = []
for _ in range(5):
result.append(chars[code % len(chars)])
code //= len(chars)
return "".join(result)
async def remove_guard(account: dict, params: dict, task_id: str = "") -> None:
"""Remove Steam Guard 2FA via revocation code from mafile."""
from app.services.steam_auth import create_steam_session, close_steam, read_mafile_data
from app.core.task_manager import task_manager
mafile = read_mafile_data(account)
revocation_code = (mafile or {}).get("revocation_code", "") if mafile else ""
if not revocation_code:
raise ValueError("revocation_code not found in mafile")
login = account["login"]
await task_manager.set_step(task_id, 1, 3, "Авторизация", acc_id=account["id"])
logger.info(f"[remove_guard] {login}: logging in to Steam...")
steam = await create_steam_session(account)
logger.info(f"[remove_guard] {login}: auth ok")
try:
await task_manager.set_step(task_id, 2, 3, "Удаление Guard", acc_id=account["id"])
logger.info(f"[remove_guard] {login}: sending Guard removal request...")
sessionid = await steam.sessionid("store.steampowered.com")
await steam.request(
url="https://store.steampowered.com/twofactor/remove",
method="GET",
params={"step": "promptdevice"},
)
await steam.request(
url="https://store.steampowered.com/twofactor/remove",
method="GET",
params={"step": "promptrcode"},
)
await steam.request(
url="https://store.steampowered.com/twofactor/manage_action",
method="POST",
data={"action": "removercode", "sessionid": sessionid},
)
response = await steam.request(
url="https://store.steampowered.com/twofactor/manage_remove_revocation_code",
method="POST",
data={"revocation_code": revocation_code, "sessionid": sessionid},
)
await task_manager.set_step(task_id, 3, 3, "Готово", acc_id=account["id"])
logger.success(f"[remove_guard] {login}: Guard removed")
finally:
await close_steam(steam)
async def generate_2fa(account: dict, params: dict, task_id: str = "") -> None:
"""Generate and log a 2FA code for an account."""
shared_secret = account.get("shared_secret", "")
if not shared_secret:
raise ValueError("No shared_secret for this account")
code = generate_2fa_code(shared_secret)
logger.debug(f"[generate_2fa] {account['login']}: code = {code}")
+122
View File
@@ -0,0 +1,122 @@
import json
import secrets
import string
from pathlib import Path
from loguru import logger
from app.core.crypto import encrypt_password
from app.services.steam_auth import create_steam_session, close_steam, read_mafile_data
from app.services.steam_wizard import (
AJAX_HEADERS,
get_rsa_key,
run_common_wizard,
)
async def change_password(account: dict, params: dict, task_id: str = "") -> None:
"""Change account password via Steam recovery wizard."""
from app.core.task_manager import task_manager
new_password = params.get("new_password", "")
if not new_password:
raise ValueError("new_password is required")
login = account["login"]
mafile_data = read_mafile_data(account)
if not mafile_data:
raise ValueError(f"No mafile found for {login} — cannot change password without 2FA")
identity_secret = mafile_data.get("identity_secret", "")
device_id = mafile_data.get("device_id")
if not identity_secret:
raise ValueError(f"No identity_secret in mafile for {login} — mobile confirmation impossible")
if not device_id:
raise ValueError(f"No device_id in mafile for {login} — mobile confirmation impossible")
await task_manager.set_step(task_id, 1, 5, "Авторизация", acc_id=account["id"])
logger.info(f"[change_password] {login}: logging in to Steam...")
steam = await create_steam_session(account)
logger.info(f"[change_password] {login}: auth ok")
try:
await task_manager.set_step(task_id, 2, 5, "Wizard", acc_id=account["id"])
logger.info(f"[change_password] {login}: starting password change wizard...")
wizard = await run_common_wizard(
steam,
entry_url="https://help.steampowered.com/wizard/HelpChangePassword?redir=store/account/",
login=login,
password=account["password"],
)
await task_manager.set_step(task_id, 3, 5, "RSA ключ", acc_id=account["id"])
logger.info(f"[change_password] {login}: wizard done, getting RSA key...")
mod, exp, timestamp = await get_rsa_key(steam, login)
logger.info(f"[change_password] {login}: checking new password availability...")
sessionid = await steam.sessionid("help.steampowered.com")
check_text = await steam.request(
url="https://help.steampowered.com/en/wizard/AjaxCheckPasswordAvailable/",
method="POST",
data={
"sessionid": sessionid,
"wizard_ajax": 1,
"password": new_password,
},
headers=AJAX_HEADERS,
)
check_data = json.loads(check_text) if isinstance(check_text, str) else check_text
if not check_data.get("available"):
raise ValueError("New password is not available (too weak or reused)")
encrypted_new = encrypt_password(new_password, mod, exp)
await task_manager.set_step(task_id, 4, 5, "Отправка запроса", acc_id=account["id"])
logger.info(f"[change_password] {login}: password available, sending change request...")
result_text = await steam.request(
url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryChangePassword/",
method="POST",
data={
"sessionid": sessionid,
"wizard_ajax": 1,
"s": wizard.s,
"account": wizard.account,
"password": encrypted_new,
"rsatimestamp": timestamp,
},
headers=AJAX_HEADERS,
)
result_data = json.loads(result_text) if isinstance(result_text, str) else result_text
logger.debug(f"[change_password] {login}: change response: success={result_data.get('success')}, hasHash={bool(result_data.get('hash'))}")
if result_data.get("errorMsg"):
raise RuntimeError(f"Password change rejected by Steam: {result_data['errorMsg']}")
if not result_data.get("hash"):
raise RuntimeError(f"Password change failed — no confirmation hash from Steam: {result_data}")
await task_manager.set_step(task_id, 5, 5, "Сохранение", acc_id=account["id"])
from app.database import get_db
db = await get_db()
await db.execute(
"UPDATE accounts SET password = ?, updated_at = datetime('now') WHERE id = ?",
(new_password, account["id"]),
)
await db.commit()
logger.success(f"[change_password] {login}: password changed")
finally:
await close_steam(steam)
def _generate_random_password(length: int = 24) -> str:
alphabet = string.ascii_letters + string.digits + "!"
return "".join(secrets.choice(alphabet) for _ in range(length))
async def random_password(account: dict, params: dict, task_id: str = "") -> None:
"""Change account password to a randomly generated one."""
new_password = _generate_random_password()
params["new_password"] = new_password
await change_password(account, params)
+216
View File
@@ -0,0 +1,216 @@
import json
import re
from loguru import logger
from app.services.steam_auth import create_steam_session, close_steam
from app.services.steam_wizard import AJAX_HEADERS, run_common_wizard
_PHONE_RE = re.compile(r"^\+\d{7,15}$")
def _get_fresh_sessionid(steam) -> str:
"""Get sessionid from the live aiohttp cookie jar (not from stale storage)."""
return steam._requests.cookies("help.steampowered.com").get("sessionid", "")
def _validate_phone(phone: str) -> str:
"""Strip spaces/dashes and verify E.164-like format."""
cleaned = re.sub(r"[\s\-()]", "", phone)
if cleaned and not cleaned.startswith("+"):
cleaned = "+" + cleaned
if not _PHONE_RE.match(cleaned):
raise ValueError(
f"Неверный формат номера: '{phone}'. "
"Ожидается международный формат, например +79123456789"
)
return cleaned
async def change_phone(account: dict, params: dict, task_id: str = "") -> None:
"""Change phone number on a Steam account via recovery wizard + interactive prompts."""
from app.core.task_manager import task_manager
login = account["login"]
new_phone = params.get("new_phone", "")
if new_phone:
new_phone = _validate_phone(new_phone)
await task_manager.set_step(task_id, 1, 5, "Авторизация", acc_id=account["id"])
logger.info(f"[change_phone] {login}: logging in to Steam...")
steam = await create_steam_session(account)
try:
password = account["password"]
await task_manager.set_step(task_id, 2, 5, "Wizard", acc_id=account["id"])
logger.info(f"[change_phone] {login}: auth ok, starting phone change wizard...")
wizard = await run_common_wizard(
steam,
entry_url="https://help.steampowered.com/en/wizard/HelpRemovePhoneNumber?redir=store/account",
login=login,
password=password,
)
logger.info(f"[change_phone] {login}: wizard done")
if not new_phone:
new_phone = await task_manager.prompt_user(task_id, f"Введите новый номер телефона для {login}", login=login)
if not new_phone:
raise ValueError("new_phone is required")
new_phone = _validate_phone(new_phone)
await task_manager.set_step(task_id, 3, 5, "Запрос смены", acc_id=account["id"])
logger.info(f"[change_phone] {login}: requesting phone change -> {new_phone}...")
fresh_sid = _get_fresh_sessionid(steam)
text = await steam.request(
url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryChangePhone/",
method="POST",
data={
"s": wizard.s,
"account": wizard.account,
"sessionid": fresh_sid,
"wizard_ajax": 1,
"gamepad": 0,
"phone_number": new_phone,
},
headers=AJAX_HEADERS,
)
data = json.loads(text) if isinstance(text, str) else text
logger.info(f"[change_phone] {login}: change request response: {data}")
if data.get("errorMsg"):
err = data["errorMsg"]
hint = ""
if "112" in err:
hint = " (номер уже привязан к другому аккаунту, виртуальный/VoIP, или не поддерживается Steam)"
raise RuntimeError(f"Phone change request failed: {err}{hint}")
await task_manager.set_step(task_id, 4, 5, "SMS код", acc_id=account["id"])
sms_code = await task_manager.prompt_user(task_id, f"Введите SMS код с номера {new_phone}", login=login)
if not sms_code:
raise ValueError("sms_code is required")
sms_code = sms_code.strip()
logger.info(f"[change_phone] {login}: confirming with SMS code...")
fresh_sid = _get_fresh_sessionid(steam)
confirm_text = await steam.request(
url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryConfirmChangePhone/",
method="POST",
data={
"s": wizard.s,
"account": wizard.account,
"sessionid": fresh_sid,
"wizard_ajax": 1,
"gamepad": 0,
"phone_number": new_phone,
"phone_change_code": sms_code,
},
headers=AJAX_HEADERS,
)
confirm_data = json.loads(confirm_text) if isinstance(confirm_text, str) else confirm_text
logger.info(f"[change_phone] {login}: confirm response: {confirm_data}")
if confirm_data.get("errorMsg"):
raise RuntimeError(f"Phone confirmation failed: {confirm_data['errorMsg']}")
await task_manager.set_step(task_id, 5, 5, "Сохранение", acc_id=account["id"])
from app.database import get_db
db = await get_db()
await db.execute(
"UPDATE accounts SET phone = ?, updated_at = datetime('now') WHERE id = ?",
(new_phone, account["id"]),
)
await db.commit()
logger.success(f"[change_phone] {login}: phone changed -> {new_phone}")
finally:
await close_steam(steam)
async def remove_phone(account: dict, params: dict, task_id: str = "") -> None:
"""Remove phone by changing it to a disposable number + SMS confirmation."""
from app.core.task_manager import task_manager
login = account["login"]
new_phone = params.get("new_phone", "")
if new_phone:
new_phone = _validate_phone(new_phone)
await task_manager.set_step(task_id, 1, 5, "Авторизация", acc_id=account["id"])
logger.info(f"[remove_phone] {login}: logging in to Steam...")
steam = await create_steam_session(account)
try:
password = account["password"]
await task_manager.set_step(task_id, 2, 5, "Wizard", acc_id=account["id"])
logger.info(f"[remove_phone] {login}: starting phone removal wizard...")
wizard = await run_common_wizard(
steam,
entry_url="https://help.steampowered.com/en/wizard/HelpRemovePhoneNumber?redir=store/account",
login=login,
password=password,
)
if not new_phone:
new_phone = await task_manager.prompt_user(task_id, "Введите новый номер телефона (на который переставится старый)", login=login)
if not new_phone:
raise ValueError("new_phone is required for phone removal")
new_phone = _validate_phone(new_phone)
await task_manager.set_step(task_id, 3, 5, "Запрос смены", acc_id=account["id"])
logger.info(f"[remove_phone] {login}: requesting phone change -> {new_phone}...")
fresh_sid = _get_fresh_sessionid(steam)
text = await steam.request(
url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryChangePhone/",
method="POST",
data={
"s": wizard.s,
"account": wizard.account,
"sessionid": fresh_sid,
"wizard_ajax": 1,
"gamepad": 0,
"phone_number": new_phone,
},
headers=AJAX_HEADERS,
)
data = json.loads(text) if isinstance(text, str) else text
logger.info(f"[remove_phone] {login}: change request response: {data}")
if data.get("errorMsg"):
raise RuntimeError(f"Phone change request failed: {data['errorMsg']}")
await task_manager.set_step(task_id, 4, 5, "SMS код", acc_id=account["id"])
sms_code = await task_manager.prompt_user(task_id, f"Введите SMS код с номера {new_phone}", login=login)
if not sms_code:
raise ValueError("sms_code is required")
sms_code = sms_code.strip()
logger.info(f"[remove_phone] {login}: confirming with SMS code...")
fresh_sid = _get_fresh_sessionid(steam)
confirm_text = await steam.request(
url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryConfirmChangePhone/",
method="POST",
data={
"s": wizard.s,
"account": wizard.account,
"sessionid": fresh_sid,
"wizard_ajax": 1,
"gamepad": 0,
"phone_number": new_phone,
"phone_change_code": sms_code,
},
headers=AJAX_HEADERS,
)
confirm_data = json.loads(confirm_text) if isinstance(confirm_text, str) else confirm_text
logger.info(f"[remove_phone] {login}: confirm response: {confirm_data}")
if confirm_data.get("errorMsg"):
raise RuntimeError(f"Phone confirmation failed: {confirm_data['errorMsg']}")
await task_manager.set_step(task_id, 5, 5, "Сохранение", acc_id=account["id"])
from app.database import get_db
db = await get_db()
await db.execute(
"UPDATE accounts SET phone = NULL, updated_at = datetime('now') WHERE id = ?",
(account["id"],),
)
await db.commit()
logger.success(f"[remove_phone] {login}: phone removed (changed to {new_phone})")
finally:
await close_steam(steam)
+90
View File
@@ -0,0 +1,90 @@
import re
import aiohttp
from loguru import logger
async def fetch_profile(steam_id: str, steam=None) -> dict | None:
"""Fetch persona name and avatar URL from Steam community profile page.
If *steam* session is provided, uses authenticated cookies.
Returns {"nickname": str, "avatar_url": str, "steam_level": int|None} or None on failure.
"""
url = f"https://steamcommunity.com/profiles/{steam_id}/"
try:
if steam is not None:
from app.services.steam_auth import raw_request
resp = await raw_request(steam, url, method="GET", allow_redirects=True)
if resp.status != 200:
logger.warning(f"Profile fetch for {steam_id}: HTTP {resp.status}")
return None
html = await resp.text()
else:
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
async with aiohttp.ClientSession(headers=headers) as session:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as resp:
if resp.status != 200:
logger.warning(f"Profile fetch for {steam_id}: HTTP {resp.status}")
return None
html = await resp.text()
except Exception as exc:
logger.warning(f"Profile fetch error for {steam_id}: {exc}")
return None
nickname = _parse_persona_name(html)
avatar_url = _parse_avatar(html)
steam_level = _parse_level(html)
last_online = _parse_last_online(html)
if not nickname and not avatar_url:
return None
return {"nickname": nickname, "avatar_url": avatar_url, "steam_level": steam_level, "last_online": last_online}
def _parse_persona_name(html: str) -> str | None:
# g_rgProfileData = {"personaname":"jeffreyoelze1999",...}
m = re.search(r'"personaname"\s*:\s*"([^"]*)"', html)
if m:
return m.group(1)
# Fallback: <span class="actual_persona_name">...</span>
m = re.search(r'<span class="actual_persona_name">([^<]+)</span>', html)
return m.group(1) if m else None
def _parse_avatar(html: str) -> str | None:
# 1) g_rgProfileData JSON: {"avatarfull":"https://avatars.fastly.steamstatic.com/..."}
m = re.search(r'"avatarfull"\s*:\s*"(https://[^"]+)"', html)
if m:
return m.group(1).replace("_full.jpg", "_medium.jpg")
# 2) <meta property="og:image" content="https://avatars...">
m = re.search(r'<meta\s+property="og:image"\s+content="(https://avatars[^"]+)"', html)
if m:
return m.group(1).replace("_full.jpg", "_medium.jpg")
# 3) playerAvatarAutoSizeInner img
m = re.search(r'playerAvatarAutoSizeInner[\s\S]{0,200}?<img[^>]+srcset="(https://[^"]+)"', html)
if m:
return m.group(1).replace("_full.jpg", "_medium.jpg")
return None
def _parse_level(html: str) -> int | None:
# <span class="friendPlayerLevelNum">100</span>
m = re.search(r'<span\s+class="friendPlayerLevelNum">(\d+)</span>', html)
return int(m.group(1)) if m else None
def _parse_last_online(html: str) -> str | None:
# "Last Online X days ago" → "Xd", "X hrs ago" → "Xh", "X min ago" → "Xm"
m = re.search(r'Last\s+Online[^<]{0,150}?(\d+)\s+day', html, re.IGNORECASE)
if m:
return f"{m.group(1)}d"
m = re.search(r'Last\s+Online[^<]{0,150}?(\d+)\s+hr', html, re.IGNORECASE)
if m:
return f"{m.group(1)}h"
m = re.search(r'Last\s+Online[^<]{0,150}?(\d+)\s+min', html, re.IGNORECASE)
if m:
return f"{m.group(1)}m"
if re.search(r'Currently\s+(In-Game|Online)', html, re.IGNORECASE):
return "online"
return None
+176
View File
@@ -0,0 +1,176 @@
"""Validation service for token accounts (refresh token → cookies)."""
import base64
import json as _json
import aiohttp
from aiohttp import FormData
from loguru import logger
from app.services.steam_auth import _resolve_proxy
from app.core.proxy_manager import proxy_manager
from app.core.task_manager import task_manager
from pysteamauth.auth.schemas import FinalizeLoginStatus
def _decode_jwt_payload(token: str) -> dict:
"""Decode the payload section of a JWT without verifying the signature."""
parts = token.split(".")
if len(parts) != 3:
raise ValueError("Token must have 3 parts (header.payload.signature)")
payload_b64 = parts[1]
# Pad base64 if needed
payload_b64 += "=" * (-len(payload_b64) % 4)
payload_bytes = base64.urlsafe_b64decode(payload_b64)
return _json.loads(payload_bytes)
async def check_token_account(account: dict, params: dict, *, task_id: str) -> None:
"""Convert refresh_token → web cookies, fetch profile via cookies, update DB."""
acc_id = account["id"]
token = account["token"]
# Step 1: Decode JWT to get steam_id
await task_manager.set_step(task_id, 1, 4, "Декодирование токена", acc_id)
try:
jwt_payload = _decode_jwt_payload(token)
except Exception as exc:
logger.error(f"[token_checker] {account.get('login', acc_id)}: bad JWT — {exc}")
await _mark_invalid(acc_id)
raise
steam_id = jwt_payload.get("sub")
# Step 2: GET steamcommunity.com for sessionid cookie
await task_manager.set_step(task_id, 2, 4, "Получение sessionid", acc_id)
proxy = await _resolve_proxy(account)
connector = proxy_manager.get_connector(proxy) if proxy else aiohttp.TCPConnector()
jar = aiohttp.CookieJar(unsafe=True)
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Origin": "https://steamcommunity.com",
}
timeout = aiohttp.ClientTimeout(total=30)
try:
async with aiohttp.ClientSession(
connector=connector, cookie_jar=jar, headers=headers, timeout=timeout,
) as session:
async with session.get("https://steamcommunity.com") as resp:
await resp.read()
sessionid = None
for cookie in jar:
if cookie.key == "sessionid":
sessionid = cookie.value
break
if not sessionid:
raise RuntimeError("Failed to acquire sessionid cookie")
# Step 3: POST /jwt/finalizelogin with refresh_token as nonce
await task_manager.set_step(task_id, 3, 4, "Получение куки", acc_id)
form = FormData(fields=[
("nonce", token),
("sessionid", sessionid),
("redir", "https://steamcommunity.com/login/home/?goto="),
])
async with session.post(
"https://login.steampowered.com/jwt/finalizelogin",
data=form,
) as resp:
resp_text = await resp.text()
if resp.status != 200:
raise RuntimeError(f"finalizelogin HTTP {resp.status}: {resp_text[:200]}")
resp_json = _json.loads(resp_text)
if not resp_json.get("steamID"):
error_msg = resp_json.get("message", resp_text[:200])
raise RuntimeError(f"finalizelogin failed: {error_msg}")
finalize = FinalizeLoginStatus.model_validate(resp_json)
steam_id = finalize.steamID or steam_id
# Post to each transfer_info URL to set domain cookies
for ti in finalize.transfer_info:
form = FormData(fields=[
("nonce", ti.params.nonce),
("auth", ti.params.auth),
("steamID", str(steam_id)),
])
try:
async with session.post(ti.url, data=form) as _resp:
await _resp.read()
except Exception:
pass
# Visit extra domains to collect all cookies
for url in ("https://store.steampowered.com", "https://help.steampowered.com"):
try:
async with session.get(url) as _resp:
await _resp.read()
except Exception:
pass
# Collect cookies from jar
all_cookies = []
for c in jar:
all_cookies.append({
"name": c.key,
"value": c.value,
"domain": c.get("domain", ""),
"path": c.get("path", "/"),
"secure": "secure" in str(c).lower(),
"httpOnly": "httponly" in str(c).lower(),
})
session_cookies_json = _json.dumps(all_cookies)
logger.debug(f"[token_checker] {account.get('login', acc_id)}: got {len(all_cookies)} cookies")
# Step 4: Fetch profile using authenticated session (cookies set)
await task_manager.set_step(task_id, 4, 4, "Получение профиля", acc_id)
nickname = None
steam_level = None
avatar_url = None
last_online = None
if steam_id and steam_id != "0":
profile_url = f"https://steamcommunity.com/profiles/{steam_id}/"
try:
async with session.get(profile_url) as resp:
if resp.status == 200:
html = await resp.text()
from app.services.steam_profile import (
_parse_persona_name, _parse_avatar, _parse_level, _parse_last_online,
)
nickname = _parse_persona_name(html)
avatar_url = _parse_avatar(html)
steam_level = _parse_level(html)
last_online = _parse_last_online(html)
except Exception as exc:
logger.warning(f"[token_checker] profile fetch error: {exc}")
# Update DB
from app.database import get_db
db = await get_db()
await db.execute(
"""UPDATE token_accounts
SET steam_id = ?, nickname = ?, steam_level = ?, avatar_url = ?, last_online = ?,
session_cookies = ?, status = 'valid', updated_at = datetime('now')
WHERE id = ?""",
(steam_id, nickname, steam_level, avatar_url, last_online, session_cookies_json, acc_id),
)
await db.commit()
logger.success(f"[token_checker] {account.get('login', acc_id)} → valid, steam_id={steam_id}")
except Exception as exc:
await _mark_invalid(acc_id)
logger.error(f"[token_checker] {account.get('login', acc_id)}{exc}")
raise
async def _mark_invalid(acc_id: int) -> None:
from app.database import get_db
db = await get_db()
await db.execute(
"UPDATE token_accounts SET status = 'invalid', updated_at = datetime('now') WHERE id = ?",
(acc_id,),
)
await db.commit()
+397
View File
@@ -0,0 +1,397 @@
"""Shared Steam account recovery wizard flow.
Steps 1-9 are identical across password change, email change, and phone removal.
This module extracts that common logic.
"""
import asyncio
import json
from dataclasses import dataclass
from urllib.parse import parse_qs, urlparse
from loguru import logger
from app.core.crypto import encrypt_password
from app.services.steam_auth import raw_request
BROWSER_UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/"
"537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36"
)
AJAX_HEADERS = {
"Accept": "*/*",
"X-Requested-With": "XMLHttpRequest",
"User-Agent": BROWSER_UA,
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Origin": "https://help.steampowered.com",
}
@dataclass
class WizardParams:
s: int
account: int
reset: int
issueid: int
lost: int = 0
sessionid: str = ""
async def parse_wizard_redirect(steam, entry_url: str) -> WizardParams:
"""Step 1-2: Navigate to wizard entry URL, follow redirects, parse params from final URL."""
logger.debug(f"[Wizard] Step 1-2: GET {entry_url}")
response = await raw_request(
steam,
url=entry_url,
method="GET",
headers={"User-Agent": BROWSER_UA},
allow_redirects=True,
)
final_url = str(response.url)
logger.debug(f"[Wizard] Redirect → {final_url}")
query = parse_qs(urlparse(final_url).query)
sessionid = await steam.sessionid("help.steampowered.com")
params = WizardParams(
s=int(query.get("s", [0])[0]),
account=int(query.get("account", [0])[0]),
reset=int(query.get("reset", [0])[0]),
issueid=int(query.get("issueid", [0])[0]),
lost=int(query.get("lost", [0])[0]),
sessionid=sessionid,
)
logger.debug(f"[Wizard] Parsed: s={params.s}, account={params.account}, reset={params.reset}, issueid={params.issueid}")
return params
async def login_info_enter_code(steam, params: WizardParams) -> dict:
"""Step 3: Enter code mode."""
logger.debug("[Wizard] Step 3: HelpWithLoginInfoEnterCode")
text = await steam.request(
url="https://help.steampowered.com/en/wizard/HelpWithLoginInfoEnterCode",
method="GET",
params={
"s": params.s,
"account": params.account,
"reset": params.reset,
"lost": params.lost,
"issueid": params.issueid,
"sessionid": params.sessionid,
"wizard_ajax": 1,
"gamepad": 0,
},
headers=AJAX_HEADERS,
)
result = json.loads(text) if isinstance(text, str) else text
logger.debug(f"[Wizard] Step 3 response: {result}")
return result
async def send_recovery_code(steam, params: WizardParams) -> dict:
"""Step 4: Send account recovery code (method=8 = mobile app)."""
logger.debug("[Wizard] Step 4: AjaxSendAccountRecoveryCode (method=8, mobile)")
text = await steam.request(
url="https://help.steampowered.com/en/wizard/AjaxSendAccountRecoveryCode",
method="POST",
data={
"s": params.s,
"account": params.account,
"reset": params.reset,
"lost": params.lost,
"issueid": params.issueid,
"sessionid": params.sessionid,
"wizard_ajax": 1,
"gamepad": 0,
"method": 8,
"link": "",
"n": 1,
},
headers=AJAX_HEADERS,
)
result = json.loads(text) if isinstance(text, str) else text
logger.debug(f"[Wizard] Step 4 response: {result}")
if result.get("errorMsg"):
raise RuntimeError(f"SendRecoveryCode failed: {result['errorMsg']}")
return result
async def mobile_confirm(steam, params: WizardParams, retries: int = 5) -> None:
"""Step 5: Confirm via Steam mobile confirmations API (getlist → find by creator_id → ajaxop allow)."""
logger.debug("[Wizard] Step 5: Mobile confirmation")
try:
sid = steam.steamid
except (ValueError, AttributeError):
sid = None
if not sid:
raise RuntimeError("No steamid on Steam object — cannot perform mobile confirmation")
try:
did = steam.device_id
except (ValueError, AttributeError):
did = None
if not did:
raise RuntimeError("No device_id on Steam object — cannot perform mobile confirmation")
try:
_ = steam.identity_secret
except (ValueError, AttributeError):
raise RuntimeError("No identity_secret on Steam object — cannot perform mobile confirmation")
logger.debug(f"[MobileConfirm] steamid={sid}, device_id={did[:20]}..., creator_id(s)={params.s}")
# Steam needs time to create the confirmation after send_recovery_code
logger.debug("[MobileConfirm] Waiting 3s for Steam to create confirmation...")
await asyncio.sleep(3)
for attempt in range(retries):
try:
server_time = await steam.get_server_time()
conf_hash = steam.get_confirmation_hash(server_time=server_time)
logger.debug(f"[MobileConfirm] Attempt {attempt + 1}/{retries}: getlist (server_time={server_time})")
conf_text = await steam.request(
url="https://steamcommunity.com/mobileconf/getlist",
method="GET",
cookies={
"mobileClient": "ios",
"mobileClientVersion": "2.0.20",
"steamid": str(sid),
"Steam_Language": "english",
},
params={
"p": did,
"a": str(sid),
"k": conf_hash,
"t": server_time,
"m": "react",
"tag": "conf",
},
)
conf_data = json.loads(conf_text) if isinstance(conf_text, str) else conf_text
confs = conf_data.get("conf", [])
logger.debug(f"[MobileConfirm] getlist response: success={conf_data.get('success')}, {len(confs)} confirmation(s)")
for i, c in enumerate(confs):
logger.debug(
f"[MobileConfirm] [{i}] id={c.get('id')}, type={c.get('type')}, "
f"type_name={c.get('type_name', '?')}, creator_id={c.get('creator_id')}, "
f"headline={c.get('headline', '')}"
)
if not conf_data.get("success"):
logger.warning(f"[MobileConfirm] getlist failed: {conf_data}")
raise RuntimeError(f"getlist failed: {conf_data}")
target = None
creator = str(params.s)
for c in confs:
if str(c.get("creator_id", "")) == creator:
target = c
break
if not target:
ids = [str(c.get("creator_id", "")) for c in confs]
logger.warning(f"[MobileConfirm] No match for creator_id={creator}, available: {ids}")
raise RuntimeError(f"No confirmation found for creator_id={creator}")
logger.debug(
f"[MobileConfirm] Found match! id={target['id']}, nonce={target.get('nonce')}, "
f"creator_id={target.get('creator_id')}"
)
allow_time = await steam.get_server_time()
allow_hash = steam.get_confirmation_hash(server_time=allow_time, tag="allow")
logger.debug(f"[MobileConfirm] Calling ajaxop allow (server_time={allow_time})")
allow_text = await steam.request(
url="https://steamcommunity.com/mobileconf/ajaxop",
method="GET",
cookies={
"mobileClient": "ios",
"mobileClientVersion": "2.0.20",
},
params={
"op": "allow",
"p": did,
"a": str(sid),
"k": allow_hash,
"t": allow_time,
"m": "react",
"tag": "allow",
"cid": target["id"],
"ck": target["nonce"],
},
)
allow_data = json.loads(allow_text) if isinstance(allow_text, str) else allow_text
logger.debug(f"[MobileConfirm] ajaxop response: {allow_data}")
if allow_data.get("success"):
logger.debug(f"[MobileConfirm] ✓ Confirmation accepted (attempt {attempt + 1})")
return
raise RuntimeError(f"ajaxop returned success=false: {allow_data}")
except Exception as exc:
logger.warning(f"[MobileConfirm] Attempt {attempt + 1}/{retries} failed: {exc}")
if attempt < retries - 1:
logger.debug(f"[MobileConfirm] Retrying in 3s...")
await asyncio.sleep(3)
raise RuntimeError(f"Mobile confirmation failed after {retries} retries")
async def poll_recovery_confirmation(steam, params: WizardParams) -> dict:
"""Step 6: Poll account recovery confirmation (single call, matches old chemail.py)."""
logger.debug("[Wizard] Step 6: AjaxPollAccountRecoveryConfirmation")
text = await steam.request(
url="https://help.steampowered.com/en/wizard/AjaxPollAccountRecoveryConfirmation",
method="POST",
data={
"s": params.s,
"reset": params.reset,
"lost": params.lost,
"issueid": params.issueid,
"sessionid": params.sessionid,
"wizard_ajax": 1,
"gamepad": 0,
"method": 8,
},
headers=AJAX_HEADERS,
)
data = json.loads(text) if isinstance(text, str) else text
logger.debug(f"[Wizard] Step 6 response: {data}")
if data.get("errorMsg"):
raise RuntimeError(f"Poll error from Steam: {data['errorMsg']}")
return data
async def verify_recovery_code(steam, params: WizardParams) -> dict:
"""Step 7: Verify recovery code (empty for mobile method)."""
logger.debug("[Wizard] Step 7: AjaxVerifyAccountRecoveryCode (code=empty, method=8)")
text = await steam.request(
url="https://help.steampowered.com/en/wizard/AjaxVerifyAccountRecoveryCode",
method="GET",
params={
"code": "",
"s": params.s,
"reset": params.reset,
"lost": params.lost,
"method": 8,
"issueid": params.issueid,
"sessionid": params.sessionid,
"wizard_ajax": 1,
"gamepad": 0,
},
headers=AJAX_HEADERS,
)
result = json.loads(text) if isinstance(text, str) else text
logger.debug(f"[Wizard] Step 7 response: {result}")
if result.get("errorMsg"):
raise RuntimeError(f"Step 7 (VerifyRecoveryCode) failed: {result['errorMsg']}")
return result
async def get_next_step(steam, params: WizardParams) -> dict:
"""Step 8: Get next wizard step (lost=2 hardcoded)."""
logger.debug("[Wizard] Step 8: AjaxAccountRecoveryGetNextStep (lost=2)")
text = await steam.request(
url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryGetNextStep",
method="POST",
data={
"s": params.s,
"account": params.account,
"reset": params.reset,
"lost": 2,
"issueid": params.issueid,
"sessionid": params.sessionid,
"wizard_ajax": 1,
"gamepad": 0,
},
headers=AJAX_HEADERS,
)
result = json.loads(text) if isinstance(text, str) else text
logger.debug(f"[Wizard] Step 8 response: {result}")
if result.get("errorMsg"):
raise RuntimeError(f"Step 8 (GetNextStep) failed: {result['errorMsg']}")
return result
async def get_rsa_key(steam, login: str) -> tuple[str, str, int]:
"""Step 9a: Get RSA public key from Steam."""
logger.debug(f"[Wizard] Getting RSA key for {login}")
sessionid = await steam.sessionid("help.steampowered.com")
text = await steam.request(
url="https://help.steampowered.com/en/login/getrsakey/",
method="POST",
data={
"sessionid": sessionid,
"username": login,
},
headers=AJAX_HEADERS,
)
data = json.loads(text) if isinstance(text, str) else text
logger.debug(f"[Wizard] RSA key received: success={data.get('success')}, timestamp={data.get('timestamp')}")
return data["publickey_mod"], data["publickey_exp"], int(data["timestamp"])
async def verify_password(steam, params: WizardParams, login: str, password: str) -> dict:
"""Step 9: Verify current password via RSA encryption."""
logger.debug(f"[Wizard] Step 9: Verifying current password for {login}")
mod, exp, timestamp = await get_rsa_key(steam, login)
encrypted = encrypt_password(password, mod, exp)
sessionid = await steam.sessionid("help.steampowered.com")
text = await steam.request(
url="https://help.steampowered.com/en/wizard/AjaxAccountRecoveryVerifyPassword/",
method="POST",
data={
"sessionid": sessionid,
"s": params.s,
"lost": 2,
"reset": 1,
"password": encrypted,
"rsatimestamp": timestamp,
},
headers=AJAX_HEADERS,
)
result = json.loads(text) if isinstance(text, str) else text
logger.debug(f"[Wizard] Step 9 response: {result}")
if result.get("errorMsg"):
raise RuntimeError(f"Step 9 (VerifyPassword) failed: {result['errorMsg']}")
return result
async def run_common_wizard(steam, entry_url: str, login: str, password: str) -> WizardParams:
"""Execute the full shared wizard flow (steps 1-9).
Returns WizardParams ready for the service-specific final steps.
"""
logger.debug(f"[Wizard] ===== Starting wizard for {login} =====")
logger.debug(f"[Wizard] Entry URL: {entry_url}")
params = await parse_wizard_redirect(steam, entry_url)
await asyncio.sleep(1)
await login_info_enter_code(steam, params)
await asyncio.sleep(1)
await send_recovery_code(steam, params)
await asyncio.sleep(1)
await mobile_confirm(steam, params)
await poll_recovery_confirmation(steam, params)
await asyncio.sleep(1)
await verify_recovery_code(steam, params)
await asyncio.sleep(1)
await get_next_step(steam, params)
await asyncio.sleep(1)
await verify_password(steam, params, login, password)
logger.debug(f"[Wizard] ===== Wizard complete for {login} =====")
return params