Compare commits

...

12 Commits

Author SHA1 Message Date
306aa55907 Fix cookie banner GDPR care blocheaza click-ul pe LOGIN in deploy
Banner-ul GDPR aparea cu intarziere dupa page load si bloca click-ul.
Adaugat wait explicit, mai multe strategii de text, force click si
fallback JavaScript pentru eliminarea banner-ului din DOM.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 15:32:44 +02:00
0fff07c55b Rescrie complet fluxul de descarcare tranzactii pentru noul UI BT George
Modificari principale:
- Noul flux download: expand -> tranzactii -> CSV -> Genereaza -> download
- Detectie inteligenta buton Tranzactii (evita butonul Delete)
- Verificare daca primul cont e deja expandat inainte de click
- Selectie conturi cu 6 strategii fallback + debug logging
- Handler pentru cookie consent "Accept toate" si "Am inteles"
- Screenshot automat la erori de selectie cont

Documentatie:
- README: sectiuni noi pentru inregistrare Playwright si testare manuala
- CLAUDE.md: selectori actualizati pentru noul UI

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 15:18:44 +02:00
e49e653e12 Adauga detectie inteligenta campuri login cu strategii fallback
Rezolva problema cand selectoarele BT se schimba - acum incearca
multiple strategii pentru a gasi username, password si submit button.
Imbunatateste si gestionarea GDPR cookie banner.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 14:32:49 +02:00
58399e25fb Adauga comanda /tranzactii pentru vizualizare tranzactii interactive
- Sistem interactiv de selectie conturi cu state management
- Filtrare pe perioade: [nr cont] [nr zile] (ex: 2 30 = ultimele 30 zile)
- Format compact: tranzactii grupate pe date, fara sold
- Extragere inteligenta comercianti din platile POS (TID pattern)
- Escape automat caractere speciale Markdown pentru Telegram
- Timeout 5 minute pentru sesiuni de selectie
- Suport comenzi: doar numar = ultimele 10, numar + zile = filtrare

Corectari:
- Fix import timedelta pentru filtrare pe date
- Fix conflict nume variabila period vs csv_period
- Fix Markdown parsing errors (underscore, dot, etc)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-12 14:32:56 +02:00
91021fa530 Permite membrilor grupului sa foloseasca bot-ul in DM prin verificare API
Adauga logica de autorizare flexibila:
- Useri in TELEGRAM_ALLOWED_USER_IDS (whitelist explicit)
- SAU membrii grupului TELEGRAM_CHAT_ID (verificare getChatMember API)
- Membrii grupului pot folosi bot-ul atat in grup cat si in DM individual

Modificari:
- telegram_trigger_bot.py: metoda is_member_of_group() cu verificare API
- telegram_trigger_bot.py: is_user_allowed() cu logica OR pentru whitelist + grup
- .env.example: comentarii actualizate pentru noua logica
- TELEGRAM_BOT_SETUP.md: documentatie completa pentru autorizare

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-12 11:26:48 +02:00
548526fdde Adauga stergere automata fisiere inainte de scraping
La comenzile /scrape* se sterg automat fisierele CSV, JSON, ZIP si PNG anterioare pentru a preveni acumularea de date vechi.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 01:15:03 +02:00
e2ec15939c Adauga comenzi Telegram pentru solduri si repara get_telegram_chat_id
- Adauga /scrape_solduri - scraping rapid doar solduri (fara CSV tranzactii)
- Adauga /solduri - afisare instant solduri din cache (fara 2FA)
- Redenumeste comenzi pentru consistenta
- Adauga suport BALANCES_ONLY in scraper (skip download tranzactii)
- Repara get_telegram_chat_id.py - elimina input() interactiv
- Imbunatateste output get_telegram_chat_id.py cu info bot si formatare

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 01:03:02 +02:00
7e8dadcbdc Adaugă trimitere ZIP pe email și elimină emoji-uri din mesaje
- /scrape_zip trimite ZIP pe ambele canale (Telegram + email)
- /zip trimite ZIP și pe email, nu doar pe Telegram
- Elimină emoji-uri din mesajele Telegram user-facing
- Adaugă ghid "NO EMOJIS" în CLAUDE.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 21:58:03 +02:00
1e20334b3d Fix Git Pull & Restart - elimină dubla confirmare
Implementează logica de restart inline în loc să apeleze restart_service.ps1
pentru a evita 2 confirmări consecutive (Read-Host).

Îmbunătățiri:
- Restart serviciu inline (stop + start direct în funcție)
- Un singur prompt la final pentru revenire în meniu
- Mesaje de status detaliate pentru fiecare pas
- Gestionare erori pentru stop și start serviciu

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 21:44:10 +02:00
4e58e663e9 Adaugă opțiunea Git Pull & Restart în menu.ps1
Nouă funcționalitate pentru actualizare automată:
- Opțiunea [G] în meniul principal
- Funcția Invoke-GitPullRestart care rulează git pull
- Restart automat al serviciului dacă este instalat
- Validare exit code și gestionare erori

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 21:38:35 +02:00
c2ca401a26 Fix UnicodeEncodeError pe Windows pentru caractere românești
Problema:
- Logging eșua cu UnicodeEncodeError când scria caractere românești (ă, î, ș)
- Windows cmd.exe folosește cp1252 implicit, nu UTF-8

Soluție:
- Forțare encoding UTF-8 pentru stdout și stderr
- Folosește io.TextIOWrapper cu encoding='utf-8'
- errors='replace' pentru caractere care nu pot fi encodate

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 21:31:55 +02:00
821c1a8e01 Adaugă progress updates pentru /scrape_zip
Modificări:
- telegram_trigger_bot.py:
  - Păstrează TELEGRAM_CHAT_ID și TELEGRAM_MESSAGE_ID pentru progress
  - Setează flag SEND_AS_ZIP=true în environment
  - NU mai dezactivează notificările

- notifications.py:
  - Verifică flag SEND_AS_ZIP din environment
  - Dacă SEND_AS_ZIP=true, trimite ZIP cu progress updates
  - Mesajul de progres e editat la fel ca /scrape normal

Comportament /scrape_zip:
1. Bot trimite "Scraper pornit (arhiva ZIP)"
2. Scraper rulează și editează mesajul cu progress
3. notifications.py detectează flag-ul SEND_AS_ZIP
4. Trimite ZIP cu solduri în loc de fișiere individuale
5. Editează mesajul final cu detalii despre ZIP

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 21:30:54 +02:00
9 changed files with 1574 additions and 277 deletions

View File

@@ -22,11 +22,12 @@ SMTP_PASSWORD=your-app-password
EMAIL_FROM=your-email@gmail.com EMAIL_FROM=your-email@gmail.com
EMAIL_TO=mmarius28@gmail.com EMAIL_TO=mmarius28@gmail.com
# Telegram # Telegram Trigger Bot (pentru declanșare remote prin Telegram)
TELEGRAM_ENABLED=false TELEGRAM_ENABLED=false
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
TELEGRAM_CHAT_ID=your-chat-id TELEGRAM_CHAT_ID=your-chat-id
# Telegram Trigger Bot (pentru declanșare remote prin Telegram) TELEGRAM_ALLOWED_USER_IDS=123456789,987654321 # User IDs autorizați individual (opțional)
TELEGRAM_ALLOWED_USER_IDS=123456789,987654321 # User IDs autorizați (goală = toți) # TELEGRAM_CHAT_ID = grup autorizat (orice membru poate folosi bot-ul în DM/grup)
# Dacă ambele sunt goale = permite oricui
TELEGRAM_POLL_TIMEOUT=60 # Long polling timeout în secunde (30-90 recomandat) TELEGRAM_POLL_TIMEOUT=60 # Long polling timeout în secunde (30-90 recomandat)

View File

@@ -4,11 +4,15 @@ This file provides guidance to Claude Code when working with code in this reposi
## Project Overview ## Project Overview
BTGO Scraper - Playwright automation for extracting account balances and transaction CSVs from Banca Transilvania George (btgo.ro). BTGO Scraper - Playwright automation for extracting account balances and transaction CSVs from Banca Transilvania George (go.bancatransilvania.ro).
**Security Context**: Authorized personal banking automation tool for educational purposes. **Security Context**: Authorized personal banking automation tool for educational purposes.
**⚠️ CRITICAL**: Docker/headless mode is **BLOCKED by WAF**. ONLY works locally with `HEADLESS=false`. **CRITICAL**: Docker/headless mode is **BLOCKED by WAF**. ONLY works locally with `HEADLESS=false`.
## Coding Guidelines
**NO EMOJIS**: Do not use emojis in user-facing messages (Telegram, email, notifications). Use plain text only.
## Running the Scraper ## Running the Scraper
@@ -50,30 +54,54 @@ login() → handle_2fa_wait() → read_accounts() → download_transactions()
- Each card: `h4` (name), `span.text-grayscale-label` (IBAN), `strong.sold` (balance) - Each card: `h4` (name), `span.text-grayscale-label` (IBAN), `strong.sold` (balance)
- Balance format: `"7,223.26 RON"` → parse to float + currency - Balance format: `"7,223.26 RON"` → parse to float + currency
#### 4. Transaction Download Modal (lines ~312-420) #### 4. Transaction Download (lines ~529-732)
**State Machine:** **Flow (2024+ version):**
``` ```
Account 1: Expand card Click "Tranzacții" → Download → Back → Modal Account 1: Expand card -> Click tranzactii icon -> Select "CSV" -> "Genereaza" -> Download from fba-document-item
Account 2+: Select from modal → [ALREADY on page] → Download → Back Account 2+: Click #selectAccountBtn -> Select by heading name -> "Genereaza" -> Download
``` ```
**Critical**: After modal selection, you're ALREADY on transactions page. Don't expand/click again. **Key methods:**
- `_download_first_account()`: Handles first account (expand + select CSV format)
- `_download_subsequent_account()`: Handles accounts 2+ (dropdown selection)
- `_wait_and_download()`: Waits for fba-document-item and downloads
**Modal selectors:** **Account selection strategies (in order):**
- Modal: `.modal-content` 1. `get_by_role("heading", name=account_name)`
- Account buttons: `#accountC14RONCRT{last_10_iban_digits}` 2. `locator("fba-account-details").filter(has_text=account_name)`
- Example: IBAN `...0637236701``#accountC14RONCRT0637236701` 3. `get_by_text(account_name, exact=True)`
### Key Selectors ### Key Selectors
- Login: `get_by_placeholder("ID de logare")`, `get_by_placeholder("Parola")` **Cookie consent:**
- Post-login: `#accountsBtn`, `goapp.bancatransilvania.ro` domain - New (2024+): `get_by_role("button", name="Accept toate")`
- Accounts: `fba-account-details-card`, `.collapse-account-btn`, `.account-transactions-btn` - One-time consent: `get_by_text("Am inteles")`
- Modal: `.modal-content`, `#accountC14RONCRT{iban_digits}`
- CSV: `get_by_role("button", name="CSV")`
**Update selectors:** `playwright codegen https://btgo.ro --target python` **Login page:**
- URL: `https://go.bancatransilvania.ro/`
- Login link: `get_by_role("link", name="Login")`
- Username: `get_by_placeholder("ID logare")` (intelligent fallback in `_find_username_field`)
- Password: `get_by_placeholder("Parola")` or `input[type='password']`
- Submit: `get_by_role("button", name="Autentifica-te")` (intelligent fallback in `_find_submit_button`)
**Post-login:**
- 2FA success indicator: `#accountsBtn` visible and enabled
- Domain: `goapp.bancatransilvania.ro`
**Accounts:**
- Cards: `fba-account-details-card`
- Expand icon: `.mx-auto .mat-icon svg`, `.collapse-account-btn`
- Transactions button: `fba-account-buttons svg`, `.account-transactions-btn`
**Transaction download:**
- Account selector: `#selectAccountBtn svg`
- Account in dropdown: `get_by_role("heading", name=account_name)`
- CSV format: `get_by_text("CSV", exact=True)`
- Generate button: `get_by_role("button", name="Genereaza")`
- Download item: `fba-document-item svg`, `fba-document-item path`
**Update selectors:** `playwright codegen https://go.bancatransilvania.ro --target python`
## Docker Limitation ## Docker Limitation
@@ -93,17 +121,19 @@ Account 2+: Select from modal → [ALREADY on page] → Download → Back
- **Fix**: Run locally with `HEADLESS=false` - **Fix**: Run locally with `HEADLESS=false`
### Transaction Download Timeout ### Transaction Download Timeout
- Modal not detected: Check `.modal-content` selector - Check `fba-document-item` selector (wait for document generation)
- Account ID mismatch: Verify IBAN mapping `#accountC14RONCRT{last_10_digits}` - Verify `#selectAccountBtn` for account dropdown
- For idx > 1: Already on page, don't expand/click - Account selection: verify heading name matches exactly
### 2FA Timeout ### 2FA Timeout
- Increase `TIMEOUT_2FA_SECONDS` in `.env` - Increase `TIMEOUT_2FA_SECONDS` in `.env`
- Verify URL redirect to `goapp.bancatransilvania.ro` - Verify URL redirect to `goapp.bancatransilvania.ro`
- Check for "Am inteles" consent dialog blocking
### "Nu exista card la pozitia X" ### Account Selection Failed
- Trying to access cards in modal context - Account name might have changed - verify exact match
- First account needs expand, subsequent accounts don't - Try running `playwright codegen` to see current UI structure
- Check if dropdown opened (`#selectAccountBtn`)
## Exit Codes ## Exit Codes

