1101 lines
45 KiB
Python
1101 lines
45 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
TF5 Mixer Telegram Bot - Con profili utente personalizzati
|
|
"""
|
|
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
|
|
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 UserProfile:
|
|
"""Rappresenta il profilo di un utente."""
|
|
|
|
ROLES = {
|
|
"mixerista": {
|
|
"name": "Mixerista",
|
|
"emoji": "🎛️",
|
|
"permissions": ["all"],
|
|
"description": "Accesso completo a tutti i canali e mix"
|
|
},
|
|
"cantante": {
|
|
"name": "Cantante",
|
|
"emoji": "🎤",
|
|
"permissions": ["own_channel", "own_mix"],
|
|
"description": "Gestione del proprio microfono e in-ear"
|
|
},
|
|
"musicista": {
|
|
"name": "Musicista",
|
|
"emoji": "🎸",
|
|
"permissions": ["own_channel", "own_mix"],
|
|
"description": "Gestione del proprio strumento e in-ear"
|
|
},
|
|
"operatore_diretta": {
|
|
"name": "Operatore Diretta",
|
|
"emoji": "📡",
|
|
"permissions": ["own_mix"],
|
|
"description": "Gestione del mix per la diretta streaming/registrazione"
|
|
},
|
|
"traduttore": {
|
|
"name": "Traduttore",
|
|
"emoji": "🌐",
|
|
"permissions": ["own_mix"],
|
|
"description": "Gestione del mix per la traduzione simultanea"
|
|
}
|
|
}
|
|
|
|
def __init__(self, user_id: int, username: str):
|
|
self.user_id = user_id
|
|
self.username = username
|
|
self.role: Optional[str] = None
|
|
self.display_name: Optional[str] = None
|
|
self.channel_labels: List[str] = [] # Etichette dei canali assegnati
|
|
self.mix_labels: List[str] = [] # Etichette dei mix/aux assegnati
|
|
self.setup_completed = False
|
|
self.created_at = time.time()
|
|
self.last_updated = time.time()
|
|
|
|
def to_dict(self) -> Dict:
|
|
"""Converte il profilo in dizionario."""
|
|
return {
|
|
"user_id": self.user_id,
|
|
"username": self.username,
|
|
"role": self.role,
|
|
"display_name": self.display_name,
|
|
"channel_labels": self.channel_labels,
|
|
"mix_labels": self.mix_labels,
|
|
"setup_completed": self.setup_completed,
|
|
"created_at": self.created_at,
|
|
"created_at_str": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.created_at)),
|
|
"last_updated": self.last_updated,
|
|
"last_updated_str": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.last_updated))
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict) -> 'UserProfile':
|
|
"""Crea un profilo da un dizionario."""
|
|
profile = cls(data["user_id"], data["username"])
|
|
profile.role = data.get("role")
|
|
profile.display_name = data.get("display_name")
|
|
profile.channel_labels = data.get("channel_labels", [])
|
|
profile.mix_labels = data.get("mix_labels", [])
|
|
profile.setup_completed = data.get("setup_completed", False)
|
|
profile.created_at = data.get("created_at", time.time())
|
|
profile.last_updated = data.get("last_updated", time.time())
|
|
return profile
|
|
|
|
def get_role_info(self) -> Dict:
|
|
"""Restituisce le info sul ruolo."""
|
|
return self.ROLES.get(self.role, {})
|
|
|
|
def get_summary(self) -> str:
|
|
"""Restituisce un riepilogo del profilo."""
|
|
if not self.setup_completed:
|
|
return "❌ Profilo non configurato. Usa /setup per iniziare."
|
|
|
|
role_info = self.get_role_info()
|
|
msg = f"{role_info['emoji']} **{self.display_name}** ({role_info['name']})\n\n"
|
|
|
|
if self.channel_labels:
|
|
msg += f"🎚️ Canali: {', '.join(self.channel_labels)}\n"
|
|
|
|
if self.mix_labels:
|
|
msg += f"🔊 Mix: {', '.join(self.mix_labels)}\n"
|
|
|
|
return msg
|
|
|
|
class ProfileManager:
|
|
"""Gestisce i profili utente."""
|
|
|
|
def __init__(self, profiles_dir: str = "profiles"):
|
|
self.profiles_dir = Path(profiles_dir)
|
|
self.profiles_dir.mkdir(exist_ok=True)
|
|
|
|
def _get_filepath(self, user_id: int) -> Path:
|
|
"""Restituisce il percorso del file per un utente."""
|
|
return self.profiles_dir / f"{user_id}.json"
|
|
|
|
def save_profile(self, profile: UserProfile) -> bool:
|
|
"""Salva il profilo di un utente."""
|
|
try:
|
|
filepath = self._get_filepath(profile.user_id)
|
|
profile.last_updated = time.time()
|
|
|
|
with open(filepath, 'w', encoding='utf-8') as f:
|
|
json.dump(profile.to_dict(), f, ensure_ascii=False, indent=2)
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"❌ Errore nel salvataggio profilo {profile.user_id}: {e}")
|
|
return False
|
|
|
|
def load_profile(self, user_id: int, username: str) -> UserProfile:
|
|
"""Carica o crea il profilo di un utente."""
|
|
try:
|
|
filepath = self._get_filepath(user_id)
|
|
|
|
if filepath.exists():
|
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
return UserProfile.from_dict(data)
|
|
|
|
except Exception as e:
|
|
print(f"❌ Errore nel caricamento profilo {user_id}: {e}")
|
|
|
|
# Crea nuovo profilo
|
|
return UserProfile(user_id, username)
|
|
|
|
def delete_profile(self, user_id: int) -> bool:
|
|
"""Elimina il profilo 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 profilo {user_id}: {e}")
|
|
return False
|
|
|
|
def get_all_profiles(self) -> List[UserProfile]:
|
|
"""Restituisce tutti i profili salvati."""
|
|
profiles = []
|
|
|
|
for filepath in self.profiles_dir.glob("*.json"):
|
|
try:
|
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
profiles.append(UserProfile.from_dict(data))
|
|
|
|
except Exception as e:
|
|
print(f"❌ Errore lettura {filepath}: {e}")
|
|
|
|
return profiles
|
|
|
|
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
|
|
|
|
class TelegramBot:
|
|
"""Bot Telegram base con long polling."""
|
|
|
|
def __init__(self, token: str):
|
|
self.token = token
|
|
self.base_url = f"https://api.telegram.org/bot{token}"
|
|
self.offset = 0
|
|
|
|
def _make_request(self, method: str, params: Optional[Dict] = None) -> Dict:
|
|
"""Esegue una richiesta all'API di Telegram."""
|
|
url = f"{self.base_url}/{method}"
|
|
|
|
try:
|
|
if params:
|
|
data = json.dumps(params).encode('utf-8')
|
|
headers = {'Content-Type': 'application/json'}
|
|
req = Request(url, data=data, headers=headers)
|
|
else:
|
|
req = Request(url)
|
|
|
|
with urlopen(req, timeout=30) as response:
|
|
return json.loads(response.read().decode('utf-8'))
|
|
|
|
except HTTPError as e:
|
|
error_body = e.read().decode('utf-8')
|
|
print(f"❌ HTTP Error {e.code}: {error_body}")
|
|
return {"ok": False, "description": error_body}
|
|
except URLError as e:
|
|
print(f"❌ URL Error: {e.reason}")
|
|
return {"ok": False, "description": str(e.reason)}
|
|
except Exception as e:
|
|
print(f"❌ Error: {e}")
|
|
return {"ok": False, "description": str(e)}
|
|
|
|
def get_updates(self, timeout: int = 30) -> List[Dict]:
|
|
"""Ottiene gli aggiornamenti usando long polling."""
|
|
params = {
|
|
"offset": self.offset,
|
|
"timeout": timeout,
|
|
"allowed_updates": ["message", "callback_query"]
|
|
}
|
|
|
|
result = self._make_request("getUpdates", params)
|
|
|
|
if result.get("ok"):
|
|
updates = result.get("result", [])
|
|
if updates:
|
|
self.offset = updates[-1]["update_id"] + 1
|
|
return updates
|
|
|
|
return []
|
|
|
|
def escape_markdown_v2(self, text: str) -> str:
|
|
"""Escape dei caratteri speciali per 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,
|
|
reply_markup: Optional[Dict] = None) -> Dict:
|
|
"""Invia un messaggio a una chat."""
|
|
params = {
|
|
"chat_id": chat_id,
|
|
"text": text
|
|
}
|
|
|
|
if parse_mode:
|
|
if parse_mode == "MarkdownV2":
|
|
params["text"] = self.escape_markdown_v2(text)
|
|
params["parse_mode"] = parse_mode
|
|
|
|
if reply_markup:
|
|
params["reply_markup"] = reply_markup
|
|
|
|
return self._make_request("sendMessage", params)
|
|
|
|
def send_chat_action(self, chat_id: int, action: str = "typing") -> Dict:
|
|
"""Invia un'azione (es. "typing")."""
|
|
params = {
|
|
"chat_id": chat_id,
|
|
"action": action
|
|
}
|
|
return self._make_request("sendChatAction", params)
|
|
|
|
def answer_callback_query(self, callback_query_id: str, text: str = "") -> Dict:
|
|
"""Risponde a una callback query."""
|
|
params = {
|
|
"callback_query_id": callback_query_id,
|
|
"text": text
|
|
}
|
|
return self._make_request("answerCallbackQuery", params)
|
|
|
|
|
|
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,
|
|
conversations_dir: str = "conversations",
|
|
profiles_dir: str = "profiles"):
|
|
self.bot = TelegramBot(telegram_token)
|
|
self.controller = TF5MixerController(mixer_host, mixer_port)
|
|
self.conversation_manager = ConversationManager(conversations_dir)
|
|
self.profile_manager = ProfileManager(profiles_dir)
|
|
|
|
# Configura Gemini
|
|
api_key = os.getenv("GEMINI_API_KEY")
|
|
if not api_key:
|
|
raise ValueError("GEMINI_API_KEY non trovata")
|
|
|
|
self.client = genai.Client(api_key=api_key)
|
|
|
|
# Configurazione Gemini con function calling
|
|
self.config = types.GenerateContentConfig(
|
|
tools=[
|
|
self.controller.recall_scene,
|
|
self.controller.set_channel_level,
|
|
self.controller.set_channel_on_off,
|
|
self.controller.set_channel_pan,
|
|
self.controller.set_mix_level,
|
|
self.controller.set_mix_on_off,
|
|
self.controller.mute_multiple_channels,
|
|
self.controller.unmute_multiple_channels,
|
|
self.controller.get_channel_info,
|
|
self.controller.get_mix_info,
|
|
self.controller.search_channels_by_name,
|
|
self.controller.search_mixes_by_name,
|
|
self.controller.get_all_channels_summary,
|
|
self.controller.get_all_mixes_summary,
|
|
self.controller.refresh_cache,
|
|
self.controller.set_channel_to_mix_level,
|
|
self.controller.set_channel_to_mix_on_off,
|
|
self.controller.get_channel_to_mix_info,
|
|
self.controller.get_full_mix_details,
|
|
],
|
|
temperature=0,
|
|
)
|
|
|
|
# Storia delle conversazioni per ogni utente (in memoria)
|
|
self.conversations: Dict[int, List[Dict]] = {}
|
|
# Profili utente (in memoria)
|
|
self.profiles: Dict[int, UserProfile] = {}
|
|
# Stati setup utente
|
|
self.setup_states: Dict[int, Dict] = {}
|
|
|
|
# System instruction base
|
|
self.base_system_instruction = """Sei un assistente per il controllo del mixer audio Yamaha TF5.
|
|
Parli in modo semplice e diretto, come un tecnico del suono esperto che aiuta i musicisti sul palco.
|
|
|
|
Il mixer ha:
|
|
- 40 canali (microfoni, strumenti, ecc.)
|
|
- 20 mix/aux (monitor, effetti, ecc.)
|
|
- Ogni canale ha volume (da silenzio a +10 dB), acceso/spento, e bilanciamento sinistra/destra
|
|
- Scene memorizzate nei banchi A e B (da 0 a 99)
|
|
|
|
IMPORTANTE - Sistema di Cache:
|
|
- Le info sui canali e mix sono salvate per 60 minuti per non sovraccaricare il mixer
|
|
- Usa search_channels_by_name e search_mixes_by_name per cercare velocemente
|
|
- Usa get_all_channels_summary e get_all_mixes_summary per vedere tutto
|
|
- Quando modifichi un canale/mix, la cache viene aggiornata automaticamente
|
|
- Quando si carica una scena, i dati vengono invalidati e aggiornati alla prossima richiesta
|
|
- Puoi fare refresh_cache solo se l'utente lo chiede esplicitamente
|
|
|
|
Come interpretare le richieste:
|
|
|
|
VOLUME/LIVELLO:
|
|
- "alza/abbassa/aumenta/diminuisci" → cambia il volume
|
|
- "più/meno forte/volume" → cambia il volume
|
|
- "al massimo" → +10 dB
|
|
- "un po' più alto" → +3 dB circa
|
|
- "metti a zero" o "unity" → 0 dB
|
|
- "abbassa di poco" → -3 dB
|
|
- "metti basso" → -20 dB
|
|
- "silenzio/muto" → spegni il canale
|
|
|
|
ON/OFF:
|
|
- "accendi/attiva/apri" → canale/mix ON
|
|
- "spegni/muta/chiudi/stacca" → canale/mix OFF
|
|
- "muto" può significare sia spegnere che abbassare molto
|
|
|
|
BILANCIAMENTO (PAN):
|
|
- "a sinistra/left" → pan -63
|
|
- "a destra/right" → pan +63
|
|
- "al centro" → pan 0
|
|
- "un po' a sinistra" → pan -30 circa
|
|
|
|
IDENTIFICAZIONE CANALI E MIX:
|
|
- Accetta sia numeri ("canale 5", "mix 3") che nomi ("il microfono del cantante", "monitor palco")
|
|
- Se non trovi un canale/mix per nome, cerca usando search_channels_by_name o search_mixes_by_name
|
|
- "il mio mic/microfono" → cerca tra i canali chi è sul palco
|
|
- "le chitarre/i vox/le tastiere" → cerca per strumento
|
|
- "tutti i mic/tutte le chitarre" → cerca e gestisci multipli
|
|
- "il monitor" / "l'aux 2" → cerca tra i mix
|
|
|
|
RICHIESTE SEMPLIFICATE (per utenti non mixeristi):
|
|
- "alzami la chitarra" / "più chitarra" → riferimento al MIO mix (quello dell'utente)
|
|
- "abbassa il basso in cuffia" → gestisci il send del basso al MIO in-ear
|
|
- "più voce" → alza la voce nel MIO mix
|
|
- "meno batteria" → abbassa la batteria nel MIO mix
|
|
|
|
SEND AI MIX/AUX:
|
|
- "alza il gelato nell'aux 2" → usa set_channel_to_mix_level
|
|
- "manda più chitarra nel monitor" → aumenta il send
|
|
- "togli la voce dal mix 3" → spegni il send o abbassa molto
|
|
- i mix 9/10, 11/12, 13/14, 15/16, 17/18, 19/20 sono stereo, fai le operazioni su entrambi
|
|
|
|
SCENE:
|
|
- "carica/richiama/vai alla scena X" → recall_scene
|
|
- Accetta "A5", "scena A 5", "la cinque del banco A", ecc.
|
|
|
|
GRUPPI DI CANALI:
|
|
- "i canali dal 3 al 7" → canali 3,4,5,6,7
|
|
- "spegni tutto tranne..." → muta tutti gli altri
|
|
- "solo i microfoni" → attiva solo quelli, spegni il resto
|
|
|
|
---
|
|
ANALISI DEL MIX (per utenti esperti)
|
|
Quando un utente chiede di "analizzare un mix", "controllare i volumi della diretta", o "dare suggerimenti per il mix X", devi usare la funzione `get_full_mix_details(mix_number)`.
|
|
Questa funzione ti darà la lista di tutti i canali inviati a quel mix, con i loro livelli.
|
|
|
|
Il tuo compito è analizzare questi dati e fornire suggerimenti pratici per migliorare l'equilibrio, specialmente per un mix destinato a una diretta streaming o registrazione.
|
|
|
|
Linee guida per l'analisi:
|
|
1. **Gerarchia dei Volumi**:
|
|
- **Predicatore/Oratore Principale**: Deve essere il canale più alto di tutti. Cerca nomi come "Predicatore", "Pastore", "Pulpito". Il suo livello deve essere il punto di riferimento.
|
|
- **Voce Solista (Lead Vocal)**: Subito dopo il predicatore. Deve essere chiaramente sopra gli strumenti e i cori. Cerca "Voce L", "Cantante P".
|
|
- **Cori/Voci Secondarie**: Dovrebbero essere a un livello inferiore rispetto alla voce solista, per supportarla senza coprirla.
|
|
- **Strumenti Melodici**: (Tastiere, Chitarre) Dovrebbero creare un "tappeto sonoro" sotto le voci. Il loro livello deve essere bilanciato per non competere con le voci.
|
|
- **Sezione Ritmica**: (Basso, Batteria) Devono essere presenti ma controllati. Il basso deve essere udibile ma non rimbombante, la batteria presente ma non invadente.
|
|
|
|
2. **Problemi Comuni da Segnalare**:
|
|
- **Canali Spenti ma Inviati**: Controlla il campo `channel_is_on`. Se è `False` per un canale con un livello di send significativo, è un errore comune. Segnalalo dicendo: "Attenzione, il canale X ('Nome Canale') è spento sul suo fader principale, ma stai cercando di mandarlo al mix. Non si sentirà nulla. Devi prima accenderlo."
|
|
- **Livelli Estremi**: Segnala i canali con send molto alti (sopra +5 dB, rischio di distorsione) o molto bassi (sotto -30 dB, forse inutili o dimenticati).
|
|
- **Equilibrio Voci/Strumenti**: Se il livello medio degli strumenti è uguale o superiore a quello della voce solista, suggerisci di abbassare gli strumenti per fare spazio alla voce. Esempio: "Le tastiere sono a -5dB e la voce a -6dB. Prova ad abbassare un po' le tastiere per far emergere meglio la voce."
|
|
|
|
3. **Formato della Risposta**:
|
|
- Inizia con un riepilogo generale del mix (es: "Ecco un'analisi del Mix 15 (Diretta Streaming):").
|
|
- Usa sezioni chiare con emoji per una lettura rapida:
|
|
- `✅ Punti di Forza`: Se vedi un buon equilibrio di base.
|
|
- `⚠️ Suggerimenti per Migliorare`: Elenco puntato con azioni concrete.
|
|
- `🤔 Problemi Rilevati`: Per i canali spenti o livelli anomali.
|
|
- Sii propositivo e offri soluzioni, non solo problemi.
|
|
|
|
Esempio di analisi: "Ho analizzato il mix 15. La voce del predicatore è correttamente il canale più forte. Tuttavia, la chitarra acustica è quasi allo stesso livello della voce solista. Ti suggerisco di abbassare la chitarra di circa 3dB per dare più risalto al cantante. Ho notato anche che il canale 12 ('Coro Dx') è spento, quindi non sta contribuendo al mix. Vuoi che lo accenda?"
|
|
---
|
|
|
|
CASI PARTICOLARI:
|
|
- Se la richiesta è ambigua, chiedi chiarimenti in modo colloquiale
|
|
- Se serve cercare un canale/mix, usa prima la cache (search_channels_by_name / search_mixes_by_name)
|
|
- Conferma sempre cosa hai fatto con un messaggio breve e chiaro
|
|
- Usa emoji occasionalmente per rendere le risposte più amichevoli (✅ ❌ 🎤 🎸 🔊)
|
|
- Se qualcosa non funziona, spiega il problema in modo semplice
|
|
|
|
Rispondi sempre in modo:
|
|
- Diretto e colloquiale
|
|
- Senza troppi tecnicismi
|
|
- Confermando chiaramente l'azione eseguita
|
|
- Suggerendo alternative se qualcosa non è possibile
|
|
|
|
Ricorda: chi ti parla è spesso sul palco, con le mani occupate da uno strumento.
|
|
Devi essere veloce, chiaro e capire anche richieste approssimative.
|
|
"""
|
|
|
|
def _get_user_system_instruction(self, profile: UserProfile) -> str:
|
|
"""Genera le istruzioni di sistema personalizzate per l'utente."""
|
|
instruction = self.base_system_instruction
|
|
|
|
if not profile.setup_completed:
|
|
return instruction
|
|
|
|
# Aggiungi contesto del profilo
|
|
role_info = profile.get_role_info()
|
|
instruction += f"\n\n{'='*70}\n"
|
|
instruction += f"PROFILO UTENTE CORRENTE:\n"
|
|
instruction += f"{'='*70}\n\n"
|
|
instruction += f"Nome: {profile.display_name}\n"
|
|
instruction += f"Ruolo: {role_info['name']} {role_info['emoji']}\n"
|
|
|
|
if profile.channel_labels:
|
|
labels_str = ', '.join([f"'{l}'" for l in profile.channel_labels])
|
|
instruction += f"\nNomi dei canali assegnati: {labels_str}\n"
|
|
instruction += "Quando l'utente dice 'il mio canale' o 'il mio mic/strumento', si riferisce a questi. "
|
|
instruction += "**DEVI usare `search_channels_by_name` con questi nomi per trovare il numero di canale corretto prima di eseguire qualsiasi altra azione.**\n"
|
|
|
|
if profile.mix_labels:
|
|
labels_str = ', '.join([f"'{l}'" for l in profile.mix_labels])
|
|
instruction += f"\nNomi dei mix assegnati: {labels_str}\n"
|
|
instruction += "Quando l'utente dice 'le mie cuffie', 'il mio monitor', 'in cuffia', si riferisce a questi mix. "
|
|
instruction += "**DEVI usare `search_mixes_by_name` con questi nomi per trovare il numero di mix corretto prima di eseguire qualsiasi altra azione.**\n"
|
|
|
|
# Istruzioni specifiche per ruolo
|
|
permissions = role_info.get("permissions", [])
|
|
|
|
if "all" in permissions:
|
|
instruction += "\n🎛️ MIXERISTA - Accesso completo:\n"
|
|
instruction += "- Puoi controllare tutti i canali e mix del mixer\n"
|
|
instruction += "- Puoi caricare scene\n"
|
|
instruction += "- Puoi fare qualsiasi operazione richiesta\n"
|
|
else:
|
|
instruction += f"\n⚠️ LIMITAZIONI DI ACCESSO ({role_info['name']}):\n"
|
|
|
|
if "own_channel" in permissions and profile.channel_labels:
|
|
instruction += f"- Canali controllabili: SOLO quelli che corrispondono ai nomi nel tuo profilo.\n"
|
|
|
|
if "own_mix" in permissions and profile.mix_labels:
|
|
instruction += f"- Mix controllabili: SOLO quelli che corrispondono ai nomi nel tuo profilo.\n"
|
|
instruction += f"- Puoi gestire i send di QUALSIASI canale verso i TUOI mix.\n"
|
|
|
|
instruction += "\n❌ NON puoi:\n"
|
|
instruction += "- Caricare scene (solo i mixeristi).\n"
|
|
instruction += "- Accedere a canali/mix non associati al tuo profilo.\n"
|
|
|
|
instruction += "\n💡 INTERPRETAZIONE RICHIESTE:\n"
|
|
instruction += "- 'alzami la chitarra' = alza il send della chitarra nel TUO mix (identificato dal suo nome).\n"
|
|
instruction += "- 'più voce' = alza il send della voce nel TUO mix.\n"
|
|
instruction += "- 'meno batteria in cuffia' = abbassa il send della batteria nel TUO mix.\n"
|
|
instruction += "- Se l'utente chiede di fare qualcosa fuori dai suoi permessi, spiega gentilmente che non può e suggerisci di chiedere al mixerista.\n"
|
|
|
|
instruction += f"\n{'='*70}\n"
|
|
|
|
return instruction
|
|
|
|
def get_profile(self, user_id: int, username: str) -> UserProfile:
|
|
"""Ottiene il profilo di un utente."""
|
|
if user_id not in self.profiles:
|
|
self.profiles[user_id] = self.profile_manager.load_profile(user_id, username)
|
|
return self.profiles[user_id]
|
|
|
|
def _start_setup(self, user_id: int) -> str:
|
|
"""Inizia il processo di setup."""
|
|
roles_text = "🎛️ **Scegli il tuo ruolo:**\n\n"
|
|
|
|
buttons = []
|
|
for role_key, role_info in UserProfile.ROLES.items():
|
|
roles_text += f"{role_info['emoji']} **{role_info['name']}**\n"
|
|
roles_text += f" {role_info['description']}\n\n"
|
|
buttons.append([{
|
|
"text": f"{role_info['emoji']} {role_info['name']}",
|
|
"callback_data": f"setup_role_{role_key}"
|
|
}])
|
|
|
|
keyboard = {"inline_keyboard": buttons}
|
|
|
|
self.bot.send_message(
|
|
user_id,
|
|
roles_text,
|
|
parse_mode="MarkdownV2",
|
|
reply_markup=keyboard
|
|
)
|
|
|
|
self.setup_states[user_id] = {"step": "role"}
|
|
return ""
|
|
|
|
def _handle_setup_callback(self, user_id: int, username: str, data: str) -> str:
|
|
"""Gestisce le callback del processo di setup."""
|
|
profile = self.get_profile(user_id, username)
|
|
state = self.setup_states.get(user_id, {})
|
|
|
|
if data.startswith("setup_role_"):
|
|
role = data.replace("setup_role_", "")
|
|
profile.role = role
|
|
role_info = profile.get_role_info()
|
|
|
|
self.bot.send_message(
|
|
user_id,
|
|
f"✅ Ruolo impostato: {role_info['emoji']} {role_info['name']}\n\n"
|
|
f"📝 Come vuoi che ti chiami? (es: Marco, Chitarra 1, Cantante)"
|
|
)
|
|
|
|
self.setup_states[user_id] = {"step": "name", "role": role}
|
|
return ""
|
|
|
|
return ""
|
|
|
|
def _handle_setup_message(self, user_id: int, username: str, text: str) -> str:
|
|
"""Gestisce i messaggi durante il setup."""
|
|
profile = self.get_profile(user_id, username)
|
|
state = self.setup_states.get(user_id, {})
|
|
|
|
if not state:
|
|
return ""
|
|
|
|
step = state.get("step")
|
|
|
|
# Step: inserimento nome
|
|
if step == "name":
|
|
profile.display_name = text.strip()
|
|
role_info = profile.get_role_info()
|
|
permissions = role_info.get("permissions", [])
|
|
|
|
if "all" in permissions:
|
|
profile.setup_completed = True
|
|
self.profile_manager.save_profile(profile)
|
|
del self.setup_states[user_id]
|
|
|
|
return (f"✅ Configurazione completata!\n\n"
|
|
f"{profile.get_summary()}\n"
|
|
f"Hai accesso completo a tutti i canali e mix del mixer.\n\n"
|
|
f"Inizia a controllare il mixer con comandi come:\n"
|
|
f"• 'Alza il canale 5'\n"
|
|
f"• 'Richiama la scena A10'\n"
|
|
f"• 'Mostrami tutti i canali'")
|
|
|
|
if "own_channel" in permissions:
|
|
self.bot.send_message(
|
|
user_id,
|
|
f"🎚️ **Come si chiama il tuo canale/strumento?**\n\n"
|
|
f"Scrivi il nome esatto che vedi sull'etichetta del mixer (es: 'VOX Marco', 'CH Acustica').\n"
|
|
f"Se hai più canali, separali con una virgola (es: 'Tastiera L', 'Tastiera R').\n\n"
|
|
f"(Scrivi 'nessuno' se non hai un canale specifico)"
|
|
)
|
|
self.setup_states[user_id] = {"step": "channels"}
|
|
elif "own_mix" in permissions:
|
|
self.bot.send_message(
|
|
user_id,
|
|
f"🔊 **Come si chiama il tuo mix per le cuffie/monitor?**\n\n"
|
|
f"Scrivi il nome esatto (es: 'InEar Marco', 'Cuffia Palco Sx').\n"
|
|
f"Se usi un mix stereo, scrivi entrambi i nomi separati da virgola (es: 'IEM L', 'IEM R').\n\n"
|
|
f"(Scrivi 'nessuno' se non usi un mix specifico)"
|
|
)
|
|
self.setup_states[user_id] = {"step": "mixes"}
|
|
|
|
return ""
|
|
|
|
# Step: inserimento etichette canali
|
|
elif step == "channels":
|
|
labels_text = text.strip()
|
|
if labels_text.lower() != 'nessuno':
|
|
profile.channel_labels = [label.strip() for label in labels_text.split(',')]
|
|
|
|
role_info = profile.get_role_info()
|
|
permissions = role_info.get("permissions", [])
|
|
|
|
if "own_mix" in permissions:
|
|
self.bot.send_message(
|
|
user_id,
|
|
f"🔊 **Come si chiama il tuo mix per le cuffie/monitor?**\n\n"
|
|
f"Scrivi il nome esatto (es: 'InEar Marco', 'Cuffia Palco Sx').\n"
|
|
f"Se usi un mix stereo, scrivi entrambi i nomi separati da virgola (es: 'IEM L', 'IEM R').\n\n"
|
|
f"(Scrivi 'nessuno' se non usi un mix specifico)"
|
|
)
|
|
self.setup_states[user_id] = {"step": "mixes"}
|
|
return ""
|
|
else:
|
|
profile.setup_completed = True
|
|
self.profile_manager.save_profile(profile)
|
|
del self.setup_states[user_id]
|
|
|
|
return (f"✅ Configurazione completata!\n\n"
|
|
f"{profile.get_summary()}\n"
|
|
f"Ora puoi controllare i tuoi canali con comandi come:\n"
|
|
f"• 'Alza il mio canale'\n"
|
|
f"• 'Spegni il mio mic'")
|
|
|
|
# Step: inserimento etichette mix
|
|
elif step == "mixes":
|
|
labels_text = text.strip()
|
|
if labels_text.lower() != 'nessuno':
|
|
profile.mix_labels = [label.strip() for label in labels_text.split(',')]
|
|
|
|
profile.setup_completed = True
|
|
self.profile_manager.save_profile(profile)
|
|
del self.setup_states[user_id]
|
|
|
|
example_commands = []
|
|
if profile.channel_labels:
|
|
example_commands.append("• 'Alza il volume del mio canale'")
|
|
if profile.mix_labels:
|
|
example_commands.extend([
|
|
"• 'Alzami la chitarra nel mio monitor'",
|
|
"• 'Più voce in cuffia'",
|
|
"• 'Meno batteria'"
|
|
])
|
|
|
|
return (f"✅ Configurazione completata!\n\n"
|
|
f"{profile.get_summary()}\n\n"
|
|
f"Esempi di comandi che puoi usare:\n" + "\n".join(example_commands))
|
|
|
|
return ""
|
|
|
|
def _get_file_path(self, file_id: str) -> str:
|
|
"""Richiede a Telegram il percorso di un file."""
|
|
result = self.bot._make_request("getFile", {"file_id": file_id})
|
|
if result.get("ok"):
|
|
return result["result"]["file_path"]
|
|
raise Exception("Impossibile ottenere il file da Telegram")
|
|
|
|
def _download_file(self, url: str) -> bytes:
|
|
"""Scarica un file da Telegram e restituisce i bytes."""
|
|
req = Request(url)
|
|
with urlopen(req, timeout=30) as response:
|
|
return response.read()
|
|
|
|
def _transcribe_audio(self, audio_bytes: bytes) -> Optional[str]:
|
|
"""Trascrive un file audio in testo usando Gemini."""
|
|
try:
|
|
response = self.client.models.generate_content(
|
|
model="gemini-2.5-flash",
|
|
contents=[
|
|
"Trascrivi in testo chiaro ciò che dice questa registrazione:",
|
|
types.Part.from_bytes(
|
|
data=audio_bytes,
|
|
mime_type="audio/ogg"
|
|
),
|
|
]
|
|
)
|
|
return response.text.strip()
|
|
except Exception as e:
|
|
print(f"❌ Errore nella trascrizione: {e}")
|
|
return None
|
|
|
|
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:
|
|
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 e salva."""
|
|
history = self.get_conversation_history(user_id)
|
|
history.append({
|
|
"role": role,
|
|
"content": content,
|
|
"timestamp": time.time(),
|
|
"timestamp_str": time.strftime("%Y-%m-%d %H:%M:%S")
|
|
})
|
|
|
|
if len(history) > 20:
|
|
self.conversations[user_id] = history[-20:]
|
|
|
|
profile = self.profiles.get(user_id)
|
|
username = profile.username if profile else "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:
|
|
profile = self.get_profile(user_id, username)
|
|
|
|
# Comandi speciali
|
|
if text.lower() in ['/start', '/help']:
|
|
return self._get_help_message(profile)
|
|
|
|
if text.lower() == '/setup':
|
|
return self._start_setup(user_id)
|
|
|
|
if text.lower() == '/profile':
|
|
return profile.get_summary()
|
|
|
|
if text.lower() == '/reset':
|
|
self.clear_history(user_id)
|
|
return "🔄 Storia della conversazione cancellata. Ricominciamo da capo!"
|
|
|
|
if text.lower() == '/resetprofile':
|
|
self.profile_manager.delete_profile(user_id)
|
|
self.profiles[user_id] = UserProfile(user_id, username)
|
|
return "🔄 Profilo cancellato. Usa /setup per configurarlo di nuovo."
|
|
|
|
if text.lower() == '/status':
|
|
return self._get_status_message()
|
|
|
|
# Gestione setup in corso
|
|
if user_id in self.setup_states:
|
|
return self._handle_setup_message(user_id, username, text)
|
|
|
|
# Verifica profilo completato
|
|
if not profile.setup_completed:
|
|
return ("⚠️ Prima di iniziare, devi configurare il tuo profilo!\n\n"
|
|
"Usa il comando /setup per iniziare.")
|
|
|
|
# Aggiungi il messaggio alla storia
|
|
self.add_to_history(user_id, "user", text)
|
|
|
|
# Prepara il prompt con la storia e il profilo
|
|
history = self.get_conversation_history(user_id)
|
|
conversation_context = "\n".join([
|
|
f"{'Utente' if msg['role'] == 'user' else 'Assistente'}: {msg['content']}"
|
|
for msg in history[:-1]
|
|
])
|
|
|
|
system_instruction = self._get_user_system_instruction(profile)
|
|
|
|
full_prompt = f"{system_instruction}\n\n"
|
|
if conversation_context:
|
|
full_prompt += f"Conversazione precedente:\n{conversation_context}\n\n"
|
|
full_prompt += f"Utente: {text}"
|
|
|
|
# Genera risposta con Gemini
|
|
response = self.client.models.generate_content(
|
|
model="gemini-2.5-flash",
|
|
contents=full_prompt,
|
|
config=self.config,
|
|
)
|
|
|
|
assistant_response = response.text
|
|
|
|
# Aggiungi la risposta alla storia
|
|
self.add_to_history(user_id, "assistant", assistant_response)
|
|
|
|
return assistant_response
|
|
|
|
except Exception as e:
|
|
return f"❌ Errore nell'elaborazione: {e}"
|
|
|
|
def _get_help_message(self, profile: UserProfile) -> str:
|
|
"""Restituisce il messaggio di aiuto."""
|
|
msg = "🎛️ **TF5 Mixer Bot**\n\n"
|
|
|
|
if not profile.setup_completed:
|
|
msg += "⚠️ Devi prima configurare il tuo profilo!\n\n"
|
|
msg += "Usa /setup per iniziare la configurazione.\n\n"
|
|
else:
|
|
role_info = profile.get_role_info()
|
|
msg += f"{role_info['emoji']} Sei: **{profile.display_name}** ({role_info['name']})\n\n"
|
|
|
|
if "all" in role_info.get("permissions", []):
|
|
msg += "**Esempi di comandi (accesso completo):**\n"
|
|
msg += "• 'Alza il canale 5 a -10 dB'\n"
|
|
msg += "• 'Spegni i canali dal 1 al 5'\n"
|
|
msg += "• 'Richiama la scena A10'\n"
|
|
msg += "• 'Analizza il mix 15 per la diretta'\n"
|
|
else:
|
|
msg += "**Esempi di comandi:**\n"
|
|
if profile.channel_labels:
|
|
msg += "• 'Alza il mio canale'\n"
|
|
msg += "• 'Spegni il mio mic'\n"
|
|
if profile.mix_labels:
|
|
msg += "• 'Alzami la chitarra' (nel tuo mix)\n"
|
|
msg += "• 'Più voce in cuffia'\n"
|
|
msg += "• 'Analizza il mio mix per la diretta'\n"
|
|
|
|
msg += "\n**Comandi speciali:**\n"
|
|
msg += "/help - Mostra questo messaggio\n"
|
|
msg += "/setup - Configura/riconfigura profilo\n"
|
|
msg += "/profile - Mostra il tuo profilo\n"
|
|
msg += "/reset - Cancella storia conversazione\n"
|
|
msg += "/resetprofile - Cancella profilo\n"
|
|
msg += "/status - Mostra stato del sistema\n"
|
|
|
|
return msg
|
|
|
|
def _get_status_message(self) -> str:
|
|
"""Restituisce lo stato del sistema."""
|
|
cache_age = time.time() - self.controller._cache.get("timestamp", 0)
|
|
cache_valid = self.controller._is_cache_valid()
|
|
|
|
status = "📊 **Status del Sistema**\n\n"
|
|
status += f"🎛️ Mixer: Connesso a {self.controller.host}:{self.controller.port}\n"
|
|
|
|
if cache_valid:
|
|
channels_count = len(self.controller._cache.get("channels", {}))
|
|
mixes_count = len(self.controller._cache.get("mixes", {}))
|
|
status += f"💾 Cache: Valida (età: {int(cache_age)}s)\n"
|
|
status += f"📊 Dati: {channels_count} canali, {mixes_count} mix\n"
|
|
else:
|
|
status += "💾 Cache: Non disponibile\n"
|
|
|
|
# Statistiche profili
|
|
all_profiles = self.profile_manager.get_all_profiles()
|
|
if all_profiles:
|
|
status += f"\n👥 Utenti registrati: {len(all_profiles)}\n\n"
|
|
|
|
# Conta per ruolo
|
|
role_counts = {}
|
|
for p in all_profiles:
|
|
role = p.get_role_info().get("name", "Unknown")
|
|
role_counts[role] = role_counts.get(role, 0) + 1
|
|
|
|
status += "**Utenti per ruolo:**\n"
|
|
for role, count in role_counts.items():
|
|
status += f"• {role}: {count}\n"
|
|
|
|
return status
|
|
|
|
def handle_update(self, update: Dict):
|
|
"""Gestisce un aggiornamento di Telegram."""
|
|
# Gestione callback query (bottoni inline)
|
|
if "callback_query" in update:
|
|
callback = update["callback_query"]
|
|
user_id = callback["from"]["id"]
|
|
username = callback["from"].get("username", "Unknown")
|
|
data = callback["data"]
|
|
|
|
self.bot.answer_callback_query(callback["id"])
|
|
self._handle_setup_callback(user_id, username, data)
|
|
return
|
|
|
|
if "message" not in update:
|
|
return
|
|
|
|
message = update["message"]
|
|
chat_id = message["chat"]["id"]
|
|
user_id = message["from"]["id"]
|
|
username = message["from"].get("username", "Unknown")
|
|
|
|
# Messaggi vocali
|
|
if "voice" in message or "audio" in message:
|
|
self.bot.send_chat_action(chat_id, "typing")
|
|
file_info = message.get("voice") or message.get("audio")
|
|
file_id = file_info["file_id"]
|
|
|
|
file_path = self._get_file_path(file_id)
|
|
file_url = f"https://api.telegram.org/file/bot{self.bot.token}/{file_path}"
|
|
audio_bytes = self._download_file(file_url)
|
|
|
|
transcript = self._transcribe_audio(audio_bytes)
|
|
if not transcript:
|
|
self.bot.send_message(chat_id, "❌ Non sono riuscito a capire l'audio.")
|
|
return
|
|
|
|
self.bot.send_message(chat_id, f"🗣️ Hai detto:\n> {transcript}")
|
|
|
|
response = self.process_message(user_id, username, transcript)
|
|
self.bot.send_message(chat_id, response, parse_mode='MarkdownV2')
|
|
return
|
|
|
|
# Messaggi testuali
|
|
if "text" not in message:
|
|
return
|
|
|
|
text = message["text"]
|
|
profile = self.get_profile(user_id, username)
|
|
print(f"📨 Messaggio da @{username} ({profile.get_role_info().get('name', 'Non configurato')}): {text}")
|
|
|
|
self.bot.send_chat_action(chat_id, "typing")
|
|
response = self.process_message(user_id, username, text)
|
|
self.bot.send_message(chat_id, response, parse_mode='MarkdownV2')
|
|
|
|
def run(self):
|
|
"""Avvia il bot con long polling."""
|
|
print("=" * 70)
|
|
print("🤖 TF5 Mixer Telegram Bot - Con Profili Utente")
|
|
print("=" * 70)
|
|
|
|
# Verifica connessione
|
|
me = self.bot._make_request("getMe")
|
|
if me.get("ok"):
|
|
bot_info = me["result"]
|
|
print(f"\n✅ Bot avviato: @{bot_info['username']}")
|
|
print(f" Nome: {bot_info['first_name']}")
|
|
print(f" ID: {bot_info['id']}")
|
|
else:
|
|
print("❌ Errore nella verifica del bot")
|
|
return
|
|
|
|
# Mostra profili registrati
|
|
profiles = self.profile_manager.get_all_profiles()
|
|
if profiles:
|
|
print(f"\n👥 Profili registrati: {len(profiles)}")
|
|
for p in profiles[:5]:
|
|
role = p.get_role_info().get("name", "Unknown")
|
|
print(f" • @{p.username} - {p.display_name} ({role})")
|
|
if len(profiles) > 5:
|
|
print(f" ... e altri {len(profiles) - 5}")
|
|
|
|
print("\n🔄 In attesa di messaggi... (Ctrl+C per terminare)\n")
|
|
|
|
try:
|
|
while True:
|
|
try:
|
|
updates = self.bot.get_updates()
|
|
|
|
for update in updates:
|
|
try:
|
|
self.handle_update(update)
|
|
except Exception as e:
|
|
print(f"❌ Errore nell'elaborazione update: {e}")
|
|
|
|
time.sleep(0.1)
|
|
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except Exception as e:
|
|
print(f"❌ Errore nel polling: {e}")
|
|
time.sleep(5)
|
|
|
|
except KeyboardInterrupt:
|
|
print("\n\n👋 Bot terminato")
|
|
|
|
def close(self):
|
|
"""Chiude le connessioni."""
|
|
self.controller.close()
|
|
|
|
|
|
def main():
|
|
"""Funzione principale."""
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description='TF5 Mixer Telegram Bot con Profili Utente'
|
|
)
|
|
parser.add_argument('--mixer-host', default=DEFAULT_HOST,
|
|
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 conversazioni (default: conversations)')
|
|
parser.add_argument('--profiles-dir', default='profiles',
|
|
help='Directory profili (default: profiles)')
|
|
|
|
args = parser.parse_args()
|
|
|
|
telegram_token = os.getenv("TELEGRAM_BOT_TOKEN")
|
|
if not telegram_token:
|
|
print("❌ Errore: TELEGRAM_BOT_TOKEN non trovata")
|
|
print("\nPer impostare il token:")
|
|
print(" export TELEGRAM_BOT_TOKEN='il-tuo-token'")
|
|
sys.exit(1)
|
|
|
|
gemini_key = os.getenv("GEMINI_API_KEY")
|
|
if not gemini_key:
|
|
print("❌ Errore: GEMINI_API_KEY non trovata")
|
|
print("\nPer impostare la chiave API:")
|
|
print(" export GEMINI_API_KEY='la-tua-chiave-api'")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
bot = TF5TelegramBot(
|
|
telegram_token=telegram_token,
|
|
mixer_host=args.mixer_host,
|
|
mixer_port=args.mixer_port,
|
|
conversations_dir=args.conversations_dir,
|
|
profiles_dir=args.profiles_dir
|
|
)
|
|
bot.run()
|
|
|
|
except Exception as e:
|
|
print(f"❌ Errore fatale: {e}")
|
|
sys.exit(1)
|
|
finally:
|
|
bot.close()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main() |