Backup automatico script del 2025-12-28 07:00

This commit is contained in:
2025-12-28 07:00:02 +01:00
parent 43d44e3e85
commit c89436c26b
3 changed files with 701 additions and 352 deletions

View File

@@ -1,22 +1,53 @@
import logging
import subprocess
import os
import datetime
import requests
from functools import wraps
from zoneinfo import ZoneInfo
from dateutil import parser
from typing import Dict, List, Optional, Tuple
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (
Application,
CommandHandler,
CallbackQueryHandler,
ContextTypes,
JobQueue
)
# =============================================================================
# LOOGLE BOT V7.0 (ULTIMATE)
# - Dashboard Sistema (SSH/Ping)
# - Meteo Arome ASCII (On-Demand + Schedulato)
# - Multi-User Security
# =============================================================================
# --- CONFIGURAZIONE ---
BOT_TOKEN = os.environ.get('BOT_TOKEN')
OWNER_ID = int(os.environ.get('ALLOWED_USER_ID'))
# Gestione Multi-Utente
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()]
# Configurazione Sistema
SSH_USER = "daniely"
NAS_USER = "daniely"
MASTER_IP = "192.168.128.80"
# Configurazione Meteo
HOME_LAT = 43.9356
HOME_LON = 12.4296
HOME_NAME = "🏠 Casa (Strada Cà Toro)"
TZ = "Europe/Rome"
TZINFO = ZoneInfo(TZ)
OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search"
MODEL = "meteofrance_arome_france_hd"
HTTP_HEADERS = {"User-Agent": "loogle-bot-v7"}
# --- LISTE DISPOSITIVI ---
CORE_DEVICES = [
{"name": "🍓 Pi-1 (Master)", "ip": MASTER_IP, "type": "pi", "user": SSH_USER},
@@ -40,7 +71,9 @@ INFRA_DEVICES = [
logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)
# --- FUNZIONI ---
# =============================================================================
# SEZIONE 1: FUNZIONI SISTEMA (SSH, PING, UTILS)
# =============================================================================
def run_cmd(command, ip=None, user=None):
try:
@@ -53,24 +86,10 @@ def run_cmd(command, ip=None, user=None):
except Exception: return "Err"
def get_ping_icon(ip):
print(f"DEBUG: Pinging {ip}...") # LOG PER CAPIRE DOVE SI BLOCCA
try:
# Timeout aggressivo: 0.5 secondi (-W 1 è il minimo di ping standard, ma Python taglia a 0.8)
subprocess.run(
["ping", "-c", "1", "-W", "1", ip],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=0.8, # Timeout Python brutale
check=True
)
subprocess.run(["ping", "-c", "1", "-W", "1", ip], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=0.8, check=True)
return ""
except subprocess.TimeoutExpired:
return "🔴" # Timeout Python
except subprocess.CalledProcessError:
return "🔴" # Risposta "Host Unreachable"
except Exception as e:
print(f"Errore Ping {ip}: {e}")
return ""
except Exception: return "🔴"
def get_device_stats(device):
ip, user, dtype = device['ip'], device['user'], device['type']
@@ -90,10 +109,8 @@ def get_device_stats(device):
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):
@@ -105,11 +122,145 @@ def run_speedtest():
try: return subprocess.check_output("speedtest-cli --simple", shell=True, timeout=50).decode('utf-8')
except: return "Errore Speedtest"
# --- BOT HANDLERS ---
# =============================================================================
# SEZIONE 2: FUNZIONI METEO (AROME ASCII)
# =============================================================================
def now_local() -> datetime.datetime:
return datetime.datetime.now(TZINFO)
def parse_time(t: str) -> datetime.datetime:
dt = parser.isoparse(t)
if dt.tzinfo is None: return dt.replace(tzinfo=TZINFO)
return dt.astimezone(TZINFO)
def degrees_to_cardinal(d: int) -> str:
dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
return dirs[round(d / 45) % 8]
def get_icon_set(prec, snow, code, is_day, cloud, vis, temp, rain, gust, cape):
sky = "☁️"
if code in (95, 96, 99): sky = "⛈️" if prec > 0 else "🌩️"
elif (code in (45, 48) or vis < 1000) and prec < 1: sky = "🌫️"
elif prec >= 0.1: sky = "🌨️" if snow > 0 else "🌧️"
elif cloud <= 20: sky = "☀️" if is_day else "🌙"
elif cloud <= 40: sky = "🌤️" if is_day else "🌙"
elif cloud <= 60: sky = "⛅️"
elif cloud <= 80: sky = "🌥️"
sgx = "-"
if snow > 0 or (code in (71,73,75,77,85,86) if code else False): sgx = "☃️"
elif temp < 0 or (code in (66,67) if code else False): sgx = "🧊"
elif cape > 2000: sgx = "🌪️"
elif cape > 1000: sgx = ""
elif temp > 35: sgx = "🥵"
elif rain > 4: sgx = "☔️"
elif gust > 50: sgx = "💨"
return sky, sgx
def get_coordinates(city_name: str) -> Optional[Tuple[float, float, str]]:
params = {"name": city_name, "count": 1, "language": "it", "format": "json"}
try:
r = requests.get(GEOCODING_URL, params=params, headers=HTTP_HEADERS, timeout=10)
data = r.json()
if "results" in data and len(data["results"]) > 0:
res = data["results"][0]
name = f"{res.get('name')} ({res.get('country_code','')})"
return res["latitude"], res["longitude"], name
except Exception as e: logger.error(f"Geocoding error: {e}")
return None
def get_forecast(lat, lon) -> Optional[Dict]:
params = {
"latitude": lat, "longitude": lon, "timezone": TZ,
"forecast_days": 3, "models": MODEL,
"wind_speed_unit": "kmh", "precipitation_unit": "mm",
"hourly": "temperature_2m,relative_humidity_2m,cloudcover,windspeed_10m,winddirection_10m,windgusts_10m,precipitation,rain,snowfall,weathercode,is_day,cape,visibility",
}
try:
r = requests.get(OPEN_METEO_URL, params=params, headers=HTTP_HEADERS, timeout=25)
r.raise_for_status()
return r.json()
except Exception as e: logger.error(f"Meteo API error: {e}"); return None
def generate_weather_report(lat, lon, location_name) -> str:
data = get_forecast(lat, lon)
if not data: return "❌ Errore API Meteo."
hourly = data.get("hourly", {})
times = hourly.get("time", [])
if not times: return "❌ Dati orari mancanti."
now = now_local().replace(minute=0, second=0, microsecond=0)
blocks = []
for (label, hours_duration, step) in [("Prime 24h", 24, 1), ("Successive 24h", 24, 2)]:
end_time = now + datetime.timedelta(hours=hours_duration)
lines = [f"{'LT':<2} {'':>3} {'h%':>3} {'mm':>2} {'Vento':<5} {'Nv%':>3} {'Sky':<2} {'Sgx':<3}", "-" * 30]
count = 0
for i, t_str in enumerate(times):
try: dt = parse_time(t_str)
except: continue
if dt < now or dt >= end_time: continue
if dt.hour % step != 0: continue
try:
T = float(hourly["temperature_2m"][i])
Rh = int(hourly["relative_humidity_2m"][i] or 0)
Cl = int(hourly["cloudcover"][i] or 0)
Pr = float(hourly["precipitation"][i] or 0)
Rn = float(hourly["rain"][i] or 0)
Sn = float(hourly["snowfall"][i] or 0)
Wspd = float(hourly["windspeed_10m"][i] or 0)
Gust = float(hourly["windgusts_10m"][i] or 0)
Wdir = int(hourly["winddirection_10m"][i] or 0)
Cape = float(hourly["cape"][i] or 0)
Vis = float(hourly["visibility"][i] or 10000)
Code = int(hourly["weathercode"][i]) if hourly["weathercode"][i] is not None else None
IsDay = int(hourly["is_day"][i] if hourly["is_day"][i] is not None else 1)
t_s = f"{int(round(T))}"
p_s = "--" if Pr < 0.2 else f"{int(round(Pr))}"
card = degrees_to_cardinal(Wdir)
w_val = Wspd
is_g = (Gust - Wspd) > 15
if is_g: w_val = Gust
w_txt = f"{card} {int(round(w_val))}"
if is_g:
g_txt = f"G{int(round(w_val))}"
if len(card)+len(g_txt) <= 5: w_txt = f"{card}{g_txt}"
elif len(card)+1+len(g_txt) <= 5: w_txt = f"{card} {g_txt}"
else: w_txt = g_txt
w_fmt = f"{w_txt:<5}"
sky, sgx = get_icon_set(Pr, Sn, Code, IsDay, Cl, Vis, T, Rn, Gust, Cape)
lines.append(f"{dt.strftime('%H'):<2} {t_s:>3} {Rh:>3} {p_s:>2} {w_fmt} {Cl:>3} {sky:<2} {sgx:<3}")
count += 1
except: continue
if count > 0:
day_label = f"{['Lun','Mar','Mer','Gio','Ven','Sab','Dom'][now.weekday()]} {now.day}"
blocks.append(f"*{day_label} ({label})*\n```text\n" + "\n".join(lines) + "\n```")
now = end_time
return f"🌤️ *METEO REPORT*\n📍 {location_name}\n\n" + "\n\n".join(blocks)
# =============================================================================
# SEZIONE 3: BOT HANDLERS & SCHEDULER
# =============================================================================
# Decoratore Sicurezza Multi-Utente
def restricted(func):
@wraps(func)
async def wrapped(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs):
if update.effective_user.id != OWNER_ID: return
user_id = update.effective_user.id
if user_id not in ALLOWED_IDS:
logger.warning(f"⚠️ ACCESSO NEGATO: User {user_id}")
return
return await func(update, context, *args, **kwargs)
return wrapped
@@ -118,14 +269,40 @@ 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("📜 Logs", callback_data="menu_logs")]
[InlineKeyboardButton("🌤️ Meteo Casa", callback_data="req_meteo_home"), InlineKeyboardButton("📜 Logs", callback_data="menu_logs")]
]
text = "🎛 **Loogle Control Center v6.2**\nSeleziona un pannello:"
text = "🎛 **Loogle Control Center v7.0**\nComandi disponibili:\n🔹 `/meteo <città>`\n🔹 Pulsanti sotto"
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")
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:
if not context.args:
await update.message.reply_text("⚠️ Usa: `/meteo <città>` (es. `/meteo Rimini`)", parse_mode="Markdown")
return
city = " ".join(context.args)
await update.message.reply_text(f"🔄 Cerco '{city}'...", parse_mode="Markdown")
coords = get_coordinates(city)
if coords:
lat, lon, name = coords
report = generate_weather_report(lat, lon, name)
await update.message.reply_text(report, parse_mode="Markdown")
else:
await update.message.reply_text(f"❌ Città '{city}' non trovata.", parse_mode="Markdown")
async def scheduled_morning_report(context: ContextTypes.DEFAULT_TYPE) -> None:
"""Funzione lanciata dallo scheduler alle 08:00"""
logger.info("⏰ Invio report automatico meteo...")
report = generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME)
for uid in ALLOWED_IDS:
try:
await context.bot.send_message(chat_id=uid, text=report, parse_mode="Markdown")
except Exception as e:
logger.error(f"Errore invio report a {uid}: {e}")
@restricted
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
@@ -134,16 +311,22 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
data = query.data
if data == "main_menu": await start(update, context)
elif data == "req_meteo_home":
await query.edit_message_text("⏳ **Scaricamento Meteo Casa...**", parse_mode="Markdown")
report = generate_weather_report(HOME_LAT, HOME_LON, HOME_NAME)
keyboard = [[InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]]
await query.edit_message_text(report, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
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**\nDettagli CPU/RAM/Temp:", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
await query.edit_message_text("🖥️ **Core Servers**", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
elif data == "stat_all":
await query.edit_message_text("⏳ **Analisi Core Servers...**", parse_mode="Markdown")
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")
@@ -154,27 +337,14 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
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")
elif data == "menu_lan":
await query.edit_message_text("⏳ **Scansione LAN rapida...**", parse_mode="Markdown")
await query.edit_message_text("⏳ **Scansione LAN...**", parse_mode="Markdown")
report = "🔍 **DIAGNOSTICA LAN**\n\n"
try:
# Core Devices
for dev in CORE_DEVICES:
report += f"{get_ping_icon(dev['ip'])} `{dev['ip']}` - {dev['name']}\n"
for dev in CORE_DEVICES: report += f"{get_ping_icon(dev['ip'])} `{dev['ip']}` - {dev['name']}\n"
report += "\n"
# Infra Devices
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 imprevisto durante scansione: {e}"
keyboard = [
[InlineKeyboardButton("⚡ Menu Riavvio", callback_data="menu_reboot")],
[InlineKeyboardButton("🔄 Aggiorna", callback_data="menu_lan"), InlineKeyboardButton("⬅️ Indietro", callback_data="main_menu")]
]
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":
@@ -182,14 +352,13 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
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**\nFunziona solo se il dispositivo supporta SSH e hai le chiavi.", reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
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"Comando inviato a {dev['name']}...\n\nRisposta:\n`{res}`", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Indietro", callback_data="menu_reboot")]]), parse_mode="Markdown")
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")
# --- ALTRI MENU STANDARD ---
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 "🔴"
@@ -209,7 +378,7 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
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 in corso...**", parse_mode="Markdown")
await query.edit_message_text("🚀 **Speedtest...**", 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")
@@ -223,9 +392,19 @@ async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
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 v7.0 (Ultimate)...")
application = Application.builder().token(BOT_TOKEN).build()
# Handlers
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("meteo", meteo_command))
application.add_handler(CallbackQueryHandler(button_handler))
# SCHEDULER (Sostituisce CRON)
# Esegue il meteo tutti i giorni alle 08:00 Europe/Rome
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__":