From 95e444aed4140b433ffef3b5392543de36aba162 Mon Sep 17 00:00:00 2001 From: streaper2 Date: Tue, 14 Apr 2026 08:41:57 +0200 Subject: [PATCH] base demo --- .dockerignore | 15 ++++ .env | 6 ++ Dockerfile | 28 +++++++ dashboard.py | 182 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 26 +++++++ main.py | 168 +++++++++++++++++++++++++++++++++++++++++ requirements.txt | 6 ++ test_chatid.py | 64 ++++++++++++++++ 8 files changed, 495 insertions(+) create mode 100644 .dockerignore create mode 100644 .env create mode 100644 Dockerfile create mode 100644 dashboard.py create mode 100644 docker-compose.yml create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 test_chatid.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..374651e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.db/ +.venv/ +env/ +venv/ +.env +.git/ +.gitignore +test_chatid.py +*.md +Dockerfile +docker-compose.yml diff --git a/.env b/.env new file mode 100644 index 0000000..2ee6fcb --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +NOCODB_BASE_URL = "https://dbtest.kaelstudio.tech/api/v2/tables/" +TABLE_BANNIES_ID = "m7mwhc3qnlmgahy" +TABLE_HISTORY_ID = "m65949r27wwrawn" +NOCODB_TOKEN = "dbgK7YJNZuW2mTK4O3umr9j08sr9GwjaKZj5cN5p" # Remplacez par votre token valide + +API_TOKEN = "5724216572:AAFdyvymS-F39dsU3KcQhG3U3GXGCknRGNM" # Remp \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..defa4a6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# Image de base légère +FROM python:3.11-slim + +# Éviter la génération de fichiers .pyc et forcer l'affichage des logs +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Dossier de travail +WORKDIR /app + +# Installation des dépendances système si nécessaire +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Copie des fichiers de dépendances +COPY requirements.txt . + +# Installation des bibliothèques Python +RUN pip install --no-cache-dir -r requirements.txt + +# Copie de tout le code source +COPY . . + +# Le port 8599 est exposé pour Streamlit +EXPOSE 8599 + +# Par défaut, on ne définit pas de CMD ici car elle sera surchargée par Docker Compose diff --git a/dashboard.py b/dashboard.py new file mode 100644 index 0000000..162695d --- /dev/null +++ b/dashboard.py @@ -0,0 +1,182 @@ +import streamlit as st +import pandas as pd +import requests +import json +import os +import time +import re +from datetime import datetime, timedelta, timezone +from dotenv import load_dotenv + +# Chargement du fichier .env pour le token Telegram +load_dotenv() +API_TOKEN = os.getenv("API_TOKEN") + +# --- CONFIGURATION --- +st.set_page_config(page_title="ModoBot Admin", layout="wide") + +NOCODB_BASE_URL = "https://dbtest.kaelstudio.tech/api/v2/tables/" +TABLE_BANNIES_ID = "m7mwhc3qnlmgahy" +TABLE_HISTORY_ID = "m65949r27wwrawn" +NOCODB_TOKEN = os.getenv("NOCODB_TOKEN", "dbgK7YJNZuW2mTK4O3umr9j08sr9GwjaKZj5cN5p") + +HEADERS = {"xc-token": NOCODB_TOKEN} + +def fetch_data(table_id): + url = f"{NOCODB_BASE_URL}{table_id}/records" + response = requests.get(url, headers=HEADERS, params={"limit": 100}) + if response.status_code == 200: + return response.json().get("list", []) + return [] + +def add_keyword(keyword): + url = f"{NOCODB_BASE_URL}{TABLE_BANNIES_ID}/records" + data = {"MotBannies": keyword} + requests.post(url, headers=HEADERS, json=data) + +def delete_keyword(id): + url = f"{NOCODB_BASE_URL}{TABLE_BANNIES_ID}/records" + requests.delete(url, headers=HEADERS, json={"Id": id}) + +def telegram_api_call(method, params): + """Envoie une requête directement à l'API Telegram""" + url = f"https://api.telegram.org/bot{API_TOKEN}/{method}" + try: + resp = requests.post(url, json=params) + return resp.json() + except Exception as e: + return {"ok": False, "description": str(e)} + +def extract_chat_id(channel_str): + """Extrait l'ID entre parenthèses à la fin d'une chaîne comme 'Mon Salon (-100123)'""" + match = re.search(r'\( (-?\d+) \)$'.replace(' ',''), str(channel_str)) + if match: + return match.group(1) + return None + +# --- UI --- +st.title("🛡️ ModoBot Dashboard") +st.markdown("---") + +tab1, tab2, tab3, tab4 = st.tabs(["📊 Statistiques", "📜 Historique", "🚫 Gestion des Mots", "🛡️ Modération"]) + +# Chargement initial des données +history_data = fetch_data(TABLE_HISTORY_ID) +bannies_data = fetch_data(TABLE_BANNIES_ID) + +# --- TAB 1: STATISTICS --- +with tab1: + st.header("État du système") + col1, col2, col3 = st.columns(3) + col1.metric("Total Suppressions", len(history_data)) + col2.metric("Mots Filtrés", len(bannies_data)) + col3.metric("Dernière Activité", history_data[0]['Timestamp'] if history_data else "N/A") + + if history_data: + df_hist = pd.DataFrame(history_data) + st.subheader("Volume de suppressions par salon") + st.bar_chart(df_hist['Channel'].value_counts()) + +# --- TAB 2: HISTORY --- +with tab2: + st.header("📜 Historique des messages supprimés") + if history_data: + df_hist = pd.DataFrame(history_data) + cols = ['Timestamp', 'User', 'UserID', 'Message', 'Channel', 'Action'] + display_cols = [c for c in cols if c in df_hist.columns] + # Utilisation de width='stretch' au lieu de use_container_width + st.dataframe(df_hist[display_cols].sort_values(by='Timestamp', ascending=False), width='stretch') + else: + st.info("Aucun message dans l'historique.") + +# --- TAB 3: MANAGEMENT --- +with tab3: + st.header("🚫 Liste des mots bannis") + new_word = st.text_input("Ajouter un nouveau mot interdit") + if st.button("Ajouter"): + if new_word: + add_keyword(new_word) + st.success(f"Mot '{new_word}' ajouté !") + st.rerun() + + st.subheader("Mots actifs") + if bannies_data: + for item in bannies_data: + col_word, col_btn = st.columns([4, 1]) + col_word.write(item['MotBannies']) + if col_btn.button("Supprimer", key=f"del_{item['Id']}"): + delete_keyword(item['Id']) + st.rerun() + else: + st.write("La liste est vide.") + +# --- TAB 4: MANUAL MODERATION --- +with tab4: + st.header("🛡️ Gestion des Utilisateurs") + + if not history_data: + st.warning("Aucun utilisateur dans l'historique pour le moment.") + else: + # Création d'une copie propre pour éviter le SettingWithCopyWarning + df_full = pd.DataFrame(history_data).copy() + + # On extrait l'ID du salon pour chaque ligne + df_full['ExtractedChatID'] = df_full['Channel'].apply(extract_chat_id) + + # Filtrage pour n'avoir que des données exploitables + df_mod = df_full[df_full['ExtractedChatID'].notnull() & df_full['UserID'].notnull()].copy() + + if df_mod.empty: + st.info("Aucun ID utilisateur/salon exploitable trouvé dans l'historique.") + else: + # Création d'un label clair + df_mod.loc[:, 'label'] = df_mod['User'] + " | Salon: " + df_mod['Channel'] + + # On ne garde que les doublons UserID + ChatID + unique_targets = df_mod.drop_duplicates(['UserID', 'ExtractedChatID']) + + selected_label = st.selectbox("Choisir une cible de modération", unique_targets['label']) + target_row = unique_targets[unique_targets['label'] == selected_label].iloc[0] + + target_user_id = target_row['UserID'] + target_chat_id = target_row['ExtractedChatID'] + + st.info(f"👉 **Cible sélectionnée** : User `{target_user_id}` sur Chat `{target_chat_id}`") + + col_act, col_timer = st.columns([2, 2]) + action = col_act.selectbox("Action à effectuer", ["Kick (Expulser)", "Ban (Bannir)", "Mute (Silencier)", "Unban (Débannir)"]) + + duration = 0 + if action in ["Ban (Bannir)", "Mute (Silencier)"]: + duration = col_timer.select_slider( + "Durée / Timer", + options=[0, 15, 60, 360, 1440, 10080], + format_func=lambda x: "Permanent" if x == 0 else (f"{x} min" if x < 60 else (f"{x//60}h" if x < 1440 else f"{x//1440}j")) + ) + + confirm = st.checkbox(f"Confirmer {action} sur cet utilisateur") + + if st.button("🚀 Appliquer la sanction", disabled=not confirm): + result = {"ok": False} + until_date = int(time.time() + (duration * 60)) if duration > 0 else None + + with st.spinner("Action en cours..."): + if "Kick" in action: + result = telegram_api_call("banChatMember", {"chat_id": target_chat_id, "user_id": target_user_id}) + telegram_api_call("unbanChatMember", {"chat_id": target_chat_id, "user_id": target_user_id}) + elif "Ban" in action: + p = {"chat_id": target_chat_id, "user_id": target_user_id} + if until_date: p["until_date"] = until_date + result = telegram_api_call("banChatMember", p) + elif "Mute" in action: + perms = {"can_send_messages": False, "can_send_media_messages": False, "can_send_other_messages": False} + p = {"chat_id": target_chat_id, "user_id": target_user_id, "permissions": perms} + if until_date: p["until_date"] = until_date + result = telegram_api_call("restrictChatMember", p) + elif "Unban" in action: + result = telegram_api_call("unbanChatMember", {"chat_id": target_chat_id, "user_id": target_user_id, "only_if_banned": False}) + + if result.get("ok"): + st.success("✅ Sanction appliquée !") + else: + st.error(f"❌ Erreur Telegram : {result.get('description', 'Inconnue')}") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7ec6227 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.8' + +services: + # Service du Bot Telegram + bot: + build: . + container_name: modobot-service + restart: always + command: python main.py + env_file: + - .env + volumes: + - .:/app + + # Service du Dashboard Streamlit + dashboard: + build: . + container_name: modobot-dashboard + restart: always + ports: + - "8599:8599" + command: streamlit run dashboard.py --server.port 8599 --server.address 0.0.0.0 + env_file: + - .env + volumes: + - .:/app diff --git a/main.py b/main.py new file mode 100644 index 0000000..0b9c4cd --- /dev/null +++ b/main.py @@ -0,0 +1,168 @@ +import logging +import asyncio +import aiohttp +import os +from datetime import datetime +from dotenv import load_dotenv +from telegram import Update +from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters + +# Chargement des variables d'environnement +load_dotenv() + +# Configuration du logging +logging.basicConfig( + format='%(asctime)s - %(levelname)s - %(message)s', + level=logging.INFO +) +logger = logging.getLogger(__name__) + +# Config NocoDB & Telegram +NOCODB_BASE_URL = os.getenv("NOCODB_BASE_URL") +TABLE_BANNIES_ID = os.getenv("TABLE_BANNIES_ID") +TABLE_HISTORY_ID = os.getenv("TABLE_HISTORY_ID") +NOCODB_TOKEN = os.getenv("NOCODB_TOKEN") +API_TOKEN = os.getenv("API_TOKEN") + +if not API_TOKEN: + logger.error("❌ API_TOKEN manquant dans le fichier .env !") + exit(1) + +# Liste globale des mots bannis +BANNED_KEYWORDS = [] + +async def fetch_banned_keywords(): + """Récupère les mots depuis NocoDB""" + global BANNED_KEYWORDS + url = f"{NOCODB_BASE_URL}{TABLE_BANNIES_ID}/records" + headers = {"xc-token": NOCODB_TOKEN} + params = {"limit": 100} + + try: + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers, params=params) as response: + if response.status == 200: + data = await response.json() + new_keywords = [ + row["MotBannies"].strip().lower() + for row in data.get("list", []) + if row.get("MotBannies") + ] + BANNED_KEYWORDS = new_keywords + logger.info(f"✅ Liste des mots bannis mise à jour ({len(BANNED_KEYWORDS)} mots)") + return True + else: + logger.error(f"❌ Erreur NocoDB : {response.status}") + return False + except Exception as e: + logger.error(f"❌ Erreur connexion NocoDB : {e}") + return False + +async def log_to_history(update: Update, action: str): + """Enregistre l'action dans la table History de NocoDB""" + url = f"{NOCODB_BASE_URL}{TABLE_HISTORY_ID}/records" + headers = {"xc-token": NOCODB_TOKEN} + + # On récupère l'objet message ou channel_post + msg = update.message or update.channel_post + if not msg: + return + + # Identification de l'auteur + user_name = "Utilisateur Inconnu" + user_id = "N/A" + logger.info(f"🎯 get info : msg '{msg}'") + if msg.from_user: + # Cas classique (Groupe/Privé) + user_full_name = msg.from_user.full_name + user_username = msg.from_user.username + user_name = f"{user_full_name} (@{user_username or 'N/A'})" + user_id = str(msg.from_user.id) + + elif msg.author_signature: + # Cas Canal avec signature + user_name = f"Admin: {msg.author_signature}" + user_id = "Canal (Anonyme)" + elif msg.sender_chat: + # Cas Canal ou Groupe anonyme + user_name = f"Chat: {msg.sender_chat.title}" + user_id = str(msg.sender_chat.id) + + payload = { + "Title": f"Mod-{datetime.now().strftime('%H%M%S')}", + "Message": msg.text or "[Média]", + "User": user_name, + "UserID": user_id, + "Timestamp": datetime.now().isoformat(), + "Action": action, + "Channel": f"{msg.chat.title or 'Privé'} ({msg.chat.id})" + } + + try: + async with aiohttp.ClientSession() as session: + async with session.post(url, headers=headers, json=payload) as response: + if response.status in [200, 201]: + logger.info(f"📊 Historique enregistré pour {user_name}") + else: + logger.error(f"❌ Échec archivage ({response.status})") + except Exception as e: + logger.error(f"❌ Erreur lors de l'archivage : {e}") + +async def filter_logic(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Analyse les messages et applique la modération""" + msg = update.message or update.channel_post + if not msg or not msg.text: + return + + text_lower = msg.text.lower() + + # Log radar pour la console + source = "Inconnu" + if msg.from_user: + source = f"@{msg.from_user.username or msg.from_user.full_name}" + elif msg.chat: + source = f"ID:{msg.chat.id} ({msg.chat.title})" + + logger.info(f"📩 Message de [{source}] : {msg.text}") + + for word in BANNED_KEYWORDS: + if word in text_lower: + try: + # Suppression du message + await msg.delete() + # Avertissement (dans les groupes/privé uniquement) + if update.message: + await context.bot.send_message( + chat_id=msg.chat.id, + text="🚫 Mot interdit détecté et supprimé." + ) + + logger.info(f"🎯 Action : Suppression du mot '{word}'") + await log_to_history(update, f"Suppression (Mot: {word})") + except Exception as e: + logger.error(f"❌ Erreur lors de la modération : {e}") + return + +async def refresh(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Rafraîchit la liste des mots depuis la commande /refresh""" + if await fetch_banned_keywords(): + await update.message.reply_text(f"✅ Liste mise à jour ! {len(BANNED_KEYWORDS)} mots actifs.") + else: + await update.message.reply_text("❌ Échec du rafraîchissement.") + +async def post_init(application): + """Effectue les tâches d'initialisation après le démarrage du bot""" + await fetch_banned_keywords() + + +if __name__ == '__main__': + # Initialisation de l'application + application = ApplicationBuilder().token(API_TOKEN).post_init(post_init).build() + + # Ajout des handlers + application.add_handler(CommandHandler("refresh", refresh)) + # Filtre pour capturer tous les messages textes (Groupes ET Canaux) + application.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), filter_logic)) + + logger.info("🚀 Bot démarré avec python-telegram-bot...") + application.run_polling() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..787bc2c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +python-telegram-bot +aiohttp +streamlit +pandas +requests +python-dotenv \ No newline at end of file diff --git a/test_chatid.py b/test_chatid.py new file mode 100644 index 0000000..675e6c6 --- /dev/null +++ b/test_chatid.py @@ -0,0 +1,64 @@ +import logging +import os +import asyncio +from dotenv import load_dotenv +from telegram import Update +from telegram.ext import ApplicationBuilder, ContextTypes, MessageHandler, filters + +# Chargement du token +load_dotenv() +API_TOKEN = os.getenv("API_TOKEN") + +# Configuration du logging +logging.basicConfig( + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO +) + +async def get_info(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Affiche toutes les informations disponibles lors de la réception d'un message""" + user = update.effective_user + chat = update.effective_chat + msg = update.effective_message + + print("\n" + "="*40) + print("🎯 NOUVELLE INTERACTION DÉTECTÉE") + print("="*40) + + if user: + print(f"👤 UTILISATEUR (Auteur) :") + print(f" - ID : {user.id}") + print(f" - Nom : {user.full_name}") + print(f" - Username : @{user.username if user.username else 'N/A'}") + print(f" - Est un Bot : {user.is_bot}") + else: + print("👤 UTILISATEUR : Non disponible (Post de canal anonyme)") + + print(f"\n💬 CONTEXTE DU CHAT :") + print(f" - Chat ID : {chat.id}") + print(f" - Titre : {chat.title if chat.title else 'Conversation Privée'}") + print(f" - Type : {chat.type}") + + print(f"\n📝 MESSAGE :") + print(f" - Contenu : {msg.text if msg.text else '[Média/Autre]'}") + print("="*40 + "\n") + + # On répond à l'utilisateur pour confirmer + if msg: + await msg.reply_text(f"✅ Infos reçues !\nTon ID : {user.id if user else 'N/A'}\nChat ID : {chat.id}") + +if __name__ == '__main__': + if not API_TOKEN: + print("❌ Erreur : API_TOKEN non trouvé dans le .env") + exit(1) + + # Création de l'application de test + application = ApplicationBuilder().token(API_TOKEN).build() + + # On écoute absolument TOUT (messages, posts de canaux, etc.) + application.add_handler(MessageHandler(filters.ALL, get_info)) + + print("🚀 SCRIPT DE TEST DÉMARRÉ") + print("👉 Envoie un message au bot pour voir tes informations s'afficher ici...") + + application.run_polling()