Files
btgo-playwright/telegram_trigger_bot.py
Marius Mutu c2ca401a26 Fix UnicodeEncodeError pe Windows pentru caractere românești
Problema:
- Logging eșua cu UnicodeEncodeError când scria caractere românești (ă, î, ș)
- Windows cmd.exe folosește cp1252 implicit, nu UTF-8

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 21:31:55 +02:00

429 lines
17 KiB
Python

#!/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