120
README.md
View File

@@ -325,12 +325,8 @@ deployment\windows\scripts\menu.ps1
- Mărește timeout: `TIMEOUT_2FA_SECONDS=180` în `.env` - Mărește timeout: `TIMEOUT_2FA_SECONDS=180` în `.env`
- Verifică notificări activate pe telefon - Verifică notificări activate pe telefon
### Selectors nu funcționează ### Selectors nu functioneaza
Site-ul s-a schimbat. Re-generează selectors: Site-ul s-a schimbat. Urmeaza pasii din sectiunea **Inregistrare Manuala cu Playwright** de mai jos.
```bash
.venv\Scripts\activate
playwright codegen https://btgo.ro --target python
```
### Notificări Email nu funcționează ### Notificări Email nu funcționează
- Pentru Gmail: folosește App Password, nu parola normală - Pentru Gmail: folosește App Password, nu parola normală
@@ -343,12 +339,116 @@ playwright codegen https://btgo.ro --target python
- Chat ID pentru grupuri trebuie să fie **negativ** (ex: `-1001234567890`) - Chat ID pentru grupuri trebuie să fie **negativ** (ex: `-1001234567890`)
- Asigură-te că botul este în grup - Asigură-te că botul este în grup
## Inregistrare Manuala cu Playwright (Codegen)
Cand site-ul BT George isi schimba interfata, trebuie sa reinregistrezi fluxul manual.
### 1. Porneste Playwright Codegen
```powershell
# Activeaza venv-ul
.venv\Scripts\Activate.ps1
# Porneste codegen
playwright codegen https://go.bancatransilvania.ro --target python
```
Se deschid 2 ferestre:
- **Browser** - aici faci actiunile manual
- **Playwright Inspector** - aici vezi codul Python generat
### 2. Inregistreaza Fluxul
1. Accept cookies ("Accept toate")
2. Click pe "Login" - se deschide popup
3. Completeaza username si parola
4. Click "Autentifica-te"
5. Asteapta 2FA pe telefon
6. Dupa login, click pe "Conturi"
7. **Pentru primul cont:**
- Expand card (click pe sageata)
- Click pe butonul "Tranzactii" (iconita cu grafic)
- Click pe "CSV" pentru format
- Click pe "Genereaza"
- Click pe documentul generat pentru download
8. **Pentru conturile urmatoare:**
- Click pe `#selectAccountBtn` (dropdown conturi)
- Selecteaza contul din lista
- Click pe "Genereaza"
- Download fisierul
### 3. Salveaza si Analizeaza Scriptul
- In Inspector: **Copy** sau **File > Save**
- Compara cu `btgo_scraper.py` si actualizeaza selectorii modificati
- Selectori cheie de verificat:
- Cookie consent: `get_by_role("button", name="...")`
- Username field: `get_by_placeholder("...")`
- Submit button: `get_by_role("button", name="...")`
- Account selector: `#selectAccountBtn`
- Download item: `fba-document-item`
## Testare Manuala a Scraperului
### Testare Pas cu Pas
```powershell
# 1. Activeaza venv
.venv\Scripts\Activate.ps1
# 2. Ruleaza scraper-ul
python btgo_scraper.py
```
### Verificare Output
Dupa rulare, verifica:
```powershell
# Fisiere generate
dir data\
# Trebuie sa vezi:
# - solduri_*.csv (solduri toate conturile)
# - solduri_*.json (metadata + solduri)
# - tranzactii_*.csv (cate un fisier per cont)
# - dashboard_*.png (screenshot final)
# Verifica log-ul pentru erori
type logs\scraper_*.log | Select-String -Pattern "EROARE|ERROR|Exception"
```
### Testare Doar Solduri (fara download tranzactii)
```powershell
# Seteaza variabila temporar
$env:BALANCES_ONLY = "true"
python btgo_scraper.py
```
### Debug - Screenshot-uri
Daca ceva nu merge, verifica screenshot-urile din `data/`:
- `debug_login_popup_*.png` - starea paginii de login
- `debug_dropdown_*.png` - dropdown-ul de selectare conturi (daca esueaza)
- `error_*.png` - screenshot la eroare
### Testare Notificari (fara scraping)
```powershell
# Test Telegram
python test_telegram.py
# Trimite ultimele fisiere manual
python send_notifications.py
```
## Securitate ## Securitate
**⚠️ IMPORTANT:** **IMPORTANT:**
- NU comite `.env` în git (deja în `.gitignore`) - NU comite `.env` in git (deja in `.gitignore`)
- NU partaja screenshots/logs - conțin date sensibile - NU partaja screenshots/logs - contin date sensibile
- Șterge fișierele vechi periodic: - Sterge fisierele vechi periodic:
```bash ```bash
# Windows # Windows

View File

@@ -27,9 +27,13 @@ Tu (Telegram) → Bot → Rulează scraper → Trimite CSV-uri înapoi
3. Alege nume pentru bot (ex: "BTGO Scraper Bot") 3. Alege nume pentru bot (ex: "BTGO Scraper Bot")
4. Copiază **token-ul** primit (ex: `123456789:ABCdefGHIjklMNOpqrs`) 4. Copiază **token-ul** primit (ex: `123456789:ABCdefGHIjklMNOpqrs`)
### 2. Obține User ID-ul Tău ### 2. Obține User ID-ul Tău (OPȚIONAL)
Ai nevoie de User ID pentru securitate (doar tu poți rula scraper-ul). User ID e necesar doar dacă vrei să autorizezi utilizatori care **NU sunt în grup**.
**Dacă folosești grup:** Toți membrii grupului pot folosi bot-ul automat (în grup sau DM)!
**Pentru whitelist suplimentar:**
**Opțiunea A - Folosește bot existent:** **Opțiunea A - Folosește bot existent:**
```bash ```bash
@@ -98,19 +102,20 @@ Editează `.env` și adaugă:
# Bot token (același ca pentru notificări sau nou) # Bot token (același ca pentru notificări sau nou)
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrs TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrs
# User IDs autorizați (separați prin virgulă) # Chat ID GRUP pentru notificări automate + autorizare membri
# DOAR acești useri pot rula /scrape din grup
TELEGRAM_ALLOWED_USER_IDS=123456789,987654321
# Chat ID GRUP pentru notificări automate + răspunsuri comenzi
# IMPORTANT: Negativ pentru grupuri! (ex: -1001234567890) # IMPORTANT: Negativ pentru grupuri! (ex: -1001234567890)
# TOȚI membrii acestui grup pot folosi bot-ul (în grup sau DM)
TELEGRAM_CHAT_ID=-1001234567890 TELEGRAM_CHAT_ID=-1001234567890
# User IDs autorizați individual (OPȚIONAL - separați prin virgulă)
# Pentru useri care NU sunt în grup dar vrei să le dai acces
TELEGRAM_ALLOWED_USER_IDS=123456789,987654321
``` ```
**Securitate:** **Autorizare:**
- `TELEGRAM_ALLOWED_USER_IDS` = doar acești useri pot rula `/scrape` din grup - **Orice membru al grupului `TELEGRAM_CHAT_ID`** poate folosi bot-ul (în grup SAU în DM)
- Lasă gol dacă vrei ca oricine din grup să poată rula (nesigur!) - **SAU** useri din `TELEGRAM_ALLOWED_USER_IDS` (chiar dacă nu sunt în grup)
- Bot-ul verifică User ID-ul celui care trimite comanda, NU group ID-ul - Dacă ambele sunt goale = bot deschis pentru oricine (NESIGUR!)
### 4. Pornire Bot ### 4. Pornire Bot
@@ -153,7 +158,7 @@ Bot pornit. Așteaptă comenzi...
/help - Ajutor utilizare /help - Ajutor utilizare
``` ```
**Securitate:** Doar userii din `TELEGRAM_ALLOWED_USER_IDS` pot rula comenzi! **Autorizare:** Membri ai grupului TELEGRAM_CHAT_ID SAU useri din TELEGRAM_ALLOWED_USER_IDS pot rula comenzi!
### Flow Tipic în Grup ### Flow Tipic în Grup
@@ -308,16 +313,18 @@ TELEGRAM_ALLOWED_USER_IDS=123456789
**⚠️ ATENȚIE:** **⚠️ ATENȚIE:**
- Bot-ul are acces la credentials din `.env` - Bot-ul are acces la credentials din `.env`
- `TELEGRAM_ALLOWED_USER_IDS` TREBUIE configurat! - `TELEGRAM_CHAT_ID` sau `TELEGRAM_ALLOWED_USER_IDS` TREBUIE configurat pentru securitate!
- Nu partaja token-ul botului - Nu partaja token-ul botului
- VM-ul trebuie securizat (firewall, VPN) - VM-ul trebuie securizat (firewall, VPN)
**Best Practices:** **Best Practices:**
```bash ```bash
# ✅ Bun - doar tu și admin # ✅ Bun - grup autorizat + whitelist individual
TELEGRAM_CHAT_ID=-1001234567890
TELEGRAM_ALLOWED_USER_IDS=123456789,987654321 TELEGRAM_ALLOWED_USER_IDS=123456789,987654321
# ❌ Rău - oricine cu acces la bot # ❌ Rău - ambele goale (oricine are acces)
TELEGRAM_CHAT_ID=
TELEGRAM_ALLOWED_USER_IDS= TELEGRAM_ALLOWED_USER_IDS=
# ✅ Bun - notificări separate de trigger # ✅ Bun - notificări separate de trigger

View File

