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