Files
loogle-scripts/services/telegram-bot/bot.py

396 lines
21 KiB
Python
Executable File

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 <città>`\n🔹 `/meteo7 <città>` (7 Giorni)\n🔹 `/cam <nome>` (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 <nome_camera>` (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()