Modificări: - telegram_trigger_bot.py: - Păstrează TELEGRAM_CHAT_ID și TELEGRAM_MESSAGE_ID pentru progress - Setează flag SEND_AS_ZIP=true în environment - NU mai dezactivează notificările - notifications.py: - Verifică flag SEND_AS_ZIP din environment - Dacă SEND_AS_ZIP=true, trimite ZIP cu progress updates - Mesajul de progres e editat la fel ca /scrape normal Comportament /scrape_zip: 1. Bot trimite "Scraper pornit (arhiva ZIP)" 2. Scraper rulează și editează mesajul cu progress 3. notifications.py detectează flag-ul SEND_AS_ZIP 4. Trimite ZIP cu solduri în loc de fișiere individuale 5. Editează mesajul final cu detalii despre ZIP 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
424 lines
17 KiB
Python
424 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Telegram Trigger Bot - Declanșează BTGO Scraper prin comandă Telegram
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
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)
|
|
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
|