From 9956e9c11e19d01a0eeb85ec362d7cc69e8026a9 Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Thu, 6 Nov 2025 20:55:35 +0200 Subject: [PATCH] initializare --- .env.example | 32 + .gitignore | 25 + CLAUDE.md | 113 ++++ Dockerfile | 27 + README.md | 338 +++++++++ TELEGRAM_BOT_SETUP.md | 380 +++++++++++ btgo_scraper.py | 639 ++++++++++++++++++ config.py | 58 ++ data/.gitkeep | 1 + deployment/windows/INDEX.md | 322 +++++++++ deployment/windows/QUICK_START.md | 264 ++++++++ deployment/windows/README.md | 341 ++++++++++ deployment/windows/scripts/deploy.ps1 | 282 ++++++++ .../windows/scripts/install_service.ps1 | 275 ++++++++ deployment/windows/scripts/menu.ps1 | 314 +++++++++ .../windows/scripts/restart_service.ps1 | 87 +++ deployment/windows/scripts/run_scraper.ps1 | 111 +++ .../scripts/run_telegram_bot_manual.ps1 | 81 +++ deployment/windows/scripts/setup_dev.ps1 | 172 +++++ deployment/windows/scripts/start_service.ps1 | 65 ++ deployment/windows/scripts/status.ps1 | 101 +++ .../windows/scripts/uninstall_service.ps1 | 133 ++++ .../windows/scripts/update_browsers.ps1 | 103 +++ deployment/windows/scripts/view_logs.ps1 | 137 ++++ deployment/windows/tools/.gitignore | 2 + docker-compose.yml | 31 + get_telegram_chat_id.py | 123 ++++ logs/.gitkeep | 1 + notifications.py | 504 ++++++++++++++ requirements.txt | 3 + send_notifications.py | 159 +++++ telegram_trigger_bot.py | 276 ++++++++ 32 files changed, 5500 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 TELEGRAM_BOT_SETUP.md create mode 100644 btgo_scraper.py create mode 100644 config.py create mode 100644 data/.gitkeep create mode 100644 deployment/windows/INDEX.md create mode 100644 deployment/windows/QUICK_START.md create mode 100644 deployment/windows/README.md create mode 100644 deployment/windows/scripts/deploy.ps1 create mode 100644 deployment/windows/scripts/install_service.ps1 create mode 100644 deployment/windows/scripts/menu.ps1 create mode 100644 deployment/windows/scripts/restart_service.ps1 create mode 100644 deployment/windows/scripts/run_scraper.ps1 create mode 100644 deployment/windows/scripts/run_telegram_bot_manual.ps1 create mode 100644 deployment/windows/scripts/setup_dev.ps1 create mode 100644 deployment/windows/scripts/start_service.ps1 create mode 100644 deployment/windows/scripts/status.ps1 create mode 100644 deployment/windows/scripts/uninstall_service.ps1 create mode 100644 deployment/windows/scripts/update_browsers.ps1 create mode 100644 deployment/windows/scripts/view_logs.ps1 create mode 100644 deployment/windows/tools/.gitignore create mode 100644 docker-compose.yml create mode 100644 get_telegram_chat_id.py create mode 100644 logs/.gitkeep create mode 100644 notifications.py create mode 100644 requirements.txt create mode 100644 send_notifications.py create mode 100644 telegram_trigger_bot.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2b70d29 --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# Credentials (NU comite valorile reale în git!) +BTGO_USERNAME= +BTGO_PASSWORD= + +# Configurare +HEADLESS=false +TIMEOUT_2FA_SECONDS=120 +SCREENSHOT_ON_ERROR=true +DOWNLOAD_TRANSACTIONS=true +OUTPUT_DIR=./data +LOG_LEVEL=INFO + +# Notificări (opțional) +ENABLE_NOTIFICATIONS=false + +# Email (SMTP) +EMAIL_ENABLED=false +SMTP_SERVER=smtp.gmail.com +SMTP_PORT=587 +SMTP_USERNAME=your-email@gmail.com +SMTP_PASSWORD=your-app-password +EMAIL_FROM=your-email@gmail.com +EMAIL_TO=mmarius28@gmail.com + +# 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_POLL_TIMEOUT=60 # Long polling timeout în secunde (30-90 recomandat) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..054edec --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +.venv/ +venv/ +ENV/ + +# Playwright +.pytest_cache/ +test-results/ + +# Environment & Secrets +.env +auth.json + +# Data & Logs +data/*.csv +data/*.json +data/*.png +logs/*.log + +# OS +.DS_Store +Thumbs.db diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5c295a5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,113 @@ +# CLAUDE.md + +This file provides guidance to Claude Code when working with code in this repository. + +## Project Overview + +BTGO Scraper - Playwright automation for extracting account balances and transaction CSVs from Banca Transilvania George (btgo.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`. + +## Running the Scraper + +```powershell +# Windows setup +deployment\windows\scripts\setup_dev.ps1 + +# Run scraper (browser visible) +deployment\windows\scripts\run_scraper.ps1 + +# Run Telegram bot (manual) +deployment\windows\scripts\run_telegram_bot_manual.ps1 + +# Interactive menu (all options) +deployment\windows\scripts\menu.ps1 +``` + +## Architecture + +### Core Flow +``` +login() → handle_2fa_wait() → read_accounts() → download_transactions() → save_results() +``` + +### Critical Implementation Details + +#### 1. Login with Popup (lines ~140-200) +- Main page → Click "LOGIN" → **Opens popup window** +- Use `page.expect_popup()` to capture popup reference +- All operations happen in `self.login_page` (the popup) + +#### 2. 2FA Auto-Detection (lines ~202-240) +- URL monitoring: `login.bancatransilvania.ro` → `goapp.bancatransilvania.ro` +- Poll for `#accountsBtn` element every 2 seconds +- After 2FA: `self.page = self.login_page` (switch page reference) + +#### 3. Account Reading (lines ~242-310) +- Elements: `fba-account-details-card`, `#accountsBtn` +- 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) + +**State Machine:** +``` +Account 1: Expand card → Click "Tranzacții" → Download → Back → Modal +Account 2+: Select from modal → [ALREADY on page] → Download → Back +``` + +**Critical**: After modal selection, you're ALREADY on transactions page. Don't expand/click again. + +**Modal selectors:** +- Modal: `.modal-content` +- Account buttons: `#accountC14RONCRT{last_10_iban_digits}` +- Example: IBAN `...0637236701` → `#accountC14RONCRT0637236701` + +### 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")` + +**Update selectors:** `playwright codegen https://btgo.ro --target python` + +## Docker Limitation + +**Docker DOES NOT WORK** - Akamai WAF blocks with "Access Denied". + +**Symptoms:** +- "Access Denied" page +- 0 links detected +- Screenshot: `data/debug_before_login_*.png` shows error + +**Solution:** Use Windows local mode with `HEADLESS=false`. + +## Common Issues + +### Access Denied (Docker) +- **Cause**: WAF blocking +- **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 + +### 2FA Timeout +- Increase `TIMEOUT_2FA_SECONDS` in `.env` +- Verify URL redirect to `goapp.bancatransilvania.ro` + +### "Nu exista card la pozitia X" +- Trying to access cards in modal context +- First account needs expand, subsequent accounts don't + +## Exit Codes + +- `0`: Success +- `1`: General error +- `4`: Config error (.env invalid) +- `99`: Unexpected error diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3a07b3f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# Folosim imaginea oficiala Microsoft Playwright cu Python +FROM mcr.microsoft.com/playwright/python:v1.48.0-noble + +# Setare working directory +WORKDIR /app + +# Copiaza fisierul de requirements +COPY requirements.txt . + +# Instaleaza dependente Python +RUN pip install --no-cache-dir -r requirements.txt + +# Copiaza codul aplicatiei +COPY btgo_scraper.py config.py ./ + +# Creaza directoare pentru output si logs +RUN mkdir -p /app/data /app/logs + +# Setare variabile de mediu pentru mod headless +ENV HEADLESS=true +ENV PYTHONUNBUFFERED=1 + +# Setare timezone (optional) +ENV TZ=Europe/Bucharest + +# Ruleaza script-ul principal +CMD ["python", "btgo_scraper.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7baf07 --- /dev/null +++ b/README.md @@ -0,0 +1,338 @@ +# BTGO Scraper - Automatizare Citire Solduri + +## Prezentare + +Automatizare Playwright pentru extragerea soldurilor și tranzacțiilor din George (btgo.ro). + +## Arhitectură + +### BTGO Scraper (btgo_scraper.py) + +``` +Entry point: btgo_scraper.py +│ +├─ config.py (Config.validate()) +│ └─ .env (load credentials) +│ +├─ sync_playwright() → Chromium browser +│ +├─ BTGOScraper.run() +│ ├─ login() +│ │ ├─ page.goto('go.bancatransilvania.ro') +│ │ ├─ page.expect_popup() → self.login_page +│ │ └─ Fill credentials + submit +│ │ +│ ├─ handle_2fa_wait() +│ │ └─ Poll URL + #accountsBtn (120s) +│ │ +│ ├─ read_accounts() +│ │ ├─ Click #accountsBtn +│ │ ├─ Iterate fba-account-details-card +│ │ └─ Extract: h4, IBAN, strong.sold +│ │ +│ ├─ download_transactions() [optional] +│ │ ├─ For each account: +│ │ │ ├─ Expand card (idx=1 only) +│ │ │ ├─ Click "Tranzacții" (idx=1) / Select from modal (idx>1) +│ │ │ ├─ Click CSV button +│ │ │ └─ page.expect_download() +│ │ └─ Save to data/tranzactii_*.csv +│ │ +│ ├─ save_results() +│ │ ├─ Write data/solduri_*.csv +│ │ └─ Write data/solduri_*.json +│ │ +│ └─ send_notifications() [optional] +│ └─ notifications.py +│ ├─ EmailNotifier.send() +│ │ └─ SMTP (smtplib) +│ └─ TelegramNotifier.send() +│ └─ requests.post(telegram API) +│ +Standalone scripts: +│ +├─ send_notifications.py (manual notification send) +├─ get_telegram_chat_id.py (telegram setup helper) +└─ test_telegram.py (telegram connectivity test) +``` + +### Telegram Trigger Bot (telegram_trigger_bot.py) + +``` +Entry point: telegram_trigger_bot.py +│ +├─ .env (TELEGRAM_BOT_TOKEN, TELEGRAM_ALLOWED_USER_IDS) +│ +├─ TelegramTriggerBot.__init__() +│ ├─ Load config from .env +│ ├─ Parse allowed user IDs +│ └─ Initialize Telegram API (api.telegram.org) +│ +├─ run() +│ ├─ send_message(startup notification) +│ └─ While True: +│ ├─ get_updates(long_polling timeout=60s) +│ ├─ Parse incoming messages +│ └─ handle_command() +│ │ +│ ├─ /scrape command: +│ │ ├─ is_user_allowed(user_id) +│ │ ├─ send_message("🤖 Scraper pornit...") +│ │ ├─ subprocess.run(['python', 'btgo_scraper.py']) +│ │ │ └─ btgo_scraper.py (full execution) +│ │ └─ send_message("✅ Succes" / "❌ Eroare") +│ │ +│ ├─ /status command: +│ │ └─ send_message(bot uptime info) +│ │ +│ └─ /help command: +│ └─ send_message(available commands) +│ +Windows Service: +│ +└─ deployment/windows/nssm/ + ├─ BTGOTelegramBot (Windows Service) + │ ├─ nssm install BTGOTelegramBot + │ ├─ Auto-restart on failure + │ └─ Log: logs/telegram_bot_*.log + │ + Deploy scripts: + ├─ deploy.ps1 (full deployment) + ├─ install_service.ps1 + ├─ start_service.ps1 + ├─ restart_service.ps1 + ├─ uninstall_service.ps1 + └─ menu.ps1 (interactive management) +``` + +**Puncte cheie depanare:** +- **Scraper login fails**: Check selectors în btgo_scraper.py:159-200 +- **Scraper 2FA timeout**: Check URL polling în btgo_scraper.py:202-240 +- **Scraper account reading fails**: Check selectors în btgo_scraper.py:242-310 +- **Scraper transaction download timeout**: Check modal logic în btgo_scraper.py:312-420 +- **Bot commands not working**: Check TELEGRAM_ALLOWED_USER_IDS în .env +- **Bot unauthorized**: User ID not in allowed list +- **Notifications fail**: Check notifications.py + config .env + +**Funcționalități:** +- ✅ Citire automată solduri pentru toate conturile +- ✅ Export CSV cu tranzacții pentru fiecare cont +- ✅ Autentificare 2FA automată (auto-detect) +- ✅ Notificări Email și Telegram cu fișierele generate + +**⚠️ LIMITARE: Docker/headless NU funcționează** - WAF-ul site-ului blochează activ containere Docker. + +## Prerequisites + +- Python 3.8+ +- Cont BTGO activ cu 2FA +- Windows (Docker nu este suportat) + +## Setup și Rulare + +### Setup Automat (RECOMANDAT) + +```powershell +# PowerShell (recomandat) +deployment\windows\scripts\setup_dev.ps1 + +# SAU meniu interactiv complet +deployment\windows\scripts\menu.ps1 +``` + +### Rulare Manuală + +```powershell +# Scraper direct +deployment\windows\scripts\run_scraper.ps1 + +# Telegram bot (manual mode) +deployment\windows\scripts\run_telegram_bot_manual.ps1 + +# SAU meniu interactiv +deployment\windows\scripts\menu.ps1 +``` + +**Ce se întâmplă:** +1. Browser Chrome se deschide vizibil +2. Login automat +3. Așteaptă aprobare 2FA pe telefon (120s timeout) +4. Citește soldurile tuturor conturilor +5. Descarcă CSV-uri cu tranzacții +6. Salvează rezultate în `data/` + +### Configurare `.env` + +```bash +# Obligatorii +BTGO_USERNAME=your_username +BTGO_PASSWORD=your_password +HEADLESS=false # OBLIGATORIU false! + +# Opționale +DOWNLOAD_TRANSACTIONS=true +TIMEOUT_2FA_SECONDS=120 +``` + +## Structură Output + +``` +data/ +├── solduri_2025-11-06_14-30-45.csv # Rezumat solduri +├── solduri_2025-11-06_14-30-45.json # Metadata + solduri +├── tranzactii_Colector_01_2025-11-06.csv # Tranzacții per cont +└── dashboard_2025-11-06_14-30-45.png # Screenshot + +logs/ +└── scraper_2025-11-06.log # Log zilnic +``` + +### Format CSV Solduri + +```csv +timestamp,nume_cont,iban,sold,moneda,tip_cont +2025-11-06 14:30:45,Colector 01,RO32BTRLRONCRT0637236701,7223.26,RON,colector +``` + +## Notificări (Email & Telegram) + +### Email (SMTP) + +```bash +# .env +ENABLE_NOTIFICATIONS=true +EMAIL_ENABLED=true +SMTP_SERVER=smtp.gmail.com +SMTP_PORT=587 +SMTP_USERNAME=your-email@gmail.com +SMTP_PASSWORD=your-app-password # Pentru Gmail: https://myaccount.google.com/apppasswords +EMAIL_FROM=your-email@gmail.com +EMAIL_TO=recipient@gmail.com +``` + +### Telegram + +1. **Crează Bot:** + - Caută @BotFather în Telegram + - Trimite `/newbot` + - Copiază token-ul primit + +2. **Obține Chat ID:** + ```bash + python get_telegram_chat_id.py + ``` + +3. **Configurare .env:** + ```bash + ENABLE_NOTIFICATIONS=true + TELEGRAM_ENABLED=true + TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrs + TELEGRAM_CHAT_ID=-1001234567890 # Negativ pentru grupuri + ``` + +### Trimitere Manuală Notificări + +```bash +python send_notifications.py +``` + +## Telegram Trigger Bot - Declanșare Remote + +### Setup Rapid (Manual Mode) + +1. **Configurare .env:** + ```bash + TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrs + TELEGRAM_ALLOWED_USER_IDS=your_user_id + TELEGRAM_CHAT_ID=-1001234567890 # Pentru grup (negativ!) + ``` + +2. **Pornire bot:** + ```powershell + deployment\windows\scripts\run_telegram_bot_manual.ps1 + ``` + +3. **Comenzi disponibile:** + - `/scrape` - Execută BTGO scraper + - `/status` - Status bot + - `/help` - Ajutor + +### Deploy ca Windows Service (RECOMANDAT pentru VM) + +```powershell +# Deploy complet + instalare serviciu +deployment\windows\scripts\deploy.ps1 + +# SAU folosește meniul interactiv +deployment\windows\scripts\menu.ps1 +``` + +**Avantaje serviciu:** +- ✅ Pornire automată cu Windows +- ✅ Auto-restart la crash +- ✅ Rulează în background (fără RDP activ) +- ✅ Logging centralizat + +**Notă:** Fișierele sunt trimise de **scraper** prin notificări (dacă `ENABLE_NOTIFICATIONS=true`), NU de bot. + +**⚠️ Cerință pentru scraper:** Desktop Windows activ (RDP conectată) - browser TREBUIE vizibil! + +**Ghid complet:** `deployment/windows/README.md` și `TELEGRAM_BOT_SETUP.md` + +## Troubleshooting + +### "Access Denied" în Docker +**Cauză:** WAF-ul BT George blochează Docker/headless browsers. +**Soluție:** Rulează DOAR local în Windows cu `HEADLESS=false`. + +### Timeout la 2FA +- 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 +``` + +### Notificări Email nu funcționează +- Pentru Gmail: folosește App Password, nu parola normală +- Verifică port: 587 (TLS) sau 465 (SSL) +- Testare: `python test_telegram.py` + +### Notificări Telegram nu funcționează +- Verifică `TELEGRAM_ENABLED=true` +- Rulează `python test_telegram.py` +- Chat ID pentru grupuri trebuie să fie **negativ** (ex: `-1001234567890`) +- Asigură-te că botul este în grup + +## Securitate + +**⚠️ IMPORTANT:** +- NU comite `.env` în git (deja în `.gitignore`) +- NU partaja screenshots/logs - conțin date sensibile +- Șterge fișierele vechi periodic: + +```bash +# Windows +del /q data\*.csv data\*.json data\*.png + +# Linux/Mac +rm data/*.{csv,json,png} +``` + +## Exit Codes + +- `0` - Success +- `1` - Eroare generală +- `4` - Eroare configurare (.env invalid) +- `99` - Eroare neașteptată + +## Licență + +MIT License + +## Disclaimer Legal + +Acest tool accesează informații personale bancare. Utilizatorul este singurul responsabil pentru conformarea cu termenii Băncii Transilvania și legislația aplicabilă. diff --git a/TELEGRAM_BOT_SETUP.md b/TELEGRAM_BOT_SETUP.md new file mode 100644 index 0000000..baa951b --- /dev/null +++ b/TELEGRAM_BOT_SETUP.md @@ -0,0 +1,380 @@ +# Telegram Trigger Bot - Setup Guide + +## Prezentare + +Telegram Trigger Bot permite declanșarea BTGO Scraper-ului de la distanță prin comenzi Telegram. Perfect pentru VM Windows 11. + +**⚠️ IMPORTANT:** VM-ul Windows 11 TREBUIE să aibă: +- Remote Desktop session activă (sau user logat) +- Browser vizibil (HEADLESS=false) +- WAF-ul BT blochează browsere headless! + +## Cum Funcționează + +``` +Tu (Telegram) → Bot → Rulează scraper → Trimite CSV-uri înapoi +``` + +## Setup Pas cu Pas + +### 1. Configurare Bot Telegram + +**Dacă ai deja bot pentru notificări:** Poți folosi același bot! + +**Dacă NU ai bot:** +1. Deschide Telegram și caută **@BotFather** +2. Trimite `/newbot` +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 + +Ai nevoie de User ID pentru securitate (doar tu poți rula scraper-ul). + +**Opțiunea A - Folosește bot existent:** +```bash +python get_telegram_chat_id.py +``` +Vei vedea: `User ID: 123456789` + +**Opțiunea B - Folosește @userinfobot:** +1. Caută `@userinfobot` în Telegram +2. Trimite `/start` +3. Copiază User ID-ul + +### 3. Setup pentru Grup Telegram (RECOMANDAT) + +**Avantaj:** Comenzi și rezultate în același grup - mai convenabil decât mesaje directe! + +#### 3.1. Adaugă Bot-ul în Grup + +1. **Deschide grupul Telegram** unde vrei să trimiți comenzi +2. **Adaugă bot-ul:** + - Click pe numele grupului → "Add Members" + - Caută bot-ul tău (ex: "BTGO Scraper Bot") + - Adaugă + +3. **Configurare Privacy Mode (important!):** + + Bot-ul trebuie să vadă comenzile din grup. Ai 2 opțiuni: + + **Opțiunea A - Dezactivează Privacy Mode (RECOMANDAT):** + ``` + 1. Deschide @BotFather în Telegram + 2. Trimite /mybots + 3. Selectează bot-ul tău + 4. Bot Settings → Group Privacy → Turn OFF + ``` + Acum bot-ul vede toate mesajele care încep cu `/` (comenzi) + + **Opțiunea B - Fă bot-ul Admin:** + ``` + 1. În grup: Click nume grup → Administrators + 2. Add Administrator → Selectează bot-ul + 3. Permisiuni minime: nicio permisiune specială necesară + ``` + Admin bots văd toate mesajele automat. + +#### 3.2. Obține Chat ID al Grupului + +**Metoda 1 - Cu scriptul helper:** +```bash +python get_telegram_chat_id.py +``` +- Trimite `/start` în grup +- Script-ul va afișa: `Chat ID: -1001234567890` (NEGATIV pentru grupuri!) + +**Metoda 2 - Manual:** +1. Adaugă `@RawDataBot` în grupul tău temporar +2. Bot-ul va afișa informații despre grup +3. Copiază `chat.id` (va fi negativ, ex: `-1001234567890`) +4. Șterge `@RawDataBot` din grup + +### 4. Configurare .env + +Editează `.env` și adaugă: + +```bash +# 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 +# IMPORTANT: Negativ pentru grupuri! (ex: -1001234567890) +TELEGRAM_CHAT_ID=-1001234567890 +``` + +**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 + +### 4. Pornire Bot + +**Opțiunea 1 - Manual (Test):** +```powershell +deployment\windows\scripts\run_telegram_bot_manual.ps1 +``` + +**Opțiunea 2 - Windows Service (RECOMANDAT pentru VM):** +```powershell +# Deploy complet +deployment\windows\scripts\deploy.ps1 + +# SAU meniu interactiv +deployment\windows\scripts\menu.ps1 +``` + +**Opțiunea 3 - Manual Python:** +```bash +.venv\Scripts\activate +python telegram_trigger_bot.py +``` + +**Bot-ul va afișa:** +``` +Bot inițializat. Useri autorizați: [123456789] +Bot pornit. Așteaptă comenzi... +``` + +## Utilizare + +### Comenzi Disponibile + +**În grupul Telegram (sau DM către bot):** + +``` +/start - Pornește bot-ul și vezi comenzile +/scrape - ⭐ Rulează scraper-ul (2-3 minute) +/status - Vezi status sistem (ultima rulare, fișiere) +/help - Ajutor utilizare +``` + +**Securitate:** Doar userii din `TELEGRAM_ALLOWED_USER_IDS` pot rula comenzi! + +### Flow Tipic în Grup + +1. **Scrii în grup:** `/scrape` +2. **Bot răspunde:** "🤖 BTGO Scraper pornit... Așteaptă 2FA!" +3. **Pe VM:** Browser-ul se deschide, face login automat +4. **Pe telefon:** Aprobă notificarea George 2FA +5. **Scraper trimite fișiere:** Prin notificări automate (email + grup) +6. **Bot confirmă:** "✅ Finalizat! Fișierele au fost trimise automat." + +**Notă:** Fișierele sunt trimise de **scraper** (`ENABLE_NOTIFICATIONS=true`), NU de bot. Bot-ul doar declanșează execuția și confirmă finalizarea. + +### Exemplu Output în Grup + +``` +Tu: /scrape + +Bot: 🤖 BTGO Scraper pornit... + Așteaptă 2FA pe telefon! + +[După 2 minute - mesaje de la scraper prin notificări] + +Bot: 📧 BTGO Scraper - Sold conturi actualizat + + 📄 solduri_2025-11-06.csv + 📄 tranzactii_Colector_01.csv + 📄 tranzactii_Antreprenor_04.csv + ... (toate fișierele) + +Bot: ✅ Scraper finalizat cu succes! + 📧 Fișierele au fost trimise automat prin notificări. +``` + +**Important:** Mesajele cu fișiere vin din **scraper** (prin `notifications.py`), nu din bot! Bot-ul doar declanșează și confirmă. + +## Setup VM Windows 11 + +### Cerințe Critice + +**⚠️ Browser TREBUIE să fie vizibil!** WAF-ul BT blochează headless. + +### Metoda Recomandată - Windows Service + +```powershell +# Deploy ca serviciu Windows (auto-start, auto-restart) +deployment\windows\scripts\deploy.ps1 +``` + +**Avantaje:** +- ✅ Pornire automată cu Windows +- ✅ Auto-restart la crash +- ✅ Management prin Services.msc +- ✅ Logging centralizat +- ✅ Nu necesită RDP conectată + +**Management:** +```powershell +# Meniu interactiv +deployment\windows\scripts\menu.ps1 + +# SAU comenzi directe +deployment\windows\scripts\start_service.ps1 +deployment\windows\scripts\stop_service.ps1 +deployment\windows\scripts\restart_service.ps1 +deployment\windows\scripts\status.ps1 +``` + +### Alternativă - Task Scheduler + +**Doar dacă nu vrei Windows Service:** + +1. Deschide Task Scheduler +2. Create Task +3. **General:** + - Name: "BTGO Telegram Bot" + - Run only when user is logged on: ✅ +4. **Triggers:** + - At log on (Specific user) +5. **Actions:** + - Action: Start a program + - Program: `C:\path\to\btgo-playwright\deployment\windows\scripts\run_telegram_bot_manual.ps1` + - Start in: `C:\path\to\btgo-playwright` +6. **Conditions:** + - Dezactivează "Start task only if computer is on AC power" + +### Verificare Funcționare + +```bash +# Pe VM, verifică că bot-ul rulează +tasklist | findstr python + +# Ar trebui să vezi: +# python.exe 12345 Console 1 45,678 K +``` + +## Troubleshooting + +### Bot nu răspunde la comenzi + +**Cauză:** Token greșit sau bot nestartă. + +**Soluție:** +```bash +# Verifică token-ul +python telegram_trigger_bot.py + +# Ar trebui să vezi: +# Bot inițializat. Useri autorizați: [123456789] +``` + +### "⛔ Acces interzis!" + +**Cauză:** User ID-ul tău nu e în `TELEGRAM_ALLOWED_USER_IDS`. + +**Soluție:** +```bash +# Verifică User ID-ul tău +python get_telegram_chat_id.py + +# Adaugă-l în .env +TELEGRAM_ALLOWED_USER_IDS=123456789 +``` + +### "Access Denied" la scraping + +**Cauză:** Browser-ul rulează în headless mode sau VM nu are GUI activă. + +**Soluție:** +- Conectează-te prin RDP și LASĂ sesiunea deschisă +- Verifică `.env`: `HEADLESS=false` +- Asigură-te că desktop-ul Windows este vizibil + +### Scraper timeout (> 10 minute) + +**Cauză:** Probabil blocat la 2FA sau browser nu răspunde. + +**Soluție:** +- Verifică logs pe VM: `logs\scraper_*.log` +- Mărește timeout în bot: `timeout=900` (15 min) +- Asigură-te că aprobați 2FA în < 2 minute + +### Bot se oprește singur + +**Cauză:** Sesiune RDP deconectată sau VM suspended. + +**Soluție:** +- Folosește Task Scheduler pentru restart automat +- Configurează VM să nu intre în sleep/hibernate +- Verifică că desktop-ul rămâne activ + +## Securitate + +**⚠️ ATENȚIE:** +- Bot-ul are acces la credentials din `.env` +- `TELEGRAM_ALLOWED_USER_IDS` TREBUIE configurat! +- Nu partaja token-ul botului +- VM-ul trebuie securizat (firewall, VPN) + +**Best Practices:** +```bash +# ✅ Bun - doar tu și admin +TELEGRAM_ALLOWED_USER_IDS=123456789,987654321 + +# ❌ Rău - oricine cu acces la bot +TELEGRAM_ALLOWED_USER_IDS= + +# ✅ Bun - notificări separate de trigger +TELEGRAM_BOT_TOKEN=bot_trigger_token +TELEGRAM_CHAT_ID=group_chat_id +``` + +## Logs și Monitoring + +**Logs bot:** +``` +[2025-11-06 17:30:00] [INFO] Bot inițializat. Useri autorizați: [123456789] +[2025-11-06 17:30:05] [INFO] Mesaj de la username (ID: 123456789): /scrape +[2025-11-06 17:30:05] [INFO] Comandă /scrape primită +[2025-11-06 17:30:05] [INFO] Pornire scraper... +[2025-11-06 17:32:45] [INFO] Scraper finalizat cu succes +[2025-11-06 17:32:50] [INFO] Trimit: solduri_2025-11-06.csv +``` + +**Logs scraper:** +``` +logs\scraper_2025-11-06.log +``` + +## Alternative Trigger Methods + +### 1. HTTP API (simplă) +```python +from flask import Flask, request +app = Flask(__name__) + +@app.route('/scrape', methods=['POST']) +def trigger_scrape(): + # Verifică token secret + subprocess.run(['python', 'btgo_scraper.py']) +``` + +### 2. Cron Job (periodicitate fixă) +``` +Task Scheduler → Daily la 9:00 AM +``` + +### 3. File Watcher +```python +# Detectează fișier "trigger.txt" +# Rulează scraper automat +``` + +## Performance + +**Timpi tipici:** +- Bot startup: < 5 secunde +- Scraper execution: 2-3 minute +- File upload: 5-10 secunde +- **Total:** ~3-4 minute de la `/scrape` la fișiere primite + +**Resource usage (VM):** +- RAM: 500MB (bot) + 800MB (browser) +- CPU: 5-10% idle, 30-40% during scrape +- Network: ~5MB download (CSV-uri) diff --git a/btgo_scraper.py b/btgo_scraper.py new file mode 100644 index 0000000..90adcaa --- /dev/null +++ b/btgo_scraper.py @@ -0,0 +1,639 @@ +""" +BTGO Scraper - Automatizare citire solduri conturi bancare +Folosește Playwright pentru automatizare browser +""" +from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout +import logging +import csv +import json +import time +import os +from datetime import datetime +from pathlib import Path +from config import Config + + +class BTGoScraper: + """Scraper pentru extragerea soldurilor de pe btgo.ro""" + + def __init__(self): + """Initializare scraper cu logging si configurare""" + self.config = Config() + Config.validate() + self._setup_logging() + self._ensure_directories() + self.page = None + self.login_page = None # Pagina de login (popup) + + # Setup pentru progress updates prin Telegram (optional) + self.telegram_chat_id = os.getenv('TELEGRAM_CHAT_ID') + self.telegram_message_id = os.getenv('TELEGRAM_MESSAGE_ID') + self.progress_notifier = None + + logging.info(f"Environment: TELEGRAM_CHAT_ID={self.telegram_chat_id}, TELEGRAM_MESSAGE_ID={self.telegram_message_id}") + + # Inițializează notifier pentru progress updates dacă avem chat_id + if self.telegram_chat_id and self.telegram_message_id: + try: + from notifications import TelegramNotifier + self.progress_notifier = TelegramNotifier(self.config) + logging.info(f"Progress updates activate pentru chat_id={self.telegram_chat_id}, message_id={self.telegram_message_id}") + except Exception as e: + logging.warning(f"Nu am putut inițializa progress notifier: {e}") + else: + logging.warning("Progress updates dezactivate - lipsesc TELEGRAM_CHAT_ID sau TELEGRAM_MESSAGE_ID") + + def _setup_logging(self): + """Configurare logging in consola si fisier zilnic""" + # Creaza director logs daca nu exista + Path(self.config.LOG_DIR).mkdir(exist_ok=True) + + # Nume fisier cu data curenta + log_file = Path(self.config.LOG_DIR) / f"scraper_{datetime.now().strftime('%Y-%m-%d')}.log" + + # Format log + log_format = '[%(asctime)s] [%(levelname)s] %(message)s' + date_format = '%Y-%m-%d %H:%M:%S' + + # Configurare logging + logging.basicConfig( + level=getattr(logging, self.config.LOG_LEVEL), + format=log_format, + datefmt=date_format, + handlers=[ + logging.StreamHandler(), # Console + logging.FileHandler(log_file, encoding='utf-8') # Fisier + ] + ) + + logging.info(f"Logging initializat: {log_file}") + + def _ensure_directories(self): + """Creaza directoarele data/ si logs/ daca nu exista""" + Path(self.config.OUTPUT_DIR).mkdir(exist_ok=True) + Path(self.config.LOG_DIR).mkdir(exist_ok=True) + logging.info(f"Directoare verificate: {self.config.OUTPUT_DIR}, {self.config.LOG_DIR}") + + def _update_progress(self, message: str): + """ + Trimite update de progres către Telegram (editează mesajul inițial) + + Args: + message: Mesajul de progres + """ + if self.progress_notifier and self.telegram_chat_id and self.telegram_message_id: + full_message = f"*{message}*" + self.progress_notifier._edit_message( + self.telegram_chat_id, + self.telegram_message_id, + full_message + ) + logging.info(f"Progress update: {message}") + + def run(self): + """Entry point principal - orchestreaza tot flow-ul""" + try: + logging.info("=" * 60) + logging.info("Start BTGO Scraper") + logging.info("=" * 60) + + with sync_playwright() as p: + # Lansare browser + browser = p.chromium.launch( + headless=self.config.HEADLESS, + slow_mo=100 if not self.config.HEADLESS else 0 # Slow motion pentru debugging + ) + + # Creaza pagina cu viewport standard + self.page = browser.new_page(viewport={'width': 1920, 'height': 1080}) + logging.info(f"Browser lansat (headless={self.config.HEADLESS})") + + # Flow complet + self.login() + self.handle_2fa_wait() + self._update_progress("2FA aprobat! Incarc conturi...") + accounts = self.read_accounts() + csv_path, json_path = self.save_results(accounts) + + # Descarcă tranzacții pentru toate conturile (optional) + downloaded_files = [] + if self.config.DOWNLOAD_TRANSACTIONS: + downloaded_files = self.download_transactions(accounts) + else: + logging.info("Download tranzacții dezactivat (DOWNLOAD_TRANSACTIONS=false)") + + # Screenshot final de confirmare + screenshot_path = Path(self.config.OUTPUT_DIR) / f"dashboard_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.png" + self.page.screenshot(path=str(screenshot_path)) + logging.info(f"Screenshot final salvat: {screenshot_path}") + + # Trimite notificări (email, Discord) + if self.config.ENABLE_NOTIFICATIONS: + self.send_notifications(csv_path, downloaded_files, accounts) + + # Afișează rezumat final + logging.info("=" * 60) + logging.info("REZUMAT FINAL") + logging.info("=" * 60) + logging.info(f"Conturi citite: {len(accounts)}") + if self.config.DOWNLOAD_TRANSACTIONS: + logging.info(f"Tranzacții descărcate: {len(downloaded_files)}") + for df in downloaded_files: + logging.info(f" - {df['cont']}: {df['fisier']}") + logging.info("=" * 60) + + browser.close() + # Nu mai trimitem update progress aici - mesajul final cu soldurile a fost deja editat de notifications.py + logging.info("=" * 60) + logging.info("✓ Scraper finalizat cu succes!") + logging.info("=" * 60) + return True + + except Exception as e: + self._handle_error(e) + return False + + def login(self): + """Autentificare cu username si password""" + self._update_progress("Deschid pagina de login...") + logging.info("Navigare catre https://go.bancatransilvania.ro/") + self.page.goto('https://go.bancatransilvania.ro/', wait_until='networkidle') + logging.info("Pagina incarcata") + + try: + # Cookie consent - asteapta si accepta + logging.info("Acceptare cookies...") + try: + cookie_button = self.page.get_by_role("button", name="Sunt de acord", exact=True) + cookie_button.click(timeout=5000) + logging.info("✓ Cookies acceptate") + except: + logging.info("Nu a fost necesar acceptul cookies (posibil deja acceptat)") + + # Click pe butonul LOGIN - deschide popup + logging.info("Click pe butonul LOGIN...") + with self.page.expect_popup() as popup_info: + login_link = self.page.get_by_role("link", name="LOGIN") + login_link.click() + + # Preia referinta la popup-ul de login + 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") + username_field.fill(self.config.BTGO_USERNAME) + logging.info("✓ Username completat") + + # Completare password + logging.info("Completare password...") + password_field = self.login_page.get_by_placeholder("Parola") + password_field.fill(self.config.BTGO_PASSWORD) + logging.info("✓ 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") + submit_button.click() + logging.info("✓ Credentials trimise, astept 2FA...") + self._update_progress("Astept aprobare 2FA pe telefon...") + + except PlaywrightTimeout as e: + logging.error(f"Timeout la login: {e}") + raise Exception("Nu am gasit elementele de login. Verifica selectors!") + except Exception as e: + logging.error(f"Eroare la login: {e}") + raise + + def handle_2fa_wait(self): + """Asteapta aprobare 2FA cu auto-detect""" + + logging.info("=" * 60) + logging.info("🔐 APROBARE 2FA NECESARA") + logging.info("=" * 60) + logging.info("Verifica aplicatia George pe telefon si aproba autentificarea.") + logging.info(f"Timeout: {self.config.TIMEOUT_2FA_SECONDS} secunde") + logging.info("=" * 60) + + # Detectam login reusit prin schimbarea URL-ului la goapp.bancatransilvania.ro + # sau prin aparitia butonului de conturi (#accountsBtn) + post_login_selectors = [ + "#accountsBtn", # Butonul pentru conturi (apare post-login) + ] + + start_time = datetime.now() + timeout = self.config.TIMEOUT_2FA_SECONDS + + while (datetime.now() - start_time).seconds < timeout: + # Verifică dacă butonul de conturi este CLICKABLE (nu doar vizibil) + # Aceasta este singura verificare sigură - butonul apare doar după 2FA reușit + try: + # Așteaptă ca #accountsBtn să fie clickable (nu doar vizibil în DOM) + accounts_btn = self.login_page.locator("#accountsBtn") + 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)") + time.sleep(2) # Asteapta ca pagina sa se stabilizeze complet + # Update page reference la login_page pentru restul operatiilor + self.page = self.login_page + return True + except: + pass + + # Afiseaza countdown + elapsed = (datetime.now() - start_time).seconds + remaining = timeout - elapsed + print(f"\rAsteptam aprobare 2FA... {remaining}s ramas", end='', flush=True) + + time.sleep(2) + + print() # New line dupa countdown + 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""" + 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'] + iban = account['iban'] + + 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 + 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ă + + # 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 + + except Exception as e: + logging.error(f" ✗ Eroare la descarcarea tranzactiilor pentru {nume_cont}: {e}") + # Încearcă să navighezi înapoi + try: + self.page.keyboard.press("Escape") + time.sleep(1) + except: + pass + continue + + logging.info("=" * 60) + logging.info(f"✓ Descarcate {len(downloaded_files)}/{len(accounts)} fisiere CSV cu tranzactii") + logging.info("=" * 60) + + return downloaded_files + + def read_accounts(self): + """Extrage soldurile tuturor conturilor""" + logging.info("Citire conturi si solduri...") + + try: + # Click pe butonul de conturi pentru a afisa lista + logging.info("Click pe butonul Conturi...") + # Simplu selector - doar butonul, nu div-ul interior + accounts_button = self.page.locator("#accountsBtn") + accounts_button.click() + time.sleep(3) # Asteapta ca lista sa se incarce + logging.info("✓ Sectiunea conturi deschisa") + + # Update progres DUPĂ ce lista de conturi s-a încărcat + self._update_progress("Citesc solduri conturi...") + + # Gaseste toate cardurile de conturi + account_cards = self.page.locator("fba-account-details-card").all() + logging.info(f"Gasit {len(account_cards)} conturi") + + accounts = [] + + for idx, card in enumerate(account_cards, 1): + try: + # Extrage nume cont (din tag

) + nume = card.locator("h4.fw-normal.mb-0").inner_text() + + # Extrage IBAN (din span cu clasa specific) + iban = card.locator("span.small.text-grayscale-label").inner_text() + + # Extrage sold (din cu clasa 'sold') + sold_text = card.locator("strong.sold").inner_text() + + # Parsing sold: "7,223.26 RON" -> sold_numeric=7223.26, moneda="RON" + # Elimina spatii, inlocuieste virgula cu punct + sold_parts = sold_text.strip().split() + + if len(sold_parts) >= 2: + sold_str = sold_parts[0] # "7,223.26" + moneda = sold_parts[1] # "RON" + else: + sold_str = sold_text + moneda = "RON" # Default + + # Converteste la float (elimina virgule care sunt separator de mii) + # Format: "7,223.26" -> 7223.26 + sold_numeric = float(sold_str.replace(',', '')) + + # Determina tip cont din nume (optional) + nume_lower = nume.lower() + if 'colector' in nume_lower: + tip_cont = 'colector' + elif 'profit' in nume_lower: + tip_cont = 'profit' + elif 'operationale' in nume_lower or 'operational' in nume_lower: + tip_cont = 'operational' + elif 'taxe' in nume_lower: + tip_cont = 'taxe' + elif 'antreprenor' in nume_lower: + tip_cont = 'antreprenor' + else: + tip_cont = 'curent' + + accounts.append({ + 'nume_cont': nume.strip(), + 'iban': iban.strip(), + 'sold': sold_numeric, + 'moneda': moneda.strip(), + 'tip_cont': tip_cont + }) + + logging.info(f" [{idx}] {nume}: {sold_numeric} {moneda}") + + except Exception as e: + logging.warning(f"Eroare la citirea contului {idx}: {e}") + continue + + if not accounts: + raise Exception("Nu am putut citi niciun cont din lista!") + + logging.info(f"✓ Citite {len(accounts)} conturi cu succes") + self._update_progress(f"Gasite {len(accounts)} conturi") + return accounts + + except Exception as e: + logging.error(f"Eroare la citirea conturilor: {e}") + raise + + def save_results(self, accounts): + """Salveaza rezultate in CSV si JSON""" + if not accounts: + logging.warning("Nu exista conturi de salvat!") + return + + timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + + # Salvare CSV + csv_path = Path(self.config.OUTPUT_DIR) / f'solduri_{timestamp}.csv' + with open(csv_path, 'w', newline='', encoding='utf-8') as f: + fieldnames = ['timestamp', 'nume_cont', 'iban', 'sold', 'moneda', 'tip_cont'] + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + + for account in accounts: + row = account.copy() + row['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + writer.writerow(row) + + logging.info(f"✓ CSV salvat: {csv_path}") + + # Salvare JSON + json_path = Path(self.config.OUTPUT_DIR) / f'solduri_{timestamp}.json' + + # Calculeaza metadata + currencies = list(set(a.get('moneda', 'N/A') for a in accounts)) + total_ron = sum(a['sold'] for a in accounts if a.get('moneda') == 'RON') + + output = { + 'metadata': { + 'timestamp': datetime.now().isoformat(), + 'scraper_version': '1.0.0', + 'total_accounts': len(accounts), + 'total_ron': round(total_ron, 2), + 'currencies': currencies + }, + 'accounts': accounts + } + + with open(json_path, 'w', encoding='utf-8') as f: + json.dump(output, f, indent=2, ensure_ascii=False) + + logging.info(f"✓ JSON salvat: {json_path}") + logging.info(f"Total conturi: {len(accounts)}, Total RON: {total_ron:.2f}") + + return csv_path, json_path + + def send_notifications(self, csv_path, downloaded_files, accounts): + """ + Trimite notificări email și Discord cu fișierele descărcate + + Args: + csv_path: Calea către fișierul CSV cu solduri + downloaded_files: Lista de dicționare cu fișierele de tranzacții descărcate + accounts: Lista cu datele conturilor (solduri, nume, etc.) + """ + try: + from notifications import NotificationService + + self._update_progress("Trimit rezultate...") + logging.info("=" * 60) + logging.info("TRIMITERE NOTIFICĂRI") + logging.info("=" * 60) + + # Colectează toate fișierele de trimis + files_to_send = [str(csv_path)] # CSV cu solduri + + # Adaugă toate CSV-urile cu tranzacții + for df in downloaded_files: + if 'fisier' in df and Path(df['fisier']).exists(): + files_to_send.append(df['fisier']) + logging.info(f"Adăugat pentru notificare: {Path(df['fisier']).name}") + + logging.info(f"Total fișiere de trimis: {len(files_to_send)}") + + # Trimite prin toate canalele configurate + service = NotificationService(self.config) + logging.info(f"Calling send_all with telegram_message_id={self.telegram_message_id}, telegram_chat_id={self.telegram_chat_id}") + results = service.send_all( + files_to_send, + accounts, + telegram_message_id=self.telegram_message_id, + telegram_chat_id=self.telegram_chat_id + ) + + # Afișează rezumat + if self.config.EMAIL_ENABLED: + status = "✓ Succes" if results['email'] else "✗ Eșuat" + logging.info(f"Email: {status}") + + if self.config.TELEGRAM_ENABLED: + status = "✓ Succes" if results['telegram'] else "✗ Eșuat" + logging.info(f"Telegram: {status}") + + logging.info("=" * 60) + + except Exception as e: + logging.warning(f"Notificările au eșuat: {e}") + logging.warning("Scraper-ul a finalizat cu succes, dar notificările nu au fost trimise") + # Nu aruncă excepția mai departe - notificările sunt opționale + + def _handle_error(self, error): + """Error handling cu screenshot""" + logging.error("=" * 60) + logging.error(f"EROARE: {error}") + logging.error("=" * 60, exc_info=True) + + # Salvare screenshot la eroare + if self.page and self.config.SCREENSHOT_ON_ERROR: + try: + error_path = Path(self.config.OUTPUT_DIR) / f"error_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" + self.page.screenshot(path=str(error_path)) + logging.error(f"Screenshot eroare salvat: {error_path}") + except Exception as e: + logging.error(f"Nu am putut salva screenshot: {e}") + + +def main(): + """Functie main pentru rulare script""" + try: + scraper = BTGoScraper() + success = scraper.run() + exit(0 if success else 1) + except ValueError as e: + # Eroare de configurare + logging.error(f"Eroare configurare: {e}") + exit(4) + except NotImplementedError as e: + # Selectors nu sunt completati + print(str(e)) + exit(5) + except Exception as e: + logging.error(f"Eroare neasteptata: {e}", exc_info=True) + exit(99) + + +if __name__ == "__main__": + main() diff --git a/config.py b/config.py new file mode 100644 index 0000000..3f5b7ee --- /dev/null +++ b/config.py @@ -0,0 +1,58 @@ +""" +Configurare pentru BTGO Scraper +Citeste configuratii din variabile de mediu (.env) +""" +from dotenv import load_dotenv +import os + +# Incarca variabilele de mediu din fisierul .env +load_dotenv() + + +class Config: + """Clasa pentru gestionarea configuratiilor aplicatiei""" + + # Credentials + BTGO_USERNAME = os.getenv('BTGO_USERNAME') + BTGO_PASSWORD = os.getenv('BTGO_PASSWORD') + + # Browser settings + HEADLESS = os.getenv('HEADLESS', 'false').lower() == 'true' + + # Timeouts + TIMEOUT_2FA_SECONDS = int(os.getenv('TIMEOUT_2FA_SECONDS', 120)) + + # Paths + OUTPUT_DIR = os.getenv('OUTPUT_DIR', './data') + LOG_DIR = './logs' + + # Features + SCREENSHOT_ON_ERROR = os.getenv('SCREENSHOT_ON_ERROR', 'true').lower() == 'true' + DOWNLOAD_TRANSACTIONS = os.getenv('DOWNLOAD_TRANSACTIONS', 'true').lower() == 'true' + LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') + + # Notification settings + ENABLE_NOTIFICATIONS = os.getenv('ENABLE_NOTIFICATIONS', 'false').lower() == 'true' + + # Email settings (SMTP) + EMAIL_ENABLED = os.getenv('EMAIL_ENABLED', 'false').lower() == 'true' + SMTP_SERVER = os.getenv('SMTP_SERVER') + SMTP_PORT = int(os.getenv('SMTP_PORT', 587)) + SMTP_USERNAME = os.getenv('SMTP_USERNAME') + SMTP_PASSWORD = os.getenv('SMTP_PASSWORD') + EMAIL_FROM = os.getenv('EMAIL_FROM') + EMAIL_TO = os.getenv('EMAIL_TO') + + # Telegram settings + TELEGRAM_ENABLED = os.getenv('TELEGRAM_ENABLED', 'false').lower() == 'true' + TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN') + TELEGRAM_CHAT_ID = os.getenv('TELEGRAM_CHAT_ID') + + @classmethod + def validate(cls): + """Valideaza ca toate configuratiile obligatorii sunt setate""" + if not cls.BTGO_USERNAME or not cls.BTGO_PASSWORD: + raise ValueError("BTGO_USERNAME si BTGO_PASSWORD trebuie setate in .env") + + if cls.TIMEOUT_2FA_SECONDS < 30: + raise ValueError("TIMEOUT_2FA_SECONDS trebuie sa fie minim 30 secunde") diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..5799943 --- /dev/null +++ b/data/.gitkeep @@ -0,0 +1 @@ +# Folder pentru rezultate (CSV, JSON, screenshots) diff --git a/deployment/windows/INDEX.md b/deployment/windows/INDEX.md new file mode 100644 index 0000000..39dc5db --- /dev/null +++ b/deployment/windows/INDEX.md @@ -0,0 +1,322 @@ +# 📁 Deployment Scripts - Index + +Structura completă de scripturi pentru deployment Windows Service. + +## 📂 Structura Directoarelor + +``` +deployment/windows/ +├── README.md # Documentație completă +├── QUICK_START.md # Ghid rapid 5 minute +├── INDEX.md # Acest fișier +├── scripts/ # Scripturi PowerShell +│ ├── menu.ps1 # ⭐ Meniu interactiv (START HERE) +│ ├── deploy.ps1 # 🚀 Deployment complet +│ ├── install_service.ps1 # Instalare serviciu Windows +│ ├── uninstall_service.ps1 # Dezinstalare serviciu +│ ├── restart_service.ps1 # Restart rapid serviciu +│ ├── status.ps1 # Status serviciu și logs +│ └── view_logs.ps1 # Viewer logs interactiv +└── tools/ # Binare (auto-download) + ├── .gitignore + └── nssm.exe # Downloaded by install_service +``` + +--- + +## 🚦 Quick Navigation + +| Dacă vrei să... | Rulează | +|-----------------|---------| +| **⭐ Meniu interactiv** | `menu.ps1` | +| **🆕 Instalezi prima dată** | `deploy.ps1` | +| **📊 Verifici statusul** | `status.ps1` | +| **🔄 Restarți serviciul** | `restart_service.ps1` | +| **📝 Vezi logs-uri** | `view_logs.ps1` | +| **🗑️ Dezinstalezi** | `uninstall_service.ps1` | +| **📖 Citești ghidul** | `README.md` sau `QUICK_START.md` | + +**💡 TIP:** Folosește `menu.ps1` pentru o experiență user-friendly! + +--- + +## 📜 Descriere Scripturi + +### 🚀 `deploy.bat` (Main Script) + +**Scop:** Deployment complet one-click + +**Ce face:** +1. Verifică Python și dependențe +2. Configurează `.env` (dacă nu există) +3. Instalează requirements Python +4. Instalează browsere Playwright +5. Creează și pornește serviciul Windows +6. Verifică status final + +**Rulare:** +```batch +cd deployment\windows\scripts +deploy.bat +``` + +**Durată:** 2-5 minute (prima dată) + +**Note:** +- Necesită Administrator +- Interactiv (așteaptă configurare `.env`) + +--- + +### ⚙️ `install_service.bat` + +**Scop:** Instalează DOAR serviciul Windows (fără dependențe) + +**Ce face:** +1. Descarcă NSSM (dacă lipsește) +2. Verifică `.env` și bot script +3. Instalează serviciu cu NSSM +4. Configurează logging, restart policy +5. Pornește serviciul + +**Rulare:** +```batch +install_service.bat +``` + +**Use case:** După ce ai instalat manual Python + requirements + +--- + +### 🗑️ `uninstall_service.bat` + +**Scop:** Elimină serviciul din Windows + +**Ce face:** +1. Oprește serviciul (dacă rulează) +2. Dezinstalează cu NSSM +3. Verifică eliminare completă + +**Rulare:** +```batch +uninstall_service.bat +``` + +**Note:** +- NU șterge fișiere proiect +- NU șterge logs +- Doar elimină serviciul din registry + +--- + +### 🔄 `restart_service.bat` + +**Scop:** Restart rapid serviciu (după modificări cod) + +**Ce face:** +1. Stop serviciu +2. Wait 2 secunde +3. Start serviciu +4. Verifică status + +**Rulare:** +```batch +restart_service.bat +``` + +**Use case:** După `git pull` sau modificări `.env` + +--- + +### 📊 `status.bat` + +**Scop:** Status detaliat serviciu + logs + +**Ce face:** +1. Verifică dacă serviciul există +2. Afișează status detaliat (`sc query`) +3. Afișează configurație (`sc qc`) +4. Afișează ultimele 10 linii din logs +5. Afișează comenzi utile + +**Rulare:** +```batch +status.bat +``` + +**Use case:** Quick health check + +--- + +### 📝 `view_logs.bat` + +**Scop:** Viewer interactiv logs + +**Meniu:** +1. View stdout (toate logs) +2. View stderr (doar erori) +3. Tail stdout (live updates) +4. Tail stderr (live erori) +5. Open Explorer în folder logs +6. Șterge toate logs + +**Rulare:** +```batch +view_logs.bat +``` + +**Use case:** Debugging, monitoring live + +--- + +## 🔧 Configurare Serviciu Windows + +### Detalii Serviciu + +| Proprietate | Valoare | +|------------|---------| +| **Service Name** | `BTGOTelegramBot` | +| **Display Name** | `BTGO Telegram Trigger Bot` | +| **Executable** | `python.exe` | +| **Arguments** | `telegram_trigger_bot.py` | +| **Working Dir** | Project root | +| **Start Type** | Automatic (Delayed) | +| **Restart Policy** | On failure, 5s delay | +| **Stdout Log** | `logs/telegram_bot_stdout.log` | +| **Stderr Log** | `logs/telegram_bot_stderr.log` | +| **Log Rotation** | 10 MB max | + +### Managed by NSSM + +**NSSM** (Non-Sucking Service Manager) este folosit pentru: +- ✅ Simplifică crearea serviciilor din Python scripts +- ✅ Gestionează logging automat +- ✅ Auto-restart la crash +- ✅ Gestiune environment variables + +**Download:** Auto-descărcat de `install_service.bat` în `tools/nssm.exe` + +**Versiune:** 2.24 (64-bit) + +--- + +## 📋 Workflow Tipic + +### Prima Instalare (VM nou) + +```batch +# 1. Clone repo +cd E:\proiecte +git clone btgo-playwright +cd btgo-playwright + +# 2. Configurează .env +copy .env.example .env +notepad .env + +# 3. Deploy complet +cd deployment\windows\scripts +deploy.bat + +# 4. Verifică status +status.bat + +# 5. Test în Telegram +# Trimite /start la bot +``` + +--- + +### Update Cod (după modificări) + +```batch +# 1. Pull changes +cd E:\proiecte\btgo-playwright +git pull + +# 2. Update requirements (dacă s-au modificat) +python -m pip install -r requirements.txt + +# 3. Restart serviciu +deployment\windows\scripts\restart_service.bat + +# 4. Verifică logs +deployment\windows\scripts\view_logs.bat +``` + +--- + +### Debug Issues + +```batch +# 1. Verifică status +deployment\windows\scripts\status.bat + +# 2. Verifică logs +deployment\windows\scripts\view_logs.bat +# Selectează opțiunea 2 (stderr) + +# 3. Testează manual (dacă e nevoie) +net stop BTGOTelegramBot +python telegram_trigger_bot.py +# Vezi output direct în terminal + +# 4. Restart serviciu +deployment\windows\scripts\restart_service.bat +``` + +--- + +### Reinstalare Completă + +```batch +# 1. Dezinstalează +deployment\windows\scripts\uninstall_service.bat + +# 2. Șterge logs vechi (opțional) +del /q logs\*.log + +# 3. Reinstalează +deployment\windows\scripts\deploy.bat +``` + +--- + +## 🛡️ Securitate + +**Protecție `.env`:** +- `.env` este în `.gitignore` (nu se commitează) +- Conține credențiale sensibile (bot token, parole BTGO) +- Setează permissions doar pentru Administrator + +**User IDs Telegram:** +- `TELEGRAM_ALLOWED_USER_IDS` restricționează accesul +- Format: `123456789,987654321` (comma-separated) +- Nu lăsa gol (permite oricui) + +--- + +## 📞 Support + +**Documentație:** +- `README.md` - Documentație completă +- `QUICK_START.md` - Ghid rapid 5 minute +- `../../TELEGRAM_BOT_SETUP.md` - Setup Telegram +- `../../CLAUDE.md` - Documentație proiect + +**Troubleshooting:** +1. Verifică `status.bat` +2. Verifică `view_logs.bat` → stderr +3. Testează manual: `python telegram_trigger_bot.py` + +--- + +## 📄 Changelog + +| Data | Versiune | Modificări | +|------|----------|-----------| +| 2025-11-06 | 1.0 | Initial release - Complete deployment suite | + +--- + +**Happy Deploying! 🚀** diff --git a/deployment/windows/QUICK_START.md b/deployment/windows/QUICK_START.md new file mode 100644 index 0000000..db31b96 --- /dev/null +++ b/deployment/windows/QUICK_START.md @@ -0,0 +1,264 @@ +# 🚀 Quick Start - BTGO Telegram Bot pe VM Windows + +Ghid rapid pentru deployment în 5 minute. + +## ✅ Pre-requisites Checklist + +- [ ] Windows Server/Desktop cu GUI (pentru browser vizibil) +- [ ] Python 3.11+ instalat în PATH +- [ ] Cont Administrator pe Windows +- [ ] Bot Telegram creat cu @BotFather +- [ ] Credențiale BTGO (LOGIN_ID + PAROLA) + +--- + +## 📥 Pas 1: Clone Repository (pe VM) + +```batch +cd E:\proiecte +git clone btgo-playwright +cd btgo-playwright +``` + +Sau copiază manualmente folderul pe VM. + +--- + +## ⚙️ Pas 2: Configurare `.env` + +```batch +REM Copiază template +copy .env.example .env + +REM Editează cu Notepad +notepad .env +``` + +**Configurare minimă necesară:** + +```ini +# TELEGRAM +TELEGRAM_BOT_TOKEN=123456:ABC-DEF... # De la @BotFather +TELEGRAM_ALLOWED_USER_IDS=123456789 # Telegram User ID-ul tău + +# BTGO +LOGIN_ID=your_btgo_username +PAROLA=your_btgo_password + +# SCRAPER +HEADLESS=false # ⚠️ CRITICAL: NU schimba în true! +``` + +**Cum afli User ID-ul tău:** + +```batch +REM După ce ai setat TELEGRAM_BOT_TOKEN în .env +python get_telegram_chat_id.py +REM Trimite un mesaj bot-ului în Telegram +REM Script-ul va afișa User ID-ul +``` + +--- + +## 🚀 Pas 3: Deploy Automat + +**Right-click pe `deploy.ps1` → "Run with PowerShell" (ca Administrator)** + +SAU din PowerShell Administrator: +```powershell +cd deployment\windows\scripts +.\deploy.ps1 +``` + +**Scriptul va instala:** +- ✅ Dependențe Python +- ✅ Playwright browsers +- ✅ Serviciu Windows +- ✅ Auto-start la boot + +**Durată:** ~2-3 minute + +--- + +## ✅ Pas 4: Verificare + +### 1. Verifică serviciul rulează + +```batch +sc query BTGOTelegramBot +``` + +**Output așteptat:** +``` +STATE : 4 RUNNING +``` + +### 2. Verifică logs + +```batch +deployment\windows\scripts\view_logs.bat +``` + +Sau direct: +```batch +type logs\telegram_bot_stdout.log +``` + +**Ar trebui să vezi:** +``` +[2025-11-06 10:15:23] [INFO] Bot inițializat. Useri autorizați: [123456789] +[2025-11-06 10:15:23] [INFO] Bot pornit. Așteaptă comenzi... +``` + +### 3. Testează în Telegram + +1. Deschide Telegram +2. Caută bot-ul tău (numele setat la @BotFather) +3. Trimite `/start` + +**Răspuns așteptat:** +``` +🤖 BTGO Scraper Trigger Bot + +Comenzi disponibile: +• /scrape - Rulează scraper-ul +• /status - Status sistem +• /help - Ajutor +``` + +4. Trimite `/scrape` pentru test complet + +**Flow 2FA:** +- Bot răspunde: "🤖 BTGO Scraper pornit... Așteaptă 2FA pe telefon!" +- Primești notificare pe telefon în George App +- Aprobi 2FA +- Bot trimite fișierele (solduri + tranzacții CSV) + +--- + +## 🎉 Gata! + +Serviciul rulează acum non-stop pe VM. La reboot, pornește automat. + +--- + +## 📋 Comenzi Utile Day-to-Day + +### Meniu Interactiv (Cel mai ușor!) + +```powershell +# Right-click → "Run with PowerShell" (ca Admin) +deployment\windows\scripts\menu.ps1 +``` + +### Comenzi Directe PowerShell + +```powershell +# Status rapid +.\status.ps1 + +# Restart serviciu (după modificări cod) +.\restart_service.ps1 + +# View logs live +.\view_logs.ps1 + +# Oprește serviciu +Stop-Service BTGOTelegramBot + +# Pornește serviciu +Start-Service BTGOTelegramBot + +# Restart serviciu +Restart-Service BTGOTelegramBot +``` + +--- + +## 🔄 Update Cod (după modificări) + +```powershell +# Pe VM +cd E:\proiecte\btgo-playwright +git pull + +# Restart serviciu (PowerShell) +deployment\windows\scripts\restart_service.ps1 + +# SAU folosește meniul interactiv +deployment\windows\scripts\menu.ps1 +# → Selectează opțiunea 6 (Restart) +``` + +--- + +## 🐛 Troubleshooting Rapid + +### Serviciul nu pornește + +```batch +# Verifică logs +type logs\telegram_bot_stderr.log +``` + +**Erori comune:** + +**"ModuleNotFoundError":** +```batch +python -m pip install -r requirements.txt +deployment\windows\scripts\restart_service.bat +``` + +**"TELEGRAM_BOT_TOKEN nu este setat":** +- Verifică `.env` există și conține token-ul +- Token-ul trebuie pe o singură linie, fără spații extra + +**"Access Denied" în browser:** +- Verifică `HEADLESS=false` în `.env` +- Nu folosi Docker/headless mode + +### Bot nu răspunde în Telegram + +1. **Verifică serviciul:** + ```batch + sc query BTGOTelegramBot + ``` + +2. **Testează manual:** + ```batch + REM Oprește serviciul + net stop BTGOTelegramBot + + REM Rulează manual pentru debug + python telegram_trigger_bot.py + + REM Trimite /start în Telegram + REM Vezi output-ul direct în terminal + ``` + +3. **Verifică firewall:** + - Bot-ul trebuie să acceseze `api.telegram.org` (port 443) + +### Scraper-ul timeout-ează la 2FA + +În `.env`: +```ini +TIMEOUT_2FA_SECONDS=600 # Crește la 10 minute +``` + +Apoi restart: +```batch +deployment\windows\scripts\restart_service.bat +``` + +--- + +## 📞 Ajutor Suplimentar + +- **README complet:** `deployment\windows\README.md` +- **Setup Telegram:** `TELEGRAM_BOT_SETUP.md` +- **Documentație proiect:** `CLAUDE.md` + +--- + +**Enjoy! 🎉** diff --git a/deployment/windows/README.md b/deployment/windows/README.md new file mode 100644 index 0000000..85715cc --- /dev/null +++ b/deployment/windows/README.md @@ -0,0 +1,341 @@ +# BTGO Telegram Bot - Windows Service Deployment + +Scripturi pentru instalarea și gestionarea bot-ului Telegram ca serviciu Windows pe VM. + +## 📋 Cerințe Preliminare + +- **Windows Server** 2016+ sau **Windows 10/11** +- **Python 3.11+** instalat și adăugat în PATH +- **Privilegii de Administrator** +- **NSSM** (Non-Sucking Service Manager) - se descarcă automat +- **Git** (opțional, pentru pull updates) + +## 🚀 Deployment Complet (Recomandat) + +```powershell +# Right-click pe script → "Run with PowerShell" (ca Administrator) +deployment\windows\scripts\deploy.ps1 + +# SAU din PowerShell Administrator: +cd deployment\windows\scripts +.\deploy.ps1 +``` + +**Acest script va:** +1. ✅ Verifica Python și dependențe +2. ✅ Copia și configura `.env` (dacă nu există) +3. ✅ Instala dependențele Python (`requirements.txt`) +4. ✅ Instala browsere Playwright +5. ✅ Instala și porni serviciul Windows +6. ✅ Verifica statusul serviciului + +### ⚠️ Înainte de Prima Rulare + +**Configurați `.env`** cu datele tale: + +```bash +# Telegram Bot +TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 +TELEGRAM_ALLOWED_USER_IDS=123456789,987654321 # User IDs autorizați +TELEGRAM_CHAT_ID=123456789 # Optional: pentru notificări + +# BTGO Credentials +LOGIN_ID=your_btgo_login +PAROLA=your_btgo_password + +# Scraper Settings +HEADLESS=false # IMPORTANT: trebuie false pentru VM Windows (WAF bypass) +TIMEOUT_2FA_SECONDS=300 +``` + +**Cum obții variabilele Telegram:** + +1. **Bot Token**: Vorbește cu [@BotFather](https://t.me/BotFather) și creează un bot +2. **User IDs**: Rulează `python get_telegram_chat_id.py` după ce configurezi token-ul + +## 📦 Scripturi Disponibile + +Toate scripturile sunt **PowerShell** (`.ps1`) pentru Windows 10/11. + +### `menu.ps1` - Meniu Interactiv ⭐ +Interfață user-friendly pentru toate operațiunile. + +```powershell +.\menu.ps1 +``` + +--- + +### `deploy.ps1` - Deployment Complet +Instalează tot ce e necesar și pornește serviciul. + +```powershell +.\deploy.ps1 +``` + +--- + +### `install_service.ps1` - Doar Instalare Serviciu +Instalează serviciul Windows (după ce ai configurat manual tot restul). + +```powershell +.\install_service.ps1 +``` + +**Ce face:** +- Descarcă NSSM dacă nu există +- Creează serviciul Windows `BTGOTelegramBot` +- Configurează logging automat în `logs/` +- Configurează auto-restart la crash +- Pornește serviciul + +--- + +### `uninstall_service.ps1` - Dezinstalare Serviciu +Oprește și elimină serviciul din sistem. + +```powershell +.\uninstall_service.ps1 +``` + +--- + +### `restart_service.ps1` - Restart Rapid +Restart rapid al serviciului (pentru aplicarea unor modificări). + +```powershell +.\restart_service.ps1 +``` + +--- + +### `status.ps1` - Status Serviciu +Afișează status detaliat al serviciului și ultimele logs. + +```powershell +.\status.ps1 +``` + +--- + +### `view_logs.ps1` - Viewer Logs Interactiv +Meniu interactiv pentru vizualizarea logs-urilor în timp real. + +```powershell +.\view_logs.ps1 +``` + +**Opțiuni:** +1. View stdout (output normal) +2. View stderr (erori) +3. Tail stdout (timp real) +4. Tail stderr (timp real) +5. Deschide Explorer în director logs +6. Șterge toate logurile + +--- + +## 🔧 Gestionare Serviciu + +### Comenzi Windows Standard + +```batch +# Status serviciu +sc query BTGOTelegramBot + +# Oprește serviciu +net stop BTGOTelegramBot + +# Pornește serviciu +net start BTGOTelegramBot + +# Restart serviciu +net stop BTGOTelegramBot && net start BTGOTelegramBot + +# Configurație serviciu +sc qc BTGOTelegramBot + +# Șterge serviciu (fără script) +sc delete BTGOTelegramBot +``` + +### Logs + +Logs-urile se salvează automat în `logs/`: + +- `telegram_bot_stdout.log` - Output normal (comenzi, mesaje) +- `telegram_bot_stderr.log` - Erori și avertizări + +**View logs în PowerShell (timp real):** + +```powershell +Get-Content logs\telegram_bot_stdout.log -Wait -Tail 20 +``` + +**Rotație logs:** +- Max 10 MB per fișier +- Rotație automată când se atinge limita + +--- + +## 🔄 Update și Redeploy + +### Update Manual (cu Git) + +```batch +# Pe VM Windows +cd E:\proiecte\btgo-playwright + +# Pull ultimele modificări +git pull origin main + +# Redeploy serviciu +deployment\windows\scripts\restart_service.bat +``` + +### Redeploy Complet (Reinstalare) + +Dacă ai modificat dependențe sau structura proiectului: + +```batch +# Dezinstalează +deployment\windows\scripts\uninstall_service.bat + +# Reinstalează tot +deployment\windows\scripts\deploy.bat +``` + +--- + +## 🐛 Troubleshooting + +### Serviciul nu pornește + +**Verifică logs:** +```batch +deployment\windows\scripts\view_logs.bat +``` + +**Probleme comune:** + +1. **Python nu este în PATH** + - Reinstalează Python cu "Add to PATH" bifat + - Sau setează manual PATH în Environment Variables + +2. **`.env` lipsă sau invalid** + - Verifică că `.env` există în root-ul proiectului + - Verifică că `TELEGRAM_BOT_TOKEN` este setat corect + +3. **Playwright browser lipsă** + ```batch + python -m playwright install chromium + ``` + +4. **Port ocupat / Serviciu duplicat** + ```batch + uninstall_service.bat + # Așteaptă 10 secunde + install_service.bat + ``` + +### Bot nu răspunde în Telegram + +1. **Verifică serviciul rulează:** + ```batch + sc query BTGOTelegramBot + ``` + +2. **Verifică logs pentru erori:** + ```batch + type logs\telegram_bot_stderr.log + ``` + +3. **Testează bot-ul manual:** + ```batch + python telegram_trigger_bot.py + # Trimite /start în Telegram + # Ctrl+C pentru a opri + ``` + +4. **Verifică firewall:** + - Bot-ul trebuie să poată accesa `api.telegram.org` (HTTPS/443) + +### Scraper-ul nu funcționează (Access Denied) + +**CRITICAL:** Docker/headless mode NU funcționează din cauza Akamai WAF. + +**Soluție:** +- Setează `HEADLESS=false` în `.env` +- Bot-ul TREBUIE să ruleze pe Windows cu browser vizibil +- Asigură-te că VM-ul are desktop environment activ + +### Performance Issues + +**Bot lent / Scraper timeout:** +- Crește `TIMEOUT_2FA_SECONDS` în `.env` +- Verifică resursele VM-ului (CPU, RAM) +- Verifică conexiunea la internet a VM-ului + +--- + +## 📊 Arhitectura Serviciului + +``` +BTGOTelegramBot (Windows Service) +├── Managed by: NSSM +├── Auto-start: Yes (Delayed) +├── Restart on crash: Yes (5s delay) +├── Working dir: E:\proiecte\btgo-playwright +├── Python script: telegram_trigger_bot.py +├── Logs: logs\telegram_bot_*.log +└── Config: .env +``` + +**Flow:** +``` +User /scrape → Telegram API → Bot Service → btgo_scraper.py + ├── Playwright login + ├── 2FA wait + ├── Extract accounts + ├── Download CSVs + └── Send files via notifiers +``` + +--- + +## 🔐 Securitate + +### Best Practices + +1. **Restricționează accesul bot-ului:** + - Setează `TELEGRAM_ALLOWED_USER_IDS` în `.env` + - Nu lăsa lista goală (permite oricui) + +2. **Protejează `.env`:** + - Nu comite `.env` în Git (este în `.gitignore`) + - Setează permisiuni restrictive pe VM + +3. **Monitorizează logs-urile:** + - Verifică periodic `stderr.log` pentru erori + - Setup alerting pentru crash-uri + +4. **Rotație credentials:** + - Regenerează `TELEGRAM_BOT_TOKEN` periodic + - Update `LOGIN_ID`/`PAROLA` la schimbări + +--- + +## 📞 Support + +**Probleme sau întrebări?** + +1. Verifică logs: `deployment\windows\scripts\view_logs.bat` +2. Verifică status: `deployment\windows\scripts\status.bat` +3. Consultă documentația principală: `TELEGRAM_BOT_SETUP.md` +4. Check issue tracker: [GitHub Issues](../../../) + +--- + +## 📄 Licență + +Acest tool este pentru **uz personal și educațional**. Asigură-te că respecti termenii și condițiile băncii tale. diff --git a/deployment/windows/scripts/deploy.ps1 b/deployment/windows/scripts/deploy.ps1 new file mode 100644 index 0000000..1a2c113 --- /dev/null +++ b/deployment/windows/scripts/deploy.ps1 @@ -0,0 +1,282 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + BTGO Telegram Bot - Complete Deployment Script +.DESCRIPTION + Deployment complet pentru bot-ul Telegram pe Windows VM + Verifica dependente, instaleaza requirements, configureaza serviciul +.NOTES + Rulare: Right-click → "Run with PowerShell" (ca Administrator) +#> + +# Error handling +$ErrorActionPreference = "Stop" + +# ============================================================================ +# FUNCTIONS +# ============================================================================ + +function Write-ColorOutput { + param( + [string]$Message, + [string]$Color = "White", + [string]$Prefix = "" + ) + + $prefixColor = switch ($Prefix) { + "[OK]" { "Green" } + "[INFO]" { "Cyan" } + "[EROARE]" { "Red" } + "[AVERTIZARE]" { "Yellow" } + default { "White" } + } + + if ($Prefix) { + Write-Host $Prefix -ForegroundColor $prefixColor -NoNewline + Write-Host " $Message" -ForegroundColor $Color + } else { + Write-Host $Message -ForegroundColor $Color + } +} + +function Write-Separator { + param([string]$Title = "") + Write-Host "" + Write-Host "=" * 80 -ForegroundColor Cyan + if ($Title) { + Write-Host $Title -ForegroundColor Yellow + Write-Host "=" * 80 -ForegroundColor Cyan + } +} + +# ============================================================================ +# CONFIGURATION +# ============================================================================ + +$ScriptDir = Split-Path -Parent $PSCommandPath +$ProjectDir = Resolve-Path (Join-Path $ScriptDir "..\..\..") | Select-Object -ExpandProperty Path +$ServiceName = "BTGOTelegramBot" + +Write-Separator "BTGO TELEGRAM BOT - DEPLOYMENT COMPLET" +Write-Host "" +Write-ColorOutput "Director proiect: $ProjectDir" -Prefix "[INFO]" +Write-Host "" + +# ============================================================================ +# [1/6] VERIFICARE PYTHON +# ============================================================================ + +Write-Separator "[1/6] VERIFICARE PYTHON" + +try { + $pythonVersion = python --version 2>&1 + if ($LASTEXITCODE -ne 0) { throw } + Write-ColorOutput "Python gasit: $pythonVersion" -Prefix "[OK]" +} catch { + Write-ColorOutput "Python nu este instalat sau nu este in PATH!" -Prefix "[EROARE]" + Write-Host "" + Write-Host "Instaleaza Python 3.11+ de la: https://www.python.org/downloads/" + Write-Host "Asigura-te ca bifezi 'Add Python to PATH' la instalare" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +# ============================================================================ +# [2/6] VERIFICARE FISIERE PROIECT +# ============================================================================ + +Write-Separator "[2/6] VERIFICARE FISIERE PROIECT" + +$criticalFiles = @( + "telegram_trigger_bot.py", + "btgo_scraper.py", + "requirements.txt" +) + +$filesOk = $true +foreach ($file in $criticalFiles) { + $filePath = Join-Path $ProjectDir $file + if (Test-Path $filePath) { + Write-ColorOutput "$file - OK" -Prefix "[OK]" + } else { + Write-ColorOutput "$file - LIPSESTE!" -Prefix "[EROARE]" + $filesOk = $false + } +} + +if (-not $filesOk) { + Write-ColorOutput "Fisiere critice lipsesc din proiect!" -Prefix "[EROARE]" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +# ============================================================================ +# [3/6] VERIFICARE/CREARE .env +# ============================================================================ + +Write-Separator "[3/6] VERIFICARE CONFIGURARE (.env)" + +$envFile = Join-Path $ProjectDir ".env" +$envExample = Join-Path $ProjectDir ".env.example" + +if (-not (Test-Path $envFile)) { + if (Test-Path $envExample) { + Write-ColorOutput ".env nu exista. Copiem .env.example..." -Prefix "[INFO]" + Copy-Item $envExample $envFile + Write-ColorOutput ".env creat din .env.example" -Prefix "[OK]" + Write-Host "" + Write-ColorOutput "IMPORTANT: Editeaza .env si configureaza:" -Prefix "[AVERTIZARE]" + Write-Host " - TELEGRAM_BOT_TOKEN" + Write-Host " - TELEGRAM_ALLOWED_USER_IDS" + Write-Host " - TELEGRAM_CHAT_ID (optional)" + Write-Host " - LOGIN_ID si PAROLA pentru BTGO" + Write-Host "" + Read-Host "Apasa Enter dupa ce ai configurat .env" + } else { + Write-ColorOutput ".env si .env.example lipsesc!" -Prefix "[EROARE]" + Write-Host "Creaza manual .env cu variabilele necesare" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 + } +} + +# Verifică variabile critice +$envContent = Get-Content $envFile -Raw +if ($envContent -notmatch "TELEGRAM_BOT_TOKEN=(?!your_).+") { + Write-ColorOutput "TELEGRAM_BOT_TOKEN nu este configurat in .env!" -Prefix "[EROARE]" + Write-Host "Editeaza .env si adauga token-ul de la @BotFather" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +if ($envContent -notmatch "LOGIN_ID=(?!your_).+") { + Write-ColorOutput "LOGIN_ID nu este configurat in .env!" -Prefix "[AVERTIZARE]" + Write-Host "Scraper-ul nu va functiona fara credentiale BTGO" +} + +Write-ColorOutput ".env exista si pare configurat" -Prefix "[OK]" + +# ============================================================================ +# [4/6] INSTALARE DEPENDENTE PYTHON +# ============================================================================ + +Write-Separator "[4/6] INSTALARE DEPENDENTE PYTHON" + +try { + Write-ColorOutput "Upgrade pip..." -Prefix "[INFO]" + python -m pip install --upgrade pip --quiet + + Write-ColorOutput "Instalare requirements.txt..." -Prefix "[INFO]" + $requirementsFile = Join-Path $ProjectDir "requirements.txt" + python -m pip install -r $requirementsFile + + if ($LASTEXITCODE -ne 0) { throw } + Write-ColorOutput "Dependente instalate cu succes" -Prefix "[OK]" +} catch { + Write-ColorOutput "Instalarea dependentelor a esuat!" -Prefix "[EROARE]" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +# ============================================================================ +# [5/6] INSTALARE PLAYWRIGHT BROWSERS (GLOBAL) +# ============================================================================ + +Write-Separator "[5/6] INSTALARE PLAYWRIGHT BROWSERS (GLOBAL)" + +# Setează locație globală pentru browsere (accesibilă pentru serviciul SYSTEM) +$globalBrowserPath = "C:\playwright-browsers" + +Write-ColorOutput "Configurare locatie globala browsere: $globalBrowserPath" -Prefix "[INFO]" + +# Creează directorul dacă nu există +if (-not (Test-Path $globalBrowserPath)) { + New-Item -ItemType Directory -Path $globalBrowserPath -Force | Out-Null + Write-ColorOutput "Director creat: $globalBrowserPath" -Prefix "[OK]" +} + +# Acordă permisiuni SYSTEM la director +Write-ColorOutput "Acordare permisiuni SYSTEM..." -Prefix "[INFO]" +icacls $globalBrowserPath /grant "SYSTEM:(OI)(CI)F" /T 2>&1 | Out-Null + +# Setează environment variable la nivel de sistem +[System.Environment]::SetEnvironmentVariable("PLAYWRIGHT_BROWSERS_PATH", $globalBrowserPath, [System.EnvironmentVariableTarget]::Machine) +$env:PLAYWRIGHT_BROWSERS_PATH = $globalBrowserPath +Write-ColorOutput "Environment variable setata: PLAYWRIGHT_BROWSERS_PATH" -Prefix "[OK]" + +# Instalează browserele +try { + Write-ColorOutput "Instalare browsere Playwright in $globalBrowserPath..." -Prefix "[INFO]" + Write-Host "Aceasta poate dura 1-2 minute..." -ForegroundColor Gray + python -m playwright install chromium + + if ($LASTEXITCODE -ne 0) { throw } + Write-ColorOutput "Playwright browsere instalate cu succes!" -Prefix "[OK]" +} catch { + Write-ColorOutput "Instalarea browserelor Playwright a esuat!" -Prefix "[AVERTIZARE]" + Write-Host "Scraper-ul poate sa nu functioneze corect" +} + +# ============================================================================ +# CREARE DIRECTOARE NECESARE +# ============================================================================ + +$dataDir = Join-Path $ProjectDir "data" +$logsDir = Join-Path $ProjectDir "logs" + +if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null } +if (-not (Test-Path $logsDir)) { New-Item -ItemType Directory -Path $logsDir -Force | Out-Null } + +Write-ColorOutput "Directoare create: data, logs" -Prefix "[OK]" + +# ============================================================================ +# [6/6] INSTALARE/RESTART SERVICIU +# ============================================================================ + +Write-Separator "[6/6] INSTALARE SERVICIU WINDOWS" + +# Verifică dacă serviciul există +$service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + +if ($service) { + Write-ColorOutput "Serviciul exista deja. Reinstalam..." -Prefix "[INFO]" + & "$ScriptDir\uninstall_service.ps1" + Start-Sleep -Seconds 3 +} + +Write-ColorOutput "Rulare install_service.ps1..." -Prefix "[INFO]" +& "$ScriptDir\install_service.ps1" + +if ($LASTEXITCODE -ne 0) { + Write-ColorOutput "Instalarea serviciului a esuat!" -Prefix "[EROARE]" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +# ============================================================================ +# FINALIZARE +# ============================================================================ + +Write-Separator "[SUCCES] DEPLOYMENT FINALIZAT!" +Write-Host "" +Write-Host "Serviciul BTGO Telegram Bot este acum instalat si ruleaza." -ForegroundColor Green +Write-Host "" +Write-Host "Comenzi utile:" -ForegroundColor Yellow +Write-Host " - Status: Get-Service $ServiceName" +Write-Host " - Logs: Get-Content '$logsDir\telegram_bot_stdout.log' -Tail 20" +Write-Host " - Opreste: Stop-Service $ServiceName" +Write-Host " - Porneste: Start-Service $ServiceName" +Write-Host " - Restart: Restart-Service $ServiceName" +Write-Host " - Dezinstala: .\uninstall_service.ps1" +Write-Host "" +Write-Host "Teste bot Telegram:" -ForegroundColor Yellow +Write-Host " 1. Deschide Telegram si cauta bot-ul tau" +Write-Host " 2. Trimite /start pentru a vedea comenzile" +Write-Host " 3. Trimite /scrape pentru a testa scraper-ul" +Write-Host "" +Write-Host "Verificare logs in timp real:" -ForegroundColor Yellow +Write-Host " Get-Content '$logsDir\telegram_bot_stdout.log' -Wait" +Write-Host "" +Write-Separator + +Read-Host "`nApasa Enter pentru a inchide" diff --git a/deployment/windows/scripts/install_service.ps1 b/deployment/windows/scripts/install_service.ps1 new file mode 100644 index 0000000..723b878 --- /dev/null +++ b/deployment/windows/scripts/install_service.ps1 @@ -0,0 +1,275 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + BTGO Telegram Bot - Service Installation Script +.DESCRIPTION + Instalează bot-ul Telegram ca serviciu Windows folosind NSSM +.NOTES + Rulare: Right-click → "Run with PowerShell" (ca Administrator) +#> + +$ErrorActionPreference = "Stop" + +# ============================================================================ +# FUNCTIONS +# ============================================================================ + +function Write-ColorOutput { + param( + [string]$Message, + [string]$Prefix = "" + ) + $color = switch ($Prefix) { + "[OK]" { "Green" } + "[INFO]" { "Cyan" } + "[EROARE]" { "Red" } + "[AVERTIZARE]" { "Yellow" } + default { "White" } + } + if ($Prefix) { + Write-Host "$Prefix " -ForegroundColor $color -NoNewline + Write-Host $Message + } else { + Write-Host $Message + } +} + +function Write-Separator { + param([string]$Title = "") + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + if ($Title) { + Write-Host $Title -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + } +} + +# ============================================================================ +# CONFIGURATION +# ============================================================================ + +$ServiceName = "BTGOTelegramBot" +$ServiceDisplayName = "BTGO Telegram Trigger Bot" +$ServiceDescription = "Bot Telegram pentru declansarea BTGO Scraper prin comanda /scrape" + +$ScriptDir = Split-Path -Parent $PSCommandPath +$ProjectDir = Resolve-Path (Join-Path $ScriptDir "..\..\..") | Select-Object -ExpandProperty Path +$BotScript = Join-Path $ProjectDir "telegram_trigger_bot.py" +$EnvFile = Join-Path $ProjectDir ".env" +$LogDir = Join-Path $ProjectDir "logs" +$ToolsDir = Join-Path $ProjectDir "deployment\windows\tools" +$NssmPath = Join-Path $ToolsDir "nssm.exe" + +Write-Separator "BTGO TELEGRAM BOT - INSTALARE SERVICIU" +Write-ColorOutput "Director proiect: $ProjectDir" -Prefix "[INFO]" + +# ============================================================================ +# VERIFICARI PRE-INSTALARE +# ============================================================================ + +Write-Separator "VERIFICARI PRE-INSTALARE" + +# Verifică Python +try { + $pythonPath = (Get-Command python).Source + Write-ColorOutput "Python: $pythonPath" -Prefix "[OK]" +} catch { + Write-ColorOutput "Python nu a fost gasit in PATH!" -Prefix "[EROARE]" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +# Verifică script bot +if (Test-Path $BotScript) { + Write-ColorOutput "Script bot: $BotScript" -Prefix "[OK]" +} else { + Write-ColorOutput "Script bot nu a fost gasit: $BotScript" -Prefix "[EROARE]" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +# Verifică .env +if (Test-Path $EnvFile) { + Write-ColorOutput "Fisier .env: $EnvFile" -Prefix "[OK]" +} else { + Write-ColorOutput "Fisier .env nu a fost gasit: $EnvFile" -Prefix "[EROARE]" + Write-Host "Copiaza .env.example la .env si configureaza-l" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +# Verifică variabile critice din .env +$envContent = Get-Content $EnvFile -Raw +if ($envContent -match "TELEGRAM_BOT_TOKEN=.+") { + Write-ColorOutput "TELEGRAM_BOT_TOKEN configurat in .env" -Prefix "[OK]" +} else { + Write-ColorOutput "TELEGRAM_BOT_TOKEN nu este setat in .env!" -Prefix "[EROARE]" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +# Creează director logs +if (-not (Test-Path $LogDir)) { + New-Item -ItemType Directory -Path $LogDir -Force | Out-Null + Write-ColorOutput "Director logs creat: $LogDir" -Prefix "[INFO]" +} + +# ============================================================================ +# DOWNLOAD NSSM (daca nu exista) +# ============================================================================ + +if (-not (Test-Path $NssmPath)) { + Write-Separator "DOWNLOAD NSSM" + Write-ColorOutput "NSSM nu a fost gasit. Descarcam..." -Prefix "[INFO]" + + if (-not (Test-Path $ToolsDir)) { + New-Item -ItemType Directory -Path $ToolsDir -Force | Out-Null + } + + try { + $nssmUrl = "https://nssm.cc/release/nssm-2.24.zip" + $nssmZip = Join-Path $env:TEMP "nssm.zip" + $nssmExtract = Join-Path $env:TEMP "nssm" + + Write-ColorOutput "Descarcare NSSM..." -Prefix "[INFO]" + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri $nssmUrl -OutFile $nssmZip -UseBasicParsing + + Write-ColorOutput "Extragere NSSM..." -Prefix "[INFO]" + Expand-Archive -Path $nssmZip -DestinationPath $nssmExtract -Force + + # Copiază executabilul + $nssmExe = Join-Path $nssmExtract "nssm-2.24\win64\nssm.exe" + Copy-Item $nssmExe $NssmPath + + # Curăță + Remove-Item $nssmZip -Force -ErrorAction SilentlyContinue + Remove-Item $nssmExtract -Recurse -Force -ErrorAction SilentlyContinue + + Write-ColorOutput "NSSM descarcat: $NssmPath" -Prefix "[OK]" + } catch { + Write-ColorOutput "Descarcarea NSSM a esuat!" -Prefix "[EROARE]" + Write-Host "Descarca manual de la: https://nssm.cc/download" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 + } +} + +# ============================================================================ +# INSTALARE SERVICIU +# ============================================================================ + +Write-Separator "INSTALARE SERVICIU WINDOWS" + +# Verifică dacă serviciul există deja +$existingService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if ($existingService) { + Write-ColorOutput "Serviciul $ServiceName exista deja!" -Prefix "[AVERTIZARE]" + Write-ColorOutput "Ruland dezinstalare mai intai..." -Prefix "[INFO]" + & "$ScriptDir\uninstall_service.ps1" + Start-Sleep -Seconds 3 +} + +Write-ColorOutput "Instalare serviciu: $ServiceName" -Prefix "[INFO]" + +# Instalează serviciul +& $NssmPath install $ServiceName $pythonPath $BotScript + +if ($LASTEXITCODE -ne 0) { + Write-ColorOutput "Instalarea serviciului a esuat!" -Prefix "[EROARE]" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +# ============================================================================ +# CONFIGURARE SERVICIU +# ============================================================================ + +Write-ColorOutput "Configurare serviciu..." -Prefix "[INFO]" + +# Display name si descriere +& $NssmPath set $ServiceName DisplayName $ServiceDisplayName +& $NssmPath set $ServiceName Description $ServiceDescription + +# Verifică și configurează Playwright browsers global (dacă nu e deja setat) +$globalBrowserPath = [System.Environment]::GetEnvironmentVariable("PLAYWRIGHT_BROWSERS_PATH", [System.EnvironmentVariableTarget]::Machine) + +if (-not $globalBrowserPath) { + Write-ColorOutput "PLAYWRIGHT_BROWSERS_PATH nu este setat!" -Prefix "[AVERTIZARE]" + Write-Host "Ruleaza 'deploy.ps1' pentru instalare completa cu browsere globale" +} else { + Write-ColorOutput "Playwright browsere: $globalBrowserPath" -Prefix "[OK]" +} + +# Working directory +& $NssmPath set $ServiceName AppDirectory $ProjectDir + +# Stdout/Stderr logging +$stdoutLog = Join-Path $LogDir "telegram_bot_stdout.log" +$stderrLog = Join-Path $LogDir "telegram_bot_stderr.log" +& $NssmPath set $ServiceName AppStdout $stdoutLog +& $NssmPath set $ServiceName AppStderr $stderrLog + +# Rotatie logs (10 MB max) +& $NssmPath set $ServiceName AppStdoutCreationDisposition 4 +& $NssmPath set $ServiceName AppStderrCreationDisposition 4 +& $NssmPath set $ServiceName AppRotateFiles 1 +& $NssmPath set $ServiceName AppRotateOnline 1 +& $NssmPath set $ServiceName AppRotateBytes 10485760 + +# Restart policy (restart la crash) +& $NssmPath set $ServiceName AppExit Default Restart +& $NssmPath set $ServiceName AppRestartDelay 5000 + +# Start mode (Automatic Delayed Start) +& $NssmPath set $ServiceName Start SERVICE_DELAYED_AUTO_START + +Write-ColorOutput "Serviciu configurat cu succes!" -Prefix "[OK]" + +# ============================================================================ +# PORNIRE SERVICIU +# ============================================================================ + +Write-Separator "PORNIRE SERVICIU" + +Write-ColorOutput "Pornire serviciu: $ServiceName" -Prefix "[INFO]" +Start-Service -Name $ServiceName + +if ((Get-Service -Name $ServiceName).Status -ne "Running") { + Write-ColorOutput "Pornirea serviciului a esuat!" -Prefix "[EROARE]" + Write-Host "Verifica logurile in: $LogDir" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +Start-Sleep -Seconds 2 + +# ============================================================================ +# VERIFICARE STATUS +# ============================================================================ + +$service = Get-Service -Name $ServiceName + +if ($service.Status -eq "Running") { + Write-Separator "[SUCCES] SERVICIU INSTALAT SI PORNIT!" + Write-Host "" + Write-Host "Serviciu: $ServiceName" -ForegroundColor Green + Write-Host "Display: $ServiceDisplayName" -ForegroundColor Green + Write-Host "Script: $BotScript" -ForegroundColor Green + Write-Host "Logs: $LogDir" -ForegroundColor Green + Write-Host "" + Write-Host "Comenzi utile:" -ForegroundColor Yellow + Write-Host " - Opreste: Stop-Service $ServiceName" + Write-Host " - Porneste: Start-Service $ServiceName" + Write-Host " - Restart: Restart-Service $ServiceName" + Write-Host " - Status: Get-Service $ServiceName" + Write-Host " - Dezinstaleaza: .\uninstall_service.ps1" + Write-Host "" + Write-Separator +} else { + Write-ColorOutput "Serviciul este instalat dar nu ruleaza!" -Prefix "[AVERTIZARE]" + Write-Host "Verifica logurile: $LogDir" + Get-Service -Name $ServiceName | Format-List +} + +Read-Host "`nApasa Enter pentru a inchide" diff --git a/deployment/windows/scripts/menu.ps1 b/deployment/windows/scripts/menu.ps1 new file mode 100644 index 0000000..4df9501 --- /dev/null +++ b/deployment/windows/scripts/menu.ps1 @@ -0,0 +1,314 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + BTGO Telegram Bot - Main Menu Launcher +.DESCRIPTION + Meniu interactiv pentru toate scripturile de management +.NOTES + Rulare: Right-click → "Run with PowerShell" (ca Administrator) +#> + +$ErrorActionPreference = "Continue" +$ScriptDir = Split-Path -Parent $PSCommandPath +$ServiceName = "BTGOTelegramBot" +$ProjectDir = Resolve-Path (Join-Path $ScriptDir "..\..\..") | Select-Object -ExpandProperty Path +$LogDir = Join-Path $ProjectDir "logs" + +function Show-MainMenu { + Clear-Host + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host " BTGO TELEGRAM BOT - MANAGEMENT MENU" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + + # Verifică status serviciu + $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + if ($service) { + if ($service.Status -eq "Running") { + Write-Host "Status: " -NoNewline + Write-Host "[RUNNING]" -ForegroundColor Green + } else { + Write-Host "Status: " -NoNewline + Write-Host "[STOPPED]" -ForegroundColor Yellow + } + } else { + Write-Host "Status: " -NoNewline + Write-Host "[NOT INSTALLED]" -ForegroundColor Red + } + + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + + Write-Host " [1] Deploy Complet (First Time Setup)" -ForegroundColor Cyan + Write-Host " [2] Install Service Only" -ForegroundColor Cyan + Write-Host " [3] Uninstall Service" -ForegroundColor Cyan + Write-Host "" + Write-Host " [4] Start Service" -ForegroundColor Green + Write-Host " [5] Stop Service" -ForegroundColor Yellow + Write-Host " [6] Restart Service" -ForegroundColor Magenta + Write-Host " [7] Service Status" -ForegroundColor White + Write-Host "" + Write-Host " [8] View Logs (Interactive)" -ForegroundColor White + Write-Host " [9] Quick Log Tail (Stdout)" -ForegroundColor White + Write-Host "" + Write-Host " Development & Manual Testing:" -ForegroundColor DarkCyan + Write-Host " [S] Setup Development Environment" -ForegroundColor Cyan + Write-Host " [R] Run Scraper (Manual)" -ForegroundColor Cyan + Write-Host " [T] Run Telegram Bot (Manual)" -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 + Write-Host " [D] Open Logs in Explorer" -ForegroundColor Gray + Write-Host " [U] Update Browsers (Playwright)" -ForegroundColor Gray + Write-Host "" + Write-Host " [0] Exit" -ForegroundColor DarkGray + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" +} + +function Invoke-Deploy { + Clear-Host + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "RULARE DEPLOY COMPLET" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + & "$ScriptDir\deploy.ps1" + Read-Host "`nApasa Enter pentru a reveni la meniu" +} + +function Invoke-Install { + Clear-Host + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "INSTALARE SERVICIU" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + & "$ScriptDir\install_service.ps1" + Read-Host "`nApasa Enter pentru a reveni la meniu" +} + +function Invoke-Uninstall { + Clear-Host + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "DEZINSTALARE SERVICIU" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + & "$ScriptDir\uninstall_service.ps1" + Read-Host "`nApasa Enter pentru a reveni la meniu" +} + +function Invoke-Start { + Clear-Host + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "START SERVICIU" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + + $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + if (-not $service) { + Write-Host "[EROARE] Serviciul $ServiceName nu este instalat!" -ForegroundColor Red + Start-Sleep -Seconds 2 + return + } + + Write-Host "[INFO] Pornire serviciu..." -ForegroundColor Cyan + try { + Start-Service -Name $ServiceName + Write-Host "" + Write-Host "[SUCCES] Serviciu pornit!" -ForegroundColor Green + } catch { + Write-Host "" + Write-Host "[EROARE] Pornirea a esuat: $_" -ForegroundColor Red + } + Start-Sleep -Seconds 3 +} + +function Invoke-Stop { + Clear-Host + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "STOP SERVICIU" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + + $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + if (-not $service) { + Write-Host "[EROARE] Serviciul $ServiceName nu este instalat!" -ForegroundColor Red + Start-Sleep -Seconds 2 + return + } + + Write-Host "[INFO] Oprire serviciu..." -ForegroundColor Cyan + try { + Stop-Service -Name $ServiceName -Force + Write-Host "" + Write-Host "[SUCCES] Serviciu oprit!" -ForegroundColor Green + } catch { + Write-Host "" + Write-Host "[INFO] Serviciul era deja oprit" -ForegroundColor Yellow + } + Start-Sleep -Seconds 3 +} + +function Invoke-Restart { + Clear-Host + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "RESTART SERVICIU" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + & "$ScriptDir\restart_service.ps1" + Read-Host "`nApasa Enter pentru a reveni la meniu" +} + +function Invoke-Status { + Clear-Host + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "STATUS SERVICIU" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + & "$ScriptDir\status.ps1" + Read-Host "`nApasa Enter pentru a reveni la meniu" +} + +function Invoke-ViewLogs { + Clear-Host + & "$ScriptDir\view_logs.ps1" +} + +function Invoke-TailLogs { + Clear-Host + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "LOG TAIL - STDOUT (LIVE)" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "Apasa Ctrl+C pentru a opri" -ForegroundColor Yellow + Write-Host "" + + $stdoutLog = Join-Path $LogDir "telegram_bot_stdout.log" + if (Test-Path $stdoutLog) { + Get-Content $stdoutLog -Wait -Tail 30 + } else { + Write-Host "[INFO] Log-ul nu exista inca" -ForegroundColor Yellow + Start-Sleep -Seconds 2 + } +} + +function Open-ReadmeFile { + $readme = Join-Path $ProjectDir "deployment\windows\README.md" + if (Test-Path $readme) { + Start-Process $readme + } +} + +function Open-QuickStartFile { + $quickstart = Join-Path $ProjectDir "deployment\windows\QUICK_START.md" + if (Test-Path $quickstart) { + Start-Process $quickstart + } +} + +function Open-ProjectExplorer { + Start-Process explorer $ProjectDir +} + +function Open-LogsExplorer { + if (Test-Path $LogDir) { + Start-Process explorer $LogDir + } else { + Write-Host "[INFO] Directorul logs nu exista inca" -ForegroundColor Yellow + Start-Sleep -Seconds 2 + } +} + +function Invoke-SetupDev { + Clear-Host + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "SETUP DEVELOPMENT ENVIRONMENT" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + & "$ScriptDir\setup_dev.ps1" +} + +function Invoke-RunScraper { + Clear-Host + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "RUN SCRAPER (MANUAL MODE)" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + & "$ScriptDir\run_scraper.ps1" +} + +function Invoke-RunTelegramBotManual { + Clear-Host + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "RUN TELEGRAM BOT (MANUAL MODE)" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + & "$ScriptDir\run_telegram_bot_manual.ps1" +} + +function Invoke-UpdateBrowsers { + Clear-Host + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "UPDATE PLAYWRIGHT BROWSERS" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + & "$ScriptDir\update_browsers.ps1" + Read-Host "`nApasa Enter pentru a reveni la meniu" +} + +# Main loop +do { + Show-MainMenu + $choice = Read-Host "Selecteaza optiune" + + switch ($choice) { + "1" { Invoke-Deploy } + "2" { Invoke-Install } + "3" { Invoke-Uninstall } + "4" { Invoke-Start } + "5" { Invoke-Stop } + "6" { Invoke-Restart } + "7" { Invoke-Status } + "8" { Invoke-ViewLogs } + "9" { Invoke-TailLogs } + "A" { Open-ReadmeFile } + "a" { Open-ReadmeFile } + "B" { Open-QuickStartFile } + "b" { Open-QuickStartFile } + "C" { Open-ProjectExplorer } + "c" { Open-ProjectExplorer } + "D" { Open-LogsExplorer } + "d" { Open-LogsExplorer } + "S" { Invoke-SetupDev } + "s" { Invoke-SetupDev } + "R" { Invoke-RunScraper } + "r" { Invoke-RunScraper } + "T" { Invoke-RunTelegramBotManual } + "t" { Invoke-RunTelegramBotManual } + "U" { Invoke-UpdateBrowsers } + "u" { Invoke-UpdateBrowsers } + "0" { + Write-Host "" + Write-Host "Goodbye!" -ForegroundColor Green + exit 0 + } + default { + Write-Host "[EROARE] Optiune invalida!" -ForegroundColor Red + Start-Sleep -Seconds 1 + } + } +} while ($true) diff --git a/deployment/windows/scripts/restart_service.ps1 b/deployment/windows/scripts/restart_service.ps1 new file mode 100644 index 0000000..c9fbe45 --- /dev/null +++ b/deployment/windows/scripts/restart_service.ps1 @@ -0,0 +1,87 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + BTGO Telegram Bot - Service Restart Script +.DESCRIPTION + Restart rapid al serviciului +#> + +$ErrorActionPreference = "Stop" +$ServiceName = "BTGOTelegramBot" + +function Write-ColorOutput { + param([string]$Message, [string]$Prefix = "") + $color = switch ($Prefix) { + "[OK]" { "Green" } + "[INFO]" { "Cyan" } + "[EROARE]" { "Red" } + default { "White" } + } + if ($Prefix) { + Write-Host "$Prefix " -ForegroundColor $color -NoNewline + Write-Host $Message + } else { + Write-Host $Message + } +} + +function Write-Separator { + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "RESTART SERVICIU: $ServiceName" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" +} + +Write-Separator + +# Verifică dacă serviciul există +$service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + +if (-not $service) { + Write-ColorOutput "Serviciul $ServiceName nu este instalat!" -Prefix "[EROARE]" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +# Oprește serviciul +Write-ColorOutput "Oprire serviciu..." -Prefix "[INFO]" +try { + Stop-Service -Name $ServiceName -Force + Write-ColorOutput "Serviciu oprit" -Prefix "[OK]" +} catch { + Write-ColorOutput "Oprirea a esuat. Incercam oprire fortata..." -Prefix "[AVERTIZARE]" + sc.exe stop $ServiceName | Out-Null +} + +Start-Sleep -Seconds 2 + +# Pornește serviciul +Write-ColorOutput "Pornire serviciu..." -Prefix "[INFO]" +try { + Start-Service -Name $ServiceName + Write-ColorOutput "Serviciu pornit" -Prefix "[OK]" +} catch { + Write-ColorOutput "Pornirea serviciului a esuat!" -Prefix "[EROARE]" + Write-Host "Verificati logurile pentru detalii" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +Start-Sleep -Seconds 2 + +# Verifică status +$service = Get-Service -Name $ServiceName + +if ($service.Status -eq "Running") { + Write-Host "" + Write-ColorOutput "[SUCCES] Serviciu restartat cu succes!" -ForegroundColor Green + Write-Host "" + Get-Service -Name $ServiceName | Format-Table -AutoSize +} else { + Write-Host "" + Write-ColorOutput "Serviciul nu ruleaza dupa restart!" -Prefix "[EROARE]" + Get-Service -Name $ServiceName | Format-Table -AutoSize +} + +Read-Host "`nApasa Enter pentru a inchide" diff --git a/deployment/windows/scripts/run_scraper.ps1 b/deployment/windows/scripts/run_scraper.ps1 new file mode 100644 index 0000000..2c7c53a --- /dev/null +++ b/deployment/windows/scripts/run_scraper.ps1 @@ -0,0 +1,111 @@ +<# +.SYNOPSIS + BTGO Scraper - Rulare Development +.DESCRIPTION + Ruleaza scraper-ul BTGO in modul development (cu browser vizibil) +.NOTES + Echivalent PowerShell pentru scripts\run_dev.bat + Rulare: Right-click → "Run with PowerShell" +#> + +$ErrorActionPreference = "Stop" +$ScriptDir = Split-Path -Parent $PSCommandPath +$ProjectDir = Resolve-Path (Join-Path $ScriptDir "..\..\..") | Select-Object -ExpandProperty Path + +Set-Location $ProjectDir + +Write-Host "" +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host " BTGO SCRAPER - RULARE DEVELOPMENT" -ForegroundColor Yellow +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "" +Write-Host "Working directory: $ProjectDir" -ForegroundColor Gray +Write-Host "" + +# Verifică dacă venv există +$venvPath = Join-Path $ProjectDir ".venv" +$activateScript = Join-Path $venvPath "Scripts\Activate.ps1" + +if (-not (Test-Path $activateScript)) { + Write-Host "[ERROR] Virtual environment nu exista sau este incomplet!" -ForegroundColor Red + Write-Host "Ruleaza intai: setup_dev.ps1" -ForegroundColor Yellow + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 +} + +# Activează venv +Write-Host "Activare virtual environment..." -ForegroundColor Cyan +try { + & $activateScript + Write-Host "[OK] Virtual environment activat." -ForegroundColor Green + Write-Host "" +} catch { + Write-Host "[ERROR] Nu am putut activa venv!" -ForegroundColor Red + Write-Host "Eroare: $_" -ForegroundColor Red + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 +} + +# Verifică dacă .env există +$envFile = Join-Path $ProjectDir ".env" +$envExample = Join-Path $ProjectDir ".env.example" + +if (-not (Test-Path $envFile)) { + Write-Host "[WARNING] Fisierul .env nu exista!" -ForegroundColor Yellow + if (Test-Path $envExample) { + Write-Host "Copiez din .env.example..." -ForegroundColor Yellow + Copy-Item $envExample $envFile + Write-Host "" + Write-Host "IMPORTANT: Editeaza .env cu credentialele tale!" -ForegroundColor Yellow + Start-Process notepad $envFile -Wait + Write-Host "" + Read-Host "Apasa Enter dupa ce salvezi .env" + } else { + Write-Host "[ERROR] .env.example nu exista!" -ForegroundColor Red + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 + } +} + +# Verifică directoare output +$dataDir = Join-Path $ProjectDir "data" +$logsDir = Join-Path $ProjectDir "logs" + +if (-not (Test-Path $dataDir)) { + New-Item -ItemType Directory -Path $dataDir | Out-Null +} +if (-not (Test-Path $logsDir)) { + New-Item -ItemType Directory -Path $logsDir | Out-Null +} + +# Rulează scraper +Write-Host "" +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "Pornire scraper..." -ForegroundColor Yellow +Write-Host "Browser-ul va fi vizibil pentru debugging." -ForegroundColor Gray +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "" + +try { + & python btgo_scraper.py +} catch { + Write-Host "" + Write-Host "[ERROR] Executie esuata: $_" -ForegroundColor Red + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 +} + +Write-Host "" +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "Executie terminata." -ForegroundColor Green +Write-Host "Verifica rezultatele in:" -ForegroundColor Cyan +Write-Host " - data\solduri_*.csv" -ForegroundColor Gray +Write-Host " - data\tranzactii_*.csv" -ForegroundColor Gray +Write-Host " - logs\scraper_*.log" -ForegroundColor Gray +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "" +Read-Host "Apasa Enter pentru a inchide" diff --git a/deployment/windows/scripts/run_telegram_bot_manual.ps1 b/deployment/windows/scripts/run_telegram_bot_manual.ps1 new file mode 100644 index 0000000..ffdcd40 --- /dev/null +++ b/deployment/windows/scripts/run_telegram_bot_manual.ps1 @@ -0,0 +1,81 @@ +<# +.SYNOPSIS + Telegram Trigger Bot pentru BTGO - Rulare Manuala +.DESCRIPTION + Ruleaza Telegram bot-ul in modul manual (nu ca serviciu Windows) +.NOTES + Echivalent PowerShell pentru scripts\run_telegram_bot.bat + Rulare: Right-click → "Run with PowerShell" + Pentru a opri: Ctrl+C +#> + +$ErrorActionPreference = "Stop" +$ScriptDir = Split-Path -Parent $PSCommandPath +$ProjectDir = Resolve-Path (Join-Path $ScriptDir "..\..\..") | Select-Object -ExpandProperty Path + +Set-Location $ProjectDir + +Write-Host "" +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host " TELEGRAM TRIGGER BOT PENTRU BTGO" -ForegroundColor Yellow +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "" +Write-Host "Working directory: $ProjectDir" -ForegroundColor Gray +Write-Host "" + +# Verifică dacă venv există +$venvPath = Join-Path $ProjectDir ".venv" +$activateScript = Join-Path $venvPath "Scripts\Activate.ps1" + +if (-not (Test-Path $activateScript)) { + Write-Host "[ERROR] Virtual environment nu exista!" -ForegroundColor Red + Write-Host "Ruleaza intai: setup_dev.ps1" -ForegroundColor Yellow + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 +} + +# Activează venv +Write-Host "Activare virtual environment..." -ForegroundColor Cyan +try { + & $activateScript + Write-Host "[OK] Virtual environment activat." -ForegroundColor Green + Write-Host "" +} catch { + Write-Host "[ERROR] Nu am putut activa venv!" -ForegroundColor Red + Write-Host "Eroare: $_" -ForegroundColor Red + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 +} + +# Verifică .env +$envFile = Join-Path $ProjectDir ".env" + +if (-not (Test-Path $envFile)) { + Write-Host "[ERROR] Fisierul .env nu exista!" -ForegroundColor Red + Write-Host "Configureaza .env cu TELEGRAM_BOT_TOKEN si TELEGRAM_ALLOWED_USER_IDS" -ForegroundColor Yellow + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 +} + +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "Pornire Telegram Bot..." -ForegroundColor Yellow +Write-Host "Bot-ul va astepta comenzi /scrape" -ForegroundColor Gray +Write-Host "Apasa Ctrl+C pentru a opri bot-ul" -ForegroundColor Yellow +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "" + +try { + & python telegram_trigger_bot.py +} catch { + Write-Host "" + Write-Host "[ERROR] Executie esuata: $_" -ForegroundColor Red + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 +} + +Write-Host "" +Read-Host "Apasa Enter pentru a inchide" diff --git a/deployment/windows/scripts/setup_dev.ps1 b/deployment/windows/scripts/setup_dev.ps1 new file mode 100644 index 0000000..ed5439c --- /dev/null +++ b/deployment/windows/scripts/setup_dev.ps1 @@ -0,0 +1,172 @@ +<# +.SYNOPSIS + BTGO Scraper - Setup Development Environment +.DESCRIPTION + Configureaza mediul de dezvoltare local (venv, dependente, browsere) +.NOTES + Echivalent PowerShell pentru scripts\setup_windows.bat + Rulare: Right-click → "Run with PowerShell" +#> + +$ErrorActionPreference = "Stop" +$ScriptDir = Split-Path -Parent $PSCommandPath +$ProjectDir = Resolve-Path (Join-Path $ScriptDir "..\..\..") | Select-Object -ExpandProperty Path + +Set-Location $ProjectDir + +Write-Host "" +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host " BTGO SCRAPER - SETUP WINDOWS" -ForegroundColor Yellow +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "" +Write-Host "Working directory: $ProjectDir" -ForegroundColor Gray +Write-Host "" + +# Verifică Python +try { + $pythonVersion = & python --version 2>&1 + Write-Host "[OK] Python gasit: $pythonVersion" -ForegroundColor Green + Write-Host "" +} catch { + Write-Host "[ERROR] Python nu este instalat!" -ForegroundColor Red + Write-Host "Descarca Python de la: https://www.python.org/downloads/" -ForegroundColor Yellow + Write-Host "Asigura-te ca bifezi 'Add Python to PATH' la instalare." -ForegroundColor Yellow + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 +} + +# Verifică dacă venv există deja +$venvPath = Join-Path $ProjectDir ".venv" +if (Test-Path (Join-Path $venvPath "Scripts\Activate.ps1")) { + Write-Host "[INFO] Virtual environment exista deja." -ForegroundColor Yellow + $recreate = Read-Host "Doresti sa il stergi si sa recreezi? (y/N)" + if ($recreate -eq "y" -or $recreate -eq "Y") { + Write-Host "Stergere venv existent..." -ForegroundColor Yellow + Remove-Item -Path $venvPath -Recurse -Force + } else { + # Skip la activare + goto ActivateVenv + } +} + +# Creează virtual environment +Write-Host "[STEP 1/5] Creare virtual environment..." -ForegroundColor Cyan +try { + & python -m venv .venv + Write-Host "[OK] Virtual environment creat." -ForegroundColor Green + Write-Host "" +} catch { + Write-Host "[ERROR] Nu am putut crea venv!" -ForegroundColor Red + Write-Host "Eroare: $_" -ForegroundColor Red + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 +} + +:ActivateVenv +# Activează venv +Write-Host "[STEP 2/5] Activare virtual environment..." -ForegroundColor Cyan +$activateScript = Join-Path $venvPath "Scripts\Activate.ps1" +if (-not (Test-Path $activateScript)) { + Write-Host "[ERROR] Nu am gasit scriptul de activare: $activateScript" -ForegroundColor Red + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 +} + +try { + & $activateScript + Write-Host "[OK] Virtual environment activat." -ForegroundColor Green + Write-Host "" +} catch { + Write-Host "[ERROR] Nu am putut activa venv!" -ForegroundColor Red + Write-Host "Eroare: $_" -ForegroundColor Red + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 +} + +# Instalează requirements +Write-Host "[STEP 3/5] Instalare dependente Python..." -ForegroundColor Cyan +try { + & pip install --upgrade pip --quiet + & pip install -r requirements.txt + Write-Host "[OK] Dependente instalate." -ForegroundColor Green + Write-Host "" +} catch { + Write-Host "[ERROR] Instalare dependente esuata!" -ForegroundColor Red + Write-Host "Eroare: $_" -ForegroundColor Red + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 +} + +# Instalează browsere Playwright +Write-Host "[STEP 4/5] Instalare browsere Playwright (Chromium)..." -ForegroundColor Cyan +Write-Host "Acest pas poate dura cateva minute..." -ForegroundColor Yellow +try { + & playwright install chromium + Write-Host "[OK] Browsere instalate." -ForegroundColor Green + Write-Host "" +} catch { + Write-Host "[ERROR] Instalare browsere esuata!" -ForegroundColor Red + Write-Host "Eroare: $_" -ForegroundColor Red + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 +} + +# Copiază .env.example la .env dacă nu există +Write-Host "[STEP 5/5] Verificare configuratie..." -ForegroundColor Cyan +$envFile = Join-Path $ProjectDir ".env" +$envExample = Join-Path $ProjectDir ".env.example" + +if (-not (Test-Path $envFile)) { + if (Test-Path $envExample) { + Write-Host "Creare fisier .env din template..." -ForegroundColor Yellow + Copy-Item $envExample $envFile + Write-Host "[INFO] Fisierul .env a fost creat." -ForegroundColor Green + Write-Host "" + Write-Host "IMPORTANT: Editeaza .env si completeaza:" -ForegroundColor Yellow + Write-Host " - BTGO_USERNAME=your_username" -ForegroundColor Gray + Write-Host " - BTGO_PASSWORD=your_password" -ForegroundColor Gray + Write-Host "" + } else { + Write-Host "[ERROR] .env.example nu exista!" -ForegroundColor Red + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 1 + } +} else { + Write-Host "[OK] Fisierul .env exista deja." -ForegroundColor Green +} + +# Verifică directoarele +$dataDir = Join-Path $ProjectDir "data" +$logsDir = Join-Path $ProjectDir "logs" + +if (-not (Test-Path $dataDir)) { + New-Item -ItemType Directory -Path $dataDir | Out-Null +} +if (-not (Test-Path $logsDir)) { + New-Item -ItemType Directory -Path $logsDir | Out-Null +} + +Write-Host "" +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host " SETUP COMPLET!" -ForegroundColor Green +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "" +Write-Host "Next steps:" -ForegroundColor Cyan +Write-Host " 1. Editeaza .env cu credentialele tale" -ForegroundColor Gray +Write-Host " 2. Ruleaza scriptul run_scraper.ps1 sau run_telegram_bot.ps1" -ForegroundColor Gray +Write-Host "" +Write-Host "Pentru debugging selectors (optional):" -ForegroundColor Cyan +Write-Host " .venv\Scripts\Activate.ps1" -ForegroundColor Gray +Write-Host " playwright codegen https://btgo.ro --target python" -ForegroundColor Gray +Write-Host "" +Write-Host "Pentru detalii, vezi README.md" -ForegroundColor Gray +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "" +Read-Host "Apasa Enter pentru a inchide" diff --git a/deployment/windows/scripts/start_service.ps1 b/deployment/windows/scripts/start_service.ps1 new file mode 100644 index 0000000..db60162 --- /dev/null +++ b/deployment/windows/scripts/start_service.ps1 @@ -0,0 +1,65 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + BTGO Telegram Bot - Start Service Script +.DESCRIPTION + Pornește serviciul Windows +#> + +$ErrorActionPreference = "Stop" +$ServiceName = "BTGOTelegramBot" + +function Write-ColorOutput { + param([string]$Message, [string]$Prefix = "") + $color = switch ($Prefix) { + "[OK]" { "Green" } + "[INFO]" { "Cyan" } + "[EROARE]" { "Red" } + default { "White" } + } + if ($Prefix) { + Write-Host "$Prefix " -ForegroundColor $color -NoNewline + Write-Host $Message + } else { + Write-Host $Message + } +} + +Write-Host "" +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "START SERVICIU: $ServiceName" -ForegroundColor Yellow +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "" + +# Verifică dacă serviciul există +$service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + +if (-not $service) { + Write-ColorOutput "Serviciul $ServiceName nu este instalat!" -Prefix "[EROARE]" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +# Pornește serviciul +Write-ColorOutput "Pornire serviciu..." -Prefix "[INFO]" +try { + Start-Service -Name $ServiceName + Write-ColorOutput "Serviciu pornit cu succes!" -Prefix "[OK]" + Start-Sleep -Seconds 2 + + # Verifică status + $service = Get-Service -Name $ServiceName + Write-Host "" + Get-Service -Name $ServiceName | Format-Table -AutoSize + +} catch { + Write-ColorOutput "Pornirea serviciului a esuat!" -Prefix "[EROARE]" + Write-Host "Eroare: $_" -ForegroundColor Red + Write-Host "" + Write-Host "Verificati logurile:" -ForegroundColor Yellow + Write-Host " Get-Content logs\telegram_bot_stderr.log" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +Read-Host "`nApasa Enter pentru a inchide" diff --git a/deployment/windows/scripts/status.ps1 b/deployment/windows/scripts/status.ps1 new file mode 100644 index 0000000..eca074f --- /dev/null +++ b/deployment/windows/scripts/status.ps1 @@ -0,0 +1,101 @@ +<# +.SYNOPSIS + BTGO Telegram Bot - Service Status Script +.DESCRIPTION + Afiseaza status detaliat al serviciului +#> + +$ServiceName = "BTGOTelegramBot" +$ScriptDir = Split-Path -Parent $PSCommandPath +$ProjectDir = Resolve-Path (Join-Path $ScriptDir "..\..\..") | Select-Object -ExpandProperty Path +$LogDir = Join-Path $ProjectDir "logs" + +function Write-ColorOutput { + param([string]$Message, [string]$Prefix = "") + $color = switch ($Prefix) { + "[OK]" { "Green" } + "[INFO]" { "Cyan" } + "[AVERTIZARE]" { "Yellow" } + default { "White" } + } + if ($Prefix) { + Write-Host "$Prefix " -ForegroundColor $color -NoNewline + Write-Host $Message + } else { + Write-Host $Message -ForegroundColor $color + } +} + +function Write-Separator { + param([string]$Title) + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host $Title -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" +} + +Write-Separator "BTGO TELEGRAM BOT - STATUS SERVICIU" + +# Verifică dacă serviciul există +$service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + +if (-not $service) { + Write-ColorOutput "Serviciul $ServiceName NU este instalat" -Prefix "[INFO]" + Write-Host "" + Write-Host "Pentru instalare, rulati:" -ForegroundColor Yellow + Write-Host " .\deploy.ps1" + Write-Host "" + Read-Host "Apasa Enter pentru a inchide" + exit 0 +} + +# Afișează informații serviciu +Write-Host "[STATUS SERVICIU]" -ForegroundColor Yellow +$service | Format-Table -Property Status, Name, DisplayName, StartType -AutoSize + +# Afișează configurație detaliată +Write-Host "[CONFIGURATIE SERVICIU]" -ForegroundColor Yellow +Get-WmiObject -Class Win32_Service -Filter "Name='$ServiceName'" | + Format-List Name, DisplayName, State, StartMode, PathName + +# Verifică ultimele logs +$stdoutLog = Join-Path $LogDir "telegram_bot_stdout.log" +if (Test-Path $stdoutLog) { + Write-Host "[ULTIMELE 10 LINII DIN LOG]" -ForegroundColor Yellow + Get-Content $stdoutLog -Tail 10 + Write-Host "" +} else { + Write-ColorOutput "Log stdout nu exista inca" -Prefix "[INFO]" + Write-Host "" +} + +# Verifică erori +$stderrLog = Join-Path $LogDir "telegram_bot_stderr.log" +if (Test-Path $stderrLog) { + $errLines = (Get-Content $stderrLog).Count + if ($errLines -gt 0) { + Write-ColorOutput "Exista erori in stderr log ($errLines linii)" -Prefix "[AVERTIZARE]" + Write-Host "Ultimele 5 erori:" + Get-Content $stderrLog -Tail 5 + Write-Host "" + } +} + +# Comenzi utile +Write-Host "[COMENZI UTILE]" -ForegroundColor Yellow +Write-Host " - Restart: .\restart_service.ps1" +Write-Host " - View logs: .\view_logs.ps1" +Write-Host " - Dezinstaleaza: .\uninstall_service.ps1" +Write-Host " - Redeploy: .\deploy.ps1" +Write-Host "" + +# PowerShell Quick Commands +Write-Host "[POWERSHELL COMMANDS]" -ForegroundColor Yellow +Write-Host " - Restart-Service $ServiceName" +Write-Host " - Stop-Service $ServiceName" +Write-Host " - Start-Service $ServiceName" +Write-Host " - Get-Content '$stdoutLog' -Wait -Tail 20" +Write-Host "" + +Read-Host "Apasa Enter pentru a inchide" diff --git a/deployment/windows/scripts/uninstall_service.ps1 b/deployment/windows/scripts/uninstall_service.ps1 new file mode 100644 index 0000000..0a04297 --- /dev/null +++ b/deployment/windows/scripts/uninstall_service.ps1 @@ -0,0 +1,133 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + BTGO Telegram Bot - Service Uninstall Script +.DESCRIPTION + Dezinstaleaza serviciul Windows BTGO Telegram Bot +.NOTES + Rulare: Right-click → "Run with PowerShell" (ca Administrator) +#> + +$ErrorActionPreference = "Stop" + +# ============================================================================ +# CONFIGURATION +# ============================================================================ + +$ServiceName = "BTGOTelegramBot" +$ScriptDir = Split-Path -Parent $PSCommandPath +$ProjectDir = Resolve-Path (Join-Path $ScriptDir "..\..\..") | Select-Object -ExpandProperty Path +$NssmPath = Join-Path $ProjectDir "deployment\windows\tools\nssm.exe" + +function Write-ColorOutput { + param([string]$Message, [string]$Prefix = "") + $color = switch ($Prefix) { + "[OK]" { "Green" } + "[INFO]" { "Cyan" } + "[EROARE]" { "Red" } + "[AVERTIZARE]" { "Yellow" } + default { "White" } + } + if ($Prefix) { + Write-Host "$Prefix " -ForegroundColor $color -NoNewline + Write-Host $Message + } else { + Write-Host $Message + } +} + +function Write-Separator { + param([string]$Title = "") + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + if ($Title) { + Write-Host $Title -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + } +} + +# ============================================================================ +# VERIFICARE SERVICIU +# ============================================================================ + +Write-Separator "DEZINSTALARE SERVICIU: $ServiceName" +Write-Host "" + +$service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + +if (-not $service) { + Write-ColorOutput "Serviciul $ServiceName nu este instalat." -Prefix "[INFO]" + Write-Host "Nimic de dezinstalat." + Read-Host "`nApasa Enter pentru a inchide" + exit 0 +} + +# ============================================================================ +# OPRIRE SERVICIU +# ============================================================================ + +Write-ColorOutput "Verificare status serviciu..." -Prefix "[INFO]" + +if ($service.Status -eq "Running") { + Write-ColorOutput "Serviciul ruleaza. Oprire..." -Prefix "[INFO]" + try { + Stop-Service -Name $ServiceName -Force + Write-ColorOutput "Serviciu oprit cu succes" -Prefix "[OK]" + Start-Sleep -Seconds 2 + } catch { + Write-ColorOutput "Oprirea serviciului a esuat!" -Prefix "[AVERTIZARE]" + Write-ColorOutput "Incercam oprire fortata..." -Prefix "[INFO]" + sc.exe stop $ServiceName | Out-Null + Start-Sleep -Seconds 5 + } +} else { + Write-ColorOutput "Serviciul este deja oprit" -Prefix "[INFO]" +} + +# ============================================================================ +# DEZINSTALARE SERVICIU +# ============================================================================ + +Write-ColorOutput "Dezinstalare serviciu..." -Prefix "[INFO]" + +try { + if (Test-Path $NssmPath) { + # Folosește NSSM pentru dezinstalare + & $NssmPath remove $ServiceName confirm + if ($LASTEXITCODE -ne 0) { throw } + } else { + # Fallback la sc delete + Write-ColorOutput "NSSM nu a fost gasit, folosim sc delete" -Prefix "[INFO]" + sc.exe delete $ServiceName | Out-Null + if ($LASTEXITCODE -ne 0) { throw } + } +} catch { + Write-ColorOutput "Dezinstalarea serviciului a esuat!" -Prefix "[EROARE]" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +# ============================================================================ +# VERIFICARE FINALA +# ============================================================================ + +Start-Sleep -Seconds 2 + +$serviceCheck = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + +if (-not $serviceCheck) { + Write-Separator "[SUCCES] SERVICIU DEZINSTALAT CU SUCCES!" + Write-Host "" + Write-Host "Serviciul $ServiceName a fost eliminat din sistem." -ForegroundColor Green + Write-Host "" + Write-Host "Pentru a reinstala serviciul, rulati:" -ForegroundColor Yellow + Write-Host " .\install_service.ps1" + Write-Host "" + Write-Separator +} else { + Write-ColorOutput "Serviciul pare sa mai existe in sistem!" -Prefix "[AVERTIZARE]" + Write-Host "Poate fi nevoie de un restart al sistemului." + Get-Service -Name $ServiceName | Format-List +} + +Read-Host "`nApasa Enter pentru a inchide" diff --git a/deployment/windows/scripts/update_browsers.ps1 b/deployment/windows/scripts/update_browsers.ps1 new file mode 100644 index 0000000..2a48c6d --- /dev/null +++ b/deployment/windows/scripts/update_browsers.ps1 @@ -0,0 +1,103 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + Update Playwright Browsers (Global Location) +.DESCRIPTION + Actualizeaza browserele Playwright in locatia globala + Foloseste aceasta comanda pentru update-uri de browsere +#> + +$ErrorActionPreference = "Stop" +$ServiceName = "BTGOTelegramBot" + +function Write-ColorOutput { + param([string]$Message, [string]$Prefix = "") + $color = switch ($Prefix) { + "[OK]" { "Green" } + "[INFO]" { "Cyan" } + "[EROARE]" { "Red" } + "[AVERTIZARE]" { "Yellow" } + default { "White" } + } + if ($Prefix) { + Write-Host "$Prefix " -ForegroundColor $color -NoNewline + Write-Host $Message + } else { + Write-Host $Message -ForegroundColor $color + } +} + +Write-Host "" +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "UPDATE PLAYWRIGHT BROWSERS" -ForegroundColor Yellow +Write-Host ("=" * 80) -ForegroundColor Cyan +Write-Host "" + +# Verifică environment variable +$globalBrowserPath = [System.Environment]::GetEnvironmentVariable("PLAYWRIGHT_BROWSERS_PATH", [System.EnvironmentVariableTarget]::Machine) + +if (-not $globalBrowserPath) { + $globalBrowserPath = "C:\playwright-browsers" + Write-ColorOutput "PLAYWRIGHT_BROWSERS_PATH nu este setat. Folosim: $globalBrowserPath" -Prefix "[AVERTIZARE]" + + # Setează environment variable + [System.Environment]::SetEnvironmentVariable("PLAYWRIGHT_BROWSERS_PATH", $globalBrowserPath, [System.EnvironmentVariableTarget]::Machine) + Write-ColorOutput "Environment variable setata" -Prefix "[OK]" +} + +$env:PLAYWRIGHT_BROWSERS_PATH = $globalBrowserPath +Write-ColorOutput "Locatie browsere: $globalBrowserPath" -Prefix "[INFO]" + +# Creează director dacă nu există +if (-not (Test-Path $globalBrowserPath)) { + New-Item -ItemType Directory -Path $globalBrowserPath -Force | Out-Null + icacls $globalBrowserPath /grant "SYSTEM:(OI)(CI)F" /T 2>&1 | Out-Null + Write-ColorOutput "Director creat cu permisiuni SYSTEM" -Prefix "[OK]" +} + +# Update browsere +Write-Host "" +Write-ColorOutput "Instalare/Update browsere Playwright..." -Prefix "[INFO]" +Write-Host "Aceasta poate dura 1-2 minute..." -ForegroundColor Gray +Write-Host "" + +try { + python -m playwright install chromium + + if ($LASTEXITCODE -ne 0) { throw } + Write-Host "" + Write-ColorOutput "Browsere actualizate cu succes!" -Prefix "[OK]" +} catch { + Write-ColorOutput "Update-ul browserelor a esuat!" -Prefix "[EROARE]" + Read-Host "`nApasa Enter pentru a inchide" + exit 1 +} + +# Restart serviciu dacă rulează +$service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + +if ($service) { + Write-Host "" + Write-ColorOutput "Restart serviciu..." -Prefix "[INFO]" + + if ($service.Status -eq "Running") { + Restart-Service -Name $ServiceName -Force + Start-Sleep -Seconds 3 + + $service = Get-Service -Name $ServiceName + if ($service.Status -eq "Running") { + Write-ColorOutput "Serviciu restartat cu succes!" -Prefix "[OK]" + } else { + Write-ColorOutput "Serviciul nu a pornit" -Prefix "[AVERTIZARE]" + } + } else { + Write-ColorOutput "Serviciul nu rula" -Prefix "[INFO]" + } +} + +Write-Host "" +Write-Host ("=" * 80) -ForegroundColor Green +Write-Host "[SUCCES] BROWSERE ACTUALIZATE!" -ForegroundColor Green +Write-Host ("=" * 80) -ForegroundColor Green + +Read-Host "`nApasa Enter pentru a inchide" diff --git a/deployment/windows/scripts/view_logs.ps1 b/deployment/windows/scripts/view_logs.ps1 new file mode 100644 index 0000000..073ee02 --- /dev/null +++ b/deployment/windows/scripts/view_logs.ps1 @@ -0,0 +1,137 @@ +<# +.SYNOPSIS + BTGO Telegram Bot - View Logs Script +.DESCRIPTION + Afiseaza logurile serviciului in timp real +#> + +$ScriptDir = Split-Path -Parent $PSCommandPath +$ProjectDir = Resolve-Path (Join-Path $ScriptDir "..\..\..") | Select-Object -ExpandProperty Path +$LogDir = Join-Path $ProjectDir "logs" +$StdoutLog = Join-Path $LogDir "telegram_bot_stdout.log" +$StderrLog = Join-Path $LogDir "telegram_bot_stderr.log" + +function Show-Menu { + Clear-Host + Write-Host "" + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host " BTGO TELEGRAM BOT - VIEWER LOGS" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + Write-Host "Logs directory: $LogDir" -ForegroundColor Cyan + Write-Host "" + Write-Host "Selecteaza optiune:" -ForegroundColor Yellow + Write-Host "" + Write-Host " 1. Vizualizare stdout (output normal)" + Write-Host " 2. Vizualizare stderr (erori)" + Write-Host " 3. Tail stdout (timp real - ultim 30 linii)" + Write-Host " 4. Tail stderr (timp real - ultim 30 linii)" + Write-Host " 5. Deschide director logs in Explorer" + Write-Host " 6. Sterge toate logurile" + Write-Host " 0. Iesire" + Write-Host "" +} + +function View-Stdout { + Clear-Host + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "STDOUT LOG" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + if (Test-Path $StdoutLog) { + Get-Content $StdoutLog + } else { + Write-Host "[INFO] Log-ul nu exista inca" -ForegroundColor Yellow + } + Write-Host "" + Read-Host "Apasa Enter pentru a reveni la meniu" +} + +function View-Stderr { + Clear-Host + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "STDERR LOG" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + if (Test-Path $StderrLog) { + Get-Content $StderrLog + } else { + Write-Host "[INFO] Log-ul nu exista inca" -ForegroundColor Yellow + } + Write-Host "" + Read-Host "Apasa Enter pentru a reveni la meniu" +} + +function Tail-Stdout { + Clear-Host + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "STDOUT TAIL - TIMP REAL (Apasa Ctrl+C pentru a opri)" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + if (Test-Path $StdoutLog) { + Get-Content $StdoutLog -Wait -Tail 30 + } else { + Write-Host "[INFO] Log-ul nu exista inca" -ForegroundColor Yellow + Start-Sleep -Seconds 2 + } +} + +function Tail-Stderr { + Clear-Host + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "STDERR TAIL - TIMP REAL (Apasa Ctrl+C pentru a opri)" -ForegroundColor Yellow + Write-Host ("=" * 80) -ForegroundColor Cyan + Write-Host "" + if (Test-Path $StderrLog) { + Get-Content $StderrLog -Wait -Tail 30 + } else { + Write-Host "[INFO] Log-ul nu exista inca" -ForegroundColor Yellow + Start-Sleep -Seconds 2 + } +} + +function Open-LogsExplorer { + if (Test-Path $LogDir) { + explorer $LogDir + } else { + Write-Host "[INFO] Directorul logs nu exista" -ForegroundColor Yellow + Start-Sleep -Seconds 2 + } +} + +function Clear-AllLogs { + Write-Host "" + Write-Host "[AVERTIZARE] Stergi TOATE logurile?" -ForegroundColor Yellow + $confirm = Read-Host "Confirma (Y/N)" + if ($confirm -eq "Y" -or $confirm -eq "y") { + Remove-Item (Join-Path $LogDir "*.log") -Force -ErrorAction SilentlyContinue + Write-Host "[OK] Loguri sterse" -ForegroundColor Green + } else { + Write-Host "[INFO] Anulat" -ForegroundColor Cyan + } + Start-Sleep -Seconds 2 +} + +# Main loop +do { + Show-Menu + $choice = Read-Host "Optiune (0-6)" + + switch ($choice) { + "1" { View-Stdout } + "2" { View-Stderr } + "3" { Tail-Stdout } + "4" { Tail-Stderr } + "5" { Open-LogsExplorer } + "6" { Clear-AllLogs } + "0" { + Write-Host "" + Write-Host "Bye!" -ForegroundColor Green + exit 0 + } + default { + Write-Host "[EROARE] Optiune invalida!" -ForegroundColor Red + Start-Sleep -Seconds 1 + } + } +} while ($true) diff --git a/deployment/windows/tools/.gitignore b/deployment/windows/tools/.gitignore new file mode 100644 index 0000000..f024164 --- /dev/null +++ b/deployment/windows/tools/.gitignore @@ -0,0 +1,2 @@ +# NSSM binaries (downloaded automatically) +nssm.exe diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..601598d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +version: '3.8' + +services: + btgo-scraper: + build: . + container_name: btgo-scraper + + # Mount volumes pentru persistenta + volumes: + - ./data:/app/data + - ./logs:/app/logs + + # Environment variables - citeste din .env + environment: + - BTGO_USERNAME=${BTGO_USERNAME} + - BTGO_PASSWORD=${BTGO_PASSWORD} + - HEADLESS=true + - TIMEOUT_2FA_SECONDS=${TIMEOUT_2FA_SECONDS:-120} + - SCREENSHOT_ON_ERROR=${SCREENSHOT_ON_ERROR:-true} + - LOG_LEVEL=${LOG_LEVEL:-INFO} + - TZ=Europe/Bucharest + + # Restart policy (schimba la 'always' pentru rulare automata) + restart: "no" + + # Resource limits (optional) + deploy: + resources: + limits: + cpus: '2' + memory: 2G diff --git a/get_telegram_chat_id.py b/get_telegram_chat_id.py new file mode 100644 index 0000000..d352341 --- /dev/null +++ b/get_telegram_chat_id.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +Helper script pentru obținerea Chat ID Telegram (pentru DM și grupuri) +""" + +import os +import requests +from dotenv import load_dotenv + +# Load environment +load_dotenv() + +BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN') + +if not BOT_TOKEN: + print("❌ TELEGRAM_BOT_TOKEN nu este setat în .env!") + print("\nAdaugă în .env:") + print("TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrs") + exit(1) + +def get_updates(): + """Preia ultimele update-uri de la bot""" + url = f"https://api.telegram.org/bot{BOT_TOKEN}/getUpdates" + response = requests.get(url) + return response.json() + +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...") + data = get_updates() + + if not data.get('ok'): + print(f"❌ Eroare 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") + return + + print(f"\n✅ Găsit {len(results)} mesaje!\n") + print("=" * 60) + + # Procesează ultimele mesaje + seen_chats = {} + + for update in results: + if 'message' in update: + msg = update['message'] + chat = msg['chat'] + chat_id = chat['id'] + chat_type = chat['type'] + + # Evită duplicate + if chat_id in seen_chats: + continue + + seen_chats[chat_id] = True + + # 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}") + 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() + + print("=" * 60) + print("\n💡 Pentru configurare .env:") + print() + + # Recomandări + for chat_id, chat_data in seen_chats.items(): + if chat_id < 0: # Grup + print(f"# Pentru grup (notificări + comenzi):") + print(f"TELEGRAM_CHAT_ID={chat_id}") + else: # DM + print(f"# Pentru DM:") + print(f"TELEGRAM_CHAT_ID={chat_id}") + + print() + print("# User IDs autorizați (pot rula /scrape):") + print("TELEGRAM_ALLOWED_USER_IDS=", end="") + + # 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) + +if __name__ == "__main__": + main() diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..729a15d --- /dev/null +++ b/logs/.gitkeep @@ -0,0 +1 @@ +# Folder pentru log files diff --git a/notifications.py b/notifications.py new file mode 100644 index 0000000..a01d13d --- /dev/null +++ b/notifications.py @@ -0,0 +1,504 @@ +""" +Notification module for BTGO Scraper +Handles email and Discord notifications with file attachments +""" + +import smtplib +import logging +import zipfile +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.base import MIMEBase +from email import encoders +from pathlib import Path +from datetime import datetime +from typing import List +import requests + + +class EmailNotifier: + """Handles email notifications via SMTP""" + + def __init__(self, config): + self.config = config + self.enabled = config.EMAIL_ENABLED + + def send(self, files: List[str], account_count: int) -> bool: + """ + Send email with CSV attachments + + Args: + files: List of file paths to attach + account_count: Number of accounts processed + + 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 + + # Create message + msg = MIMEMultipart() + msg['From'] = self.config.EMAIL_FROM + msg['To'] = self.config.EMAIL_TO + msg['Subject'] = f'BTGO Scraper Results - {datetime.now().strftime("%Y-%m-%d %H:%M")}' + + # Email body + body = self._create_email_body(files, account_count) + msg.attach(MIMEText(body, 'html')) + + # Attach files + total_size = 0 + for file_path in files: + if not Path(file_path).exists(): + logging.warning(f"File not found for email: {file_path}") + continue + + file_size = Path(file_path).stat().st_size + total_size += file_size + + # 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) + + with open(file_path, 'rb') as f: + part = MIMEBase('application', 'octet-stream') + part.set_payload(f.read()) + encoders.encode_base64(part) + part.add_header( + 'Content-Disposition', + f'attachment; filename={Path(file_path).name}' + ) + msg.attach(part) + + # Send email + logging.info(f"Sending email 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 sent successfully to {self.config.EMAIL_TO}") + return True + + except Exception as e: + logging.error(f"Failed to send email: {e}") + return False + + def _send_with_zip(self, files: List[str], account_count: int) -> bool: + """Send email with files compressed as ZIP""" + try: + # Create ZIP archive + timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + zip_path = Path(self.config.OUTPUT_DIR) / f'btgo_export_{timestamp}.zip' + + with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + for file_path in files: + if Path(file_path).exists(): + zipf.write(file_path, Path(file_path).name) + + logging.info(f"Created ZIP archive: {zip_path}") + + # Send with ZIP attachment instead + msg = MIMEMultipart() + msg['From'] = self.config.EMAIL_FROM + 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) + msg.attach(MIMEText(body, 'html')) + + 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) + + 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 ZIP: {e}") + return False + + def _create_email_body(self, files: List[str], account_count: int, is_zip: bool = False) -> str: + """Create HTML email body""" + file_list = '
'.join([f'• {Path(f).name}' for f in files]) + file_count = 1 if is_zip else len(files) + + return f""" + + +

