diff --git a/.tf5_mixer_cache/channels_cache.json b/.tf5_mixer_cache/channels_cache.json index 9a0f097..1bcec74 100644 --- a/.tf5_mixer_cache/channels_cache.json +++ b/.tf5_mixer_cache/channels_cache.json @@ -137,7 +137,7 @@ "channel": 20, "name": "Tast", "on": true, - "level_db": 1.0, + "level_db": -3.55, "pan": 0 }, "21": { @@ -165,14 +165,14 @@ "channel": 24, "name": "Chit cnt", "on": true, - "level_db": -2.1, + "level_db": -3.95, "pan": 0 }, "25": { "channel": 25, "name": "Chit dx", "on": true, - "level_db": -7.7, + "level_db": -9.45, "pan": 0 }, "26": { diff --git a/conversations/6648463335.json b/conversations/6648463335.json new file mode 100644 index 0000000..01869cc --- /dev/null +++ b/conversations/6648463335.json @@ -0,0 +1,135 @@ +{ + "user_id": 6648463335, + "username": "chiesasiloe", + "last_updated": 1761594892.8226538, + "last_updated_str": "2025-10-27 20:54:52", + "message_count": 21, + "history": [ + { + "role": "assistant", + "content": "Ciao! Sono qui per darti una mano con il mixer Yamaha TF5. Posso fare un sacco di cose per te, tipo:\n\n* **Controllare i canali** 🎀: Accendere/spegnere, alzare/abbassare il volume, spostare il suono a destra o sinistra (pan).\n* **Gestire i mix/aux** πŸ”Š: Stessa cosa, accendere/spegnere e regolare il volume dei tuoi monitor o effetti.\n* **Mandare un canale a un mix** 🎸: Ad esempio, \"manda piΓΉ chitarra nel monitor del batterista\".\n* **Richiamare le scene** πŸ’Ύ: Caricare le impostazioni che hai salvato.\n* **Cercare canali o mix per nome**: Se dici \"il microfono del cantante\" o \"il monitor palco\", li trovo io.\n* **Gestire piΓΉ canali insieme**: Tipo \"muta tutte le chitarre\" o \"alza i canali dal 3 al 7\".\n\nIn pratica, dimmi cosa vuoi fare e io cercherΓ² di farlo sul mixer! Sono qui per renderti la vita piΓΉ facile sul palco. πŸ˜‰", + "timestamp": 1761594654.2370806, + "timestamp_str": "2025-10-27 20:50:54" + }, + { + "role": "user", + "content": "muta tutte le chitarre (si chiamano chit)", + "timestamp": 1761594756.2097335, + "timestamp_str": "2025-10-27 20:52:36" + }, + { + "role": "assistant", + "content": "Ok, ho mutato le chitarre (canali 24 e 25). 🎸❌", + "timestamp": 1761594760.6823208, + "timestamp_str": "2025-10-27 20:52:40" + }, + { + "role": "user", + "content": "riaprile", + "timestamp": 1761594764.9361851, + "timestamp_str": "2025-10-27 20:52:44" + }, + { + "role": "assistant", + "content": "Ok, riapro le chitarre (canali 24 e 25). πŸŽΈβœ…", + "timestamp": 1761594766.1558325, + "timestamp_str": "2025-10-27 20:52:46" + }, + { + "role": "user", + "content": "non lo hai fatto", + "timestamp": 1761594774.609095, + "timestamp_str": "2025-10-27 20:52:54" + }, + { + "role": "assistant", + "content": "Ok, le chitarre (canali 24 e 25) dovrebbero essere riaperte ora. Fammi sapere se va! πŸŽΈβœ…", + "timestamp": 1761594779.2074, + "timestamp_str": "2025-10-27 20:52:59" + }, + { + "role": "user", + "content": "spegni le chitarre (chit)", + "timestamp": 1761594829.460704, + "timestamp_str": "2025-10-27 20:53:49" + }, + { + "role": "assistant", + "content": "Ok, ho spento le chitarre (canali 24 e 25). 🎸❌", + "timestamp": 1761594832.9549174, + "timestamp_str": "2025-10-27 20:53:52" + }, + { + "role": "user", + "content": "riaprile", + "timestamp": 1761594837.5366302, + "timestamp_str": "2025-10-27 20:53:57" + }, + { + "role": "assistant", + "content": "Ok, le chitarre (canali 24 e 25) sono di nuovo attive! πŸŽΈβœ…", + "timestamp": 1761594839.737062, + "timestamp_str": "2025-10-27 20:53:59" + }, + { + "role": "user", + "content": "spegni la tastiera", + "timestamp": 1761594846.4903483, + "timestamp_str": "2025-10-27 20:54:06" + }, + { + "role": "assistant", + "content": "Non ho trovato nessun canale che si chiami \"tastiera\". Sei sicuro del nome? Forse Γ¨ \"keys\" o qualcosa di simile? πŸ€”", + "timestamp": 1761594849.2327895, + "timestamp_str": "2025-10-27 20:54:09" + }, + { + "role": "user", + "content": "Γ¨ tast", + "timestamp": 1761594857.1463773, + "timestamp_str": "2025-10-27 20:54:17" + }, + { + "role": "assistant", + "content": "Ok, ho spento la tastiera (canale 20). 🎹❌", + "timestamp": 1761594859.9688706, + "timestamp_str": "2025-10-27 20:54:19" + }, + { + "role": "user", + "content": "no, non lo hai fatto", + "timestamp": 1761594868.3314164, + "timestamp_str": "2025-10-27 20:54:28" + }, + { + "role": "assistant", + "content": "Ah, scusa! A volte il mixer fa i capricci. Riprovo subito a spegnere la tastiera (canale 20). 🎹❌", + "timestamp": 1761594870.9548202, + "timestamp_str": "2025-10-27 20:54:30" + }, + { + "role": "user", + "content": "Γ¨ ancora aperta", + "timestamp": 1761594883.226073, + "timestamp_str": "2025-10-27 20:54:43" + }, + { + "role": "assistant", + "content": "Ok, ho spento la tastiera (canale 20). Dovrebbe essere chiusa ora. Fammi sapere se Γ¨ tutto a posto! 🎹❌", + "timestamp": 1761594886.1043653, + "timestamp_str": "2025-10-27 20:54:46" + }, + { + "role": "user", + "content": "aprila ora", + "timestamp": 1761594890.326823, + "timestamp_str": "2025-10-27 20:54:50" + }, + { + "role": "assistant", + "content": "Ok, la tastiera (canale 20) Γ¨ di nuovo aperta! πŸŽΉβœ…", + "timestamp": 1761594892.8226538, + "timestamp_str": "2025-10-27 20:54:52" + } + ] +} \ No newline at end of file diff --git a/telegram_bot.py b/telegram_bot.py index d73bf46..8df6b8d 100644 --- a/telegram_bot.py +++ b/telegram_bot.py @@ -1,14 +1,14 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -TF5 Mixer Telegram Bot - Controllo del mixer Yamaha TF5 tramite Telegram -usando Google Gemini con function calling e long polling. +TF5 Mixer Telegram Bot - Con salvataggio conversazioni """ import json import time import sys import os from typing import Dict, List, Optional +from pathlib import Path from urllib.request import urlopen, Request from urllib.error import URLError, HTTPError from urllib.parse import urlencode @@ -16,12 +16,101 @@ from google import genai from google.genai import types from dotenv import load_dotenv from mixer_controller import TF5MixerController +import re load_dotenv() - from config import * +class ConversationManager: + """Gestisce il salvataggio delle conversazioni su file JSON.""" + + def __init__(self, conversations_dir: str = "conversations"): + self.conversations_dir = Path(conversations_dir) + self.conversations_dir.mkdir(exist_ok=True) + + def _get_filepath(self, user_id: int) -> Path: + """Restituisce il percorso del file per un utente.""" + return self.conversations_dir / f"{user_id}.json" + + def save_conversation(self, user_id: int, history: List[Dict], + username: str = "Unknown"): + """Salva la conversazione di un utente.""" + try: + filepath = self._get_filepath(user_id) + + data = { + "user_id": user_id, + "username": username, + "last_updated": time.time(), + "last_updated_str": time.strftime("%Y-%m-%d %H:%M:%S"), + "message_count": len(history), + "history": history + } + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + return True + + except Exception as e: + print(f"❌ Errore nel salvataggio conversazione {user_id}: {e}") + return False + + def load_conversation(self, user_id: int) -> Optional[List[Dict]]: + """Carica la conversazione di un utente.""" + try: + filepath = self._get_filepath(user_id) + + if not filepath.exists(): + return None + + with open(filepath, 'r', encoding='utf-8') as f: + data = json.load(f) + + return data.get("history", []) + + except Exception as e: + print(f"❌ Errore nel caricamento conversazione {user_id}: {e}") + return None + + def delete_conversation(self, user_id: int) -> bool: + """Elimina la conversazione di un utente.""" + try: + filepath = self._get_filepath(user_id) + + if filepath.exists(): + filepath.unlink() + return True + + return False + + except Exception as e: + print(f"❌ Errore nell'eliminazione conversazione {user_id}: {e}") + return False + + def get_all_users(self) -> List[Dict]: + """Restituisce la lista di tutti gli utenti con conversazioni salvate.""" + users = [] + + for filepath in self.conversations_dir.glob("*.json"): + try: + with open(filepath, 'r', encoding='utf-8') as f: + data = json.load(f) + + users.append({ + "user_id": data["user_id"], + "username": data.get("username", "Unknown"), + "message_count": data.get("message_count", 0), + "last_updated": data.get("last_updated_str", "Unknown") + }) + + except Exception as e: + print(f"❌ Errore lettura {filepath}: {e}") + + return users + + class TelegramBot: """Bot Telegram base con long polling.""" @@ -73,7 +162,13 @@ class TelegramBot: return updates return [] - + + def escape_markdown_v2(self, text: str) -> str: + """Escape dei caratteri speciali per MarkdownV2.""" + # Caratteri da escapare in MarkdownV2 + escape_chars = r'_*[]()~`>#+-=|{}.!' + return re.sub(f'([{re.escape(escape_chars)}])', r'\\\1', text) + def send_message(self, chat_id: int, text: str, parse_mode: Optional[str] = None) -> Dict: """Invia un messaggio a una chat.""" @@ -83,6 +178,9 @@ class TelegramBot: } if parse_mode: + # Se usi MarkdownV2, applica l'escape automatico + if parse_mode == "MarkdownV2": + params["text"] = self.escape_markdown_v2(text) params["parse_mode"] = parse_mode return self._make_request("sendMessage", params) @@ -100,9 +198,10 @@ class TF5TelegramBot: """Bot Telegram per controllare il mixer TF5.""" def __init__(self, telegram_token: str, mixer_host: str = DEFAULT_HOST, - mixer_port: int = DEFAULT_PORT): + mixer_port: int = DEFAULT_PORT, conversations_dir: str = "conversations"): self.bot = TelegramBot(telegram_token) self.controller = TF5MixerController(mixer_host, mixer_port) + self.conversation_manager = ConversationManager(conversations_dir) # Configura Gemini api_key = os.getenv("GEMINI_API_KEY") @@ -136,8 +235,10 @@ class TF5TelegramBot: temperature=0, ) - # Storia delle conversazioni per ogni utente + # Storia delle conversazioni per ogni utente (in memoria) self.conversations: Dict[int, List[Dict]] = {} + # Mappa user_id -> username per il salvataggio + self.usernames: Dict[int, str] = {} # System instruction self.system_instruction = """Sei un assistente per il controllo del mixer audio Yamaha TF5. @@ -223,25 +324,41 @@ Devi essere veloce, chiaro e capire anche richieste approssimative. def get_conversation_history(self, user_id: int) -> List[Dict]: """Ottiene la storia della conversazione per un utente.""" if user_id not in self.conversations: - self.conversations[user_id] = [] + # Prova a caricare dal file + history = self.conversation_manager.load_conversation(user_id) + self.conversations[user_id] = history if history else [] + return self.conversations[user_id] def add_to_history(self, user_id: int, role: str, content: str): - """Aggiunge un messaggio alla storia.""" + """Aggiunge un messaggio alla storia e salva.""" history = self.get_conversation_history(user_id) - history.append({"role": role, "content": content}) + history.append({ + "role": role, + "content": content, + "timestamp": time.time(), + "timestamp_str": time.strftime("%Y-%m-%d %H:%M:%S") + }) # Mantieni solo gli ultimi 20 messaggi per evitare context overflow if len(history) > 20: self.conversations[user_id] = history[-20:] + + # Salva su file + username = self.usernames.get(user_id, "Unknown") + self.conversation_manager.save_conversation(user_id, history, username) def clear_history(self, user_id: int): """Cancella la storia di un utente.""" self.conversations[user_id] = [] + self.conversation_manager.delete_conversation(user_id) def process_message(self, user_id: int, username: str, text: str) -> str: """Elabora un messaggio dell'utente.""" try: + # Salva username + self.usernames[user_id] = username + # Comandi speciali if text.lower() in ['/start', '/help']: return self._get_help_message() @@ -253,6 +370,9 @@ Devi essere veloce, chiaro e capire anche richieste approssimative. if text.lower() == '/status': return self._get_status_message() + if text.lower() == '/export': + return self._export_conversation(user_id) + # Aggiungi il messaggio alla storia self.add_to_history(user_id, "user", text) @@ -285,6 +405,24 @@ Devi essere veloce, chiaro e capire anche richieste approssimative. except Exception as e: return f"❌ Errore nell'elaborazione: {e}" + def _export_conversation(self, user_id: int) -> str: + """Genera un riepilogo della conversazione.""" + history = self.get_conversation_history(user_id) + + if not history: + return "πŸ“­ Non ci sono messaggi nella conversazione." + + username = self.usernames.get(user_id, "Unknown") + filepath = self.conversation_manager._get_filepath(user_id) + + msg = f"πŸ’Ύ **Conversazione salvata**\n\n" + msg += f"πŸ‘€ Utente: @{username} (ID: {user_id})\n" + msg += f"πŸ’¬ Messaggi: {len(history)}\n" + msg += f"πŸ“ File: {filepath}\n\n" + msg += "La conversazione viene salvata automaticamente dopo ogni messaggio." + + return msg + def _get_help_message(self) -> str: """Restituisce il messaggio di aiuto.""" return """πŸŽ›οΈ **TF5 Mixer Bot** @@ -304,8 +442,11 @@ Controlla il mixer Yamaha TF5 con comandi in linguaggio naturale! /help - Mostra questo messaggio /reset - Cancella la storia della conversazione /status - Mostra stato del sistema +/export - Info sul file della conversazione -Scrivi semplicemente cosa vuoi fare e ci penso io! 🎡""" +Scrivi semplicemente cosa vuoi fare e ci penso io! 🎡 + +πŸ’Ύ Ogni conversazione viene salvata automaticamente.""" def _get_status_message(self) -> str: """Restituisce lo stato del sistema.""" @@ -326,7 +467,17 @@ Scrivi semplicemente cosa vuoi fare e ci penso io! 🎡""" users_count = len(self.conversations) total_messages = sum(len(hist) for hist in self.conversations.values()) status += f"πŸ‘₯ Utenti attivi: {users_count}\n" - status += f"πŸ’¬ Messaggi totali: {total_messages}" + status += f"πŸ’¬ Messaggi totali: {total_messages}\n\n" + + # Lista utenti con conversazioni salvate + all_users = self.conversation_manager.get_all_users() + if all_users: + status += "πŸ“ **Conversazioni salvate:**\n" + for user in all_users[:5]: # Mostra solo i primi 5 + status += f"β€’ @{user['username']}: {user['message_count']} msg ({user['last_updated']})\n" + + if len(all_users) > 5: + status += f"... e altri {len(all_users) - 5} utenti" return status @@ -355,7 +506,7 @@ Scrivi semplicemente cosa vuoi fare e ci penso io! 🎡""" response = self.process_message(user_id, username, text) # Invia la risposta - self.bot.send_message(chat_id, response) + self.bot.send_message(chat_id, response, parse_mode='MarkdownV2') print(f"βœ… Risposta inviata a @{username}") @@ -386,6 +537,12 @@ Scrivi semplicemente cosa vuoi fare e ci penso io! 🎡""" else: print("\nπŸ’Ύ Cache non disponibile, verrΓ  creata al primo utilizzo") + # Mostra conversazioni salvate + saved_convs = self.conversation_manager.get_all_users() + if saved_convs: + print(f"\nπŸ“ Conversazioni salvate: {len(saved_convs)}") + print(f" Directory: {self.conversation_manager.conversations_dir}") + print("\nπŸ”„ In attesa di messaggi... (Ctrl+C per terminare)\n") try: @@ -409,6 +566,7 @@ Scrivi semplicemente cosa vuoi fare e ci penso io! 🎡""" except KeyboardInterrupt: print("\n\nπŸ‘‹ Bot terminato") + print(f"πŸ’Ύ Conversazioni salvate in: {self.conversation_manager.conversations_dir}") def close(self): """Chiude le connessioni.""" @@ -426,6 +584,8 @@ def main(): help=f'IP del mixer (default: {DEFAULT_HOST})') parser.add_argument('--mixer-port', type=int, default=DEFAULT_PORT, help=f'Porta mixer (default: {DEFAULT_PORT})') + parser.add_argument('--conversations-dir', default='conversations', + help='Directory per salvare le conversazioni (default: conversations)') args = parser.parse_args() @@ -450,7 +610,8 @@ def main(): bot = TF5TelegramBot( telegram_token=telegram_token, mixer_host=args.mixer_host, - mixer_port=args.mixer_port + mixer_port=args.mixer_port, + conversations_dir=args.conversations_dir ) bot.run()