Compare commits
10 Commits
c2ca401a26
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 306aa55907 | |||
| 0fff07c55b | |||
| e49e653e12 | |||
| 58399e25fb | |||
| 91021fa530 | |||
| 548526fdde | |||
| e2ec15939c | |||
| 7e8dadcbdc | |||
| 1e20334b3d | |||
| 4e58e663e9 |
@@ -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)
|
||||||
|
|||||||
76
CLAUDE.md
76
CLAUDE.md
@@ -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
120
README.md
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
770
btgo_scraper.py
770
btgo_scraper.py
@@ -90,11 +90,298 @@ 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)
|
||||||
logging.info("Start BTGO Scraper")
|
if balances_only:
|
||||||
|
logging.info("Start BTGO Scraper (DOAR SOLDURI)")
|
||||||
|
else:
|
||||||
|
logging.info("Start BTGO Scraper")
|
||||||
logging.info("=" * 60)
|
logging.info("=" * 60)
|
||||||
|
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
@@ -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:
|
|
||||||
logging.error(f" ✗ Eroare la descarcarea CSV: {e}")
|
|
||||||
|
|
||||||
# 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:
|
except Exception as e:
|
||||||
logging.error(f" ✗ Eroare la descarcarea tranzactiilor pentru {nume_cont}: {e}")
|
logging.error(f" [EROARE] Eroare la descarcarea tranzactiilor pentru {nume_cont}: {e}")
|
||||||
# Încearcă să navighezi înapoi
|
# Incearca sa revii la o stare stabila
|
||||||
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...")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"\n✅ Gă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" Username: @{user.get('username', 'N/A')}")
|
||||||
print(f" User ID: {user['id']}")
|
print(f" User ID: {user['id']}")
|
||||||
print(f" Username: @{user.get('username', 'N/A')}")
|
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()
|
||||||
|
|||||||
@@ -45,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
|
||||||
@@ -138,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])
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ 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()
|
||||||
@@ -43,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
|
||||||
@@ -63,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"}
|
||||||
@@ -100,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']
|
||||||
@@ -119,8 +175,46 @@ 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()
|
||||||
@@ -136,6 +230,11 @@ class TelegramTriggerBot:
|
|||||||
if send_as_zip:
|
if send_as_zip:
|
||||||
env['SEND_AS_ZIP'] = 'true'
|
env['SEND_AS_ZIP'] = 'true'
|
||||||
logging.info("Mod ZIP activat - va trimite arhivă ZIP")
|
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")
|
||||||
|
|
||||||
@@ -170,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:
|
||||||
@@ -238,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"
|
||||||
@@ -256,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:
|
||||||
@@ -277,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()
|
||||||
|
|
||||||
@@ -308,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"
|
||||||
@@ -315,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"
|
||||||
@@ -331,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)
|
||||||
@@ -364,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)
|
||||||
|
|||||||
Reference in New Issue
Block a user