fix(login): citeste credentialele din .env, nu din env-ul mostenit

Scraper-ul e pornit ca subproces de telegram_trigger_bot.py cu
env=os.environ.copy(). Bot-ul isi incarca .env la pornire, deci copia
contine parola veche; load_dotenv() din config.py nu suprascrie o
variabila deja existenta, asa ca dupa schimbarea parolei scraper-ul
trimitea in continuare parola veche - BT respingea login-ul, push-ul 2FA
nu mai pleca si ramaneam blocati 300s in "Timeout 2FA".

- config.py: BTGO_USERNAME/BTGO_PASSWORD se citesc direct din .env
  (dotenv_values), restul variabilelor raman din environment ca sa nu
  strice TELEGRAM_MESSAGE_ID / SEND_AS_ZIP transmise de bot la fiecare run.
- btgo_scraper.py: detectie a mesajului de eroare de pe pagina de login in
  bucla de asteptare 2FA -> esec rapid cu motivul real, plus screenshot
  de diagnostic (si la respingere, si la timeout, cat browser-ul e viu).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YbuWZ699EFeYfPA3nNraa9
This commit is contained in:
2026-08-19 11:30:38 +03:00
parent 584d59a8cf
commit d0e5c0c460
2 changed files with 73 additions and 3 deletions

View File

@@ -331,6 +331,50 @@ class BTGoScraper:
logging.error(" [SUBMIT] Nu am gasit butonul cu nicio strategie!") logging.error(" [SUBMIT] Nu am gasit butonul cu nicio strategie!")
return None return None
# Texte care apar in mesajele BT cand autentificarea e respinsa
LOGIN_ERROR_PATTERNS = (
"invalid", "incorect", "gresit", "greșit", "eronat",
"blocat", "suspendat", "date de autentificare",
)
def _debug_shot(self, page, tag):
"""Salveaza un screenshot de diagnostic; nu arunca daca esueaza."""
try:
path = Path(self.config.OUTPUT_DIR) / f"debug_{tag}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
page.screenshot(path=str(path))
logging.info(f"Screenshot diagnostic salvat: {path}")
return path
except Exception as e:
logging.warning(f"Nu am putut salva screenshot ({tag}): {e}")
return None
def _detect_login_error(self, page):
"""
Returneaza textul erorii afisate de pagina de login (parola gresita,
cont blocat etc.) sau None. Fara asta, credentialele respinse arata
identic cu un 2FA neaprobat: asteptam degeaba pana la timeout.
"""
try:
candidates = page.locator(
"mat-error, .mat-error, [role='alert'], .alert, "
".error, .error-message, .invalid-feedback"
)
count = min(candidates.count(), 10)
except Exception:
return None
for i in range(count):
try:
el = candidates.nth(i)
if not el.is_visible(timeout=500):
continue
text = (el.inner_text() or "").strip()
except Exception:
continue
if text and any(pat in text.lower() for pat in self.LOGIN_ERROR_PATTERNS):
return " ".join(text.split())
return None
def _try_field_strategy(self, page, strategy_type, value): def _try_field_strategy(self, page, strategy_type, value):
"""Helper pentru a incerca o strategie de gasire a unui camp""" """Helper pentru a incerca o strategie de gasire a unui camp"""
if strategy_type == "placeholder_exact": if strategy_type == "placeholder_exact":
@@ -622,6 +666,7 @@ class BTGoScraper:
start_time = datetime.now() start_time = datetime.now()
timeout = self.config.TIMEOUT_2FA_SECONDS timeout = self.config.TIMEOUT_2FA_SECONDS
iteration = 0
while (datetime.now() - start_time).seconds < timeout: while (datetime.now() - start_time).seconds < timeout:
# Verifică dacă butonul de conturi este CLICKABLE (nu doar vizibil) # Verifică dacă butonul de conturi este CLICKABLE (nu doar vizibil)
@@ -644,6 +689,19 @@ class BTGoScraper:
except: except:
pass pass
# Daca BT a respins credentialele, push-ul 2FA nu pleaca niciodata -
# iesim imediat cu motivul real, nu dupa timeout.
iteration += 1
if iteration % 3 == 0:
login_error = self._detect_login_error(self.login_page)
if login_error:
print()
self._debug_shot(self.login_page, 'login_respins')
raise Exception(
f"Autentificare respinsa de BT: {login_error} "
"(2FA nu a fost trimis - verifica user/parola din .env)"
)
# Afiseaza countdown # Afiseaza countdown
elapsed = (datetime.now() - start_time).seconds elapsed = (datetime.now() - start_time).seconds
remaining = timeout - elapsed remaining = timeout - elapsed
@@ -652,6 +710,8 @@ class BTGoScraper:
time.sleep(2) time.sleep(2)
print() # New line dupa countdown print() # New line dupa countdown
# Screenshot cat browser-ul e inca viu (dupa raise, event loop-ul e inchis)
self._debug_shot(self.login_page, 'timeout_2fa')
raise TimeoutError(f"Timeout 2FA dupa {timeout} secunde. Verifica ca ai aprobat pe telefon!") raise TimeoutError(f"Timeout 2FA dupa {timeout} secunde. Verifica ca ai aprobat pe telefon!")
def download_transactions(self, accounts): def download_transactions(self, accounts):

View File

@@ -2,19 +2,29 @@
Configurare pentru BTGO Scraper Configurare pentru BTGO Scraper
Citeste configuratii din variabile de mediu (.env) Citeste configuratii din variabile de mediu (.env)
""" """
from dotenv import load_dotenv from dotenv import load_dotenv, dotenv_values
import os import os
# Incarca variabilele de mediu din fisierul .env # Incarca variabilele de mediu din fisierul .env
load_dotenv() load_dotenv()
# Credentialele se citesc DIRECT din .env, nu din environment.
# Motiv: scraper-ul e pornit ca subproces de telegram_trigger_bot.py, care ii da
# o copie a propriului environment. Bot-ul si-a incarcat .env-ul la pornire, deci
# environment-ul mostenit contine parola veche, iar load_dotenv() (fara override)
# nu suprascrie o variabila deja existenta -> se trimitea parola veche la BT si
# nu mai pleca push-ul 2FA. Nu folosim load_dotenv(override=True) pentru ca
# bot-ul transmite intentionat prin environment variabile de rulare
# (TELEGRAM_MESSAGE_ID, TELEGRAM_CHAT_ID, SEND_AS_ZIP, BALANCES_ONLY).
_ENV_FILE = dotenv_values()
class Config: class Config:
"""Clasa pentru gestionarea configuratiilor aplicatiei""" """Clasa pentru gestionarea configuratiilor aplicatiei"""
# Credentials # Credentials
BTGO_USERNAME = os.getenv('BTGO_USERNAME') BTGO_USERNAME = _ENV_FILE.get('BTGO_USERNAME') or os.getenv('BTGO_USERNAME')
BTGO_PASSWORD = os.getenv('BTGO_PASSWORD') BTGO_PASSWORD = _ENV_FILE.get('BTGO_PASSWORD') or os.getenv('BTGO_PASSWORD')
# Browser settings # Browser settings
HEADLESS = os.getenv('HEADLESS', 'false').lower() == 'true' HEADLESS = os.getenv('HEADLESS', 'false').lower() == 'true'