@@ -90,10 +90,297 @@ class BTGoScraper:
) )
logging.info(f"Progress update: {message}") logging.info(f"Progress update: {message}")
def _dismiss_gdpr_cookies(self, page, wait_for_banner=True):
"""
Inchide GDPR cookie banner daca este vizibil
Args:
page: Pagina Playwright pe care sa verifice
wait_for_banner: Daca True, asteapta aparitia banner-ului inainte de a incerca
"""
try:
# Asteapta ca banner-ul GDPR sa apara (poate intarzia dupa page load)
if wait_for_banner:
try:
page.wait_for_selector(".gdprcookie-wrapper", timeout=5000, state="visible")
logging.info(" [INFO] Banner GDPR detectat, incerc sa-l inchid...")
except:
# Banner-ul nu a aparut in 5 secunde, continuam
pass
# Strategii pentru cookie consent (in ordinea probabilitatii)
cookie_strategies = [
# 1. Noul buton BT (2024+)
("role", "button", "Accept toate"),
("role", "button", "Accepta toate"),
("role", "button", "Acceptă toate"),
# 2. Vechiul GDPR wrapper - text specific
("css", ".gdprcookie-wrapper button:has-text('Accept')"),
("css", ".gdprcookie-wrapper button:has-text('Sunt de acord')"),
("css", ".gdprcookie-wrapper button:has-text('Accepta')"),
("css", ".gdprcookie-wrapper button:has-text('Acceptă')"),
("css", ".gdprcookie-wrapper button:has-text('OK')"),
# 3. GDPR wrapper - orice buton (fallback agresiv)
("css", ".gdprcookie-wrapper button"),
("css", ".gdprcookie-wrapper .btn"),
("css", ".gdprcookie-wrapper [role='button']"),
# 4. Fallback generic
("role", "button", "Accept"),
("role", "button", "Accepta"),
("role", "button", "OK"),
]
for strategy in cookie_strategies:
try:
if strategy[0] == "role":
btn = page.get_by_role(strategy[1], name=strategy[2])
else:
btn = page.locator(strategy[2]).first
if btn.is_visible(timeout=3000):
btn.click(force=True)
logging.info(f" [OK] Cookies acceptate ({strategy})")
time.sleep(1)
# Verifica ca banner-ul a disparut
try:
page.wait_for_selector(".gdprcookie-wrapper", timeout=2000, state="hidden")
logging.info(" [OK] Banner GDPR inchis cu succes")
except:
# Incearca inca o data cu force click
logging.info(" [WARN] Banner inca vizibil, reincercare...")
try:
btn.click(force=True)
time.sleep(1)
except:
pass
return True
except:
continue
logging.info(" Nu exista cookie banner (sau deja acceptat)")
return False
except:
logging.info(" Nu exista cookie banner (sau deja acceptat)")
return False
def _dismiss_one_time_consent(self, page):
"""
Inchide dialoguri one-time (ex: 'Am inteles') daca apar
Args:
page: Pagina Playwright pe care sa verifice
"""
consent_buttons = [
("text", "Am înțeles"),
("text", "Am inteles"),
("role", "button", "Am înțeles"),
("role", "button", "OK"),
("role", "button", "Continua"),
]
for strategy in consent_buttons:
try:
if strategy[0] == "text":
btn = page.get_by_text(strategy[1], exact=True)
elif strategy[0] == "role":
btn = page.get_by_role(strategy[1], name=strategy[2])
if btn.is_visible(timeout=2000):
btn.click()
logging.info(f" [OK] Consent inchis ({strategy})")
time.sleep(1)
return True
except:
continue
return False
def _find_username_field(self, page):
"""
Detecteaza inteligent campul de username folosind multiple strategii.
Ordinea: selectori specifici -> selectori generici -> detectie structurala
Returns:
Locator daca gasit, None altfel
"""
strategies = [
# 1. Selectori specifici BT (pot sa se schimbe)
("placeholder_exact", "ID logare"),
("placeholder_exact", "ID de logare"),
("id", "user"),
("name", "user"),
# 2. Selectori generici (mai stabili)
("placeholder_contains", "logare"),
("placeholder_contains", "user"),
("placeholder_contains", "utilizator"),
("name_contains", "user"),
("name_contains", "login"),
("id_contains", "user"),
("id_contains", "login"),
# 3. Selectori structurali (foarte stabili)
("css", "form input[type='text']:not([type='hidden'])"),
("css", "form input:not([type='password']):not([type='hidden']):not([type='submit'])"),
("css", "input[type='text']"),
("css", ".form-control[type='text']"),
# 4. Fallback - primul input vizibil care nu e password/submit
("first_text_input", None),
]
for strategy_type, value in strategies:
try:
field = self._try_field_strategy(page, strategy_type, value)
if field and field.is_visible(timeout=1000):
logging.info(f" [USERNAME] Gasit cu strategia: {strategy_type}='{value}'")
return field
except Exception:
continue
logging.error(" [USERNAME] Nu am gasit campul cu nicio strategie!")
return None
def _find_password_field(self, page):
"""
Detecteaza inteligent campul de parola.
Campul password e foarte stabil - type='password' e standard HTML.
Returns:
Locator daca gasit, None altfel
"""
strategies = [
# 1. Cel mai stabil - type='password' (standard HTML)
("css", "input[type='password']"),
# 2. Selectori specifici BT
("id", "password"),
("name", "password"),
("name", "pass"),
("placeholder_exact", "Parola"),
("placeholder_exact", "Password"),
# 3. Selectori generici
("placeholder_contains", "parola"),
("placeholder_contains", "password"),
("name_contains", "pass"),
("id_contains", "pass"),
# 4. Fallback structural
("css", "form input[type='password']"),
("css", ".form-control[type='password']"),
]
for strategy_type, value in strategies:
try:
field = self._try_field_strategy(page, strategy_type, value)
if field and field.is_visible(timeout=1000):
logging.info(f" [PASSWORD] Gasit cu strategia: {strategy_type}='{value}'")
return field
except Exception:
continue
logging.error(" [PASSWORD] Nu am gasit campul cu nicio strategie!")
return None
def _find_submit_button(self, page):
"""
Detecteaza inteligent butonul de submit.
Returns:
Locator daca gasit, None altfel
"""
strategies = [
# 1. Selectori specifici BT
("css", "input[value='Autentifică-te']"),
("css", "button:has-text('Autentifică-te')"),
("css", "input[value*='Autentific']"),
# 2. Selectori generici pentru login buttons
("css", "input[type='submit']"),
("css", "button[type='submit']"),
("css", "form button.btn-primary"),
("css", "form input.btn-primary"),
# 3. Text-based (mai putin stabil dar functional)
("text_contains", "Login"),
("text_contains", "Conectare"),
("text_contains", "Autentificare"),
("text_contains", "Intra"),
("text_contains", "Submit"),
# 4. Fallback - orice buton din form
("css", "form button"),
("css", "form input[type='button']"),
("css", ".btn-primary"),
]
for strategy_type, value in strategies:
try:
button = self._try_button_strategy(page, strategy_type, value)
if button and button.is_visible(timeout=1000):
logging.info(f" [SUBMIT] Gasit cu strategia: {strategy_type}='{value}'")
return button
except Exception:
continue
logging.error(" [SUBMIT] Nu am gasit butonul cu nicio strategie!")
return None
def _try_field_strategy(self, page, strategy_type, value):
"""Helper pentru a incerca o strategie de gasire a unui camp"""
if strategy_type == "placeholder_exact":
return page.get_by_placeholder(value, exact=True)
elif strategy_type == "placeholder_contains":
return page.locator(f"input[placeholder*='{value}' i]").first
elif strategy_type == "id":
return page.locator(f"#{value}")
elif strategy_type == "id_contains":
return page.locator(f"input[id*='{value}' i]").first
elif strategy_type == "name":
return page.locator(f"input[name='{value}']")
elif strategy_type == "name_contains":
return page.locator(f"input[name*='{value}' i]").first
elif strategy_type == "css":
return page.locator(value).first
elif strategy_type == "label":
return page.get_by_label(value)
elif strategy_type == "first_text_input":
# Gaseste primul input care nu e password, hidden sau submit
inputs = page.locator("input:visible").all()
for inp in inputs:
try:
inp_type = inp.get_attribute("type", timeout=500) or "text"
if inp_type.lower() not in ["password", "hidden", "submit", "button", "checkbox", "radio"]:
return inp
except:
continue
return None
def _try_button_strategy(self, page, strategy_type, value):
"""Helper pentru a incerca o strategie de gasire a butonului"""
if strategy_type == "css":
return page.locator(value).first
elif strategy_type == "text_contains":
return page.locator(f"button:has-text('{value}'), input[value*='{value}' i]").first
elif strategy_type == "role":
return page.get_by_role("button", name=value)
return None
def run(self): def run(self):
"""Entry point principal - orchestreaza tot flow-ul""" """Entry point principal - orchestreaza tot flow-ul"""
try: try:
# Check dacă rulăm în mod balances_only
balances_only = os.getenv('BALANCES_ONLY', 'false').lower() == 'true'
logging.info("=" * 60) logging.info("=" * 60)
if balances_only:
logging.info("Start BTGO Scraper (DOAR SOLDURI)")
else:
logging.info("Start BTGO Scraper") logging.info("Start BTGO Scraper")
logging.info("=" * 60) logging.info("=" * 60)
@@ -115,9 +402,11 @@ class BTGoScraper:
accounts = self.read_accounts() accounts = self.read_accounts()
csv_path, json_path = self.save_results(accounts) csv_path, json_path = self.save_results(accounts)
# Descarcă tranzacții pentru toate conturile (optional) # Descarcă tranzacții pentru toate conturile (doar dacă nu e balances_only)
downloaded_files = [] downloaded_files = []
if self.config.DOWNLOAD_TRANSACTIONS: if balances_only:
logging.info("Mod DOAR SOLDURI - skip download tranzactii")
elif self.config.DOWNLOAD_TRANSACTIONS:
downloaded_files = self.download_transactions(accounts) downloaded_files = self.download_transactions(accounts)
else: else:
logging.info("Download tranzacții dezactivat (DOWNLOAD_TRANSACTIONS=false)") logging.info("Download tranzacții dezactivat (DOWNLOAD_TRANSACTIONS=false)")
@@ -161,14 +450,20 @@ class BTGoScraper:
logging.info("Pagina incarcata") logging.info("Pagina incarcata")
try: try:
# Cookie consent - asteapta si accepta # Cookie consent - asteapta si accepta (GDPR wrapper)
logging.info("Acceptare cookies...") logging.info("Verificare GDPR cookie banner...")
self._dismiss_gdpr_cookies(self.page)
# Verificare finala ca banner-ul nu mai blocheaza
try: try:
cookie_button = self.page.get_by_role("button", name="Sunt de acord", exact=True) gdpr_wrapper = self.page.locator(".gdprcookie-wrapper")
cookie_button.click(timeout=5000) if gdpr_wrapper.is_visible(timeout=1000):
logging.info("✓ Cookies acceptate") logging.warning(" [WARN] Banner GDPR inca vizibil, fortez inchiderea...")
# Incearca sa inchida prin JavaScript
self.page.evaluate("document.querySelector('.gdprcookie-wrapper')?.remove()")
time.sleep(0.5)
except: except:
logging.info("Nu a fost necesar acceptul cookies (posibil deja acceptat)") pass
# Click pe butonul LOGIN - deschide popup # Click pe butonul LOGIN - deschide popup
logging.info("Click pe butonul LOGIN...") logging.info("Click pe butonul LOGIN...")
@@ -180,23 +475,40 @@ class BTGoScraper:
self.login_page = popup_info.value self.login_page = popup_info.value
logging.info("✓ Popup login deschis") logging.info("✓ Popup login deschis")
# Completare username # Verifica GDPR cookies si pe popup
logging.info("Completare username...") self._dismiss_gdpr_cookies(self.login_page)
username_field = self.login_page.get_by_placeholder("ID de logare")
# Asteapta sa se incarce pagina de login
time.sleep(2)
# Screenshot debug pentru a vedea starea paginii
debug_path = Path(self.config.OUTPUT_DIR) / f"debug_login_popup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
self.login_page.screenshot(path=str(debug_path))
logging.info(f"Screenshot debug salvat: {debug_path}")
# Completare username - detectie inteligenta cu fallback
logging.info("Detectie camp username...")
username_field = self._find_username_field(self.login_page)
if not username_field:
raise Exception("Nu am gasit campul de username cu nicio strategie!")
username_field.fill(self.config.BTGO_USERNAME) username_field.fill(self.config.BTGO_USERNAME)
logging.info(" Username completat") logging.info("[OK] Username completat")
# Completare password # Completare password - detectie inteligenta cu fallback
logging.info("Completare password...") logging.info("Detectie camp password...")
password_field = self.login_page.get_by_placeholder("Parola") password_field = self._find_password_field(self.login_page)
if not password_field:
raise Exception("Nu am gasit campul de parola cu nicio strategie!")
password_field.fill(self.config.BTGO_PASSWORD) password_field.fill(self.config.BTGO_PASSWORD)
logging.info(" Password completat") logging.info("[OK] Password completat")
# Click pe butonul de submit # Click pe butonul de submit - detectie inteligenta cu fallback
logging.info("Click pe 'Mergi mai departe'...") logging.info("Detectie buton submit...")
submit_button = self.login_page.get_by_role("button", name="Mergi mai departe") submit_button = self._find_submit_button(self.login_page)
if not submit_button:
raise Exception("Nu am gasit butonul de submit cu nicio strategie!")
submit_button.click() submit_button.click()
logging.info(" Credentials trimise, astept 2FA...") logging.info("[OK] Credentials trimise, astept 2FA...")
self._update_progress("Astept aprobare 2FA pe telefon...") self._update_progress("Astept aprobare 2FA pe telefon...")
except PlaywrightTimeout as e: except PlaywrightTimeout as e:
@@ -234,10 +546,14 @@ class BTGoScraper:
if accounts_btn.is_visible(timeout=1000): if accounts_btn.is_visible(timeout=1000):
# Verifică că este și clickable (enabled) # Verifică că este și clickable (enabled)
if accounts_btn.is_enabled(): if accounts_btn.is_enabled():
logging.info(" Autentificare 2FA reusita! (Buton conturi activ)") logging.info("[OK] Autentificare 2FA reusita! (Buton conturi activ)")
time.sleep(2) # Asteapta ca pagina sa se stabilizeze complet time.sleep(2) # Asteapta ca pagina sa se stabilizeze complet
# Update page reference la login_page pentru restul operatiilor # Update page reference la login_page pentru restul operatiilor
self.page = self.login_page self.page = self.login_page
# Inchide dialoguri one-time (ex: "Am inteles") daca apar
self._dismiss_one_time_consent(self.page)
return True return True
except: except:
pass pass
@@ -253,30 +569,19 @@ class BTGoScraper:
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):
"""Descarca CSV-uri cu tranzactiile pentru fiecare cont""" """
Descarca CSV-uri cu tranzactiile pentru fiecare cont.
Flux nou (2024+):
1. Primul cont: expand card -> click tranzactii -> select CSV -> Genereaza -> download
2. Conturile urmatoare: #selectAccountBtn -> select cont by heading -> Genereaza -> download
"""
logging.info("=" * 60) logging.info("=" * 60)
logging.info("Descarcare tranzactii pentru toate conturile...") logging.info("Descarcare tranzactii pentru toate conturile...")
logging.info("=" * 60) logging.info("=" * 60)
downloaded_files = [] downloaded_files = []
# IMPORTANT: Collapse toate conturile mai intai
logging.info("Collapse toate conturile...")
all_expanded = self.page.locator(".mat-icon.rotate-90").all()
for expanded_icon in all_expanded:
try:
expanded_icon.click()
time.sleep(0.3)
except:
pass
time.sleep(1)
logging.info("✓ Toate conturile sunt collapse")
# Re-gaseste toate cardurile de conturi
all_cards = self.page.locator("fba-account-details-card").all()
logging.info(f"Gasit {len(all_cards)} carduri de conturi")
for idx, account in enumerate(accounts, 1): for idx, account in enumerate(accounts, 1):
try: try:
nume_cont = account['nume_cont'] nume_cont = account['nume_cont']
@@ -285,118 +590,19 @@ class BTGoScraper:
self._update_progress(f"Descarc tranzactii ({idx}/{len(accounts)})...") self._update_progress(f"Descarc tranzactii ({idx}/{len(accounts)})...")
logging.info(f"[{idx}/{len(accounts)}] Descarcare tranzactii pentru: {nume_cont}") logging.info(f"[{idx}/{len(accounts)}] Descarcare tranzactii pentru: {nume_cont}")
# Doar pentru PRIMUL cont trebuie expand + click Tranzacții
# Pentru restul, suntem deja pe pagina de tranzacții (din selectarea din modal)
if idx == 1: if idx == 1:
# Primul cont - expand și click Tranzacții # PRIMUL CONT: expand -> click tranzactii -> select CSV
if idx - 1 >= len(all_cards): downloaded = self._download_first_account(account)
logging.error(f" ✗ Nu exista card la pozitia {idx-1}")
continue
card = all_cards[idx - 1]
# Expand contul (click pe săgeată)
expand_button = card.locator(".collapse-account-btn").first
expand_button.click()
time.sleep(2) # Așteaptă expandare
logging.info(f" Contul expandat")
# Click pe butonul Tranzacții
try:
transactions_button = card.locator(".account-transactions-btn").first
transactions_button.click()
time.sleep(3) # Așteaptă încărcarea paginii cu tranzacții
logging.info(f" Click pe buton Tranzactii - pagina se incarca...")
except Exception as e:
logging.error(f" ✗ Nu am gasit butonul Tranzactii: {e}")
try:
expand_button.click()
time.sleep(0.5)
except:
pass
continue
else: else:
# Conturile 2-5: suntem deja pe pagina de tranzacții (din modal) # CONTURILE URMATOARE: selecteaza din dropdown -> Genereaza -> download
logging.info(f" Deja pe pagina tranzactii (selectat din modal)") downloaded = self._download_subsequent_account(account)
time.sleep(2) # Așteaptă stabilizare pagină
# Așteaptă să apară butonul CSV (indica că pagina s-a încărcat) if downloaded:
try: downloaded_files.append(downloaded)
self.page.wait_for_selector('button:has-text("CSV")', timeout=5000)
logging.info(f" Buton CSV detectat")
except:
logging.warning(f" Timeout asteptand butonul CSV")
# Click pe butonul CSV și așteaptă download
try:
with self.page.expect_download(timeout=15000) as download_info:
csv_button = self.page.get_by_role("button", name="CSV")
csv_button.click()
logging.info(f" Click pe butonul CSV - astept download...")
download = download_info.value
# Salvează fișierul cu un nume descriptiv
timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
nume_safe = nume_cont.replace(' ', '_').replace('/', '_')
filename = f"tranzactii_{nume_safe}_{timestamp}.csv"
save_path = Path(self.config.OUTPUT_DIR) / filename
download.save_as(save_path)
logging.info(f" ✓ Salvat: {save_path}")
downloaded_files.append({
'cont': nume_cont,
'iban': iban,
'fisier': str(save_path)
})
except Exception as e: except Exception as e:
logging.error(f" Eroare la descarcarea CSV: {e}") logging.error(f" [EROARE] Eroare la descarcarea tranzactiilor pentru {nume_cont}: {e}")
# Incearca sa revii la o stare stabila
# Navighează înapoi la lista de conturi
try:
# Click pe butonul back/close (săgeată stânga sau X)
back_button = self.page.locator('button[aria-label="Back"], .back-button, #selectAccountBtn').first
back_button.click()
time.sleep(1.5)
logging.info(f" Navigat inapoi - verific modal...")
except Exception as e:
logging.warning(f" Nu am putut naviga inapoi: {e}")
time.sleep(1)
# Verifică dacă a apărut modal de selectare cont
try:
modal_visible = self.page.locator('.modal-content').is_visible(timeout=2000)
if modal_visible and idx < len(accounts):
logging.info(f" Modal detectat - selectez contul urmator...")
# Calculează ID-ul contului următor
next_account = accounts[idx] # idx este 0-indexed pentru next
next_iban = next_account['iban']
next_iban_digits = ''.join(filter(str.isdigit, next_iban))[-10:]
next_account_id = f"accountC14RONCRT{next_iban_digits}"
# Click pe contul următor din modal
modal_account = self.page.locator(f'#{next_account_id}').first
modal_account.click()
time.sleep(2)
logging.info(f" ✓ Selectat cont din modal: {next_account['nume_cont']}")
else:
# Nu e modal - e ultima iteratie sau nu a aparut modal
logging.info(f" Nu e modal - continuam normal")
except Exception as e:
logging.warning(f" Eroare verificare modal: {e}")
# Re-găsește cardurile (pentru flow normal fără modal)
try:
all_cards = self.page.locator("fba-account-details-card").all()
except:
pass
except Exception as e:
logging.error(f" ✗ Eroare la descarcarea tranzactiilor pentru {nume_cont}: {e}")
# Încearcă să navighezi înapoi
try: try:
self.page.keyboard.press("Escape") self.page.keyboard.press("Escape")
time.sleep(1) time.sleep(1)
@@ -405,11 +611,279 @@ class BTGoScraper:
continue continue
logging.info("=" * 60) logging.info("=" * 60)
logging.info(f" Descarcate {len(downloaded_files)}/{len(accounts)} fisiere CSV cu tranzactii") logging.info(f"[OK] Descarcate {len(downloaded_files)}/{len(accounts)} fisiere CSV cu tranzactii")
logging.info("=" * 60) logging.info("=" * 60)
return downloaded_files return downloaded_files
def _download_first_account(self, account):
"""
Descarca tranzactii pentru primul cont.
Flow: expand card (daca nu e deja) -> click buton tranzactii -> select CSV -> Genereaza -> download
"""
nume_cont = account['nume_cont']
iban = account['iban']
try:
# Gaseste primul card
first_card = self.page.locator("fba-account-details-card").first
time.sleep(1)
# Verifica daca butonul de tranzactii e DEJA vizibil (cont expandat)
transactions_btn = first_card.locator("fba-account-buttons svg, fba-account-buttons, .account-transactions-btn").first
is_already_expanded = False
try:
is_already_expanded = transactions_btn.is_visible(timeout=2000)
except:
pass
if is_already_expanded:
logging.info(" Contul deja expandat - skip expand click")
else:
# Click pe expand icon (sageata din card)
logging.info(" Contul collapsed - expandez...")
expand_icon = first_card.locator(".mx-auto .mat-icon svg, .collapse-account-btn").first
expand_icon.click()
time.sleep(2)
logging.info(" Contul expandat")
# Click pe butonul de tranzactii (NU pe delete/inchide cont!)
# Butonul corect are: clasa .account-transactions-btn, SVG cu documentChartList, <p> cu "Tranzactii"
transactions_btn = None
# Strategie 1 (PRINCIPALA): container cu clasa account-transactions-btn
try:
btn = first_card.locator(".account-transactions-btn").first
if btn.is_visible(timeout=2000):
transactions_btn = btn
logging.info(" Buton tranzactii gasit prin .account-transactions-btn")
except:
pass
# Strategie 2: SVG cu data-mat-icon-name="documentChartList"
if not transactions_btn:
try:
btn = first_card.locator("mat-icon[data-mat-icon-name='documentChartList']").first
if btn.is_visible(timeout=1000):
transactions_btn = btn
logging.info(" Buton tranzactii gasit prin documentChartList icon")
except:
pass
# Strategie 3: element care contine <p> cu text "Tranzactii"
if not transactions_btn:
try:
btn = first_card.locator("div:has(p:text('Tranzacții')), div:has(p:text('Tranzactii'))").first
if btn.is_visible(timeout=1000):
transactions_btn = btn
logging.info(" Buton tranzactii gasit prin <p> text")
except:
pass
# Strategie 4: click direct pe textul "Tranzactii" din card
if not transactions_btn:
try:
btn = first_card.get_by_text("Tranzacții", exact=True)
if btn.is_visible(timeout=1000):
transactions_btn = btn
logging.info(" Buton tranzactii gasit prin get_by_text")
except:
pass
if not transactions_btn:
raise Exception("Nu am gasit butonul de tranzactii!")
transactions_btn.click()
time.sleep(3)
logging.info(" Pagina tranzactii se incarca...")
# Selecteaza format CSV (click pe text "CSV")
csv_option = self.page.get_by_text("CSV", exact=True)
csv_option.click()
time.sleep(1)
logging.info(" Format CSV selectat")
# Click pe butonul Genereaza
generate_btn = self.page.get_by_role("button", name="Generează")
generate_btn.click()
time.sleep(2)
logging.info(" Click Genereaza - astept generare...")
# Asteapta si descarca fisierul
return self._wait_and_download(nume_cont, iban)
except Exception as e:
logging.error(f" [EROARE] Download primul cont: {e}")
return None
def _download_subsequent_account(self, account):
"""
Descarca tranzactii pentru conturile 2+.
Flow: click #selectAccountBtn -> select cont by heading -> Genereaza -> download
"""
nume_cont = account['nume_cont']
iban = account['iban']
try:
# Click pe butonul de selectare cont (#selectAccountBtn)
select_btn = self.page.locator("#selectAccountBtn svg, #selectAccountBtn").first
select_btn.click()
time.sleep(2)
logging.info(" Dropdown conturi deschis")
# Debug: listeaza toate heading-urile vizibile din dropdown
try:
headings = self.page.locator("fba-account-details h4, .account-name, h4").all()
visible_names = []
for h in headings[:10]: # Max 10
try:
if h.is_visible(timeout=500):
visible_names.append(h.inner_text().strip())
except:
pass
if visible_names:
logging.info(f" Conturi in dropdown: {visible_names}")
except:
pass
# Selecteaza contul dupa nume - strategii multiple
account_selected = False
# Strategie 1: heading cu numele exact
try:
heading = self.page.get_by_role("heading", name=nume_cont, exact=True)
if heading.is_visible(timeout=2000):
heading.click()
account_selected = True
logging.info(f" Cont selectat prin heading exact: {nume_cont}")
except Exception as e:
logging.debug(f" Heading exact failed: {e}")
# Strategie 2: heading cu numele partial (fara exact match)
if not account_selected:
try:
heading = self.page.get_by_role("heading", name=nume_cont)
if heading.is_visible(timeout=2000):
heading.click()
account_selected = True
logging.info(f" Cont selectat prin heading partial: {nume_cont}")
except Exception as e:
logging.debug(f" Heading partial failed: {e}")
# Strategie 3: fba-account-details cu has_text
if not account_selected:
try:
account_item = self.page.locator("fba-account-details").filter(has_text=nume_cont).first
if account_item.is_visible(timeout=2000):
account_item.click()
account_selected = True
logging.info(f" Cont selectat prin fba-account-details: {nume_cont}")
except Exception as e:
logging.debug(f" fba-account-details failed: {e}")
# Strategie 4: locator h4 care contine textul
if not account_selected:
try:
h4_elem = self.page.locator(f"h4:has-text('{nume_cont}')").first
if h4_elem.is_visible(timeout=2000):
h4_elem.click()
account_selected = True
logging.info(f" Cont selectat prin h4:has-text: {nume_cont}")
except Exception as e:
logging.debug(f" h4:has-text failed: {e}")
# Strategie 5: orice element cu textul contului
if not account_selected:
try:
text_elem = self.page.get_by_text(nume_cont, exact=True)
if text_elem.is_visible(timeout=2000):
text_elem.click()
account_selected = True
logging.info(f" Cont selectat prin text exact: {nume_cont}")
except Exception as e:
logging.debug(f" text exact failed: {e}")
# Strategie 6: text partial match
if not account_selected:
try:
text_elem = self.page.get_by_text(nume_cont)
if text_elem.is_visible(timeout=2000):
text_elem.click()
account_selected = True
logging.info(f" Cont selectat prin text partial: {nume_cont}")
except Exception as e:
logging.debug(f" text partial failed: {e}")
if not account_selected:
logging.error(f" [EROARE] Nu am putut selecta contul: {nume_cont}")
# Screenshot pentru debug
try:
debug_path = Path(self.config.OUTPUT_DIR) / f"debug_dropdown_{nume_cont.replace(' ', '_')}_{datetime.now().strftime('%H%M%S')}.png"
self.page.screenshot(path=str(debug_path))
logging.error(f" Screenshot debug salvat: {debug_path}")
except:
pass
return None
time.sleep(2)
# Click pe butonul Genereaza (CSV deja selectat de la primul cont)
generate_btn = self.page.get_by_role("button", name="Generează")
generate_btn.click()
time.sleep(2)
logging.info(" Click Genereaza - astept generare...")
# Asteapta si descarca fisierul
return self._wait_and_download(nume_cont, iban)
except Exception as e:
logging.error(f" [EROARE] Download cont ulterior {nume_cont}: {e}")
return None
def _wait_and_download(self, nume_cont, iban, timeout=20000):
"""
Asteapta generarea fisierului si il descarca.
Args:
nume_cont: Numele contului (pentru filename)
iban: IBAN-ul contului
timeout: Timeout pentru download (ms)
Returns:
Dict cu informatii despre fisierul descarcat sau None
"""
try:
# Asteapta sa apara fba-document-item (indica ca fisierul e gata)
self.page.wait_for_selector("fba-document-item", timeout=timeout)
logging.info(" Document generat - descarc...")
# Click pe document item pentru a descarca
with self.page.expect_download(timeout=timeout) as download_info:
download_btn = self.page.locator("fba-document-item svg, fba-document-item path").first
download_btn.click()
download = download_info.value
# Salveaza fisierul cu nume descriptiv
timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
nume_safe = nume_cont.replace(' ', '_').replace('/', '_').replace('\\', '_')
filename = f"tranzactii_{nume_safe}_{timestamp}.csv"
save_path = Path(self.config.OUTPUT_DIR) / filename
download.save_as(save_path)
logging.info(f" [OK] Salvat: {save_path}")
return {
'cont': nume_cont,
'iban': iban,
'fisier': str(save_path)
}
except Exception as e:
logging.error(f" [EROARE] Download fisier: {e}")
return None
def read_accounts(self): def read_accounts(self):
"""Extrage soldurile tuturor conturilor""" """Extrage soldurile tuturor conturilor"""
logging.info("Citire conturi si solduri...") logging.info("Citire conturi si solduri...")

