import logging import subprocess import os import datetime import requests from functools import wraps from zoneinfo import ZoneInfo from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ( Application, CommandHandler, CallbackQueryHandler, ContextTypes, JobQueue ) # ============================================================================= # LOOGLE BOT V9.0 (ULTIMATE + CAMERAS + MODULAR) # - Dashboard Sistema (SSH/WOL/Monitor) # - Meteo Smart (Meteo.py / Previsione7.py) # - CCTV Hub (Cam.py + FFMPEG) # ============================================================================= # --- CONFIGURAZIONE AMBIENTE --- BOT_TOKEN = os.environ.get('BOT_TOKEN') allowed_users_raw = os.environ.get('ALLOWED_USER_ID', '') ALLOWED_IDS = [int(x.strip()) for x in allowed_users_raw.split(',') if x.strip().isdigit()] SSH_USER = "daniely" NAS_USER = "daniely" MASTER_IP = "192.168.128.80" TZ = "Europe/Rome" TZINFO = ZoneInfo(TZ) # --- GESTIONE PERCORSI DINAMICA (DOCKER FRIENDLY) --- SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) METEO_SCRIPT = os.path.join(SCRIPT_DIR, "meteo.py") METEO7_SCRIPT = os.path.join(SCRIPT_DIR, "previsione7.py") CAM_SCRIPT = os.path.join(SCRIPT_DIR, "cam.py") # --- LISTE DISPOSITIVI --- CORE_DEVICES = [ {"name": "πŸ“ Pi-1 (Master)", "ip": MASTER_IP, "type": "pi", "user": SSH_USER}, {"name": "πŸ“ Pi-2 (Backup)", "ip": "127.0.0.1", "type": "local", "user": ""}, {"name": "πŸ—„οΈ NAS 920+", "ip": "192.168.128.100", "type": "nas", "user": NAS_USER}, {"name": "πŸ—„οΈ NAS 214", "ip": "192.168.128.90", "type": "nas", "user": NAS_USER} ] INFRA_DEVICES = [ {"name": "πŸ“‘ Router", "ip": "192.168.128.1"}, {"name": "πŸ“Ά WiFi Sala", "ip": "192.168.128.101"}, {"name": "πŸ“Ά WiFi Luca", "ip": "192.168.128.102"}, {"name": "πŸ“Ά WiFi Taverna", "ip": "192.168.128.103"}, {"name": "πŸ“Ά WiFi Dado", "ip": "192.168.128.104"}, {"name": "πŸ”Œ Sw Sala", "ip": "192.168.128.105"}, {"name": "πŸ”Œ Sw Main", "ip": "192.168.128.106"}, {"name": "πŸ”Œ Sw Lav", "ip": "192.168.128.107"}, {"name": "πŸ”Œ Sw Tav", "ip": "192.168.128.108"} ] # Configurazione Logging logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO) logger = logging.getLogger(__name__) # ============================================================================= # SEZIONE 1: FUNZIONI UTILI E HELPER # ============================================================================= def run_cmd(command, ip=None, user=None): """Esegue comandi shell locali o via SSH""" try: if ip == "127.0.0.1" or ip is None: return subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, timeout=8).decode('utf-8').strip() else: safe_cmd = command.replace("'", "'\\''") full_cmd = f"ssh -o LogLevel=ERROR -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=3 {user}@{ip} '{safe_cmd}'" return subprocess.check_output(full_cmd, shell=True, stderr=subprocess.STDOUT, timeout=8).decode('utf-8').strip() except Exception: return "Err" def get_ping_icon(ip): try: subprocess.run(["ping", "-c", "1", "-W", "1", ip], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=0.8, check=True) return "βœ…" except Exception: return "πŸ”΄" def get_device_stats(device): ip, user, dtype = device['ip'], device['user'], device['type'] uptime_raw = run_cmd("uptime -p 2>/dev/null || uptime", ip, user) if not uptime_raw or "Err" in uptime_raw: return "πŸ”΄ **OFFLINE**" uptime = uptime_raw.replace("up ", "").split(", load")[0].split(", ")[0] temp = "N/A" if dtype in ["pi", "local"]: t = run_cmd("cat /sys/class/thermal/thermal_zone0/temp", ip, user) if t.isdigit(): temp = f"{int(t)/1000:.1f}Β°C" elif dtype == "nas": t = run_cmd("cat /sys/class/hwmon/hwmon0/temp1_input 2>/dev/null || cat /sys/class/thermal/thermal_zone0/temp", ip, user) if t.isdigit(): v = int(t); temp = f"{v/1000:.1f}Β°C" if v > 1000 else f"{v}Β°C" if dtype == "nas": ram_cmd = "free | grep Mem | awk '{printf \"%.0f%%\", $3*100/$2}'" else: ram_cmd = "free -m | awk 'NR==2{if ($2>0) printf \"%.0f%%\", $3*100/$2; else print \"0%\"}'" disk_path = "/" if dtype != "nas" else "/volume1" disk_cmd = f"df -h {disk_path} | awk 'NR==2{{print $5}}'" return f"βœ… **ONLINE**\n⏱️ Up: {uptime}\n🌑️ Temp: {temp} | 🧠 RAM: {run_cmd(ram_cmd, ip, user)} | πŸ’Ύ Disk: {run_cmd(disk_cmd, ip, user)}" def read_log_file(filepath, lines=15): if not os.path.exists(filepath): return "⚠️ File non trovato." try: return subprocess.check_output(['tail', '-n', str(lines), filepath], stderr=subprocess.STDOUT).decode('utf-8') except Exception as e: return f"Errore: {str(e)}" def run_speedtest(): try: return subprocess.check_output("speedtest-cli --simple", shell=True, timeout=60).decode('utf-8') except: return "Errore Speedtest" def call_script_text(script_path, args_list): """Wrapper per lanciare script che restituiscono testo (Meteo)""" try: cmd = ["python3", script_path] + args_list result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) return result.stdout.strip() if result.returncode == 0 else f"⚠️ Errore Script:\n{result.stderr}" except Exception as e: return f"❌ Errore esecuzione: {e}" # ============================================================================= # SEZIONE 2: GESTORI COMANDI (HANDLERS) # ============================================================================= # Decoratore Sicurezza def restricted(func): @wraps(func) async def wrapped(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs): user_id = update.effective_user.id if user_id not in ALLOWED_IDS: return return await func(update, context, *args, **kwargs) return wrapped @restricted async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: keyboard = [ [InlineKeyboardButton("πŸ–₯️ Core Server", callback_data="menu_core"), InlineKeyboardButton("πŸ” Scan LAN", callback_data="menu_lan")], [InlineKeyboardButton("πŸ›‘οΈ Pi-hole", callback_data="menu_pihole"), InlineKeyboardButton("🌐 Rete", callback_data="menu_net")], [InlineKeyboardButton("🌀️ Meteo Casa", callback_data="req_meteo_home"), InlineKeyboardButton("πŸ“Ή Camere", callback_data="menu_cams")], [InlineKeyboardButton("πŸ“œ Logs", callback_data="menu_logs")] ] text = "πŸŽ› **Loogle Control Center v9.0**\n\nπŸ”Ή `/meteo `\nπŸ”Ή `/meteo7 ` (7 Giorni)\nπŸ”Ή `/cam ` (Snapshot)" if update.message: await update.message.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") else: await update.callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") @restricted async def meteo_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: query = " ".join(context.args).strip() if not query or query.lower() == "casa": await update.message.reply_text("⏳ **Scarico Meteo Casa...**", parse_mode="Markdown") report = call_script_text(METEO_SCRIPT, ["--home"]) else: await update.message.reply_text(f"πŸ”„ Cerco '{query}'...", parse_mode="Markdown") report = call_script_text(METEO_SCRIPT, ["--query", query]) await update.message.reply_text(report, parse_mode="Markdown") @restricted async def meteo7_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: chat_id = update.effective_chat.id query = "casa" if context.args: query = " ".join(context.args) await update.message.reply_text(f"πŸ“‘ Calcolo previsione 7gg per: {query}...", parse_mode="Markdown") subprocess.Popen(["python3", METEO7_SCRIPT, query, "--chat_id", str(chat_id)]) @restricted async def cam_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not context.args: # Se non c'Γ¨ argomento, mostra il menu camere keyboard = [ [InlineKeyboardButton("πŸ“· Sala", callback_data="req_cam_sala"), InlineKeyboardButton("πŸ“· Ingresso", callback_data="req_cam_ingresso")], [InlineKeyboardButton("πŸ“· Taverna", callback_data="req_cam_taverna"), InlineKeyboardButton("πŸ“· Retro", callback_data="req_cam_retro")], [InlineKeyboardButton("πŸ“· Matrim.", callback_data="req_cam_matrimoniale"), InlineKeyboardButton("πŸ“· Luca", callback_data="req_cam_luca")], [InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")] ] await update.message.reply_text("πŸ“Ή **Scegli una telecamera:**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") return cam_name = context.args[0] await update.message.reply_chat_action(action="upload_photo") try: # Timeout 15s per RTSP result = subprocess.run(["python3", CAM_SCRIPT, cam_name], capture_output=True, text=True, timeout=15) output = result.stdout.strip() if output.startswith("OK:"): img_path = output.split(":", 1)[1] await update.message.reply_photo(photo=open(img_path, 'rb'), caption=f"πŸ“· **{cam_name.capitalize()}**") elif output.startswith("ERR:"): await update.message.reply_text(output.split(":", 1)[1]) else: await update.message.reply_text(f"❌ Risposta imprevista dallo script: {output}") except Exception as e: await update.message.reply_text(f"❌ Errore critico: {e}") async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None: # Meteo automatico alle 8:00 report = call_script_text(METEO_SCRIPT, ["--home"]) for uid in ALLOWED_IDS: try: await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown") except: pass @restricted async def clip_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if not context.args: await update.message.reply_text("⚠️ Usa: `/clip ` (es. /clip sala)", parse_mode="Markdown") return cam_name = context.args[0] await update.message.reply_chat_action(action="upload_video") # Icona "sta inviando video..." await update.message.reply_text(f"πŸŽ₯ **Registro 10s da {cam_name}...**", parse_mode="Markdown") try: # Lancia lo script con flag --video result = subprocess.run(["python3", CAM_SCRIPT, cam_name, "--video"], capture_output=True, text=True, timeout=20) output = result.stdout.strip() if output.startswith("OK:"): vid_path = output.split(":", 1)[1] await update.message.reply_video(video=open(vid_path, 'rb'), caption=f"πŸŽ₯ **Clip: {cam_name.capitalize()}**") elif output.startswith("ERR:"): await update.message.reply_text(output.split(":", 1)[1]) except Exception as e: await update.message.reply_text(f"❌ Errore critico: {e}") @restricted async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: query = update.callback_query await query.answer() # Risposta immediata per togliere il loading dal pulsante data = query.data # --- NAVIGAZIONE MENU --- if data == "main_menu": await start(update, context) # --- SEZIONE METEO --- elif data == "req_meteo_home": await query.edit_message_text("⏳ **Scaricamento Meteo Casa...**", parse_mode="Markdown") report = call_script_text(METEO_SCRIPT, ["--home"]) keyboard = [[InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") # --- SEZIONE CAMERE --- elif data == "menu_cams": keyboard = [ [InlineKeyboardButton("πŸ“· Sala", callback_data="req_cam_sala"), InlineKeyboardButton("πŸ“· Ingresso", callback_data="req_cam_ingresso")], [InlineKeyboardButton("πŸ“· Taverna", callback_data="req_cam_taverna"), InlineKeyboardButton("πŸ“· Retro", callback_data="req_cam_retro")], [InlineKeyboardButton("πŸ“· Matrim.", callback_data="req_cam_matrimoniale"), InlineKeyboardButton("πŸ“· Luca", callback_data="req_cam_luca")], [InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")] ] await query.edit_message_text("πŸ“Ή **Centrale Video**\nSeleziona una telecamera:", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data.startswith("req_cam_"): cam_name = data.replace("req_cam_", "") # Non editiamo il messaggio, inviamo una nuova foto sotto try: result = subprocess.run(["python3", CAM_SCRIPT, cam_name], capture_output=True, text=True, timeout=15) output = result.stdout.strip() if output.startswith("OK:"): img_path = output.split(":", 1)[1] await context.bot.send_photo(chat_id=update.effective_chat.id, photo=open(img_path, 'rb'), caption=f"πŸ“· **{cam_name.capitalize()}**") elif output.startswith("ERR:"): await context.bot.send_message(chat_id=update.effective_chat.id, text=output.split(":", 1)[1]) except Exception as e: await context.bot.send_message(chat_id=update.effective_chat.id, text=f"❌ Errore richiesta cam: {e}") elif data.startswith("req_vid_"): cam_name = data.replace("req_vid_", "") await query.answer("πŸŽ₯ Registrazione in corso (10s)...") # Inviamo un messaggio di attesa perchΓ© ci mette un po' msg = await context.bot.send_message(chat_id=update.effective_chat.id, text=f"⏳ Registro clip: {cam_name}...") try: result = subprocess.run(["python3", CAM_SCRIPT, cam_name, "--video"], capture_output=True, text=True, timeout=20) output = result.stdout.strip() # Cancelliamo il messaggio di attesa await context.bot.delete_message(chat_id=update.effective_chat.id, message_id=msg.message_id) if output.startswith("OK:"): vid_path = output.split(":", 1)[1] await context.bot.send_video(chat_id=update.effective_chat.id, video=open(vid_path, 'rb'), caption=f"πŸŽ₯ **Clip: {cam_name.capitalize()}**") elif output.startswith("ERR:"): await context.bot.send_message(chat_id=update.effective_chat.id, text=output.split(":", 1)[1]) except Exception as e: await context.bot.send_message(chat_id=update.effective_chat.id, text=f"❌ Errore: {e}") # --- SEZIONE SISTEMA CORE --- elif data == "menu_core": keyboard = [] for i, dev in enumerate(CORE_DEVICES): keyboard.append([InlineKeyboardButton(dev['name'], callback_data=f"stat_{i}")]) keyboard.append([InlineKeyboardButton("πŸ“Š Report Completo", callback_data="stat_all")]) keyboard.append([InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]) await query.edit_message_text("πŸ–₯️ **Core Servers**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data == "stat_all": await query.edit_message_text("⏳ **Analisi...**", parse_mode="Markdown") report = "πŸ“Š **REPORT CORE**\n" for dev in CORE_DEVICES: report += f"\nπŸ”Ή **{dev['name']}**\n{get_device_stats(dev)}\n" await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_core")]]), parse_mode="Markdown") elif data.startswith("stat_"): dev = CORE_DEVICES[int(data.split("_")[1])] await query.edit_message_text(f"⏳ Controllo {dev['name']}...", parse_mode="Markdown") await query.edit_message_text(f"πŸ”Ή **{dev['name']}**\n\n{get_device_stats(dev)}", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_core")]]), parse_mode="Markdown") # --- SEZIONE LAN --- elif data == "menu_lan": await query.edit_message_text("⏳ **Scansione LAN...**", parse_mode="Markdown") report = "πŸ” **DIAGNOSTICA LAN**\n\n" try: for dev in CORE_DEVICES: report += f"{get_ping_icon(dev['ip'])} `{dev['ip']}` - {dev['name']}\n" report += "\n" for dev in INFRA_DEVICES: report += f"{get_ping_icon(dev['ip'])} `{dev['ip']}` - {dev['name']}\n" except Exception as e: report += f"\n⚠️ Errore: {e}" keyboard = [[InlineKeyboardButton("⚑ Menu Riavvio", callback_data="menu_reboot")], [InlineKeyboardButton("πŸ”„ Aggiorna", callback_data="menu_lan"), InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data == "menu_reboot": keyboard = [] for i, dev in enumerate(INFRA_DEVICES): if "Router" not in dev['name']: keyboard.append([InlineKeyboardButton(f"⚑ {dev['name']}", callback_data=f"reboot_{i}")]) keyboard.append([InlineKeyboardButton("⬅️ Indietro", callback_data="menu_lan")]) await query.edit_message_text("⚠️ **RIAVVIO REMOTO**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data.startswith("reboot_"): dev = INFRA_DEVICES[int(data.split("_")[1])] res = run_cmd("reboot", dev['ip'], "admin") await query.edit_message_text(f"⚑ Inviato a {dev['name']}...\n\n`{res}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_reboot")]]), parse_mode="Markdown") # --- SEZIONE PI-HOLE --- elif data == "menu_pihole": status_raw = run_cmd("sudo pihole status", MASTER_IP, SSH_USER) icon = "βœ…" if "Enabled" in status_raw or "enabled" in status_raw else "πŸ”΄" text = f"πŸ›‘οΈ **Pi-hole Master**\nStato: {icon}\n\n`{status_raw}`" keyboard = [[InlineKeyboardButton("⏸️ 5m", callback_data="ph_disable_300"), InlineKeyboardButton("⏸️ 30m", callback_data="ph_disable_1800")], [InlineKeyboardButton("▢️ Attiva", callback_data="ph_enable"), InlineKeyboardButton("πŸ”„ Restart", callback_data="ph_restart")], [InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data.startswith("ph_"): if "disable" in data: run_cmd(f"sudo pihole disable {data.split('_')[2]}s", MASTER_IP, SSH_USER) elif "enable" in data: run_cmd("sudo pihole enable", MASTER_IP, SSH_USER) elif "restart" in data: run_cmd("sudo systemctl restart pihole-FTL", MASTER_IP, SSH_USER) await query.edit_message_text("βœ… Comando inviato.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_pihole")]]), parse_mode="Markdown") # --- SEZIONE RETE --- elif data == "menu_net": ip = run_cmd("curl -s ifconfig.me") keyboard = [[InlineKeyboardButton("πŸš€ Speedtest", callback_data="net_speedtest")], [InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text(f"🌐 **Rete**\n🌍 IP: `{ip}`", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data == "net_speedtest": await query.edit_message_text("πŸš€ **Speedtest... (attendi 40s)**", parse_mode="Markdown") res = run_speedtest() await query.edit_message_text(f"πŸš€ **Risultato:**\n\n`{res}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_net")]]), parse_mode="Markdown") # --- SEZIONE LOGS --- elif data == "menu_logs": keyboard = [[InlineKeyboardButton("🐢 Watchdog", callback_data="log_wd"), InlineKeyboardButton("πŸ’Ύ Backup", callback_data="log_bk")], [InlineKeyboardButton("πŸ”„ NPM Sync", callback_data="log_npm"), InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]] await query.edit_message_text("πŸ“œ **Logs**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown") elif data.startswith("log_"): paths = {"log_wd": "/logs/dhcp-watchdog.log", "log_bk": "/logs/raspiBackup.log", "log_npm": "/logs/sync-npm.log"} log_c = read_log_file(paths[data]) await query.edit_message_text(f"πŸ“œ **Log:**\n\n`{log_c}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_logs")]]), parse_mode="Markdown") def main(): logger.info("Avvio Loogle Bot v9.0 (Modular)...") application = Application.builder().token(BOT_TOKEN).build() # Registrazione Comandi application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("meteo", meteo_command)) application.add_handler(CommandHandler("meteo7", meteo7_command)) application.add_handler(CommandHandler("cam", cam_command)) application.add_handler(CommandHandler("clip", clip_command)) # Registrazione Callback Menu application.add_handler(CallbackQueryHandler(button_handler)) # Scheduler job_queue = application.job_queue job_queue.run_daily(scheduled_morning_report, time=datetime.time(hour=8, minute=0, tzinfo=TZINFO), days=(0, 1, 2, 3, 4, 5, 6)) application.run_polling() if __name__ == "__main__": main()