"""DomClick session cookie management — load/save/invalidate encrypted cookies. Empirically verified live (2026-07-04): DomClick's QRATOR anti-bot reputation-block (даже на прокси-IP, уже помеченном как suspicious) полностью обходится инъекцией cookies валидной аутентифицированной test-аккаунт сессии (Sber ID login) перед навигацией на карточку. Cookies хранятся зашифрованно (pgp_sym_encrypt) в domclick_session_cookies (зеркалит cian_session_cookies, но account_cas_id вместо account_user_id). MVP: без verify_session (в отличие от app.services.cian_session). DomClick- верификация требует реального browser-фетча (SSR-стейт страницы), а не простого curl_cffi-запроса как у Cian Valuation Calculator — вне рамок этой итерации, future enhancement. """ from __future__ import annotations import json import logging from sqlalchemy import text from sqlalchemy.orm import Session from app.core.config import settings logger = logging.getLogger(__name__) # Cookies критичные для DomClick auth (Sber ID) — фильтр перед сохранением. # Список составлен по реальному DevTools/Cookie-Editor дампу авторизованной # test-аккаунт сессии (Sber ID login), 2026-07-04. DOMCLICK_REQUIRED_COOKIES: set[str] = { "qrator_jsid2", "CAS_ID", "CAS_ID_SIGNED", "sessionId", "UNIQ_SESSION_ID", "_visitId", "ns_session", "RETENTION_COOKIES_NAME", "regionName", "region", "currentRegionGuid", "currentLocalityGuid", "adtech_uid", "ftgl_cookie_id", "_ym_uid", "_ym_d", } def save_session( db: Session, account_cas_id: int, cookies: dict[str, str], ttl_days: int = 30, ) -> None: """Encrypt cookies via pgp_sym_encrypt and UPSERT into domclick_session_cookies. Uses settings.cookie_encryption_key as the encryption secret. Никогда не логирует сырые значения cookies. """ cookies_json = json.dumps(cookies) db.execute( text(""" INSERT INTO domclick_session_cookies ( account_cas_id, cookies_encrypted, expires_at_estimate, uploaded_at ) VALUES ( CAST(:cas_id AS bigint), pgp_sym_encrypt(:cookies_json, :key), NOW() + (CAST(:ttl_days AS int) || ' days')::interval, NOW() ) ON CONFLICT (account_cas_id) DO UPDATE SET cookies_encrypted = EXCLUDED.cookies_encrypted, expires_at_estimate = EXCLUDED.expires_at_estimate, uploaded_at = NOW(), last_invalid_at = NULL """), { "cas_id": account_cas_id, "cookies_json": cookies_json, "key": settings.cookie_encryption_key, "ttl_days": ttl_days, }, ) db.commit() logger.info( "DomClick cookies saved for casId=%s (count=%d, ttl=%d days)", account_cas_id, len(cookies), ttl_days, ) def load_session(db: Session) -> dict[str, str] | None: """Load most-recently-uploaded valid DomClick cookies (decrypt). Returns dict[cookie_name, cookie_value] или None если нет валидной session. Выбирает только записи где expires_at_estimate > NOW() и сессия не была инвалидирована после последнего upload'а. """ row = ( db.execute( text(""" SELECT account_cas_id, pgp_sym_decrypt(cookies_encrypted, :key)::text AS cookies_json, expires_at_estimate FROM domclick_session_cookies WHERE expires_at_estimate > NOW() AND (last_invalid_at IS NULL OR last_invalid_at < uploaded_at) ORDER BY uploaded_at DESC LIMIT 1 """), {"key": settings.cookie_encryption_key}, ) .mappings() .first() ) if row is None: logger.warning("No valid DomClick session cookies in DB") return None cookies: dict[str, str] = json.loads(row["cookies_json"]) # Обновляем last_used_at — не критично, игнорируем ошибки. try: db.execute( text( "UPDATE domclick_session_cookies SET last_used_at = NOW()" " WHERE account_cas_id = CAST(:cas_id AS bigint)" ), {"cas_id": row["account_cas_id"]}, ) db.commit() except Exception as exc: logger.warning("Failed to update last_used_at for casId=%s: %s", row["account_cas_id"], exc) logger.info( "DomClick cookies loaded for casId=%s (count=%d)", row["account_cas_id"], len(cookies), ) return cookies def mark_session_invalid(db: Session, account_cas_id: int) -> None: """Flag session как expired/invalid (например после блока во время scrape).""" db.execute( text( "UPDATE domclick_session_cookies SET last_invalid_at = NOW()" " WHERE account_cas_id = CAST(:cas_id AS bigint)" ), {"cas_id": account_cas_id}, ) db.commit() logger.warning("DomClick session marked invalid for casId=%s", account_cas_id)