update update
This commit is contained in:
+334
-23
@@ -17,6 +17,7 @@ from fonction.first_class import IA_generator, messageTwitch, storage
|
||||
from twitch_bot import chat_state
|
||||
from twitch_bot.controller import bot_controller
|
||||
from twitch_bot.interaction_chat import InteractionChatConfig
|
||||
import requests
|
||||
|
||||
|
||||
def _first_enabled_user_index(config_path: str = "config/user.json") -> int:
|
||||
@@ -30,6 +31,114 @@ def _first_enabled_user_index(config_path: str = "config/user.json") -> int:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def _read_json_file(path: str, default):
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return data if data is not None else default
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _twitch_client_id() -> str:
|
||||
cfg = _read_json_file("config/config.json", default={})
|
||||
if isinstance(cfg, dict):
|
||||
v = cfg.get("twitch_client_id") or cfg.get("twitch_clientId") or cfg.get("twitchClientId")
|
||||
if isinstance(v, str):
|
||||
return v.strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _user_bearer_token(user_id: int) -> str:
|
||||
users = _read_json_file("config/user.json", default=[])
|
||||
if not isinstance(users, list) or user_id < 0 or user_id >= len(users):
|
||||
raise ValueError("Utilisateur non trouvé")
|
||||
u = users[user_id]
|
||||
if not isinstance(u, dict):
|
||||
raise ValueError("Utilisateur invalide")
|
||||
raw = (u.get("tw_acc_token") or "").strip()
|
||||
if raw.lower().startswith("oauth:"):
|
||||
raw = raw.split(":", 1)[1].strip()
|
||||
if not raw:
|
||||
raise ValueError("Token OAuth manquant")
|
||||
return raw
|
||||
|
||||
|
||||
def _twitch_helix_headers(user_bearer: str) -> dict:
|
||||
client_id = _twitch_client_id()
|
||||
if not client_id:
|
||||
raise ValueError("Client-ID Twitch manquant. Ajoutez `twitch_client_id` dans config/config.json")
|
||||
return {
|
||||
"Client-ID": client_id,
|
||||
"Authorization": f"Bearer {user_bearer}",
|
||||
}
|
||||
|
||||
|
||||
def _twitch_get_user_id_by_login(login: str, user_bearer: str) -> str:
|
||||
login = (login or "").strip().lstrip("@")
|
||||
if not login:
|
||||
raise ValueError("Nom de chaîne requis")
|
||||
|
||||
headers = _twitch_helix_headers(user_bearer)
|
||||
r = requests.get("https://api.twitch.tv/helix/users", headers=headers, params={"login": login}, timeout=10)
|
||||
try:
|
||||
payload = r.json()
|
||||
except Exception:
|
||||
payload = None
|
||||
|
||||
if r.status_code >= 400:
|
||||
msg = ""
|
||||
if isinstance(payload, dict):
|
||||
msg = payload.get("message") or payload.get("error") or ""
|
||||
raise RuntimeError(f"Erreur Twitch /helix/users ({r.status_code}) {msg}".strip())
|
||||
|
||||
data = (payload or {}).get("data") if isinstance(payload, dict) else None
|
||||
if not isinstance(data, list) or not data:
|
||||
raise ValueError("Chaîne Twitch introuvable (login)")
|
||||
user = data[0] or {}
|
||||
broadcaster_id = user.get("id")
|
||||
if not broadcaster_id:
|
||||
raise RuntimeError("Réponse Twitch invalide: id manquant")
|
||||
return str(broadcaster_id)
|
||||
|
||||
|
||||
def _twitch_create_clip(broadcaster_id: str, user_bearer: str, has_delay: bool = False) -> dict:
|
||||
headers = _twitch_helix_headers(user_bearer)
|
||||
params = {"broadcaster_id": str(broadcaster_id), "has_delay": "true" if has_delay else "false"}
|
||||
r = requests.post("https://api.twitch.tv/helix/clips", headers=headers, params=params, timeout=15)
|
||||
try:
|
||||
payload = r.json()
|
||||
except Exception:
|
||||
payload = None
|
||||
|
||||
if r.status_code >= 400:
|
||||
msg = ""
|
||||
if isinstance(payload, dict):
|
||||
msg = payload.get("message") or payload.get("error") or ""
|
||||
hint = ""
|
||||
if r.status_code in (401, 403):
|
||||
hint = " (vérifiez que le token a le scope `clips:edit` et qu'il n'est pas expiré)"
|
||||
raise RuntimeError(f"Erreur Twitch /helix/clips ({r.status_code}) {msg}{hint}".strip())
|
||||
|
||||
data = (payload or {}).get("data") if isinstance(payload, dict) else None
|
||||
if not isinstance(data, list) or not data:
|
||||
raise RuntimeError("Réponse Twitch invalide: data manquant")
|
||||
|
||||
clip = data[0] or {}
|
||||
clip_id = clip.get("id")
|
||||
edit_url = clip.get("edit_url")
|
||||
if not clip_id:
|
||||
raise RuntimeError("Réponse Twitch invalide: clip id manquant")
|
||||
|
||||
# URL publique approximative (le retour officiel donne surtout edit_url)
|
||||
public_url = f"https://clips.twitch.tv/{clip_id}"
|
||||
return {
|
||||
"id": clip_id,
|
||||
"edit_url": edit_url,
|
||||
"url": public_url,
|
||||
}
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
||||
socketio = SocketIO(app, cors_allowed_origins="*")
|
||||
@@ -47,12 +156,14 @@ def add_flux():
|
||||
data = request.json
|
||||
channel_name = data.get('channel_name')
|
||||
record_audio = data.get('record_audio', True)
|
||||
send_messages = bool(data.get('send_messages', True))
|
||||
enable_ia = bool(data.get('enable_ia', True))
|
||||
|
||||
if not channel_name:
|
||||
return jsonify({'error': 'Nom du canal requis'}), 400
|
||||
|
||||
try:
|
||||
flux_id = bot_controller.add_flux(channel_name, record_audio)
|
||||
flux_id = bot_controller.add_flux(channel_name, record_audio, send_messages, enable_ia)
|
||||
return jsonify({'success': True, 'flux_id': flux_id})
|
||||
except Exception as e:
|
||||
error_msg = f"Erreur lors de l'ajout du flux: {str(e)}"
|
||||
@@ -129,9 +240,10 @@ def toggle_flux(flux_id):
|
||||
threading.Thread(target=bots['record_bot'].main, daemon=True).start()
|
||||
if bots['subtitle_bot']:
|
||||
bots['subtitle_bot'].start_main_loop()
|
||||
if bots['ia_bot']:
|
||||
if bool(flux.get("enable_ia", True)) and bots.get('ia_bot'):
|
||||
bots['ia_bot'].start_main_loop()
|
||||
if bots['message_bot']:
|
||||
# Respecter le flag par-flux send_messages
|
||||
if bool(flux.get("send_messages", True)) and bool(flux.get("enable_ia", True)) and bots.get('message_bot'):
|
||||
bots['message_bot'].start_loop_respond()
|
||||
except Exception as e:
|
||||
print(f"Erreur lors du redémarrage des bots: {e}")
|
||||
@@ -356,23 +468,99 @@ def delete_user(user_id):
|
||||
|
||||
@app.route('/api/subtitles', methods=['GET'])
|
||||
def get_subtitles():
|
||||
data = storage.read("subtitle_data")
|
||||
return jsonify(data)
|
||||
flux_id = request.args.get("flux_id")
|
||||
channel = request.args.get("channel")
|
||||
key = "subtitle_data"
|
||||
if channel:
|
||||
key = f"subtitle_data__{str(channel).strip().lower()}"
|
||||
elif flux_id:
|
||||
try:
|
||||
fid = int(flux_id)
|
||||
flux = next((f for f in bot_controller.flux_list if f.get("id") == fid), None)
|
||||
if flux and flux.get("twitchname"):
|
||||
key = f"subtitle_data__{str(flux.get('twitchname')).strip().lower()}"
|
||||
except Exception:
|
||||
pass
|
||||
data = storage.read(key)
|
||||
return jsonify(data if isinstance(data, dict) else {})
|
||||
|
||||
@app.route('/api/subtitles/last', methods=['GET'])
|
||||
def get_last_subtitle():
|
||||
"""Retourne le dernier sous-titre (clé + texte)."""
|
||||
try:
|
||||
flux_id = request.args.get("flux_id")
|
||||
channel = request.args.get("channel")
|
||||
key = "subtitle_data"
|
||||
if channel:
|
||||
key = f"subtitle_data__{str(channel).strip().lower()}"
|
||||
elif flux_id:
|
||||
try:
|
||||
fid = int(flux_id)
|
||||
flux = next((f for f in bot_controller.flux_list if f.get("id") == fid), None)
|
||||
if flux and flux.get("twitchname"):
|
||||
key = f"subtitle_data__{str(flux.get('twitchname')).strip().lower()}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
subtitle_data = storage.read(key) or {}
|
||||
if not isinstance(subtitle_data, dict) or not subtitle_data:
|
||||
return jsonify({'success': True, 'last_key': None, 'last_subtitle': ''})
|
||||
sorted_keys = sorted(subtitle_data.keys())
|
||||
|
||||
def _good_candidate(text: str) -> bool:
|
||||
t = (text or "").strip()
|
||||
if not t:
|
||||
return False
|
||||
# Heuristique: éviter les "bruits" très courts pour la génération IA
|
||||
words = [w for w in t.split() if w]
|
||||
return (len(t) >= 35) or (len(words) >= 8)
|
||||
|
||||
# Chercher le plus récent "utile", sinon fallback sur le tout dernier
|
||||
picked_key = None
|
||||
picked_text = ""
|
||||
for k in reversed(sorted_keys):
|
||||
t = subtitle_data.get(k, "")
|
||||
if _good_candidate(str(t)):
|
||||
picked_key = k
|
||||
picked_text = str(t)
|
||||
break
|
||||
|
||||
if picked_key is None:
|
||||
picked_key = sorted_keys[-1]
|
||||
picked_text = str(subtitle_data.get(picked_key, "") or "")
|
||||
|
||||
return jsonify({'success': True, 'last_key': picked_key, 'last_subtitle': picked_text})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/subtitles/clear', methods=['POST'])
|
||||
def clear_subtitles():
|
||||
"""Nettoyer l'historique des sous-titres"""
|
||||
try:
|
||||
flux_id = request.args.get("flux_id")
|
||||
channel = request.args.get("channel")
|
||||
storage_key = "subtitle_data"
|
||||
if channel:
|
||||
storage_key = f"subtitle_data__{str(channel).strip().lower()}"
|
||||
elif flux_id:
|
||||
try:
|
||||
fid = int(flux_id)
|
||||
flux = next((f for f in bot_controller.flux_list if f.get("id") == fid), None)
|
||||
if flux and flux.get("twitchname"):
|
||||
storage_key = f"subtitle_data__{str(flux.get('twitchname')).strip().lower()}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Récupérer toutes les clés de sous-titres
|
||||
subtitle_data = storage.read("subtitle_data")
|
||||
subtitle_data = storage.read(storage_key)
|
||||
|
||||
# Supprimer chaque clé une par une
|
||||
for key in list(subtitle_data.keys()):
|
||||
storage.delete("subtitle_data", key)
|
||||
for loop_key in list((subtitle_data or {}).keys()):
|
||||
storage.delete(storage_key, loop_key)
|
||||
|
||||
# Supprimer également le fichier JSON s'il existe
|
||||
storage_dir = "storage"
|
||||
subtitle_file = os.path.join(storage_dir, "subtitle_data.json")
|
||||
subtitle_file = os.path.join(storage_dir, f"{storage_key}.json")
|
||||
if os.path.exists(subtitle_file):
|
||||
os.remove(subtitle_file)
|
||||
|
||||
@@ -386,6 +574,74 @@ def clear_subtitles():
|
||||
'error': f'Erreur lors du nettoyage: {str(e)}'
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/subtitles/rules/config', methods=['GET'])
|
||||
def get_subtitle_rules_config():
|
||||
try:
|
||||
processor = None
|
||||
for flux_id, bots in bot_controller.bots.items():
|
||||
processor = bots.get('subtitle_rules')
|
||||
if processor:
|
||||
break
|
||||
if not processor:
|
||||
from twitch_bot.subtitle_rules import SubtitleRulesStorage, SubtitleRulesConfig
|
||||
st = SubtitleRulesStorage()
|
||||
cfg = SubtitleRulesConfig(st.read_json("subtitle_rules_config", default={}))
|
||||
data = cfg.to_dict()
|
||||
else:
|
||||
data = processor.load_config().to_dict()
|
||||
return jsonify({'success': True, 'config': data})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/subtitles/rules/config', methods=['POST'])
|
||||
def save_subtitle_rules_config():
|
||||
try:
|
||||
payload = request.json or {}
|
||||
config_dict = payload.get('config') or {}
|
||||
processor = None
|
||||
for flux_id, bots in bot_controller.bots.items():
|
||||
processor = bots.get('subtitle_rules')
|
||||
if processor:
|
||||
break
|
||||
if processor:
|
||||
cfg = processor.save_config(config_dict)
|
||||
data = cfg.to_dict()
|
||||
else:
|
||||
from twitch_bot.subtitle_rules import SubtitleRulesStorage, SubtitleRulesConfig
|
||||
st = SubtitleRulesStorage()
|
||||
cfg = SubtitleRulesConfig(config_dict)
|
||||
st.write_json("subtitle_rules_config", cfg.to_dict())
|
||||
data = cfg.to_dict()
|
||||
return jsonify({'success': True, 'config': data})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/subtitles/rules/log', methods=['GET'])
|
||||
def get_subtitle_rules_log():
|
||||
try:
|
||||
limit = int(request.args.get('limit', '120'))
|
||||
limit = max(1, min(500, limit))
|
||||
processor = None
|
||||
for flux_id, bots in bot_controller.bots.items():
|
||||
processor = bots.get('subtitle_rules')
|
||||
if processor:
|
||||
break
|
||||
if processor:
|
||||
logs = processor.read_log(limit=limit)
|
||||
else:
|
||||
from twitch_bot.subtitle_rules import SubtitleRulesStorage
|
||||
st = SubtitleRulesStorage()
|
||||
logs = st.read_json("subtitle_rules_log", default=[])
|
||||
if not isinstance(logs, list):
|
||||
logs = []
|
||||
logs = logs[-limit:]
|
||||
return jsonify({'success': True, 'logs': logs})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/subtitles/process', methods=['POST'])
|
||||
def process_subtitles():
|
||||
"""Lancer manuellement le traitement des sous-titres"""
|
||||
@@ -509,10 +765,6 @@ def send_message():
|
||||
if not message:
|
||||
return jsonify({'error': 'Message requis'}), 400
|
||||
|
||||
# Vérifier si l'envoi de messages est activé
|
||||
if not chat_state.chat_messages_enabled:
|
||||
return jsonify({'error': 'Envoi de messages désactivé'}), 403
|
||||
|
||||
# Trouver le bot de message pour ce canal
|
||||
try:
|
||||
msg_bot = messageTwitch("config/user.json", channel)
|
||||
@@ -573,10 +825,6 @@ def send_chat_message(flux_id):
|
||||
if not message:
|
||||
return jsonify({'error': 'Message requis'}), 400
|
||||
|
||||
# Vérifier si l'envoi de messages est activé
|
||||
if not chat_state.chat_messages_enabled:
|
||||
return jsonify({'error': 'Envoi de messages désactivé'}), 403
|
||||
|
||||
try:
|
||||
# Trouver le flux
|
||||
flux = None
|
||||
@@ -587,6 +835,9 @@ def send_chat_message(flux_id):
|
||||
|
||||
if not flux:
|
||||
return jsonify({'error': 'Flux non trouvé'}), 404
|
||||
|
||||
if not bool(flux.get("send_messages", True)):
|
||||
return jsonify({'error': 'Envoi de messages désactivé pour ce flux'}), 403
|
||||
|
||||
# Envoyer le message avec l'utilisateur spécifié
|
||||
msg_bot = messageTwitch("config/user.json", flux['twitchname'])
|
||||
@@ -998,12 +1249,6 @@ def auto_message_loop():
|
||||
|
||||
while auto_message_running:
|
||||
try:
|
||||
# Vérifier si l'envoi de messages est activé
|
||||
if not chat_state.chat_messages_enabled:
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] Envoi de messages désactivé, attente...")
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
# Vérifier s'il y a des générations disponibles
|
||||
generation_data = storage.read("IA_generator")
|
||||
if not generation_data:
|
||||
@@ -1102,6 +1347,72 @@ def get_system_status():
|
||||
"""Obtenir le statut de tous les composants"""
|
||||
return jsonify(bot_controller.get_system_status())
|
||||
|
||||
|
||||
@app.route('/api/clips/create', methods=['POST'])
|
||||
def create_clip():
|
||||
"""
|
||||
Créer un clip Twitch via Helix.
|
||||
Nécessite:
|
||||
- `twitch_client_id` dans config/config.json
|
||||
- un user token avec scope `clips:edit` (config/user.json, champ tw_acc_token)
|
||||
"""
|
||||
try:
|
||||
data = request.json or {}
|
||||
channel_login = (data.get("channel_login") or "").strip()
|
||||
user_id = int(data.get("user_id", _first_enabled_user_index()))
|
||||
has_delay = bool(data.get("has_delay", False))
|
||||
|
||||
bearer = _user_bearer_token(user_id)
|
||||
broadcaster_id = _twitch_get_user_id_by_login(channel_login, bearer)
|
||||
clip = _twitch_create_clip(broadcaster_id=broadcaster_id, user_bearer=bearer, has_delay=has_delay)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"clip": {
|
||||
"broadcaster_login": channel_login.lstrip("@"),
|
||||
"broadcaster_id": broadcaster_id,
|
||||
**clip,
|
||||
}
|
||||
})
|
||||
except ValueError as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 400
|
||||
except RuntimeError as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 502
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
# === CLIPS AUTOMATION (stub for future) ===
|
||||
_clips_automation_running = False
|
||||
_clips_automation_mode = "manual"
|
||||
|
||||
|
||||
@app.route('/api/clips/automation/status', methods=['GET'])
|
||||
def clip_automation_status():
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"running": _clips_automation_running,
|
||||
"mode": _clips_automation_mode,
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/clips/automation/start', methods=['POST'])
|
||||
def clip_automation_start():
|
||||
# Intentionally not implemented yet: future hook point.
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Automatisation clips: pas encore implémentée (stub prêt)",
|
||||
}), 501
|
||||
|
||||
|
||||
@app.route('/api/clips/automation/stop', methods=['POST'])
|
||||
def clip_automation_stop():
|
||||
# Intentionally not implemented yet: future hook point.
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "Automatisation clips: pas encore implémentée (stub prêt)",
|
||||
}), 501
|
||||
|
||||
@app.route('/api/interaction/config', methods=['GET'])
|
||||
def get_interaction_config():
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user