Compare commits

..

19 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 21:30:54 +02:00
bc353308b5 Documentare comandă /scrape_zip în README
- Actualizare listă comenzi Telegram bot
- Clarificare diferențe între /scrape, /scrape_zip, /zip

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 21:23:12 +02:00
463b51da99 Adaugă comanda /scrape_zip în Telegram bot
Funcționalitate:
- /scrape_zip - Rulează scraper + trimite rezultatele ca ZIP
- Dezactivează notificările automate din scraper
- Bot trimite manual ZIP-ul după scraping

Logică:
- run_scraper() primește parametru send_as_zip
- Dacă send_as_zip=True:
  - Setează ENABLE_NOTIFICATIONS=false în env
  - După scraping, apelează send_zip_files()
- Rezultat: un singur ZIP în loc de fișiere individuale

Comenzi disponibile:
- /scrape - Fisiere individuale (comportament original)
- /scrape_zip - Un singur ZIP (nou)
- /zip - Trimite fișierele existente ca ZIP (fără scraping)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 21:22:48 +02:00
f088308323 Documentare comandă /zip în README
- Actualizare listă comenzi Telegram bot
- Clarificare: /zip trimite fără scraping

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 21:20:43 +02:00
c665ad7813 Adaugă comanda /zip în Telegram bot
Funcționalitate:
- /zip - Trimite ultimele fișiere ca arhivă ZIP
- Găsește automat fișierele din ultima sesiune
- Include solduri per cont + total RON în caption
- Verifică limita Telegram (50 MB)
- Șterge automat ZIP-ul temporar după trimitere

Avantaje:
- Mai rapid decât /scrape (nu rulează scraper-ul)
- Un singur fișier în loc de mai multe
- Include toate datele (CSV + JSON)
- Afișează solduri complete în mesaj

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 21:20:17 +02:00
2439d0b62a Adaugă solduri în notificările email
- EmailNotifier primește lista de conturi (nu doar count)
- _create_email_body afișează solduri per cont + total RON
- Format tabel HTML frumos cu styling
- send_notifications.py citește date din JSON
- Sincronizare cu TelegramNotifier (deja avea solduri)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 21:15:52 +02:00
5aa4900b23 Update secțiune actualizare - folosește scripturi existente
- restart_service.ps1 pentru Windows Service
- Instrucțiuni separate pentru dev manual

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 21:05:06 +02:00
355b291096 Adaugă secțiune actualizare după modificări
- git pull origin main
- Reinstalare dependențe
- Restart serviciu

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 21:03:55 +02:00
10 changed files with 1787 additions and 285 deletions

View File

@@ -22,11 +22,12 @@ SMTP_PASSWORD=your-app-password
EMAIL_FROM=your-email@gmail.com
EMAIL_TO=mmarius28@gmail.com
# Telegram
# Telegram Trigger Bot (pentru declanșare remote prin Telegram)
TELEGRAM_ENABLED=false
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
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 (goală = toți)
TELEGRAM_ALLOWED_USER_IDS=123456789,987654321 # User IDs autorizați individual (opțional)
# 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)

View File

@@ -4,11 +4,15 @@ This file provides guidance to Claude Code when working with code in this reposi
## 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.
**⚠️ 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
@@ -50,30 +54,54 @@ login() → handle_2fa_wait() → read_accounts() → download_transactions()
- Each card: `h4` (name), `span.text-grayscale-label` (IBAN), `strong.sold` (balance)
- 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 2+: Select from modal → [ALREADY on page] → Download → Back
Account 1: Expand card -> Click tranzactii icon -> Select "CSV" -> "Genereaza" -> Download from fba-document-item
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:**
- Modal: `.modal-content`
- Account buttons: `#accountC14RONCRT{last_10_iban_digits}`
- Example: IBAN `...0637236701``#accountC14RONCRT0637236701`
**Account selection strategies (in order):**
1. `get_by_role("heading", name=account_name)`
2. `locator("fba-account-details").filter(has_text=account_name)`
3. `get_by_text(account_name, exact=True)`
### Key Selectors
- Login: `get_by_placeholder("ID de logare")`, `get_by_placeholder("Parola")`
- Post-login: `#accountsBtn`, `goapp.bancatransilvania.ro` domain
- Accounts: `fba-account-details-card`, `.collapse-account-btn`, `.account-transactions-btn`
- Modal: `.modal-content`, `#accountC14RONCRT{iban_digits}`
- CSV: `get_by_role("button", name="CSV")`
**Cookie consent:**
- New (2024+): `get_by_role("button", name="Accept toate")`
- One-time consent: `get_by_text("Am inteles")`
**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
@@ -93,17 +121,19 @@ Account 2+: Select from modal → [ALREADY on page] → Download → Back
- **Fix**: Run locally with `HEADLESS=false`
### Transaction Download Timeout
- Modal not detected: Check `.modal-content` selector
- Account ID mismatch: Verify IBAN mapping `#accountC14RONCRT{last_10_digits}`
- For idx > 1: Already on page, don't expand/click
- Check `fba-document-item` selector (wait for document generation)
- Verify `#selectAccountBtn` for account dropdown
- Account selection: verify heading name matches exactly
### 2FA Timeout
- Increase `TIMEOUT_2FA_SECONDS` in `.env`
- Verify URL redirect to `goapp.bancatransilvania.ro`
- Check for "Am inteles" consent dialog blocking
### "Nu exista card la pozitia X"
- Trying to access cards in modal context
- First account needs expand, subsequent accounts don't
### Account Selection Failed
- Account name might have changed - verify exact match
- Try running `playwright codegen` to see current UI structure
- Check if dropdown opened (`#selectAccountBtn`)
## Exit Codes