BTGO Scraper Results

+

Execution time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

+

Accounts processed: {account_count}

+

Files attached: {file_count}

+
+

{'Archive contents:' if is_zip else 'Attached files:'}

+ {file_list} +
+

+ This email was automatically generated by BTGO Scraper.
+ For issues, check the logs at your deployment location. +

+ + + """ + + +class TelegramNotifier: + """Handles Telegram notifications via Bot API""" + + def __init__(self, config): + self.config = config + self.enabled = config.TELEGRAM_ENABLED + self.bot_token = config.TELEGRAM_BOT_TOKEN + self.chat_id = config.TELEGRAM_CHAT_ID + self.max_file_size = 50 * 1024 * 1024 # 50MB Telegram limit + + def send(self, files: List[str], accounts: list, telegram_message_id: str = None, telegram_chat_id: str = None) -> bool: + """ + Send Telegram message with file attachments + + Args: + files: List of file paths to attach + accounts: List of account data with balances + telegram_message_id: Message ID to edit (for progress updates) + telegram_chat_id: Chat ID to edit (for progress updates) + + Returns: + True if successful, False otherwise + """ + if not self.enabled: + logging.info("Telegram notifications disabled") + return False + + try: + if not self.bot_token or not self.chat_id: + logging.error("Telegram bot token or chat ID not configured") + return False + + # Store message IDs for editing + self.progress_message_id = telegram_message_id + self.progress_chat_id = telegram_chat_id or self.chat_id + logging.info(f"Received telegram_message_id: {telegram_message_id}, telegram_chat_id: {telegram_chat_id}") + logging.info(f"Stored progress_message_id: {self.progress_message_id}, progress_chat_id: {self.progress_chat_id}") + + # Check total file size + total_size = sum(Path(f).stat().st_size for f in files if Path(f).exists()) + + if total_size > self.max_file_size: + logging.warning(f"Files exceed Telegram 50MB limit, creating ZIP archive") + return self._send_with_zip(files, accounts) + else: + return self._send_files(files, accounts) + + except Exception as e: + logging.error(f"Failed to send Telegram notification: {e}") + return False + + def _send_message(self, text: str) -> bool: + """Send text message to Telegram""" + try: + url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage" + payload = { + 'chat_id': self.chat_id, + 'text': text, + 'parse_mode': 'HTML' + } + response = requests.post(url, json=payload) + return response.status_code == 200 + + except Exception as e: + logging.error(f"Failed to send Telegram message: {e}") + return False + + def _edit_message(self, chat_id: str, message_id: str, text: str) -> bool: + """Edit existing Telegram message (for progress updates)""" + try: + url = f"https://api.telegram.org/bot{self.bot_token}/editMessageText" + payload = { + 'chat_id': chat_id, + 'message_id': message_id, + 'text': text, + 'parse_mode': 'Markdown' + } + response = requests.post(url, json=payload) + return response.status_code == 200 + + except Exception as e: + # Silent fail pentru progress updates - nu blochez scraper-ul + logging.debug(f"Progress update failed: {e}") + return False + + def _send_files(self, files: List[str], accounts: list) -> bool: + """Send files directly to Telegram""" + try: + # Calculate total balance + total_ron = sum(acc['sold'] for acc in accounts if acc.get('moneda') == 'RON') + + # Build message with account balances + message = f"BTGO SCRAPER - FINALIZAT CU SUCCES\n\n" + message += f"Timp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" + message += f"Conturi procesate: {len(accounts)}\n" + message += f"Fisiere: {len(files)}\n\n" + + message += "SOLDURI:\n" + for acc in accounts: + nume = acc['nume_cont'] + sold = acc['sold'] + moneda = acc['moneda'] + message += f"{nume}: {sold:,.2f} {moneda}\n" + message += f"\nTOTAL: {total_ron:,.2f} RON\n" + + logging.info(f"Sending Telegram notification to chat {self.progress_chat_id}...") + logging.info(f"Progress message_id: {self.progress_message_id}, chat_id: {self.progress_chat_id}") + + # Edit initial message or send new one + if self.progress_message_id and self.progress_chat_id: + # Edit the initial progress message + logging.info(f"Editing initial message {self.progress_message_id} in chat {self.progress_chat_id}") + url = f"https://api.telegram.org/bot{self.bot_token}/editMessageText" + payload = { + 'chat_id': self.progress_chat_id, + 'message_id': self.progress_message_id, + 'text': message, + 'parse_mode': 'HTML' + } + response = requests.post(url, json=payload) + if response.status_code == 200: + logging.info("✓ Message edited successfully with balances") + else: + logging.error(f"Failed to edit message: {response.status_code} - {response.text}") + else: + # Send new message if no message_id provided + logging.warning("No progress_message_id provided, sending new message") + self._send_message(message) + + # Send each file as document + url = f"https://api.telegram.org/bot{self.bot_token}/sendDocument" + success_count = 0 + + for file_path in files: + if not Path(file_path).exists(): + logging.warning(f"File not found: {file_path}") + continue + + file_size = Path(file_path).stat().st_size + if file_size > self.max_file_size: + logging.warning(f"File too large for Telegram: {Path(file_path).name} ({file_size / 1024 / 1024:.2f} MB)") + continue + + with open(file_path, 'rb') as f: + files_data = {'document': f} + data = { + 'chat_id': self.chat_id, + 'caption': f"{Path(file_path).name}" + } + response = requests.post(url, data=data, files=files_data) + + if response.status_code == 200: + logging.info(f"✓ Sent to Telegram: {Path(file_path).name}") + success_count += 1 + else: + logging.error(f"Failed to send {Path(file_path).name}: {response.text}") + + if success_count > 0: + logging.info(f"✓ Telegram notification sent successfully ({success_count}/{len(files)} files)") + return True + else: + logging.error("No files were sent to Telegram") + return False + + except Exception as e: + logging.error(f"Failed to send Telegram files: {e}") + return False + + def _send_with_zip(self, files: List[str], accounts: list) -> bool: + """Send files as ZIP archive to Telegram""" + try: + # Create ZIP archive + timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + zip_path = Path(self.config.OUTPUT_DIR) / f'btgo_export_{timestamp}.zip' + + with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + for file_path in files: + if Path(file_path).exists(): + zipf.write(file_path, Path(file_path).name) + + zip_size = Path(zip_path).stat().st_size + logging.info(f"Created ZIP archive: {zip_path} ({zip_size / 1024 / 1024:.2f} MB)") + + # Check if ZIP is still too large + if zip_size > self.max_file_size: + logging.error(f"ZIP archive exceeds Telegram 50MB limit ({zip_size / 1024 / 1024:.2f} MB)") + # Send message without attachment + return self._send_message_only(accounts, files) + + # Calculate total balance + total_ron = sum(acc['sold'] for acc in accounts if acc.get('moneda') == 'RON') + + # Build message with balances + message = f"BTGO SCRAPER - FINALIZAT CU SUCCES (ARHIVA ZIP)\n\n" + message += f"Timp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" + message += f"Conturi procesate: {len(accounts)}\n" + message += f"Dimensiune arhiva: {zip_size / 1024 / 1024:.2f} MB\n" + message += f"Fisiere in arhiva: {len(files)}\n\n" + + message += "SOLDURI:\n" + for acc in accounts: + nume = acc['nume_cont'] + sold = acc['sold'] + moneda = acc['moneda'] + message += f"{nume}: {sold:,.2f} {moneda}\n" + message += f"\nTOTAL: {total_ron:,.2f} RON\n" + + # Edit initial message or send new one + if self.progress_message_id and self.progress_chat_id: + url = f"https://api.telegram.org/bot{self.bot_token}/editMessageText" + payload = { + 'chat_id': self.progress_chat_id, + 'message_id': self.progress_message_id, + 'text': message, + 'parse_mode': 'HTML' + } + response = requests.post(url, json=payload) + if response.status_code != 200: + logging.error(f"Failed to edit message: {response.text}") + else: + self._send_message(message) + + # Send ZIP file + url = f"https://api.telegram.org/bot{self.bot_token}/sendDocument" + with open(zip_path, 'rb') as f: + files_data = {'document': f} + data = { + 'chat_id': self.chat_id, + 'caption': f"BTGO Export Archive - {timestamp}" + } + response = requests.post(url, data=data, files=files_data) + + if response.status_code == 200: + logging.info(f"✓ Telegram notification with ZIP sent successfully") + return True + else: + logging.error(f"Telegram send failed: {response.status_code} - {response.text}") + return False + + except Exception as e: + logging.error(f"Failed to send Telegram ZIP: {e}") + return False + + def _send_message_only(self, accounts: list, files: List[str]) -> bool: + """Send Telegram message without files (when too large)""" + try: + file_list = '\n'.join([f'{Path(f).name} ({Path(f).stat().st_size / 1024:.1f} KB)' for f in files if Path(f).exists()]) + + # Calculate total balance + total_ron = sum(acc['sold'] for acc in accounts if acc.get('moneda') == 'RON') + + message = f"BTGO SCRAPER - FINALIZAT CU SUCCES\n\n" + message += f"Timp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" + message += f"Conturi procesate: {len(accounts)}\n\n" + + message += "SOLDURI:\n" + for acc in accounts: + nume = acc['nume_cont'] + sold = acc['sold'] + moneda = acc['moneda'] + message += f"{nume}: {sold:,.2f} {moneda}\n" + message += f"\nTOTAL: {total_ron:,.2f} RON\n\n" + + message += f"ATENTIE: Fisiere prea mari pentru Telegram\n\n" + message += f"Fisiere generate:\n{file_list}\n\n" + message += f"Verifica locatia de deployment pentru fisiere." + + # Edit initial message or send new one + if self.progress_message_id and self.progress_chat_id: + url = f"https://api.telegram.org/bot{self.bot_token}/editMessageText" + payload = { + 'chat_id': self.progress_chat_id, + 'message_id': self.progress_message_id, + 'text': message, + 'parse_mode': 'HTML' + } + response = requests.post(url, json=payload) + return response.status_code == 200 + else: + return self._send_message(message) + + except Exception as e: + logging.error(f"Failed to send Telegram message: {e}") + return False + + +class NotificationService: + """Orchestrates all notification channels""" + + def __init__(self, config): + self.config = config + self.email = EmailNotifier(config) + self.telegram = TelegramNotifier(config) + + def send_all(self, files: List[str], accounts: list, telegram_message_id: str = None, telegram_chat_id: str = None) -> dict: + """ + Send notifications via all enabled channels + + Args: + files: List of file paths to send + accounts: List of account data with balances + telegram_message_id: Message ID to edit (for Telegram progress updates) + telegram_chat_id: Chat ID to edit (for Telegram progress updates) + + Returns: + Dictionary with status of each channel + """ + results = { + 'email': False, + 'telegram': False + } + + account_count = len(accounts) + logging.info(f"Sending notifications for {len(files)} files...") + + # Send via email + if self.config.EMAIL_ENABLED: + results['email'] = self.email.send(files, account_count) + + # Send via Telegram + if self.config.TELEGRAM_ENABLED: + results['telegram'] = self.telegram.send( + files, + accounts, + telegram_message_id=telegram_message_id, + telegram_chat_id=telegram_chat_id + ) + + # Log summary + success_count = sum(results.values()) + total_enabled = sum([self.config.EMAIL_ENABLED, self.config.TELEGRAM_ENABLED]) + + if success_count == total_enabled: + logging.info(f"✓ All notifications sent successfully ({success_count}/{total_enabled})") + elif success_count > 0: + logging.warning(f"Partial notification success ({success_count}/{total_enabled})") + else: + logging.error("All notifications failed") + + return results diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..12166c9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +playwright==1.48.0 +python-dotenv==1.0.0 +requests==2.31.0 diff --git a/send_notifications.py b/send_notifications.py new file mode 100644 index 0000000..dd9acd5 --- /dev/null +++ b/send_notifications.py @@ -0,0 +1,159 @@ +""" +Script pentru trimiterea notificărilor cu fișiere existente +Trimite ultimele fișiere CSV generate pe Email și Telegram +""" +import logging +import sys +from pathlib import Path +from datetime import datetime +from config import Config +from notifications import NotificationService + +# Setup logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(sys.stdout) + ] +) + + +def find_latest_files(data_dir='./data', time_window_seconds=300): + """ + Găsește ultimele fișiere CSV generate + + 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) + """ + data_path = Path(data_dir) + + if not data_path.exists(): + logging.error(f"Directorul {data_dir} nu există!") + 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, [] + + latest_solduri = solduri_files[0] + solduri_time = latest_solduri.stat().st_mtime + + 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 toate fișierele tranzactii_*.csv modificate în ultimele X secunde față de solduri + all_transaction_files = list(data_path.glob('tranzactii_*.csv')) + transaction_files = [] + + for tf in all_transaction_files: + tf_time = tf.stat().st_mtime + time_diff = abs(tf_time - solduri_time) + + # Dacă fișierul tranzacții este din aceeași sesiune (în intervalul de timp) + if time_diff <= time_window_seconds: + transaction_files.append(tf) + logging.info(f" ✓ {tf.name} (diferență: {int(time_diff)}s)") + + # Sortează după timp + transaction_files = sorted(transaction_files, key=lambda x: x.stat().st_mtime) + + logging.info(f"✓ Găsite {len(transaction_files)} fișiere tranzacții din aceeași sesiune") + + return latest_solduri, transaction_files + + +def send_existing_files(): + """Trimite fișierele existente pe Email și Telegram""" + try: + logging.info("=" * 60) + logging.info("TRIMITERE NOTIFICĂRI CU FIȘIERE EXISTENTE") + logging.info("=" * 60) + + # Validează configurația + Config.validate() + + if not Config.ENABLE_NOTIFICATIONS: + logging.error("ENABLE_NOTIFICATIONS=false în .env!") + logging.error("Setează ENABLE_NOTIFICATIONS=true pentru a trimite notificări") + return False + + # Găsește ultimele fișiere + solduri_csv, transaction_csvs = find_latest_files(Config.OUTPUT_DIR) + + if not solduri_csv: + logging.error("Nu există fișiere de trimis!") + return False + + # Pregătește lista de fișiere + files_to_send = [str(solduri_csv)] + for tf in transaction_csvs: + files_to_send.append(str(tf)) + + logging.info("=" * 60) + 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) + + # Trimite notificările + service = NotificationService(Config) + results = service.send_all(files_to_send, account_count) + + # Afișează rezumat + logging.info("=" * 60) + logging.info("REZUMAT NOTIFICĂRI") + logging.info("=" * 60) + + if Config.EMAIL_ENABLED: + status = "✓ Succes" if results['email'] else "✗ Eșuat" + logging.info(f"Email: {status}") + else: + logging.info("Email: Dezactivat") + + if Config.TELEGRAM_ENABLED: + status = "✓ Succes" if results['telegram'] else "✗ Eșuat" + logging.info(f"Telegram: {status}") + else: + logging.info("Telegram: Dezactivat") + + logging.info("=" * 60) + + # Verifică dacă cel puțin un canal a avut succes + success = any(results.values()) + + if success: + logging.info("✓ Notificări trimise cu succes!") + else: + logging.warning("Notificările au eșuat. Verifică configurația în .env") + + return success + + except ValueError as e: + logging.error(f"Eroare configurație: {e}") + logging.error("Verifică setările din .env") + return False + except Exception as e: + logging.error(f"Eroare neașteptată: {e}", exc_info=True) + return False + + +if __name__ == "__main__": + logging.info("") + logging.info("🔔 BTGO Scraper - Trimitere Notificări") + logging.info("") + + success = send_existing_files() + + if success: + sys.exit(0) + else: + sys.exit(1) diff --git a/telegram_trigger_bot.py b/telegram_trigger_bot.py new file mode 100644 index 0000000..0cb6130 --- /dev/null +++ b/telegram_trigger_bot.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +""" +Telegram Trigger Bot - Declanșează BTGO Scraper prin comandă Telegram +""" + +import os +import sys +import subprocess +import logging +from pathlib import Path +from datetime import datetime +import glob +import requests +from dotenv import load_dotenv + +# Load environment +load_dotenv() + +# Configuration +BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN') +ALLOWED_USER_IDS = os.getenv('TELEGRAM_ALLOWED_USER_IDS', '').split(',') # Ex: "123456,789012" +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) +logging.basicConfig( + level=logging.INFO, + format='[%(asctime)s] [%(levelname)s] %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + stream=sys.stdout, + force=True +) + +class TelegramTriggerBot: + def __init__(self): + 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.base_url = f"https://api.telegram.org/bot{self.bot_token}" + self.last_update_id = 0 + self.poll_timeout = POLL_TIMEOUT + + 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"Long polling timeout: {self.poll_timeout}s") + + # Înregistrare comenzi în meniul Telegram + self._register_commands() + + def _register_commands(self): + """Înregistrează comenzile bot în meniul Telegram (pentru DM și grupuri)""" + try: + url = f"{self.base_url}/setMyCommands" + commands = [ + {"command": "scrape", "description": "Rulează scraper-ul BTGO"}, + {"command": "status", "description": "Status sistem"}, + {"command": "help", "description": "Ajutor comenzi"} + ] + + response = requests.post(url, json={"commands": commands}) + if response.status_code == 200 and response.json().get('ok'): + logging.info("✓ Comenzi înregistrate în meniul Telegram") + else: + logging.warning(f"Nu am putut înregistra comenzile: {response.text}") + except Exception as e: + logging.warning(f"Eroare înregistrare comenzi: {e}") + + def send_message(self, chat_id, text, reply_to_message_id=None): + """Trimite mesaj text""" + url = f"{self.base_url}/sendMessage" + data = { + 'chat_id': chat_id, + 'text': text, + 'parse_mode': 'Markdown' + } + if reply_to_message_id: + data['reply_to_message_id'] = reply_to_message_id + response = requests.post(url, json=data) + return response + + def send_document(self, chat_id, file_path, caption=None): + """Trimite document (CSV/JSON)""" + url = f"{self.base_url}/sendDocument" + with open(file_path, 'rb') as file: + files = {'document': file} + data = {'chat_id': chat_id} + if caption: + data['caption'] = caption + 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 run_scraper(self, chat_id, reply_to_message_id=None): + """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) + message_id = None + try: + message_id = response.json()['result']['message_id'] + logging.info(f"Mesaj progress creat cu ID: {message_id}") + except: + logging.warning("Nu am putut salva message_id pentru progress updates") + + try: + # Rulează scraper-ul + logging.info("Pornire scraper...") + + # Prepare environment with global playwright path + Telegram progress info + env = os.environ.copy() + env['PLAYWRIGHT_BROWSERS_PATH'] = 'C:\\playwright-browsers' + 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}") + else: + logging.warning("No message_id available for progress updates") + + result = subprocess.run( + [sys.executable, 'btgo_scraper.py'], + capture_output=True, + text=True, + timeout=600, # 10 minute timeout + cwd=os.path.dirname(os.path.abspath(__file__)), # Run in bot's directory + env=env # Pass environment with playwright path + ) + + if result.returncode == 0: + # Succes - mesajul final va fi editat de notifications.py + logging.info("Scraper finalizat cu succes") + + else: + # Eroare + logging.error(f"Scraper eșuat cu cod {result.returncode}") + error_msg = result.stderr[-1000:] if result.stderr else "Eroare necunoscută" + self.send_message( + chat_id, + f"*EROARE SCRAPER*\n\n```\n{error_msg}\n```", + reply_to_message_id + ) + + except subprocess.TimeoutExpired: + logging.error("Timeout scraper") + self.send_message(chat_id, "*TIMEOUT*\n\nScraper-ul a depasit 10 minute.", reply_to_message_id) + + except Exception as e: + logging.error(f"Eroare execuție: {e}") + self.send_message(chat_id, f"*EROARE EXECUTIE*\n\n```\n{str(e)}\n```", reply_to_message_id) + + def handle_command(self, message): + """Procesează comenzi primite""" + chat_id = message['chat']['id'] + chat_type = message['chat']['type'] # 'private', 'group', 'supergroup' + chat_title = message['chat'].get('title', 'DM') + user_id = message['from']['id'] + username = message['from'].get('username', 'Unknown') + text = message.get('text', '') + message_id = message.get('message_id') + + # Normalizează comanda - elimină @username pentru grupuri (ex: /scrape@botname → /scrape) + if '@' in text: + text = text.split('@')[0] + + # Log context + context = f"grup '{chat_title}'" if chat_type in ['group', 'supergroup'] else "DM" + logging.info(f"Mesaj de la {username} (ID: {user_id}) în {context}: {text}") + + # Verifică autorizare + if not self.is_user_allowed(user_id): + logging.warning(f"User neautorizat: {user_id} în {context}") + self.send_message(chat_id, "*ACCES INTERZIS*\n\nNu ai permisiunea sa folosesti acest bot.", message_id) + return + + # Procesează comenzi + if text == '/start': + welcome_msg = "*BTGO Scraper Trigger Bot*\n\n" + if chat_type in ['group', 'supergroup']: + welcome_msg += f"Bot activ in grupul *{chat_title}*\n\n" + welcome_msg += ( + "Comenzi disponibile:\n" + "`/scrape` - Ruleaza scraper-ul\n" + "`/status` - Status sistem\n" + "`/help` - Ajutor" + ) + self.send_message(chat_id, welcome_msg, message_id) + + elif text == '/scrape': + logging.info(f"Comandă /scrape primită în {context}") + self.run_scraper(chat_id, message_id) + + elif text == '/status': + data_dir = Path('data') + csv_count = len(list(data_dir.glob('*.csv'))) + json_count = len(list(data_dir.glob('*.json'))) + + # Ultimul fișier + all_files = sorted(data_dir.glob('solduri_*.csv'), key=os.path.getmtime, reverse=True) + last_run = "N/A" + if all_files: + last_run = datetime.fromtimestamp(os.path.getmtime(all_files[0])).strftime('%Y-%m-%d %H:%M:%S') + + self.send_message( + chat_id, + f"*STATUS SISTEM*\n\n" + f"Ultima rulare: `{last_run}`\n" + f"Fisiere CSV: {csv_count}\n" + f"Fisiere JSON: {json_count}\n" + f"Working dir: `{os.getcwd()}`", + message_id + ) + + elif text == '/help': + help_msg = "*GHID DE UTILIZARE*\n\n" + 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" + "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" + ) + self.send_message(chat_id, help_msg, message_id) + + else: + self.send_message(chat_id, f"*COMANDA NECUNOSCUTA*\n\n`{text}`\n\nFoloseste /help pentru comenzi.", message_id) + + def get_updates(self): + """Preia update-uri de la Telegram""" + url = f"{self.base_url}/getUpdates" + params = { + 'offset': self.last_update_id + 1, + 'timeout': self.poll_timeout + } + response = requests.get(url, params=params, timeout=self.poll_timeout + 5) + return response.json() + + def run(self): + """Loop principal bot""" + logging.info("Bot pornit. Așteaptă comenzi...") + + while True: + try: + updates = self.get_updates() + + if updates.get('ok'): + for update in updates.get('result', []): + self.last_update_id = update['update_id'] + + if 'message' in update: + self.handle_command(update['message']) + + except KeyboardInterrupt: + logging.info("Bot oprit de utilizator") + break + + except Exception as e: + logging.error(f"Eroare loop: {e}") + import time + time.sleep(5) + +if __name__ == "__main__": + try: + bot = TelegramTriggerBot() + bot.run() + except Exception as e: + logging.error(f"Eroare fatală: {e}") + raise