#!/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 zipfile 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) # 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.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": "scrape_zip", "description": "Rulează scraper + trimite ZIP"}, {"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_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, send_as_zip=False): """Execută scraper-ul""" # Trimite mesaj inițial și salvează message_id pentru editare ulterioară zip_msg = " (arhiva ZIP)" if send_as_zip else "" response = self.send_message(chat_id, f"*BTGO Scraper pornit{zip_msg}*\n\nAsteapta 2FA pe telefon.", reply_to_message_id) 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(f"Pornire scraper (send_as_zip={send_as_zip})...") # 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") 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 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) # Ș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ă 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" "`/scrape_zip` - Ruleaza scraper + trimite ZIP\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 == '/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" "`/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` sau `/scrape_zip`\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" "• `/zip` - Rapid, foloseste datele existente\n\n" "*NOTE:*\n" "- Scraper-ul ruleaza ~2-3 minute\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