138
README.md
View File

@@ -148,6 +148,20 @@ deployment\windows\scripts\setup_dev.ps1
deployment\windows\scripts\run_scraper.ps1
```
### Actualizare după Modificări
```powershell
# Pull ultimele modificări
git pull origin main
# Dacă rulează ca Windows Service (bot):
deployment\windows\scripts\restart_service.ps1
# Dacă rulează manual (dev local):
.venv\Scripts\Activate.ps1
pip install -r requirements.txt
```
## Setup și Rulare
### Setup Automat (RECOMANDAT)
@@ -273,7 +287,9 @@ python send_notifications.py
```
3. **Comenzi disponibile:**
- `/scrape` - Execută BTGO scraper
- `/scrape` - Execută BTGO scraper + trimite fișiere individuale
- `/scrape_zip` - Execută BTGO scraper + trimite arhivă ZIP
- `/zip` - Trimite ultimele fișiere ca arhivă ZIP (fără scraping)
- `/status` - Status bot
- `/help` - Ajutor
@@ -309,12 +325,8 @@ deployment\windows\scripts\menu.ps1
- Mărește timeout: `TIMEOUT_2FA_SECONDS=180` în `.env`
- Verifică notificări activate pe telefon
### Selectors nu funcționează
Site-ul s-a schimbat. Re-generează selectors:
```bash
.venv\Scripts\activate
playwright codegen https://btgo.ro --target python
```
### Selectors nu functioneaza
Site-ul s-a schimbat. Urmeaza pasii din sectiunea **Inregistrare Manuala cu Playwright** de mai jos.
### Notificări Email nu funcționează
- Pentru Gmail: folosește App Password, nu parola normală
@@ -327,12 +339,116 @@ playwright codegen https://btgo.ro --target python
- Chat ID pentru grupuri trebuie să fie **negativ** (ex: `-1001234567890`)
- 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
**⚠️ IMPORTANT:**
- NU comite `.env` în git (deja în `.gitignore`)
- NU partaja screenshots/logs - conțin date sensibile
- Șterge fișierele vechi periodic:
**IMPORTANT:**
- NU comite `.env` in git (deja in `.gitignore`)
- NU partaja screenshots/logs - contin date sensibile
- Sterge fisierele vechi periodic:
```bash
# Windows

View File

@@ -27,9 +27,13 @@ Tu (Telegram) → Bot → Rulează scraper → Trimite CSV-uri înapoi
3. Alege nume pentru bot (ex: "BTGO Scraper Bot")
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:**
```bash
@@ -98,19 +102,20 @@ Editează `.env` și adaugă:
# Bot token (același ca pentru notificări sau nou)
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrs
# User IDs autorizați (separați prin virgulă)
# 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
# Chat ID GRUP pentru notificări automate + autorizare membri
# IMPORTANT: Negativ pentru grupuri! (ex: -1001234567890)
# TOȚI membrii acestui grup pot folosi bot-ul (în grup sau DM)
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:**
- `TELEGRAM_ALLOWED_USER_IDS` = doar acești useri pot rula `/scrape` din grup
- Lasă gol dacă vrei ca oricine din grup să poată rula (nesigur!)
- Bot-ul verifică User ID-ul celui care trimite comanda, NU group ID-ul
**Autorizare:**
- **Orice membru al grupului `TELEGRAM_CHAT_ID`** poate folosi bot-ul (în grup SAU în DM)
- **SAU** useri din `TELEGRAM_ALLOWED_USER_IDS` (chiar dacă nu sunt în grup)
- Dacă ambele sunt goale = bot deschis pentru oricine (NESIGUR!)
### 4. Pornire Bot
@@ -153,7 +158,7 @@ Bot pornit. Așteaptă comenzi...
/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
@@ -308,16 +313,18 @@ TELEGRAM_ALLOWED_USER_IDS=123456789
**⚠️ ATENȚIE:**
- 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
- VM-ul trebuie securizat (firewall, VPN)
**Best Practices:**
```bash
# ✅ Bun - doar tu și admin
# ✅ Bun - grup autorizat + whitelist individual
TELEGRAM_CHAT_ID=-1001234567890
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=
# ✅ Bun - notificări separate de trigger

View File

@@ -90,11 +90,298 @@ class BTGoScraper:
)
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):
"""Entry point principal - orchestreaza tot flow-ul"""
try:
# Check dacă rulăm în mod balances_only
balances_only = os.getenv('BALANCES_ONLY', 'false').lower() == 'true'
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)
with sync_playwright() as p:
@@ -115,9 +402,11 @@ class BTGoScraper:
accounts = self.read_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 = []
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)
else:
logging.info("Download tranzacții dezactivat (DOWNLOAD_TRANSACTIONS=false)")
@@ -161,14 +450,20 @@ class BTGoScraper:
logging.info("Pagina incarcata")
try:
# Cookie consent - asteapta si accepta
logging.info("Acceptare cookies...")
# Cookie consent - asteapta si accepta (GDPR wrapper)
logging.info("Verificare GDPR cookie banner...")
self._dismiss_gdpr_cookies(self.page)
# Verificare finala ca banner-ul nu mai blocheaza
try:
cookie_button = self.page.get_by_role("button", name="Sunt de acord", exact=True)
cookie_button.click(timeout=5000)
logging.info("✓ Cookies acceptate")
gdpr_wrapper = self.page.locator(".gdprcookie-wrapper")
if gdpr_wrapper.is_visible(timeout=1000):
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:
logging.info("Nu a fost necesar acceptul cookies (posibil deja acceptat)")
pass
# Click pe butonul LOGIN - deschide popup
logging.info("Click pe butonul LOGIN...")
@@ -180,23 +475,40 @@ class BTGoScraper:
self.login_page = popup_info.value
logging.info("✓ Popup login deschis")
# Completare username
logging.info("Completare username...")
username_field = self.login_page.get_by_placeholder("ID de logare")
# Verifica GDPR cookies si pe popup
self._dismiss_gdpr_cookies(self.login_page)
# 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)
logging.info(" Username completat")
logging.info("[OK] Username completat")
# Completare password
logging.info("Completare password...")
password_field = self.login_page.get_by_placeholder("Parola")
# Completare password - detectie inteligenta cu fallback
logging.info("Detectie camp password...")
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)
logging.info(" Password completat")
logging.info("[OK] Password completat")
# Click pe butonul de submit
logging.info("Click pe 'Mergi mai departe'...")
submit_button = self.login_page.get_by_role("button", name="Mergi mai departe")
# Click pe butonul de submit - detectie inteligenta cu fallback
logging.info("Detectie buton submit...")
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()
logging.info(" Credentials trimise, astept 2FA...")
logging.info("[OK] Credentials trimise, astept 2FA...")
self._update_progress("Astept aprobare 2FA pe telefon...")
except PlaywrightTimeout as e:
@@ -234,10 +546,14 @@ class BTGoScraper:
if accounts_btn.is_visible(timeout=1000):
# Verifică că este și clickable (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
# Update page reference la login_page pentru restul operatiilor
self.page = self.login_page
# Inchide dialoguri one-time (ex: "Am inteles") daca apar
self._dismiss_one_time_consent(self.page)
return True
except:
pass
@@ -253,30 +569,19 @@ class BTGoScraper:
raise TimeoutError(f"Timeout 2FA dupa {timeout} secunde. Verifica ca ai aprobat pe telefon!")
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("Descarcare tranzactii pentru toate conturile...")
logging.info("=" * 60)
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):
try:
nume_cont = account['nume_cont']
@@ -285,118 +590,19 @@ class BTGoScraper:
self._update_progress(f"Descarc tranzactii ({idx}/{len(accounts)})...")
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:
# Primul cont - expand și click Tranzacții
if idx - 1 >= len(all_cards):
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
# PRIMUL CONT: expand -> click tranzactii -> select CSV
downloaded = self._download_first_account(account)
else:
# Conturile 2-5: suntem deja pe pagina de tranzacții (din modal)
logging.info(f" Deja pe pagina tranzactii (selectat din modal)")
time.sleep(2) # Așteaptă stabilizare pagină
# CONTURILE URMATOARE: selecteaza din dropdown -> Genereaza -> download
downloaded = self._download_subsequent_account(account)
# Așteaptă să apară butonul CSV (indica că pagina s-a încărcat)
try:
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
if downloaded:
downloaded_files.append(downloaded)
except Exception as e:
logging.error(f" Eroare la descarcarea tranzactiilor pentru {nume_cont}: {e}")
# Încearcă să navighezi înapoi
logging.error(f" [EROARE] Eroare la descarcarea tranzactiilor pentru {nume_cont}: {e}")
# Incearca sa revii la o stare stabila
try:
self.page.keyboard.press("Escape")
time.sleep(1)
@@ -405,11 +611,279 @@ class BTGoScraper:
continue
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)
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):
"""Extrage soldurile tuturor conturilor"""
logging.info("Citire conturi si solduri...")

View File

@@ -58,6 +58,9 @@ function Show-MainMenu {
Write-Host " [R] Run Scraper (Manual)" -ForegroundColor Cyan
Write-Host " [T] Run Telegram Bot (Manual)" -ForegroundColor Cyan
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 " [B] Open Quick Start Guide" -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"
}
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
do {
Show-MainMenu
@@ -301,6 +371,8 @@ do {
"t" { Invoke-RunTelegramBotManual }
"U" { Invoke-UpdateBrowsers }
"u" { Invoke-UpdateBrowsers }
"G" { Invoke-GitPullRestart }
"g" { Invoke-GitPullRestart }
"0" {
Write-Host ""
Write-Host "Goodbye!" -ForegroundColor Green

View File

@@ -4,6 +4,7 @@ Helper script pentru obținerea Chat ID Telegram (pentru DM și grupuri)
"""
import os
import sys
import requests
from dotenv import load_dotenv
@@ -13,11 +14,20 @@ load_dotenv()
BOT_TOKEN = os.getenv('TELEGRAM_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("TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrs")
print("=" * 60)
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():
"""Preia ultimele update-uri de la bot"""
url = f"https://api.telegram.org/bot{BOT_TOKEN}/getUpdates"
@@ -28,96 +38,102 @@ def main():
print("=" * 60)
print(" Telegram Chat ID Helper")
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()
if not data.get('ok'):
print(f"❌ Eroare API: {data}")
print(f"\nERORE API: {data}")
return
results = data.get('result', [])
if not results:
print("❌ Niciun mesaj găsit!")
print("\nAsigură-te că:")
print("• Ai trimis /start către bot SAU în grup")
print("• Bot-ul este adăugat în grup (dacă folosești grup)")
print("• Token-ul este corect")
print("\nNU S-AU GĂSIT MESAJE!")
print("\n" + "=" * 60)
print("INSTRUCȚIUNI:")
print("=" * 60)
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
print(f"\nGăsit {len(results)} mesaje!\n")
print(f"\nGăsit {len(results)} mesaje în total")
# Procesează ultimele mesaje (ultimele 20)
seen_chats = {}
user_ids = set()
print("\n" + "=" * 60)
print("CHAT IDs DETECTATE:")
print("=" * 60)
# Procesează ultimele mesaje
seen_chats = {}
for update in results:
for update in results[-20:]: # Ultimele 20 mesaje
if 'message' in update:
msg = update['message']
chat = msg['chat']
chat_id = chat['id']
chat_type = chat['type']
user = msg['from']
user_ids.add(user['id'])
text = msg.get('text', '(no text)')
# Evită duplicate
if chat_id in seen_chats:
continue
seen_chats[chat_id] = True
seen_chats[chat_id] = chat
# Detalii chat
if chat_type == 'private':
# DM
user = msg['from']
print(f"📱 DM cu {user.get('first_name', 'Unknown')}")
print(f" User ID: {user['id']}")
print(f" Username: @{user.get('username', 'N/A')}")
print(f" Chat ID: {chat_id}")
print(f"\n[DM] {user.get('first_name', '')} {user.get('last_name', '')}")
print(f" Username: @{user.get('username', 'N/A')}")
print(f" User ID: {user['id']}")
print(f" Chat ID: {chat_id}")
elif chat_type in ['group', 'supergroup']:
# Grup
print(f"👥 Grup: {chat.get('title', 'Unknown')}")
print(f" Chat ID: {chat_id} ⚠️ NEGATIV pentru grupuri!")
print(f" Tip: {chat_type}")
# User care a trimis mesajul
user = msg['from']
print(f" Mesaj de la: @{user.get('username', 'Unknown')} (ID: {user['id']})")
print(f"\n[GRUP] {chat.get('title', 'Unknown')}")
print(f" Chat ID: {chat_id}")
print(f" Tip: {chat_type}")
print(f" User: @{user.get('username', 'Unknown')} (ID: {user['id']})")
print()
print(f" Mesaj: \"{text[:60]}\"")
print("\n" + "=" * 60)
print("CONFIGURARE .env:")
print("=" * 60)
print("\n💡 Pentru configurare .env:")
print()
# Recomandări
for chat_id, chat_data in seen_chats.items():
for chat_id, chat in seen_chats.items():
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}")
else: # DM
print(f"# Pentru DM:")
print(f"\n# DM")
print(f"TELEGRAM_CHAT_ID={chat_id}")
print()
print("# User IDs autorizați (pot rula /scrape):")
print("TELEGRAM_ALLOWED_USER_IDS=", end="")
if user_ids:
user_ids_str = ",".join(str(uid) for uid in sorted(user_ids))
print(f"\n# User IDs autorizați")
print(f"TELEGRAM_ALLOWED_USER_IDS={user_ids_str}")
# Colectează user IDs unice
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)
print("\n" + "=" * 60)
if __name__ == "__main__":
main()

View File

@@ -6,6 +6,7 @@ Handles email and Discord notifications with file attachments
import smtplib
import logging
import zipfile
import os
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
@@ -23,13 +24,13 @@ class EmailNotifier:
self.config = config
self.enabled = config.EMAIL_ENABLED
def send(self, files: List[str], account_count: int) -> bool:
def send(self, files: List[str], accounts: list) -> bool:
"""
Send email with CSV attachments
Args:
files: List of file paths to attach
account_count: Number of accounts processed
accounts: List of account data with balances
Returns:
True if successful, False otherwise
@@ -44,6 +45,13 @@ class EmailNotifier:
logging.error("Email configuration incomplete")
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
msg = MIMEMultipart()
msg['From'] = self.config.EMAIL_FROM
@@ -51,7 +59,7 @@ class EmailNotifier:
msg['Subject'] = f'BTGO Scraper Results - {datetime.now().strftime("%Y-%m-%d %H:%M")}'
# Email body
body = self._create_email_body(files, account_count)
body = self._create_email_body(files, accounts)
msg.attach(MIMEText(body, 'html'))
# Attach files
@@ -67,7 +75,7 @@ class EmailNotifier:
# Check size limit (10MB typical SMTP limit)
if total_size > 10 * 1024 * 1024:
logging.warning(f"Email attachments exceed 10MB, creating ZIP archive")
return self._send_with_zip(files, account_count)
return self._send_with_zip(files, accounts)
with open(file_path, 'rb') as f:
part = MIMEBase('application', 'octet-stream')
@@ -94,7 +102,7 @@ class EmailNotifier:
logging.error(f"Failed to send email: {e}")
return False
def _send_with_zip(self, files: List[str], account_count: int) -> bool:
def _send_with_zip(self, files: List[str], accounts: list) -> bool:
"""Send email with files compressed as ZIP"""
try:
# Create ZIP archive
@@ -114,7 +122,7 @@ class EmailNotifier:
msg['To'] = self.config.EMAIL_TO
msg['Subject'] = f'BTGO Scraper Results (ZIP) - {datetime.now().strftime("%Y-%m-%d %H:%M")}'
body = self._create_email_body([str(zip_path)], account_count, is_zip=True)
body = self._create_email_body([str(zip_path)], accounts, is_zip=True)
msg.attach(MIMEText(body, 'html'))
with open(zip_path, 'rb') as f:
@@ -137,19 +145,97 @@ class EmailNotifier:
logging.error(f"Failed to send email with ZIP: {e}")
return False
def _create_email_body(self, files: List[str], account_count: int, is_zip: bool = False) -> str:
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:
"""Create HTML email body"""
file_list = '<br>'.join([f'{Path(f).name}' for f in files])
file_count = 1 if is_zip else len(files)
# Calculate total balance
total_ron = sum(acc['sold'] for acc in accounts if acc.get('moneda') == 'RON')
# Build account balance list
account_balances = ""
for acc in accounts:
nume = acc['nume_cont']
sold = acc['sold']
moneda = acc['moneda']
account_balances += f'<tr><td style="padding: 8px; border-bottom: 1px solid #ecf0f1;">{nume}</td><td style="padding: 8px; text-align: right; border-bottom: 1px solid #ecf0f1;"><strong>{sold:,.2f} {moneda}</strong></td></tr>'
return f"""
<html>
<body style="font-family: Arial, sans-serif;">
<h2 style="color: #2c3e50;">BTGO Scraper Results</h2>
<p><strong>Execution time:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<p><strong>Accounts processed:</strong> {account_count}</p>
<p><strong>Accounts processed:</strong> {len(accounts)}</p>
<p><strong>Files attached:</strong> {file_count}</p>
<hr>
<h3 style="color: #27ae60;">Solduri:</h3>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
{account_balances}
<tr style="background-color: #f8f9fa;">
<td style="padding: 12px; font-weight: bold;">TOTAL</td>
<td style="padding: 12px; text-align: right; font-weight: bold; color: #27ae60; font-size: 1.2em;">{total_ron:,.2f} RON</td>
</tr>
</table>
<hr>
<h3>{'Archive contents:' if is_zip else 'Attached files:'}</h3>
{file_list}
<hr>
@@ -200,6 +286,13 @@ class TelegramNotifier:
logging.info(f"Received telegram_message_id: {telegram_message_id}, telegram_chat_id: {telegram_chat_id}")
logging.info(f"Stored progress_message_id: {self.progress_message_id}, progress_chat_id: {self.progress_chat_id}")
# Check if SEND_AS_ZIP flag is set (from Telegram bot /scrape_zip command)
send_as_zip = os.getenv('SEND_AS_ZIP', 'false').lower() == 'true'
if send_as_zip:
logging.info("SEND_AS_ZIP flag detected - sending as ZIP archive")
return self._send_with_zip(files, accounts)
# Check total file size
total_size = sum(Path(f).stat().st_size for f in files if Path(f).exists())
@@ -479,7 +572,7 @@ class NotificationService:
# Send via email
if self.config.EMAIL_ENABLED:
results['email'] = self.email.send(files, account_count)
results['email'] = self.email.send(files, accounts)
# Send via Telegram
if self.config.TELEGRAM_ENABLED:

View File

@@ -4,6 +4,7 @@ Trimite ultimele fișiere CSV generate pe Email și Telegram
"""
import logging
import sys
import json
from pathlib import Path
from datetime import datetime
from config import Config
@@ -21,27 +22,27 @@ logging.basicConfig(
def find_latest_files(data_dir='./data', time_window_seconds=300):
"""
Găsește ultimele fișiere CSV generate
Găsește ultimele fișiere CSV generate și date despre conturi
Args:
data_dir: Directorul cu fișiere
time_window_seconds: Intervalul de timp (în secunde) pentru a considera fișierele din aceeași sesiune
Returns:
tuple: (solduri_csv_path, list_of_transaction_csvs)
tuple: (solduri_csv_path, list_of_transaction_csvs, accounts_data)
"""
data_path = Path(data_dir)
if not data_path.exists():
logging.error(f"Directorul {data_dir} nu există!")
return None, []
return None, [], []
# Găsește ultimul fișier solduri_*.csv
solduri_files = sorted(data_path.glob('solduri_*.csv'), key=lambda x: x.stat().st_mtime, reverse=True)
if not solduri_files:
logging.error("Nu s-a găsit niciun fișier solduri_*.csv!")
return None, []
return None, [], []
latest_solduri = solduri_files[0]
solduri_time = latest_solduri.stat().st_mtime
@@ -49,6 +50,22 @@ def find_latest_files(data_dir='./data', time_window_seconds=300):
logging.info(f"✓ Găsit fișier solduri: {latest_solduri.name}")
logging.info(f" Timestamp: {datetime.fromtimestamp(solduri_time).strftime('%Y-%m-%d %H:%M:%S')}")
# Găsește fișierul JSON corespunzător
json_filename = latest_solduri.stem + '.json'
json_path = data_path / json_filename
accounts_data = []
if json_path.exists():
try:
with open(json_path, 'r', encoding='utf-8') as f:
json_data = json.load(f)
accounts_data = json_data.get('conturi', [])
logging.info(f"✓ Găsit fișier JSON: {json_filename} ({len(accounts_data)} conturi)")
except Exception as e:
logging.warning(f"Nu s-a putut citi fișierul JSON: {e}")
else:
logging.warning(f"Nu s-a găsit fișierul JSON: {json_filename}")
# Găsește toate fișierele tranzactii_*.csv modificate în ultimele X secunde față de solduri
all_transaction_files = list(data_path.glob('tranzactii_*.csv'))
transaction_files = []
@@ -67,7 +84,7 @@ def find_latest_files(data_dir='./data', time_window_seconds=300):
logging.info(f"✓ Găsite {len(transaction_files)} fișiere tranzacții din aceeași sesiune")
return latest_solduri, transaction_files
return latest_solduri, transaction_files, accounts_data
def send_existing_files():
@@ -86,7 +103,7 @@ def send_existing_files():
return False
# Găsește ultimele fișiere
solduri_csv, transaction_csvs = find_latest_files(Config.OUTPUT_DIR)
solduri_csv, transaction_csvs, accounts_data = find_latest_files(Config.OUTPUT_DIR)
if not solduri_csv:
logging.error("Nu există fișiere de trimis!")
@@ -101,12 +118,14 @@ def send_existing_files():
logging.info(f"Total fișiere de trimis: {len(files_to_send)}")
logging.info("=" * 60)
# Estimează numărul de conturi din numărul de fișiere tranzacții
account_count = len(transaction_csvs)
# Dacă nu avem date despre conturi, creăm o listă goală
if not accounts_data:
logging.warning("Nu s-au găsit date despre conturi din JSON")
accounts_data = []
# Trimite notificările
service = NotificationService(Config)
results = service.send_all(files_to_send, account_count)
results = service.send_all(files_to_send, accounts_data)
# Afișează rezumat
logging.info("=" * 60)

View File

@@ -5,13 +5,19 @@ Telegram Trigger Bot - Declanșează BTGO Scraper prin comandă Telegram
import os
import sys
import io
import subprocess
import logging
import json
import csv
import zipfile
from pathlib import Path
from datetime import datetime
from datetime import datetime, timedelta
import glob
import requests
from dotenv import load_dotenv
from config import Config
from notifications import EmailNotifier
# Load environment
load_dotenv()
@@ -23,6 +29,10 @@ CHAT_ID = os.getenv('TELEGRAM_CHAT_ID')
POLL_TIMEOUT = int(os.getenv('TELEGRAM_POLL_TIMEOUT', 60)) # Default 60 secunde
# Logging - force stdout instead of stderr (for Windows service logging)
# Set UTF-8 encoding for stdout to support Romanian characters
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
logging.basicConfig(
level=logging.INFO,
format='[%(asctime)s] [%(levelname)s] %(message)s',
@@ -36,14 +46,19 @@ class TelegramTriggerBot:
self.bot_token = BOT_TOKEN
self.allowed_users = [int(uid.strip()) for uid in ALLOWED_USER_IDS
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.last_update_id = 0
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:
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"Grup autorizat: {self.allowed_group_id}")
logging.info(f"Long polling timeout: {self.poll_timeout}s")
# Înregistrare comenzi în meniul Telegram
@@ -55,6 +70,11 @@ class TelegramTriggerBot:
url = f"{self.base_url}/setMyCommands"
commands = [
{"command": "scrape", "description": "Rulează scraper-ul BTGO"},
{"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": "status", "description": "Status sistem"},
{"command": "help", "description": "Ajutor comenzi"}
]
@@ -91,16 +111,62 @@ class TelegramTriggerBot:
response = requests.post(url, data=data, files=files)
return response.json()
def is_user_allowed(self, user_id):
"""Verifică dacă user-ul are permisiune"""
if not self.allowed_users: # Dacă lista e goală, permite oricui
return True
return user_id in self.allowed_users
def is_member_of_group(self, user_id, group_id):
"""Verifică dacă user_id este membru al group_id prin Telegram API"""
try:
url = f"{self.base_url}/getChatMember"
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):
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"""
# Trimite mesaj inițial și salvează message_id pentru editare ulterioară
response = self.send_message(chat_id, "*BTGO Scraper pornit*\n\nAsteapta 2FA pe telefon.", reply_to_message_id)
zip_msg = " (arhiva ZIP)" if send_as_zip else ""
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
try:
message_id = response.json()['result']['message_id']
@@ -109,16 +175,66 @@ class TelegramTriggerBot:
logging.warning("Nu am putut salva message_id pentru progress updates")
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
logging.info("Pornire scraper...")
logging.info(f"Pornire scraper (send_as_zip={send_as_zip}, balances_only={balances_only})...")
# Prepare environment with global playwright path + Telegram progress info
env = os.environ.copy()
env['PLAYWRIGHT_BROWSERS_PATH'] = 'C:\\playwright-browsers'
# Setează progress updates pentru Telegram
if message_id:
env['TELEGRAM_CHAT_ID'] = str(chat_id)
env['TELEGRAM_MESSAGE_ID'] = str(message_id)
logging.info(f"Setting environment: TELEGRAM_CHAT_ID={chat_id}, TELEGRAM_MESSAGE_ID={message_id}")
# Dacă send_as_zip, comunică să trimită ZIP în loc de fișiere individuale
if send_as_zip:
env['SEND_AS_ZIP'] = 'true'
logging.info("Mod ZIP activat - va trimite arhivă ZIP")
# Dacă balances_only, comunică să nu descarce tranzacții
if balances_only:
env['BALANCES_ONLY'] = 'true'
logging.info("Mod DOAR SOLDURI activat - nu va descărca tranzacții")
else:
logging.warning("No message_id available for progress updates")
@@ -132,8 +248,8 @@ class TelegramTriggerBot:
)
if result.returncode == 0:
# Succes - mesajul final va fi editat de notifications.py
logging.info("Scraper finalizat cu succes")
# Mesajul final va fi editat de notifications.py (cu ZIP sau fișiere individuale)
else:
# Eroare
@@ -153,6 +269,503 @@ class TelegramTriggerBot:
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)
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):
"""Trimite ultimele fișiere ca arhivă ZIP"""
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
# Găsește fișierele tranzacții din aceeași sesiune (ultimele 5 minute)
time_window = 300 # 5 minute
transaction_files = []
for tf in data_dir.glob('tranzactii_*.csv'):
if abs(tf.stat().st_mtime - solduri_time) <= time_window:
transaction_files.append(tf)
# Găsește fișierul JSON corespunzător
json_file = data_dir / (latest_solduri.stem + '.json')
accounts_data = []
if json_file.exists():
try:
with open(json_file, 'r', encoding='utf-8') as f:
json_data = json.load(f)
accounts_data = json_data.get('conturi', [])
except Exception as e:
logging.warning(f"Nu s-a putut citi JSON: {e}")
# Creează arhiva ZIP
timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
zip_filename = f'btgo_export_{timestamp}.zip'
zip_path = data_dir / zip_filename
files_to_zip = [latest_solduri] + transaction_files
if json_file.exists():
files_to_zip.append(json_file)
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for file_path in files_to_zip:
zipf.write(file_path, file_path.name)
zip_size = zip_path.stat().st_size / (1024 * 1024) # MB
logging.info(f"Arhivă ZIP creată: {zip_filename} ({zip_size:.2f} MB)")
# Verifică limita Telegram (50 MB)
if zip_size > 50:
self.send_message(
chat_id,
f"*EROARE*\n\nArhiva ZIP este prea mare ({zip_size:.2f} MB)\n"
f"Limita Telegram: 50 MB",
reply_to_message_id
)
zip_path.unlink() # Șterge fișierul
return
# Construiește mesaj cu solduri
caption = f"*BTGO Export (ZIP)*\n\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"Fișiere: {len(files_to_zip)}\n\n"
if accounts_data:
total_ron = sum(acc['sold'] for acc in accounts_data if acc.get('moneda') == 'RON')
caption += "*SOLDURI:*\n"
for acc in accounts_data:
nume = acc['nume_cont']
sold = acc['sold']
moneda = acc['moneda']
caption += f"{nume}: {sold:,.2f} {moneda}\n"
caption += f"\n*TOTAL: {total_ron:,.2f} RON*"
else:
caption += f"Conturi: {len(transaction_files)}"
# Trimite ZIP-ul
self.send_message(chat_id, "*Creare arhivă ZIP...*", reply_to_message_id)
url = f"{self.base_url}/sendDocument"
with open(zip_path, 'rb') as f:
files = {'document': f}
data = {
'chat_id': chat_id,
'caption': caption,
'parse_mode': 'Markdown'
}
if reply_to_message_id:
data['reply_to_message_id'] = reply_to_message_id
response = requests.post(url, data=data, files=files)
if response.status_code == 200:
logging.info("✓ ZIP trimis cu succes pe Telegram")
else:
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)
# 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
zip_path.unlink()
except Exception as e:
logging.error(f"Eroare send_zip_files: {e}", exc_info=True)
self.send_message(chat_id, f"*EROARE*\n\n```\n{str(e)}\n```", reply_to_message_id)
def handle_command(self, message):
"""Procesează comenzi primite"""
chat_id = message['chat']['id']
@@ -177,6 +790,25 @@ class TelegramTriggerBot:
self.send_message(chat_id, "*ACCES INTERZIS*\n\nNu ai permisiunea sa folosesti acest bot.", message_id)
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
if text == '/start':
welcome_msg = "*BTGO Scraper Trigger Bot*\n\n"
@@ -184,7 +816,12 @@ class TelegramTriggerBot:
welcome_msg += f"Bot activ in grupul *{chat_title}*\n\n"
welcome_msg += (
"Comenzi disponibile:\n"
"`/scrape` - Ruleaza scraper-ul\n"
"`/scrape` - Ruleaza scraper-ul complet\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"
"`/status` - Status sistem\n"
"`/help` - Ajutor"
)
@@ -194,6 +831,26 @@ class TelegramTriggerBot:
logging.info(f"Comandă /scrape primită în {context}")
self.run_scraper(chat_id, message_id)
elif text == '/scrape_zip':
logging.info(f"Comandă /scrape_zip primită în {context}")
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':
logging.info(f"Comandă /zip primită în {context}")
self.send_zip_files(chat_id, message_id)
elif text == '/status':
data_dir = Path('data')
csv_count = len(list(data_dir.glob('*.csv')))
@@ -220,13 +877,30 @@ class TelegramTriggerBot:
if chat_type in ['group', 'supergroup']:
help_msg += "IN GRUP: Toti membrii vad comenzile si rezultatele\n\n"
help_msg += (
"1. Trimite `/scrape` pentru a porni scraper-ul\n"
"*COMENZI:*\n"
"`/scrape` - Ruleaza scraper + trimite fisiere individuale\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"
"`/status` - Informatii sistem\n"
"`/help` - Acest mesaj\n\n"
"*GHID SCRAPER:*\n"
"1. Trimite `/scrape`, `/scrape_zip` sau `/scrape_solduri`\n"
"2. Asteapta notificarea de 2FA pe telefon\n"
"3. Aproba in aplicatia George\n"
"4. Primesti fisierele automat\n\n"
"NOTE:\n"
"- Scraper-ul ruleaza ~2-3 minute\n"
"- Asigura-te ca VM-ul are browser vizibil"
"*DIFERENTE:*\n"
"• `/scrape` - Fisiere individuale (CSV + JSON)\n"
"• `/scrape_zip` - Un singur ZIP cu toate fisierele\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"
"- Scraper complet: ~2-3 minute\n"
"- Scraper solduri: ~30-40 secunde\n"
"- VM-ul trebuie sa aiba browser vizibil"
)
self.send_message(chat_id, help_msg, message_id)