View File

@@ -58,6 +58,9 @@ function Show-MainMenu {
Write-Host " [R] Run Scraper (Manual)" -ForegroundColor Cyan Write-Host " [R] Run Scraper (Manual)" -ForegroundColor Cyan
Write-Host " [T] Run Telegram Bot (Manual)" -ForegroundColor Cyan Write-Host " [T] Run Telegram Bot (Manual)" -ForegroundColor Cyan
Write-Host "" Write-Host ""
Write-Host " Maintenance:" -ForegroundColor DarkCyan
Write-Host " [G] Git Pull & Restart Service" -ForegroundColor Cyan
Write-Host ""
Write-Host " [A] Open Deployment README" -ForegroundColor Gray Write-Host " [A] Open Deployment README" -ForegroundColor Gray
Write-Host " [B] Open Quick Start Guide" -ForegroundColor Gray Write-Host " [B] Open Quick Start Guide" -ForegroundColor Gray
Write-Host " [C] Open Project in Explorer" -ForegroundColor Gray Write-Host " [C] Open Project in Explorer" -ForegroundColor Gray
@@ -270,6 +273,73 @@ function Invoke-UpdateBrowsers {
Read-Host "`nApasa Enter pentru a reveni la meniu" Read-Host "`nApasa Enter pentru a reveni la meniu"
} }
function Invoke-GitPullRestart {
Clear-Host
Write-Host ""
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "GIT PULL & RESTART SERVICE" -ForegroundColor Yellow
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host ""
# Git pull
Write-Host "[INFO] Actualizare proiect din Git..." -ForegroundColor Cyan
Write-Host ""
Push-Location $ProjectDir
try {
git pull
$gitExitCode = $LASTEXITCODE
Write-Host ""
if ($gitExitCode -eq 0) {
Write-Host "[SUCCES] Proiect actualizat!" -ForegroundColor Green
Write-Host ""
# Restart service if installed (inline, fara script extern)
$service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($service) {
Write-Host "[INFO] Oprire serviciu..." -ForegroundColor Cyan
try {
Stop-Service -Name $ServiceName -Force
Write-Host "[OK] Serviciu oprit" -ForegroundColor Green
Start-Sleep -Seconds 2
} catch {
Write-Host "[AVERTIZARE] Oprirea a esuat, incercam oprire fortata..." -ForegroundColor Yellow
sc.exe stop $ServiceName | Out-Null
Start-Sleep -Seconds 2
}
Write-Host "[INFO] Pornire serviciu..." -ForegroundColor Cyan
try {
Start-Service -Name $ServiceName
Start-Sleep -Seconds 2
$service = Get-Service -Name $ServiceName
if ($service.Status -eq "Running") {
Write-Host "[SUCCES] Serviciu restartat cu succes!" -ForegroundColor Green
} else {
Write-Host "[EROARE] Serviciul nu ruleaza dupa restart!" -ForegroundColor Red
}
} catch {
Write-Host "[EROARE] Pornirea serviciului a esuat!" -ForegroundColor Red
Write-Host "Verificati logurile pentru detalii" -ForegroundColor Yellow
}
} else {
Write-Host "[INFO] Serviciul nu este instalat, restart nu este necesar" -ForegroundColor Yellow
}
} else {
Write-Host "[EROARE] Git pull a esuat cu codul $gitExitCode" -ForegroundColor Red
}
} catch {
Write-Host "[EROARE] Eroare la git pull: $_" -ForegroundColor Red
} finally {
Pop-Location
}
Write-Host ""
Read-Host "Apasa Enter pentru a reveni la meniu"
}
# Main loop # Main loop
do { do {
Show-MainMenu Show-MainMenu
@@ -301,6 +371,8 @@ do {
"t" { Invoke-RunTelegramBotManual } "t" { Invoke-RunTelegramBotManual }
"U" { Invoke-UpdateBrowsers } "U" { Invoke-UpdateBrowsers }
"u" { Invoke-UpdateBrowsers } "u" { Invoke-UpdateBrowsers }
"G" { Invoke-GitPullRestart }
"g" { Invoke-GitPullRestart }
"0" { "0" {
Write-Host "" Write-Host ""
Write-Host "Goodbye!" -ForegroundColor Green Write-Host "Goodbye!" -ForegroundColor Green

View File

@@ -4,6 +4,7 @@ Helper script pentru obținerea Chat ID Telegram (pentru DM și grupuri)
""" """
import os import os
import sys
import requests import requests
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -13,11 +14,20 @@ load_dotenv()
BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN') BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
if not BOT_TOKEN: if not BOT_TOKEN:
print("❌ TELEGRAM_BOT_TOKEN nu este setat în .env!") print("=" * 60)
print("EROARE: TELEGRAM_BOT_TOKEN nu este setat în .env")
print("=" * 60)
print("\nAdaugă în .env:") print("\nAdaugă în .env:")
print("TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrs") print("TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrs")
print("=" * 60)
exit(1) exit(1)
def get_bot_info():
"""Preia informații despre bot"""
url = f"https://api.telegram.org/bot{BOT_TOKEN}/getMe"
response = requests.get(url)
return response.json()
def get_updates(): def get_updates():
"""Preia ultimele update-uri de la bot""" """Preia ultimele update-uri de la bot"""
url = f"https://api.telegram.org/bot{BOT_TOKEN}/getUpdates" url = f"https://api.telegram.org/bot{BOT_TOKEN}/getUpdates"
@@ -28,96 +38,102 @@ def main():
print("=" * 60) print("=" * 60)
print(" Telegram Chat ID Helper") print(" Telegram Chat ID Helper")
print("=" * 60) print("=" * 60)
print()
print("📱 Instrucțiuni:")
print("1. Trimite /start către bot (DM)")
print(" SAU")
print("2. Trimite /start în grupul unde ai bot-ul")
print()
print("Apasă Enter după ce ai trimis mesajul...")
input()
print("\n🔍 Căutare mesaje...") # Verifică info bot
bot_data = get_bot_info()
if bot_data.get('ok'):
bot = bot_data['result']
print(f"\nBot: @{bot.get('username')} ({bot.get('first_name')})")
print(f"Bot ID: {bot.get('id')}")
print(f"Status: ACTIV")
else:
print(f"\nERORE: Token invalid - {bot_data}")
exit(1)
print("\n" + "=" * 60)
print("Căutare mesaje...")
print("=" * 60)
data = get_updates() data = get_updates()
if not data.get('ok'): if not data.get('ok'):
print(f"❌ Eroare API: {data}") print(f"\nERORE API: {data}")
return return
results = data.get('result', []) results = data.get('result', [])
if not results: if not results:
print("❌ Niciun mesaj găsit!") print("\nNU S-AU GĂSIT MESAJE!")
print("\nAsigură-te că:") print("\n" + "=" * 60)
print("• Ai trimis /start către bot SAU în grup") print("INSTRUCȚIUNI:")
print("• Bot-ul este adăugat în grup (dacă folosești grup)") print("=" * 60)
print("• Token-ul este corect") print("1. Deschide Telegram")
print(f"2. Caută @{bot.get('username')} SAU deschide grupul cu bot-ul")
print("3. Trimite un mesaj (ex: /start sau /info)")
print("4. Rulează din nou acest script")
print("=" * 60)
return return
print(f"\nGăsit {len(results)} mesaje!\n") print(f"\nGăsit {len(results)} mesaje în total")
# Procesează ultimele mesaje (ultimele 20)
seen_chats = {}
user_ids = set()
print("\n" + "=" * 60)
print("CHAT IDs DETECTATE:")
print("=" * 60) print("=" * 60)
# Procesează ultimele mesaje for update in results[-20:]: # Ultimele 20 mesaje
seen_chats = {}
for update in results:
if 'message' in update: if 'message' in update:
msg = update['message'] msg = update['message']
chat = msg['chat'] chat = msg['chat']
chat_id = chat['id'] chat_id = chat['id']
chat_type = chat['type'] chat_type = chat['type']
user = msg['from']
user_ids.add(user['id'])
text = msg.get('text', '(no text)')
# Evită duplicate # Evită duplicate
if chat_id in seen_chats: if chat_id in seen_chats:
continue continue
seen_chats[chat_id] = True seen_chats[chat_id] = chat
# Detalii chat # Detalii chat
if chat_type == 'private': if chat_type == 'private':
# DM # DM
user = msg['from'] print(f"\n[DM] {user.get('first_name', '')} {user.get('last_name', '')}")
print(f"📱 DM cu {user.get('first_name', 'Unknown')}")
print(f" User ID: {user['id']}")
print(f" Username: @{user.get('username', 'N/A')}") print(f" Username: @{user.get('username', 'N/A')}")
print(f" User ID: {user['id']}")
print(f" Chat ID: {chat_id}") print(f" Chat ID: {chat_id}")
elif chat_type in ['group', 'supergroup']: elif chat_type in ['group', 'supergroup']:
# Grup # Grup
print(f"👥 Grup: {chat.get('title', 'Unknown')}") print(f"\n[GRUP] {chat.get('title', 'Unknown')}")
print(f" Chat ID: {chat_id} ⚠️ NEGATIV pentru grupuri!") print(f" Chat ID: {chat_id}")
print(f" Tip: {chat_type}") print(f" Tip: {chat_type}")
# User care a trimis mesajul print(f" User: @{user.get('username', 'Unknown')} (ID: {user['id']})")
user = msg['from']
print(f" Mesaj de la: @{user.get('username', 'Unknown')} (ID: {user['id']})")
print() print(f" Mesaj: \"{text[:60]}\"")
print("\n" + "=" * 60)
print("CONFIGURARE .env:")
print("=" * 60) print("=" * 60)
print("\n💡 Pentru configurare .env:")
print()
# Recomandări # Recomandări
for chat_id, chat_data in seen_chats.items(): for chat_id, chat in seen_chats.items():
if chat_id < 0: # Grup if chat_id < 0: # Grup
print(f"# Pentru grup (notificări + comenzi):") print(f"\n# Grup: {chat.get('title', 'Unknown')}")
print(f"TELEGRAM_CHAT_ID={chat_id}") print(f"TELEGRAM_CHAT_ID={chat_id}")
else: # DM else: # DM
print(f"# Pentru DM:") print(f"\n# DM")
print(f"TELEGRAM_CHAT_ID={chat_id}") print(f"TELEGRAM_CHAT_ID={chat_id}")
print() if user_ids:
print("# User IDs autorizați (pot rula /scrape):") user_ids_str = ",".join(str(uid) for uid in sorted(user_ids))
print("TELEGRAM_ALLOWED_USER_IDS=", end="") print(f"\n# User IDs autorizați")
print(f"TELEGRAM_ALLOWED_USER_IDS={user_ids_str}")
# Colectează user IDs unice print("\n" + "=" * 60)
user_ids = set()
for update in results:
if 'message' in update:
user_id = update['message']['from']['id']
user_ids.add(user_id)
print(",".join(str(uid) for uid in sorted(user_ids)))
print()
print("=" * 60)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -6,6 +6,7 @@ Handles email and Discord notifications with file attachments
import smtplib import smtplib
import logging import logging
import zipfile import zipfile
import os
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
@@ -44,6 +45,13 @@ class EmailNotifier:
logging.error("Email configuration incomplete") logging.error("Email configuration incomplete")
return False return False
# Check if SEND_AS_ZIP flag is set (from Telegram bot /scrape_zip command)
send_as_zip = os.getenv('SEND_AS_ZIP', 'false').lower() == 'true'
if send_as_zip:
logging.info("SEND_AS_ZIP flag detected - sending email with ZIP archive")
return self._send_with_zip(files, accounts)
# Create message # Create message
msg = MIMEMultipart() msg = MIMEMultipart()
msg['From'] = self.config.EMAIL_FROM msg['From'] = self.config.EMAIL_FROM
@@ -137,6 +145,64 @@ class EmailNotifier:
logging.error(f"Failed to send email with ZIP: {e}") logging.error(f"Failed to send email with ZIP: {e}")
return False return False
def send_existing_zip(self, zip_path: Path, accounts: list) -> bool:
"""
Send email with existing ZIP file
Args:
zip_path: Path to existing ZIP file
accounts: List of account data with balances
Returns:
True if successful, False otherwise
"""
if not self.enabled:
logging.info("Email notifications disabled")
return False
try:
# Validate config
if not all([self.config.SMTP_SERVER, self.config.EMAIL_FROM, self.config.EMAIL_TO]):
logging.error("Email configuration incomplete")
return False
if not zip_path.exists():
logging.error(f"ZIP file not found: {zip_path}")
return False
# Create message
msg = MIMEMultipart()
msg['From'] = self.config.EMAIL_FROM
msg['To'] = self.config.EMAIL_TO
msg['Subject'] = f'BTGO Export (ZIP) - {datetime.now().strftime("%Y-%m-%d %H:%M")}'
# Email body
body = self._create_email_body([str(zip_path)], accounts, is_zip=True)
msg.attach(MIMEText(body, 'html'))
# Attach ZIP file
with open(zip_path, 'rb') as f:
part = MIMEBase('application', 'zip')
part.set_payload(f.read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', f'attachment; filename={zip_path.name}')
msg.attach(part)
# Send email
logging.info(f"Sending email with existing ZIP to {self.config.EMAIL_TO}...")
with smtplib.SMTP(self.config.SMTP_SERVER, self.config.SMTP_PORT) as server:
server.starttls()
if self.config.SMTP_USERNAME and self.config.SMTP_PASSWORD:
server.login(self.config.SMTP_USERNAME, self.config.SMTP_PASSWORD)
server.send_message(msg)
logging.info(f"✓ Email with ZIP sent successfully to {self.config.EMAIL_TO}")
return True
except Exception as e:
logging.error(f"Failed to send email with existing ZIP: {e}")
return False
def _create_email_body(self, files: List[str], accounts: list, is_zip: bool = False) -> str: def _create_email_body(self, files: List[str], accounts: list, is_zip: bool = False) -> str:
"""Create HTML email body""" """Create HTML email body"""
file_list = '<br>'.join([f'{Path(f).name}' for f in files]) file_list = '<br>'.join([f'{Path(f).name}' for f in files])
@@ -220,6 +286,13 @@ class TelegramNotifier:
logging.info(f"Received telegram_message_id: {telegram_message_id}, telegram_chat_id: {telegram_chat_id}") logging.info(f"Received telegram_message_id: {telegram_message_id}, telegram_chat_id: {telegram_chat_id}")
logging.info(f"Stored progress_message_id: {self.progress_message_id}, progress_chat_id: {self.progress_chat_id}") logging.info(f"Stored progress_message_id: {self.progress_message_id}, progress_chat_id: {self.progress_chat_id}")
# Check if SEND_AS_ZIP flag is set (from Telegram bot /scrape_zip command)
send_as_zip = os.getenv('SEND_AS_ZIP', 'false').lower() == 'true'
if send_as_zip:
logging.info("SEND_AS_ZIP flag detected - sending as ZIP archive")
return self._send_with_zip(files, accounts)
# Check total file size # Check total file size
total_size = sum(Path(f).stat().st_size for f in files if Path(f).exists()) total_size = sum(Path(f).stat().st_size for f in files if Path(f).exists())

View File

@@ -5,15 +5,19 @@ Telegram Trigger Bot - Declanșează BTGO Scraper prin comandă Telegram
import os import os
import sys import sys
import io
import subprocess import subprocess
import logging import logging
import json import json
import csv
import zipfile import zipfile
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime, timedelta
import glob import glob
import requests import requests
from dotenv import load_dotenv from dotenv import load_dotenv
from config import Config
from notifications import EmailNotifier
# Load environment # Load environment
load_dotenv() load_dotenv()
@@ -25,6 +29,10 @@ CHAT_ID = os.getenv('TELEGRAM_CHAT_ID')
POLL_TIMEOUT = int(os.getenv('TELEGRAM_POLL_TIMEOUT', 60)) # Default 60 secunde POLL_TIMEOUT = int(os.getenv('TELEGRAM_POLL_TIMEOUT', 60)) # Default 60 secunde
# Logging - force stdout instead of stderr (for Windows service logging) # Logging - force stdout instead of stderr (for Windows service logging)
# Set UTF-8 encoding for stdout to support Romanian characters
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format='[%(asctime)s] [%(levelname)s] %(message)s', format='[%(asctime)s] [%(levelname)s] %(message)s',
@@ -38,14 +46,19 @@ class TelegramTriggerBot:
self.bot_token = BOT_TOKEN self.bot_token = BOT_TOKEN
self.allowed_users = [int(uid.strip()) for uid in ALLOWED_USER_IDS self.allowed_users = [int(uid.strip()) for uid in ALLOWED_USER_IDS
if uid.strip() and not uid.strip().startswith('#')] if uid.strip() and not uid.strip().startswith('#')]
self.allowed_group_id = CHAT_ID.strip() if CHAT_ID else None
self.base_url = f"https://api.telegram.org/bot{self.bot_token}" self.base_url = f"https://api.telegram.org/bot{self.bot_token}"
self.last_update_id = 0 self.last_update_id = 0
self.poll_timeout = POLL_TIMEOUT self.poll_timeout = POLL_TIMEOUT
# State management pentru selecție interactivă conturi
self.pending_account_selection = {} # {user_id: {'accounts': [...], 'timestamp': ...}}
if not self.bot_token: if not self.bot_token:
raise ValueError("TELEGRAM_BOT_TOKEN nu este setat în .env!") raise ValueError("TELEGRAM_BOT_TOKEN nu este setat în .env!")
logging.info(f"Bot inițializat. Useri autorizați: {self.allowed_users}") logging.info(f"Bot inițializat. Useri autorizați: {self.allowed_users}")
logging.info(f"Grup autorizat: {self.allowed_group_id}")
logging.info(f"Long polling timeout: {self.poll_timeout}s") logging.info(f"Long polling timeout: {self.poll_timeout}s")
# Înregistrare comenzi în meniul Telegram # Înregistrare comenzi în meniul Telegram
@@ -58,6 +71,9 @@ class TelegramTriggerBot:
commands = [ commands = [
{"command": "scrape", "description": "Rulează scraper-ul BTGO"}, {"command": "scrape", "description": "Rulează scraper-ul BTGO"},
{"command": "scrape_zip", "description": "Rulează scraper + trimite ZIP"}, {"command": "scrape_zip", "description": "Rulează scraper + trimite ZIP"},
{"command": "scrape_solduri", "description": "Extrage doar soldurile (fără CSV)"},
{"command": "solduri", "description": "Afișează ultimul fișier solduri"},
{"command": "tranzactii", "description": "Afișează tranzacții recente din cont"},
{"command": "zip", "description": "Trimite ultimele fișiere ca ZIP"}, {"command": "zip", "description": "Trimite ultimele fișiere ca ZIP"},
{"command": "status", "description": "Status sistem"}, {"command": "status", "description": "Status sistem"},
{"command": "help", "description": "Ajutor comenzi"} {"command": "help", "description": "Ajutor comenzi"}
@@ -95,17 +111,62 @@ class TelegramTriggerBot:
response = requests.post(url, data=data, files=files) response = requests.post(url, data=data, files=files)
return response.json() return response.json()
def is_user_allowed(self, user_id): def is_member_of_group(self, user_id, group_id):
"""Verifică dacă user-ul are permisiune""" """Verifică dacă user_id este membru al group_id prin Telegram API"""
if not self.allowed_users: # Dacă lista e goală, permite oricui try:
return True url = f"{self.base_url}/getChatMember"
return user_id in self.allowed_users params = {
'chat_id': group_id,
'user_id': user_id
}
response = requests.get(url, params=params, timeout=5)
def run_scraper(self, chat_id, reply_to_message_id=None, send_as_zip=False): if response.status_code == 200 and response.json().get('ok'):
result = response.json().get('result', {})
status = result.get('status', '')
# Statusuri valide: creator, administrator, member
if status in ['creator', 'administrator', 'member']:
logging.info(f"User {user_id} este membru al grupului {group_id} (status: {status})")
return True
else:
logging.info(f"User {user_id} NU este membru al grupului {group_id} (status: {status})")
return False
else:
logging.warning(f"Eroare verificare membership: {response.text}")
return False
except Exception as e:
logging.error(f"Excepție verificare membership pentru user {user_id}: {e}")
return False
def is_user_allowed(self, user_id):
"""Verifică dacă user-ul are permisiune (whitelist sau membru al grupului autorizat)"""
# 1. Verifică dacă e în whitelist explicit
if user_id in self.allowed_users:
logging.info(f"User {user_id} autorizat prin TELEGRAM_ALLOWED_USER_IDS")
return True
# 2. Verifică dacă e membru al grupului autorizat
if self.allowed_group_id:
if self.is_member_of_group(user_id, self.allowed_group_id):
logging.info(f"User {user_id} autorizat prin membership în grup {self.allowed_group_id}")
return True
# 3. Dacă ambele liste sunt goale, permite oricui (backwards compatible)
if not self.allowed_users and not self.allowed_group_id:
logging.warning("Nicio restricție configurată - bot DESCHIS pentru toți userii!")
return True
# 4. Altfel, respinge
logging.warning(f"User {user_id} RESPINS - nu e în whitelist și nu e membru al grupului")
return False
def run_scraper(self, chat_id, reply_to_message_id=None, send_as_zip=False, balances_only=False):
"""Execută scraper-ul""" """Execută scraper-ul"""
# Trimite mesaj inițial și salvează message_id pentru editare ulterioară # Trimite mesaj inițial și salvează message_id pentru editare ulterioară
zip_msg = " (arhiva ZIP)" if send_as_zip else "" zip_msg = " (arhiva ZIP)" if send_as_zip else ""
response = self.send_message(chat_id, f"*BTGO Scraper pornit{zip_msg}*\n\nAsteapta 2FA pe telefon.", reply_to_message_id) balances_msg = " - DOAR SOLDURI" if balances_only else ""
response = self.send_message(chat_id, f"*BTGO Scraper pornit{zip_msg}{balances_msg}*\n\nAsteapta 2FA pe telefon.", reply_to_message_id)
message_id = None message_id = None
try: try:
message_id = response.json()['result']['message_id'] message_id = response.json()['result']['message_id']
@@ -114,21 +175,66 @@ class TelegramTriggerBot:
logging.warning("Nu am putut salva message_id pentru progress updates") logging.warning("Nu am putut salva message_id pentru progress updates")
try: try:
# Șterge fișierele CSV, ZIP și PNG anterioare
data_dir = Path('data')
if data_dir.exists():
deleted_count = 0
# Șterge CSV-uri de solduri
for f in data_dir.glob('solduri_*.csv'):
f.unlink()
deleted_count += 1
logging.info(f"Șters: {f.name}")
# Șterge CSV-uri de tranzacții
for f in data_dir.glob('tranzactii_*.csv'):
f.unlink()
deleted_count += 1
logging.info(f"Șters: {f.name}")
# Șterge JSON-uri
for f in data_dir.glob('solduri_*.json'):
f.unlink()
deleted_count += 1
logging.info(f"Șters: {f.name}")
# Șterge ZIP-uri
for f in data_dir.glob('btgo_export_*.zip'):
f.unlink()
deleted_count += 1
logging.info(f"Șters: {f.name}")
# Șterge PNG-uri (screenshot-uri Playwright)
for f in data_dir.glob('*.png'):
f.unlink()
deleted_count += 1
logging.info(f"Șters: {f.name}")
if deleted_count > 0:
logging.info(f"Total {deleted_count} fisiere sterse inainte de scraping")
# Rulează scraper-ul # Rulează scraper-ul
logging.info(f"Pornire scraper (send_as_zip={send_as_zip})...") logging.info(f"Pornire scraper (send_as_zip={send_as_zip}, balances_only={balances_only})...")
# Prepare environment with global playwright path + Telegram progress info # Prepare environment with global playwright path + Telegram progress info
env = os.environ.copy() env = os.environ.copy()
env['PLAYWRIGHT_BROWSERS_PATH'] = 'C:\\playwright-browsers' env['PLAYWRIGHT_BROWSERS_PATH'] = 'C:\\playwright-browsers'
# Dacă send_as_zip, dezactivează notificările - bot-ul va trimite ZIP-ul manual # Setează progress updates pentru Telegram
if send_as_zip: if message_id:
env['ENABLE_NOTIFICATIONS'] = 'false'
logging.info("Notificări dezactivate - bot va trimite ZIP manual")
elif message_id:
env['TELEGRAM_CHAT_ID'] = str(chat_id) env['TELEGRAM_CHAT_ID'] = str(chat_id)
env['TELEGRAM_MESSAGE_ID'] = str(message_id) env['TELEGRAM_MESSAGE_ID'] = str(message_id)
logging.info(f"Setting environment: TELEGRAM_CHAT_ID={chat_id}, TELEGRAM_MESSAGE_ID={message_id}") logging.info(f"Setting environment: TELEGRAM_CHAT_ID={chat_id}, TELEGRAM_MESSAGE_ID={message_id}")
# Dacă send_as_zip, comunică să trimită ZIP în loc de fișiere individuale
if send_as_zip:
env['SEND_AS_ZIP'] = 'true'
logging.info("Mod ZIP activat - va trimite arhivă ZIP")
# Dacă balances_only, comunică să nu descarce tranzacții
if balances_only:
env['BALANCES_ONLY'] = 'true'
logging.info("Mod DOAR SOLDURI activat - nu va descărca tranzacții")
else: else:
logging.warning("No message_id available for progress updates") logging.warning("No message_id available for progress updates")
@@ -143,12 +249,7 @@ class TelegramTriggerBot:
if result.returncode == 0: if result.returncode == 0:
logging.info("Scraper finalizat cu succes") logging.info("Scraper finalizat cu succes")
# Mesajul final va fi editat de notifications.py (cu ZIP sau fișiere individuale)
# Dacă send_as_zip, trimite ZIP manual
if send_as_zip:
logging.info("Trimitere rezultate ca ZIP...")
self.send_zip_files(chat_id, reply_to_message_id)
# Altfel, mesajul final va fi editat de notifications.py
else: else:
# Eroare # Eroare
@@ -168,6 +269,374 @@ class TelegramTriggerBot:
logging.error(f"Eroare execuție: {e}") logging.error(f"Eroare execuție: {e}")
self.send_message(chat_id, f"*EROARE EXECUTIE*\n\n```\n{str(e)}\n```", reply_to_message_id) self.send_message(chat_id, f"*EROARE EXECUTIE*\n\n```\n{str(e)}\n```", reply_to_message_id)
def show_cached_balances(self, chat_id, reply_to_message_id=None):
"""Afișează soldurile din cel mai recent fișier solduri.csv"""
try:
data_dir = Path('data')
if not data_dir.exists():
self.send_message(chat_id, "*EROARE*\n\nDirectorul 'data' nu există!", reply_to_message_id)
return
# Găsește ultimul fișier solduri
solduri_files = sorted(data_dir.glob('solduri_*.csv'), key=lambda x: x.stat().st_mtime, reverse=True)
if not solduri_files:
self.send_message(chat_id, "*EROARE*\n\nNu s-au găsit fișiere solduri!", reply_to_message_id)
return
latest_solduri = solduri_files[0]
solduri_time = latest_solduri.stat().st_mtime
file_datetime = datetime.fromtimestamp(solduri_time).strftime('%Y-%m-%d %H:%M:%S')
# Citește fișierul CSV
accounts = []
with open(latest_solduri, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
accounts.append({
'nume_cont': row['nume_cont'],
'sold': float(row['sold']),
'moneda': row['moneda']
})
# Construiește mesaj cu solduri
total_ron = sum(acc['sold'] for acc in accounts if acc.get('moneda') == 'RON')
message = f"*SOLDURI BANCARE*\n\n"
message += f"Data: {file_datetime}\n"
message += f"Conturi: {len(accounts)}\n\n"
for acc in accounts:
nume = acc['nume_cont']
sold = acc['sold']
moneda = acc['moneda']
message += f"{nume}: {sold:,.2f} {moneda}\n"
message += f"\n*TOTAL: {total_ron:,.2f} RON*"
self.send_message(chat_id, message, reply_to_message_id)
logging.info(f"Afișat solduri cached din {latest_solduri.name}")
except Exception as e:
logging.error(f"Eroare show_cached_balances: {e}", exc_info=True)
self.send_message(chat_id, f"*EROARE*\n\n```\n{str(e)}\n```", reply_to_message_id)
def show_transactions_menu(self, chat_id, user_id, reply_to_message_id=None):
"""Afișează meniu cu conturi disponibile pentru selecție tranzacții"""
try:
data_dir = Path('data')
if not data_dir.exists():
self.send_message(chat_id, "*EROARE*\n\nDirectorul 'data' nu există!", reply_to_message_id)
return
# Găsește toate fișierele de tranzacții
transaction_files = sorted(
data_dir.glob('tranzactii_*.csv'),
key=lambda x: x.stat().st_mtime,
reverse=True
)
if not transaction_files:
self.send_message(
chat_id,
"*EROARE*\n\nNu s-au găsit fișiere cu tranzacții!\n\nRulează mai întâi /scrape pentru a descărca tranzacții.",
reply_to_message_id
)
return
# Extrage nume conturi unice din numele fișierelor
# Format: tranzactii_Nume_Cont_YYYY-MM-DD_HH-MM-SS.csv
accounts = {}
for file_path in transaction_files:
filename = file_path.stem # fără extensie
# Elimină prefixul "tranzactii_" și sufixul timestamp
# rsplit('_', 3) split-uiește: ['Nume', 'Cont', 'YYYY-MM-DD', 'HH-MM-SS']
parts = filename.replace('tranzactii_', '').rsplit('_', 3)
if len(parts) >= 4:
# parts[0] = nume de bază, parts[1] = număr cont
account_name = f"{parts[0]}_{parts[1]}" # "Nume_Cont"
# Convertește Nume_Cont → Nume Cont
display_name = account_name.replace('_', ' ')
# Păstrează doar cel mai recent fișier pentru fiecare cont
if display_name not in accounts:
accounts[display_name] = file_path
if not accounts:
self.send_message(
chat_id,
"*EROARE*\n\nNu s-au putut procesa fișierele de tranzacții!",
reply_to_message_id
)
return
# Sortează conturile alfabetic
sorted_accounts = sorted(accounts.items())
# Construiește mesaj
message = "*TRANZACTII BANCARE*\n\n"
message += "Conturi disponibile:\n\n"
for idx, (account_name, file_path) in enumerate(sorted_accounts, 1):
message += f"{idx}. {account_name}\n"
message += f"\n*Scrie numarul contului (1-{len(sorted_accounts)}):*\n"
message += " • Doar numar (ex: 2) = ultimele 10\n"
message += " • Numar + zile (ex: 2 7, 2 30)"
# Salvează starea pentru user
self.pending_account_selection[user_id] = {
'accounts': sorted_accounts,
'timestamp': datetime.now().timestamp()
}
self.send_message(chat_id, message, reply_to_message_id)
logging.info(f"Afișat meniu tranzacții pentru user {user_id}: {len(sorted_accounts)} conturi")
except Exception as e:
logging.error(f"Eroare show_transactions_menu: {e}", exc_info=True)
self.send_message(chat_id, f"*EROARE*\n\n```\n{str(e)}\n```", reply_to_message_id)
def show_account_transactions(self, chat_id, user_id, account_index, reply_to_message_id=None, period="10"):
"""Afișează tranzacții pentru contul selectat (perioada: 10/luna/sapt)"""
try:
# Verifică dacă userul are o selecție pending
if user_id not in self.pending_account_selection:
self.send_message(
chat_id,
"*EROARE*\n\nNu există selecție activă. Folosește /tranzactii pentru a începe.",
reply_to_message_id
)
return
pending_data = self.pending_account_selection[user_id]
accounts = pending_data['accounts']
# Verifică timeout (5 minute)
if datetime.now().timestamp() - pending_data['timestamp'] > 300:
del self.pending_account_selection[user_id]
self.send_message(
chat_id,
"*TIMEOUT*\n\nSelecția a expirat. Folosește /tranzactii pentru a începe din nou.",
reply_to_message_id
)
return
# Verifică index valid
if account_index < 1 or account_index > len(accounts):
self.send_message(
chat_id,
f"*EROARE*\n\nNumăr invalid! Scrie un număr între 1 și {len(accounts)}.",
reply_to_message_id
)
return
# Obține contul selectat
account_name, csv_path = accounts[account_index - 1]
# Verifică dacă fișierul există
if not csv_path.exists():
del self.pending_account_selection[user_id]
self.send_message(
chat_id,
f"*EROARE*\n\nFișierul pentru contul {account_name} nu mai există!",
reply_to_message_id
)
return
# Citește CSV-ul
# Format BT: Primele 17 linii = metadata, linia 18 = header, linia 19+ = date
transactions = []
account_iban = ""
csv_period = "" # Perioada din CSV (nu parametrul funcției!)
with open(csv_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Extrage metadate
if len(lines) > 8:
# Linia 9: Perioada:,29.10.2025-12.11.2025
period_line = lines[8].strip()
if 'Perioada:' in period_line:
csv_period = period_line.split(',')[1] if ',' in period_line else ""
# Linia 8: Numar cont:,RO32BTRLRONCRT0637236701 RON
iban_line = lines[7].strip()
if 'Numar cont:' in iban_line:
account_iban = iban_line.split(',')[1] if ',' in iban_line else ""
# Citește tranzacțiile (de la header - linia 18, index 17)
if len(lines) > 17:
csv_content = ''.join(lines[17:]) # Include header + date
reader = csv.DictReader(csv_content.splitlines())
for row in reader:
if row.get('Data tranzactie'): # Skip linii goale
transactions.append(row)
# Șterge selecția pending
del self.pending_account_selection[user_id]
if not transactions:
self.send_message(
chat_id,
f"*{account_name}*\n\nNu există tranzacții în acest fișier.",
reply_to_message_id
)
return
# Filtrează tranzacțiile în funcție de perioada selectată (nr zile)
from collections import defaultdict
recent_transactions = []
period_description = ""
# Convertește period în număr de zile
try:
num_days = int(period)
except (ValueError, TypeError):
num_days = 10 # Default
if num_days <= 0:
num_days = 10 # Sigură
# Filtrează pe bază de zile
if num_days == 10:
# Optimizare: ultimele 10 tranzacții direct
recent_transactions = transactions[:10]
period_description = "Ultimele 10 tranzactii"
else:
# Filtrare pe bază de dată
now = datetime.now()
cutoff_date = now - timedelta(days=num_days)
for tx in transactions:
tx_date_str = tx.get('Data tranzactie', '')
if tx_date_str:
try:
tx_date = datetime.strptime(tx_date_str, '%Y-%m-%d')
if tx_date >= cutoff_date:
recent_transactions.append(tx)
except ValueError:
continue
period_description = f"Ultimele {num_days} zile"
# Grupează tranzacțiile pe date
transactions_by_date = defaultdict(list)
for tx in recent_transactions:
date = tx.get('Data tranzactie', '')
if date:
transactions_by_date[date].append(tx)
# Construiește mesaj
message = f"*TRANZACTII - {account_name}*\n\n"
if period_description:
message += f"Perioada: {period_description}\n"
message += f"Total: {len(recent_transactions)} tranzactii\n"
message += "=" * 30 + "\n\n"
# Sortează datele descrescător (cele mai recente primul)
sorted_dates = sorted(transactions_by_date.keys(), reverse=True)
for date in sorted_dates:
# Header pentru dată
message += f"*{date}*\n"
# Tranzacțiile din acea zi
for tx in transactions_by_date[date]:
description = tx.get('Descriere', '')
debit = tx.get('Debit', '').strip().replace('"', '').replace(',', '')
credit = tx.get('Credit', '').strip().replace('"', '').replace(',', '')
# Extrage nume mai inteligent din descriere
display_name = description
desc_parts = description.split(';')
if len(desc_parts) > 2:
# Caz special: Plăți POS - extrage comerciantul din Parts[1]
if 'POS' in desc_parts[0] and len(desc_parts) > 1:
# Căutăm pattern: "TID:XXXXXXX <COMERCIANT> <REST>"
import re
tid_match = re.search(r'TID:\S+\s+(.+?)\s{2,}', desc_parts[1])
if tid_match:
candidate = tid_match.group(1).strip()
else:
# Fallback: încearcă să extragă după TID până la două spații
if 'TID:' in desc_parts[1]:
after_tid = desc_parts[1].split('TID:')[1]
# Skip ID-ul TID și ia textul până la două spații consecutive
parts_after = after_tid.split(None, 1) # Split la primul spațiu
if len(parts_after) > 1:
# Ia textul până la " " sau până la sfârșitul
merchant_text = parts_after[1]
double_space_idx = merchant_text.find(' ')
if double_space_idx > 0:
candidate = merchant_text[:double_space_idx].strip()
else:
candidate = merchant_text.strip()
else:
candidate = desc_parts[2].strip()
else:
candidate = desc_parts[2].strip()
else:
# Încearcă part[2] (de obicei numele)
candidate = desc_parts[2].strip()
# Dacă part[2] este doar număr/scurt/REF, încearcă part[3]
if (candidate.isdigit() or
len(candidate) < 3 or
candidate.startswith('REF:')):
if len(desc_parts) > 3:
candidate = desc_parts[3].strip()
# Dacă tot e invalid, folosește tipul tranzacției (part[0])
if len(candidate) < 3 or candidate.startswith('REF:'):
candidate = desc_parts[0].strip()
display_name = candidate
# Truncate dacă prea lung
if len(display_name) > 35:
display_name = display_name[:32] + "..."
# Escape caractere speciale Markdown pentru Telegram
# Caracterele care trebuie escapate: _ * [ ] ( ) ~ ` > # + - = | { } . !
markdown_chars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
for char in markdown_chars:
display_name = display_name.replace(char, '\\' + char)
# Determină suma (debit poate avea deja minus în CSV)
if credit:
amount_str = f"+{credit}"
elif debit:
# Elimină minus-ul dacă există deja
debit_clean = debit.lstrip('-')
amount_str = f"-{debit_clean}"
else:
amount_str = "0.00"
# Format compact: " • Nume: +suma RON"
message += f" {display_name}: {amount_str} RON\n"
message += "\n"
self.send_message(chat_id, message, reply_to_message_id)
logging.info(f"Afișat {len(recent_transactions)} tranzacții pentru {account_name}")
except Exception as e:
# Curăță selecția în caz de eroare
if user_id in self.pending_account_selection:
del self.pending_account_selection[user_id]
logging.error(f"Eroare show_account_transactions: {e}", exc_info=True)
self.send_message(chat_id, f"*EROARE*\n\n```\n{str(e)}\n```", reply_to_message_id)
def send_zip_files(self, chat_id, reply_to_message_id=None): def send_zip_files(self, chat_id, reply_to_message_id=None):
"""Trimite ultimele fișiere ca arhivă ZIP""" """Trimite ultimele fișiere ca arhivă ZIP"""
try: try:
@@ -236,7 +705,7 @@ class TelegramTriggerBot:
return return
# Construiește mesaj cu solduri # Construiește mesaj cu solduri
caption = f"📦 *BTGO Export (ZIP)*\n\n" caption = f"*BTGO Export (ZIP)*\n\n"
caption += f"Timp: {datetime.fromtimestamp(solduri_time).strftime('%Y-%m-%d %H:%M:%S')}\n" caption += f"Timp: {datetime.fromtimestamp(solduri_time).strftime('%Y-%m-%d %H:%M:%S')}\n"
caption += f"Dimensiune: {zip_size:.2f} MB\n" caption += f"Dimensiune: {zip_size:.2f} MB\n"
caption += f"Fișiere: {len(files_to_zip)}\n\n" caption += f"Fișiere: {len(files_to_zip)}\n\n"
@@ -254,7 +723,7 @@ class TelegramTriggerBot:
caption += f"Conturi: {len(transaction_files)}" caption += f"Conturi: {len(transaction_files)}"
# Trimite ZIP-ul # Trimite ZIP-ul
self.send_message(chat_id, "📦 *Creare arhivă ZIP...*", reply_to_message_id) self.send_message(chat_id, "*Creare arhivă ZIP...*", reply_to_message_id)
url = f"{self.base_url}/sendDocument" url = f"{self.base_url}/sendDocument"
with open(zip_path, 'rb') as f: with open(zip_path, 'rb') as f:
@@ -275,6 +744,21 @@ class TelegramTriggerBot:
logging.error(f"Eroare trimitere ZIP: {response.text}") logging.error(f"Eroare trimitere ZIP: {response.text}")
self.send_message(chat_id, f"*EROARE*\n\nNu s-a putut trimite arhiva.", reply_to_message_id) self.send_message(chat_id, f"*EROARE*\n\nNu s-a putut trimite arhiva.", reply_to_message_id)
# Trimite și pe email dacă este configurat
try:
config = Config()
if config.EMAIL_ENABLED:
email_notifier = EmailNotifier(config)
logging.info("Trimitere ZIP pe email...")
if email_notifier.send_existing_zip(zip_path, accounts_data):
logging.info("✓ ZIP trimis cu succes pe email")
else:
logging.warning("Nu s-a putut trimite ZIP-ul pe email")
else:
logging.info("Email notifications disabled - skipping email")
except Exception as e:
logging.error(f"Eroare trimitere ZIP pe email: {e}")
# Șterge fișierul ZIP temporar # Șterge fișierul ZIP temporar
zip_path.unlink() zip_path.unlink()
@@ -306,6 +790,25 @@ class TelegramTriggerBot:
self.send_message(chat_id, "*ACCES INTERZIS*\n\nNu ai permisiunea sa folosesti acest bot.", message_id) self.send_message(chat_id, "*ACCES INTERZIS*\n\nNu ai permisiunea sa folosesti acest bot.", message_id)
return return
# Procesează răspuns numeric pentru selecție cont tranzacții
if user_id in self.pending_account_selection:
# Parse număr cont + opțional nr zile
# Formate acceptate: "2" (default 10), "2 7" (7 zile), "2 30" (30 zile)
parts = text.strip().split()
if len(parts) >= 1 and parts[0].isdigit():
account_index = int(parts[0])
# Determină numărul de zile
period = "10" # default: ultimele 10 tranzacții
if len(parts) >= 2 and parts[1].isdigit():
period = parts[1] # număr de zile
logging.info(f"Răspuns selecție cont: {account_index}, perioada: {period} zile de la user {user_id}")
self.show_account_transactions(chat_id, user_id, account_index, message_id, period)
return
# Procesează comenzi # Procesează comenzi
if text == '/start': if text == '/start':
welcome_msg = "*BTGO Scraper Trigger Bot*\n\n" welcome_msg = "*BTGO Scraper Trigger Bot*\n\n"
@@ -313,8 +816,11 @@ class TelegramTriggerBot:
welcome_msg += f"Bot activ in grupul *{chat_title}*\n\n" welcome_msg += f"Bot activ in grupul *{chat_title}*\n\n"
welcome_msg += ( welcome_msg += (
"Comenzi disponibile:\n" "Comenzi disponibile:\n"
"`/scrape` - Ruleaza scraper-ul\n" "`/scrape` - Ruleaza scraper-ul complet\n"
"`/scrape_zip` - Ruleaza scraper + trimite ZIP\n" "`/scrape_zip` - Ruleaza scraper + trimite ZIP\n"
"`/scrape_solduri` - Extrage doar soldurile (rapid)\n"
"`/solduri` - Afiseaza ultimul fisier solduri\n"
"`/tranzactii` - Afiseaza tranzactii recente\n"
"`/zip` - Trimite ultimele fisiere ca ZIP\n" "`/zip` - Trimite ultimele fisiere ca ZIP\n"
"`/status` - Status sistem\n" "`/status` - Status sistem\n"
"`/help` - Ajutor" "`/help` - Ajutor"
@@ -329,6 +835,18 @@ class TelegramTriggerBot:
logging.info(f"Comandă /scrape_zip primită în {context}") logging.info(f"Comandă /scrape_zip primită în {context}")
self.run_scraper(chat_id, message_id, send_as_zip=True) self.run_scraper(chat_id, message_id, send_as_zip=True)
elif text == '/scrape_solduri':
logging.info(f"Comandă /scrape_solduri primită în {context}")
self.run_scraper(chat_id, message_id, balances_only=True)
elif text == '/solduri':
logging.info(f"Comandă /solduri primită în {context}")
self.show_cached_balances(chat_id, message_id)
elif text == '/tranzactii':
logging.info(f"Comandă /tranzactii primită în {context}")
self.show_transactions_menu(chat_id, user_id, message_id)
elif text == '/zip': elif text == '/zip':
logging.info(f"Comandă /zip primită în {context}") logging.info(f"Comandă /zip primită în {context}")
self.send_zip_files(chat_id, message_id) self.send_zip_files(chat_id, message_id)
@@ -362,20 +880,26 @@ class TelegramTriggerBot:
"*COMENZI:*\n" "*COMENZI:*\n"
"`/scrape` - Ruleaza scraper + trimite fisiere individuale\n" "`/scrape` - Ruleaza scraper + trimite fisiere individuale\n"
"`/scrape_zip` - Ruleaza scraper + trimite arhiva ZIP\n" "`/scrape_zip` - Ruleaza scraper + trimite arhiva ZIP\n"
"`/scrape_solduri` - Extrage doar soldurile (fara CSV tranzactii)\n"
"`/solduri` - Afiseaza ultimul fisier solduri (instant)\n"
"`/tranzactii` - Afiseaza tranzactii recente din cont (interactiv)\n"
"`/zip` - Trimite ultimele fisiere ca arhiva ZIP (fara scraping)\n" "`/zip` - Trimite ultimele fisiere ca arhiva ZIP (fara scraping)\n"
"`/status` - Informatii sistem\n" "`/status` - Informatii sistem\n"
"`/help` - Acest mesaj\n\n" "`/help` - Acest mesaj\n\n"
"*GHID SCRAPER:*\n" "*GHID SCRAPER:*\n"
"1. Trimite `/scrape` sau `/scrape_zip`\n" "1. Trimite `/scrape`, `/scrape_zip` sau `/scrape_solduri`\n"
"2. Asteapta notificarea de 2FA pe telefon\n" "2. Asteapta notificarea de 2FA pe telefon\n"
"3. Aproba in aplicatia George\n" "3. Aproba in aplicatia George\n"
"4. Primesti fisierele automat\n\n" "4. Primesti fisierele automat\n\n"
"*DIFERENTE:*\n" "*DIFERENTE:*\n"
"• `/scrape` - Fisiere individuale (CSV + JSON)\n" "• `/scrape` - Fisiere individuale (CSV + JSON)\n"
"• `/scrape_zip` - Un singur ZIP cu toate fisierele\n" "• `/scrape_zip` - Un singur ZIP cu toate fisierele\n"
"• `/zip` - Rapid, foloseste datele existente\n\n" "• `/scrape_solduri` - Doar solduri (RAPID - fara CSV tranzactii)\n"
"• `/solduri` - Vizualizare rapida (fara 2FA)\n"
"• `/zip` - Fisiere existente (fara scraping)\n\n"
"*NOTE:*\n" "*NOTE:*\n"
"- Scraper-ul ruleaza ~2-3 minute\n" "- Scraper complet: ~2-3 minute\n"
"- Scraper solduri: ~30-40 secunde\n"
"- VM-ul trebuie sa aiba browser vizibil" "- VM-ul trebuie sa aiba browser vizibil"
) )
self.send_message(chat_id, help_msg, message_id) self.send_message(chat_id, help_msg, message_id)