mirror of
https://github.com/skylanix/MamieHenriette.git
synced 2026-02-06 14:50:34 +01:00
Compare commits
26 Commits
api-timeou
...
5c76b50797
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c76b50797 | ||
|
|
d5d3e45a62 | ||
|
|
cb559c2863 | ||
|
|
a0a14abf57 | ||
|
|
a987ca311e | ||
|
|
6411b1e73c | ||
|
|
3b2886a41f | ||
|
|
0e43313366 | ||
|
|
0b9b9a4a23 | ||
|
|
4a3cf400a0 | ||
|
|
c45f83df6c | ||
|
|
8a194f7b0e | ||
|
|
30d0a4160b | ||
|
|
95edb9a523 | ||
|
|
81be00da28 | ||
|
|
9cdf26c3ba | ||
|
|
d63d81f2b8 | ||
|
|
a26214ed68 | ||
|
|
18a883c27b | ||
|
|
eb9bf0e67e | ||
|
|
db03c382cd | ||
|
|
02abe1e1a7 | ||
|
|
2815022219 | ||
|
|
6a171e795f | ||
|
|
fd172e2ea0 | ||
|
|
aff236fd0c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,5 +3,5 @@
|
|||||||
**/.venv
|
**/.venv
|
||||||
__pycache__
|
__pycache__
|
||||||
instance
|
instance
|
||||||
logs
|
|
||||||
.tio.tokens.json
|
.tio.tokens.json
|
||||||
|
**/logs
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ WORKDIR /app
|
|||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
ENV LANG=fr_FR.UTF-8
|
ENV LANG=fr_FR.UTF-8
|
||||||
ENV LC_ALL=fr_FR.UTF-8
|
ENV LC_ALL=fr_FR.UTF-8
|
||||||
ENV PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
apt-utils \
|
apt-utils \
|
||||||
@@ -35,7 +34,7 @@ RUN python3 -m venv /app/venv && \
|
|||||||
chmod +x /start.sh && \
|
chmod +x /start.sh && \
|
||||||
mkdir -p /app/logs
|
mkdir -p /app/logs
|
||||||
|
|
||||||
HEALTHCHECK --interval=1s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD pgrep python > /dev/null && ! (tail -n 1000 $(ls -t /app/logs/*.log 2>/dev/null | head -1) 2>/dev/null | grep -iE "(ERROR|CRITICAL|Exception|sqlite3\.OperationalError)")
|
CMD pgrep python > /dev/null && ! (tail -n 1000 $(ls -t /app/logs/*.log 2>/dev/null | head -1) 2>/dev/null | grep -iE "(ERROR|CRITICAL|Exception|sqlite3\.OperationalError)")
|
||||||
|
|
||||||
CMD ["/start.sh"]
|
CMD ["/start.sh"]
|
||||||
|
|||||||
192
README.md
192
README.md
@@ -14,6 +14,8 @@
|
|||||||
- [Prérequis](#prérequis)
|
- [Prérequis](#prérequis)
|
||||||
- [Création du bot Discord](#création-du-bot-discord)
|
- [Création du bot Discord](#création-du-bot-discord)
|
||||||
- [Démarrage rapide](#démarrage-rapide)
|
- [Démarrage rapide](#démarrage-rapide)
|
||||||
|
- [Build local (développement)](#build-local-développement)
|
||||||
|
- [Déploiement avec Portainer](#déploiement-avec-portainer)
|
||||||
- [Volumes persistants](#volumes-persistants)
|
- [Volumes persistants](#volumes-persistants)
|
||||||
- [Commandes Docker utiles](#commandes-docker-utiles)
|
- [Commandes Docker utiles](#commandes-docker-utiles)
|
||||||
- [Mise à jour](#mise-à-jour)
|
- [Mise à jour](#mise-à-jour)
|
||||||
@@ -50,12 +52,49 @@ Mamie Henriette est un bot intelligent open-source développé spécifiquement p
|
|||||||
- **Statuts dynamiques** : Rotation automatique des humeurs (10 min)
|
- **Statuts dynamiques** : Rotation automatique des humeurs (10 min)
|
||||||
- **Notifications Humble Bundle** : Surveillance et alertes automatiques (30 min)
|
- **Notifications Humble Bundle** : Surveillance et alertes automatiques (30 min)
|
||||||
- **Commandes personnalisées** : Gestion via interface web
|
- **Commandes personnalisées** : Gestion via interface web
|
||||||
- **Recherche ProtonDB** : Commande `!protondb <nom_du_jeu>` pour vérifier la compatibilité Linux/Steam Deck
|
- **Recherche ProtonDB** :
|
||||||
- **Modération** : Outils intégrés
|
- Commande `!protondb nom_du_jeu` ou `!pdb nom_du_jeu` pour vérifier la compatibilité Linux/Steam Deck
|
||||||
|
- Recherche intelligente avec support des alias de jeux
|
||||||
|
- Affichage du score de compatibilité, nombre de rapports et lien direct
|
||||||
|
- **Intégration anti-cheat** : Affiche automatiquement les systèmes anti-cheat et leur statut (supporté, cassé, refusé)
|
||||||
|
- Cache mis à jour automatiquement depuis AreWeAntiCheatYet
|
||||||
|
- **Modération** : Système complet de modération avec historique
|
||||||
|
- **Avertissements** : `!averto`, `!warn`, `!av`, `!avertissement`
|
||||||
|
- Envoi automatique de DM à l'utilisateur averti
|
||||||
|
- Support des timeouts combinés : `!warn @user raison --to durée`
|
||||||
|
- **Timeout** : `!timeout`, `!to` - Exclusion temporaire d'un utilisateur
|
||||||
|
- Syntaxe : `!to @user durée raison` (ex: `!to @User 10m Spam`)
|
||||||
|
- Durées supportées : secondes (s), minutes (m), heures (h), jours (j/days)
|
||||||
|
- **Gestion des avertissements** : `!delaverto`, `!removewarn`, `!delwarn`
|
||||||
|
- **Liste des événements** : `!warnings`, `!listevent`, `!listwarn`
|
||||||
|
- **Inspection utilisateur** : `!inspect @user`
|
||||||
|
- Historique complet des sanctions
|
||||||
|
- Date d'arrivée et durée sur le serveur
|
||||||
|
- Détection des comptes suspects (< 7 jours)
|
||||||
|
- Affichage du code d'invitation utilisé et de l'inviteur
|
||||||
|
- **Bannissement** : `!ban @user raison`, `!banlist`
|
||||||
|
- `!unban @user raison` ou `!unban #ID raison` (débannir par ID de sanction)
|
||||||
|
- Invitation automatique par DM lors du débannissement
|
||||||
|
- **Expulsion** : `!kick @user raison`
|
||||||
|
- **Annonces** : `!say #canal message` - Envoi de messages en tant que bot (staff uniquement)
|
||||||
|
- **Aide** : `!aide`, `!help` - Liste complète des commandes disponibles
|
||||||
|
- **Configuration avancée** :
|
||||||
|
- Support de multiples rôles staff
|
||||||
|
- Canal de logs dédié pour toutes les actions
|
||||||
|
- Suppression automatique des messages de modération (délai configurable)
|
||||||
|
- Activation/désactivation individuelle des fonctionnalités
|
||||||
|
- Panneau d'administration web pour consulter, éditer et supprimer l'historique
|
||||||
|
- **Messages de bienvenue et départ** :
|
||||||
|
- Messages personnalisables avec variables : `{member.mention}`, `{member.name}`, `{server.name}`, `{server.member_count}`
|
||||||
|
- **Système de tracking d'invitations** : Affiche qui a invité le nouveau membre
|
||||||
|
- **Messages de départ intelligents** : Détection automatique de la raison (volontaire, kick, ban)
|
||||||
|
- Affichage de la durée passée sur le serveur
|
||||||
|
- Embeds enrichis avec avatar et informations détaillées
|
||||||
|
|
||||||
### Twitch
|
### Twitch
|
||||||
- **Chat bot** : Commandes et interactions automatiques
|
- **Chat bot** : Commandes et interactions automatiques
|
||||||
- **Alertes Live** : Surveillance automatique des streamers (vérification toutes les 5 minutes)
|
- **Alertes Live** :
|
||||||
|
- Surveillance automatique des streamers
|
||||||
- Support jusqu'à 100 chaînes simultanément
|
- Support jusqu'à 100 chaînes simultanément
|
||||||
- Notifications Discord avec aperçu du stream
|
- Notifications Discord avec aperçu du stream
|
||||||
- Gestion via interface d'administration
|
- Gestion via interface d'administration
|
||||||
@@ -67,10 +106,23 @@ Mamie Henriette est un bot intelligent open-source développé spécifiquement p
|
|||||||
|
|
||||||
### Interface d'administration
|
### Interface d'administration
|
||||||
- **Dashboard** : Vue d'ensemble et statistiques
|
- **Dashboard** : Vue d'ensemble et statistiques
|
||||||
- **Configuration** : Tokens, paramètres des plateformes, configuration ProtonDB
|
- **Configuration** :
|
||||||
- **Gestion des humeurs** : Création et modification des statuts
|
- Tokens Discord/Twitch et paramètres des plateformes
|
||||||
- **Commandes** : Édition des commandes personnalisées
|
- Configuration ProtonDB (API Algolia)
|
||||||
- **Modération** : Outils de gestion communautaire
|
- Gestion des rôles staff (support de multiples rôles)
|
||||||
|
- Activation/désactivation individuelle des fonctionnalités (modération, ban, kick, welcome, leave)
|
||||||
|
- Configuration du délai de suppression automatique des messages de modération
|
||||||
|
- **Gestion des humeurs** : Création et modification des statuts Discord rotatifs
|
||||||
|
- **Commandes** : Édition des commandes personnalisées multi-plateformes
|
||||||
|
- **Modération** :
|
||||||
|
- Consultation de l'historique complet des sanctions
|
||||||
|
- Édition des raisons des événements de modération
|
||||||
|
- Suppression d'événements de modération
|
||||||
|
- Filtrage et recherche dans l'historique
|
||||||
|
- **Messages de bienvenue/départ** :
|
||||||
|
- Personnalisation des messages avec variables dynamiques
|
||||||
|
- Configuration des canaux de bienvenue et départ
|
||||||
|
- Activation/désactivation indépendante
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -115,22 +167,123 @@ Avant d'installer MamieHenriette, vous devez créer un bot Discord et obtenir so
|
|||||||
```bash
|
```bash
|
||||||
# 1. Cloner le projet
|
# 1. Cloner le projet
|
||||||
git clone https://github.com/skylanix/MamieHenriette.git
|
git clone https://github.com/skylanix/MamieHenriette.git
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd MamieHenriette
|
cd MamieHenriette
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 2. Lancer avec Docker
|
# 2. Récupérer l'image depuis GitHub Container Registry et lancer
|
||||||
docker compose up --build -d
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
> ⚠️ **Important** : Après configuration via l'interface web http://localhost:5000, **redémarrez le conteneur** pour que les changements soient pris en compte :
|
> 📝 L'interface web sera accessible sur http://localhost:5000
|
||||||
|
>
|
||||||
|
> ⚠️ **Important** : Après configuration via l'interface web, **redémarrez le conteneur** pour que les changements soient pris en compte :
|
||||||
> ```bash
|
> ```bash
|
||||||
> docker compose restart MamieHenriette
|
> docker compose restart MamieHenriette
|
||||||
> ```
|
> ```
|
||||||
|
|
||||||
|
### Build local (développement)
|
||||||
|
|
||||||
|
Si vous souhaitez modifier le code et builder l'image localement :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Cloner et accéder au projet
|
||||||
|
git clone https://github.com/skylanix/MamieHenriette.git
|
||||||
|
cd MamieHenriette
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 2. Modifier le docker-compose.yml
|
||||||
|
# Commentez la ligne 'image:' et décommentez la section 'build:' :
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mamiehenriette:
|
||||||
|
container_name: MamieHenriette
|
||||||
|
restart: unless-stopped
|
||||||
|
build: . # ← Décommentez cette ligne
|
||||||
|
image: mamiehenriette # ← Décommentez cette ligne
|
||||||
|
# image: ghcr.io/skylanix/mamiehenriette:latest # ← Commentez cette ligne
|
||||||
|
# ... reste de la configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 3. Builder et lancer
|
||||||
|
docker compose up --build -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Déploiement avec Portainer
|
||||||
|
|
||||||
|
Si vous utilisez Portainer pour gérer vos conteneurs Docker, voici la configuration Docker Compose à utiliser :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mamiehenriette:
|
||||||
|
container_name: MamieHenriette
|
||||||
|
image: ghcr.io/skylanix/mamiehenriette:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Paris
|
||||||
|
volumes:
|
||||||
|
# Adaptez ces chemins selon votre configuration
|
||||||
|
- ./instance:/app/instance
|
||||||
|
- ./logs:/app/logs
|
||||||
|
ports:
|
||||||
|
- 5000:5000
|
||||||
|
|
||||||
|
watchtower: # Mise à jour automatique de l'image
|
||||||
|
image: containrrr/watchtower:latest
|
||||||
|
container_name: watchtower
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Paris
|
||||||
|
WATCHTOWER_INCLUDE: "MamieHenriette"
|
||||||
|
WATCHTOWER_SCHEDULE: "0 */30 * * * *" # Vérification toutes les 30 min
|
||||||
|
WATCHTOWER_MONITOR_ONLY: "false"
|
||||||
|
WATCHTOWER_CLEANUP: "true"
|
||||||
|
WATCHTOWER_INCLUDE_RESTARTING: "true"
|
||||||
|
# Décommentez pour activer les notifications Discord :
|
||||||
|
# WATCHTOWER_NOTIFICATION_URL: "discord://token@id"
|
||||||
|
# WATCHTOWER_NOTIFICATIONS: shoutrrr
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
# Décommentez pour accéder à la base de données via interface web (localhost:5001)
|
||||||
|
# sqlite-web:
|
||||||
|
# image: ghcr.io/coleifer/sqlite-web:latest
|
||||||
|
# container_name: sqlite_web
|
||||||
|
# ports:
|
||||||
|
# - "5001:8080"
|
||||||
|
# volumes:
|
||||||
|
# - ./instance/database.db:/data/database.db
|
||||||
|
# environment:
|
||||||
|
# - SQLITE_DATABASE=/data/database.db
|
||||||
|
```
|
||||||
|
|
||||||
|
**Étapes dans Portainer :**
|
||||||
|
|
||||||
|
1. **Accéder à Portainer** : Ouvrez votre interface Portainer (généralement http://votre-serveur:9000)
|
||||||
|
|
||||||
|
2. **Créer une Stack** :
|
||||||
|
- Allez dans "Stacks" → "Add stack"
|
||||||
|
- Donnez un nom : `MamieHenriette`
|
||||||
|
- Collez la configuration ci-dessus dans l'éditeur
|
||||||
|
|
||||||
|
3. **Adapter les chemins des volumes** :
|
||||||
|
- Modifiez `./instance` et `./logs` selon votre configuration
|
||||||
|
- Exemple : `/opt/containers/MamieHenriette/instance` et `/opt/containers/MamieHenriette/logs`
|
||||||
|
|
||||||
|
4. **Déployer** :
|
||||||
|
- Cliquez sur "Deploy the stack"
|
||||||
|
- Attendez que le conteneur démarre
|
||||||
|
|
||||||
|
5. **Accéder à l'interface** :
|
||||||
|
- Ouvrez http://votre-serveur:5000
|
||||||
|
- Configurez le bot via l'interface web
|
||||||
|
- Redémarrez le conteneur depuis Portainer après configuration
|
||||||
|
|
||||||
### Volumes persistants
|
### Volumes persistants
|
||||||
- `./instance/` : Base de données SQLite et configuration
|
- `./instance/` : Base de données SQLite et configuration
|
||||||
- `./logs/` : Logs applicatifs rotatifs (50MB max par fichier)
|
- `./logs/` : Logs applicatifs rotatifs (50MB max par fichier)
|
||||||
@@ -165,10 +318,12 @@ git pull origin main
|
|||||||
# 3. Mettre à jour l'image Docker
|
# 3. Mettre à jour l'image Docker
|
||||||
docker compose pull
|
docker compose pull
|
||||||
|
|
||||||
# 4. Reconstruire et relancer
|
# 4. Relancer
|
||||||
docker compose up --build -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 💡 **Note** : Si vous utilisez Watchtower, les mises à jour de l'image sont automatiques (vérification toutes les 30 minutes).
|
||||||
|
|
||||||
#### Sans Docker (installation locale)
|
#### Sans Docker (installation locale)
|
||||||
```bash
|
```bash
|
||||||
# 1. Arrêter l'application
|
# 1. Arrêter l'application
|
||||||
@@ -236,13 +391,16 @@ python run-web.py
|
|||||||
## Spécifications techniques
|
## Spécifications techniques
|
||||||
|
|
||||||
### Base de données (SQLite)
|
### Base de données (SQLite)
|
||||||
- **Configuration** : Paramètres et tokens des plateformes
|
- **Configuration** : Paramètres et tokens des plateformes, configuration des fonctionnalités
|
||||||
- **Humeur** : Statuts Discord rotatifs avec gestion automatique
|
- **Humeur** : Statuts Discord rotatifs avec gestion automatique
|
||||||
- **Commande** : Commandes personnalisées multi-plateformes (Discord/Twitch)
|
- **Commande** : Commandes personnalisées multi-plateformes (Discord/Twitch)
|
||||||
- **LiveAlert** : Configuration surveillance streamers Twitch (nom, canal Discord, statut)
|
- **LiveAlert** : Configuration surveillance streamers Twitch (nom, canal Discord, statut)
|
||||||
- **GameAlias** : Alias pour améliorer les recherches ProtonDB
|
- **GameAlias** : Alias pour améliorer les recherches ProtonDB
|
||||||
- **GameBundle** : Historique et notifications Humble Bundle
|
- **GameBundle** : Historique et notifications Humble Bundle
|
||||||
- **Message** : Messages automatiques périodiques (implémenté)
|
- **AntiCheatCache** : Cache des informations anti-cheat pour ProtonDB (mise à jour automatique hebdomadaire)
|
||||||
|
- **Message** : Messages automatiques périodiques
|
||||||
|
- **Moderation** : Historique complet des actions de modération (avertissements, timeouts, bans, kicks, unbans) avec raison, staff, timestamp et durée
|
||||||
|
- **MemberInvites** : Tracking des invitations (code d'invitation, inviteur, date de join)
|
||||||
|
|
||||||
### Architecture multi-thread
|
### Architecture multi-thread
|
||||||
- **Thread 1** : Interface web Flask (port 5000) avec logging rotatif
|
- **Thread 1** : Interface web Flask (port 5000) avec logging rotatif
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from sqlalchemy import event
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from sqlite3 import Cursor, Connection
|
from sqlite3 import Cursor, Connection
|
||||||
@@ -9,8 +11,28 @@ from webapp import webapp
|
|||||||
|
|
||||||
basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
basedir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||||
webapp.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{os.path.join(basedir, "instance", "database.db")}'
|
webapp.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{os.path.join(basedir, "instance", "database.db")}'
|
||||||
|
# Options moteur pour améliorer la concurrence SQLite
|
||||||
|
webapp.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
|
||||||
|
'connect_args': {
|
||||||
|
'check_same_thread': False,
|
||||||
|
'timeout': 30
|
||||||
|
},
|
||||||
|
}
|
||||||
|
webapp.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
db = SQLAlchemy(webapp)
|
db = SQLAlchemy(webapp)
|
||||||
|
|
||||||
|
# PRAGMA pour SQLite (WAL, busy timeout)
|
||||||
|
@event.listens_for(Engine, "connect")
|
||||||
|
def _set_sqlite_pragma(dbapi_connection, connection_record):
|
||||||
|
try:
|
||||||
|
cursor = dbapi_connection.cursor()
|
||||||
|
cursor.execute("PRAGMA journal_mode=WAL;")
|
||||||
|
cursor.execute("PRAGMA synchronous=NORMAL;")
|
||||||
|
cursor.execute("PRAGMA busy_timeout=30000;")
|
||||||
|
cursor.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _tableHaveColumn(table_name:str, column_name:str, cursor:Cursor) -> bool:
|
def _tableHaveColumn(table_name:str, column_name:str, cursor:Cursor) -> bool:
|
||||||
cursor.execute(f'PRAGMA table_info({table_name})')
|
cursor.execute(f'PRAGMA table_info({table_name})')
|
||||||
columns = cursor.fetchall()
|
columns = cursor.fetchall()
|
||||||
|
|||||||
@@ -40,3 +40,24 @@ class Commande(db.Model):
|
|||||||
trigger = db.Column(db.String(32), unique=True)
|
trigger = db.Column(db.String(32), unique=True)
|
||||||
response = db.Column(db.String(2000))
|
response = db.Column(db.String(2000))
|
||||||
|
|
||||||
|
class ModerationEvent(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
type = db.Column(db.String(32))
|
||||||
|
username = db.Column(db.String(256))
|
||||||
|
discord_id = db.Column(db.String(64))
|
||||||
|
created_at = db.Column(db.DateTime)
|
||||||
|
reason = db.Column(db.String(1024))
|
||||||
|
staff_id = db.Column(db.String(64))
|
||||||
|
staff_name = db.Column(db.String(256))
|
||||||
|
duration = db.Column(db.Integer)
|
||||||
|
|
||||||
|
class AntiCheatCache(db.Model):
|
||||||
|
__tablename__ = 'anticheat_cache'
|
||||||
|
steam_id = db.Column(db.String(32), primary_key=True)
|
||||||
|
game_name = db.Column(db.String(256))
|
||||||
|
status = db.Column(db.String(32))
|
||||||
|
anticheats = db.Column(db.String(512))
|
||||||
|
reference = db.Column(db.String(512))
|
||||||
|
notes = db.Column(db.String(1024))
|
||||||
|
updated_at = db.Column(db.DateTime)
|
||||||
|
|
||||||
|
|||||||
@@ -45,3 +45,34 @@ CREATE TABLE IF NOT EXISTS `commande` (
|
|||||||
`trigger` VARCHAR(16) UNIQUE NOT NULL,
|
`trigger` VARCHAR(16) UNIQUE NOT NULL,
|
||||||
`response` VARCHAR(2000) NOT NULL
|
`response` VARCHAR(2000) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `moderation_event` (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
`type` VARCHAR(32) NOT NULL,
|
||||||
|
`username` VARCHAR(256) NOT NULL,
|
||||||
|
`discord_id` VARCHAR(64) NOT NULL,
|
||||||
|
`created_at` DATETIME NOT NULL,
|
||||||
|
`reason` VARCHAR(1024) NOT NULL,
|
||||||
|
`staff_id` VARCHAR(64) NOT NULL,
|
||||||
|
`staff_name` VARCHAR(256) NOT NULL,
|
||||||
|
`duration` INTEGER NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `anticheat_cache` (
|
||||||
|
steam_id VARCHAR(32) PRIMARY KEY,
|
||||||
|
game_name VARCHAR(256) NOT NULL,
|
||||||
|
status VARCHAR(32) NOT NULL,
|
||||||
|
anticheats VARCHAR(512),
|
||||||
|
reference VARCHAR(512),
|
||||||
|
notes VARCHAR(1024),
|
||||||
|
updated_at DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `member_invites` (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
`user_id` VARCHAR(64) NOT NULL,
|
||||||
|
`guild_id` VARCHAR(64) NOT NULL,
|
||||||
|
`invite_code` VARCHAR(256),
|
||||||
|
`inviter_name` VARCHAR(256),
|
||||||
|
`join_date` DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|||||||
@@ -6,8 +6,22 @@ import random
|
|||||||
from database import db
|
from database import db
|
||||||
from database.helpers import ConfigurationHelper
|
from database.helpers import ConfigurationHelper
|
||||||
from database.models import Configuration, Humeur, Commande
|
from database.models import Configuration, Humeur, Commande
|
||||||
from discord import Message, TextChannel
|
from discord import Message, TextChannel, Member
|
||||||
from discordbot.humblebundle import checkHumbleBundleAndNotify
|
from discordbot.humblebundle import checkHumbleBundleAndNotify
|
||||||
|
from discordbot.moderation import (
|
||||||
|
handle_warning_command,
|
||||||
|
handle_remove_warning_command,
|
||||||
|
handle_list_warnings_command,
|
||||||
|
handle_ban_command,
|
||||||
|
handle_kick_command,
|
||||||
|
handle_unban_command,
|
||||||
|
handle_inspect_command,
|
||||||
|
handle_ban_list_command,
|
||||||
|
handle_staff_help_command,
|
||||||
|
handle_timeout_command,
|
||||||
|
handle_say_command
|
||||||
|
)
|
||||||
|
from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache
|
||||||
from protondb import searhProtonDb
|
from protondb import searhProtonDb
|
||||||
|
|
||||||
class DiscordBot(discord.Client):
|
class DiscordBot(discord.Client):
|
||||||
@@ -16,6 +30,9 @@ class DiscordBot(discord.Client):
|
|||||||
for c in self.get_all_channels() :
|
for c in self.get_all_channels() :
|
||||||
logging.info(f'{c.id} {c.name}')
|
logging.info(f'{c.id} {c.name}')
|
||||||
|
|
||||||
|
for guild in self.guilds:
|
||||||
|
await updateInviteCache(guild)
|
||||||
|
|
||||||
self.loop.create_task(self.updateStatus())
|
self.loop.create_task(self.updateStatus())
|
||||||
self.loop.create_task(self.updateHumbleBundle())
|
self.loop.create_task(self.updateHumbleBundle())
|
||||||
|
|
||||||
@@ -42,20 +59,34 @@ class DiscordBot(discord.Client):
|
|||||||
if isinstance(channel, TextChannel):
|
if isinstance(channel, TextChannel):
|
||||||
channels.append(channel)
|
channels.append(channel)
|
||||||
return channels
|
return channels
|
||||||
|
|
||||||
|
def getAllRoles(self):
|
||||||
|
guilds_roles = []
|
||||||
|
for guild in self.guilds:
|
||||||
|
roles = []
|
||||||
|
for role in guild.roles:
|
||||||
|
if role.name != "@everyone":
|
||||||
|
roles.append(role)
|
||||||
|
if roles:
|
||||||
|
guilds_roles.append({
|
||||||
|
'guild_name': guild.name,
|
||||||
|
'guild_id': guild.id,
|
||||||
|
'roles': roles
|
||||||
|
})
|
||||||
|
return guilds_roles
|
||||||
|
|
||||||
|
|
||||||
def begin(self) :
|
def begin(self) :
|
||||||
token = Configuration.query.filter_by(key='discord_token').first()
|
token = Configuration.query.filter_by(key='discord_token').first()
|
||||||
if token :
|
if token and token.value and token.value.strip():
|
||||||
try:
|
self.run(token.value)
|
||||||
self.run(token.value)
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f'Erreur fatale lors du démarrage du bot Discord : {e}')
|
|
||||||
else :
|
else :
|
||||||
logging.error('Aucun token Discord configuré. Le bot ne peut pas être démarré')
|
logging.error('Aucun token Discord configuré. Le bot ne peut pas être démarré')
|
||||||
|
|
||||||
intents = discord.Intents.default()
|
intents = discord.Intents.default()
|
||||||
intents.message_content = True
|
intents.message_content = True
|
||||||
|
intents.members = True
|
||||||
|
intents.invites = True
|
||||||
bot = DiscordBot(intents=intents)
|
bot = DiscordBot(intents=intents)
|
||||||
|
|
||||||
# https://discordpy.readthedocs.io/en/stable/quickstart.html
|
# https://discordpy.readthedocs.io/en/stable/quickstart.html
|
||||||
@@ -66,37 +97,150 @@ async def on_message(message: Message):
|
|||||||
if not message.content.startswith('!'):
|
if not message.content.startswith('!'):
|
||||||
return
|
return
|
||||||
command_name = message.content.split()[0]
|
command_name = message.content.split()[0]
|
||||||
|
|
||||||
|
if ConfigurationHelper().getValue('moderation_enable'):
|
||||||
|
if command_name in ['!averto', '!av', '!avertissement', '!warn']:
|
||||||
|
await handle_warning_command(message, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
if command_name in ['!to', '!timeout']:
|
||||||
|
await handle_timeout_command(message, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
if command_name in ['!delaverto', '!removewarn', '!unwarn']:
|
||||||
|
await handle_remove_warning_command(message, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
if command_name in ['!listevent', '!listwarn', '!warnings']:
|
||||||
|
await handle_list_warnings_command(message, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
if ConfigurationHelper().getValue('moderation_ban_enable'):
|
||||||
|
if command_name == '!ban':
|
||||||
|
await handle_ban_command(message, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
if command_name == '!unban':
|
||||||
|
await handle_unban_command(message, bot)
|
||||||
|
return
|
||||||
|
if command_name == '!banlist':
|
||||||
|
await handle_ban_list_command(message, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
if ConfigurationHelper().getValue('moderation_kick_enable'):
|
||||||
|
if command_name == '!kick':
|
||||||
|
await handle_kick_command(message, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
if ConfigurationHelper().getValue('moderation_enable'):
|
||||||
|
if command_name == '!inspect':
|
||||||
|
await handle_inspect_command(message, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
if command_name == '!say':
|
||||||
|
await handle_say_command(message, bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
if command_name in ['!aide', '!help']:
|
||||||
|
await handle_staff_help_command(message, bot)
|
||||||
|
return
|
||||||
|
|
||||||
commande = Commande.query.filter_by(discord_enable=True, trigger=command_name).first()
|
commande = Commande.query.filter_by(discord_enable=True, trigger=command_name).first()
|
||||||
if commande:
|
if commande:
|
||||||
try:
|
try:
|
||||||
await asyncio.wait_for(message.channel.send(commande.response, suppress_embeds=True), timeout=30.0)
|
await message.channel.send(commande.response, suppress_embeds=True)
|
||||||
return
|
return
|
||||||
except asyncio.TimeoutError:
|
|
||||||
logging.error(f'Timeout lors de l\'envoi de la commande Discord : {command_name}')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f'Échec de l\'exécution de la commande Discord : {e}')
|
logging.error(f'Échec de l\'exécution de la commande Discord : {e}')
|
||||||
|
|
||||||
if(ConfigurationHelper().getValue('proton_db_enable_enable') and message.content.find('!protondb')==0) :
|
# Commande !protondb ou !pdb avec embed
|
||||||
|
if (ConfigurationHelper().getValue('proton_db_enable_enable') and (message.content.startswith('!protondb') or message.content.startswith('!pdb'))):
|
||||||
if (message.content.find('<@')>0) :
|
if (message.content.find('<@')>0) :
|
||||||
mention = message.content[message.content.find('<@'):]
|
mention = message.content[message.content.find('<@'):]
|
||||||
else :
|
else :
|
||||||
mention = message.author.mention
|
mention = message.author.mention
|
||||||
name = message.content.replace('!protondb', '').replace(f'{mention}', '').strip();
|
# Nettoyer le nom en enlevant la commande (!protondb ou !pdb)
|
||||||
|
name = message.content
|
||||||
|
if name.startswith('!protondb'):
|
||||||
|
name = name.replace('!protondb', '', 1)
|
||||||
|
elif name.startswith('!pdb'):
|
||||||
|
name = name.replace('!pdb', '', 1)
|
||||||
|
name = name.replace(f'{mention}', '').strip();
|
||||||
games = searhProtonDb(name)
|
games = searhProtonDb(name)
|
||||||
if (len(games)==0) :
|
if (len(games)==0) :
|
||||||
msg = f'{mention} Je n\'ai pas trouvé de jeux correspondant à **{name}**. Es-tu sûr que le jeu est disponible sur Steam ?'
|
msg = f'{mention} Je n\'ai pas trouvé de jeux correspondant à **{name}**. Es-tu sûr que le jeu est disponible sur Steam ?'
|
||||||
else :
|
try:
|
||||||
msg = f'{mention} J\'ai trouvé {len(games)} jeux :\n'
|
await message.channel.send(msg, suppress_embeds=True)
|
||||||
ite = iter(games)
|
except Exception as e:
|
||||||
while (game := next(ite, None)) is not None and len(msg) < 1850 :
|
logging.error(f"Échec de l'envoi du message ProtonDB : {e}")
|
||||||
msg += f'- [{game.get('name')}](https://www.protondb.com/app/{game.get('id')}) classé **{game.get('tier')}**\n'
|
return
|
||||||
rest = sum(1 for _ in ite)
|
|
||||||
if (rest > 0):
|
|
||||||
msg += f'- et encore {rest} autres jeux'
|
|
||||||
try :
|
|
||||||
await asyncio.wait_for(message.channel.send(msg, suppress_embeds=True), timeout=30.0)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
logging.error(f'Timeout lors de l\'envoi du message ProtonDB')
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f'Échec de l\'envoi du message ProtonDB : {e}')
|
|
||||||
|
|
||||||
|
# Construire un bel embed
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"🔎 Résultats ProtonDB pour {name}",
|
||||||
|
color=discord.Color.blurple()
|
||||||
|
)
|
||||||
|
embed.set_footer(text=f"Demandé par {message.author.name}")
|
||||||
|
|
||||||
|
max_fields = 10
|
||||||
|
count = 0
|
||||||
|
for game in games:
|
||||||
|
if count >= max_fields:
|
||||||
|
break
|
||||||
|
g_name = str(game.get('name'))
|
||||||
|
g_id = str(game.get('id'))
|
||||||
|
tier = str(game.get('tier') or 'N/A')
|
||||||
|
# Anti-cheat info si disponible
|
||||||
|
ac_status = game.get('anticheat_status')
|
||||||
|
ac_emoji = ''
|
||||||
|
ac_text = ''
|
||||||
|
if ac_status:
|
||||||
|
status_lower = str(ac_status).lower()
|
||||||
|
if status_lower == 'supported':
|
||||||
|
ac_emoji, ac_text = '✅', 'Supporté'
|
||||||
|
elif status_lower == 'running':
|
||||||
|
ac_emoji, ac_text = '⚠️', 'Fonctionne'
|
||||||
|
elif status_lower == 'broken':
|
||||||
|
ac_emoji, ac_text = '❌', 'Cassé'
|
||||||
|
elif status_lower == 'denied':
|
||||||
|
ac_emoji, ac_text = '🚫', 'Refusé'
|
||||||
|
elif status_lower == 'planned':
|
||||||
|
ac_emoji, ac_text = '📅', 'Planifié'
|
||||||
|
else:
|
||||||
|
ac_emoji, ac_text = '❔', str(ac_status)
|
||||||
|
acs = game.get('anticheats') or []
|
||||||
|
ac_list = ', '.join([str(ac) for ac in acs if ac])
|
||||||
|
ac_line = f" | Anti-cheat: {ac_emoji} **{ac_text}**"
|
||||||
|
if ac_list:
|
||||||
|
ac_line += f" ({ac_list})"
|
||||||
|
else:
|
||||||
|
ac_line = ''
|
||||||
|
value = f"Tier: **{tier}**{ac_line}\nLien: https://www.protondb.com/app/{g_id}"
|
||||||
|
embed.add_field(name=g_name, value=value[:1024], inline=False)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
rest = max(0, len(games) - count)
|
||||||
|
if rest > 0:
|
||||||
|
embed.add_field(name="…", value=f"et encore {rest} autres jeux", inline=False)
|
||||||
|
|
||||||
|
try :
|
||||||
|
await message.channel.send(content=mention, embed=embed)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Échec de l'envoi de l'embed ProtonDB : {e}")
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_member_join(member: Member):
|
||||||
|
await sendWelcomeMessage(bot, member)
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_member_remove(member: Member):
|
||||||
|
await sendLeaveMessage(bot, member)
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_invite_create(invite):
|
||||||
|
await updateInviteCache(invite.guild)
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_invite_delete(invite):
|
||||||
|
await updateInviteCache(invite.guild)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import asyncio
|
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
@@ -15,18 +14,11 @@ def _isEnable():
|
|||||||
return helper.getValue('humble_bundle_enable') and helper.getIntValue('humble_bundle_channel') != 0
|
return helper.getValue('humble_bundle_enable') and helper.getIntValue('humble_bundle_channel') != 0
|
||||||
|
|
||||||
def _callGithub():
|
def _callGithub():
|
||||||
try:
|
response = requests.get("https://raw.githubusercontent.com/shionn/HumbleBundleGamePack/refs/heads/master/data/game-bundles.json")
|
||||||
response = requests.get("https://raw.githubusercontent.com/shionn/HumbleBundleGamePack/refs/heads/master/data/game-bundles.json", timeout=30)
|
if response.status_code == 200:
|
||||||
if response.status_code == 200:
|
return response.json()
|
||||||
return response.json()
|
logging.error(f"Échec de la connexion à la ressource Humble Bundle. Code de statut HTTP : {response.status_code}")
|
||||||
logging.error(f"Échec de la connexion à la ressource Humble Bundle. Code de statut HTTP : {response.status_code}")
|
return None
|
||||||
return None
|
|
||||||
except (requests.exceptions.SSLError, requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
|
|
||||||
logging.error(f"Erreur de connexion à la ressource Humble Bundle : {e}")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Erreur inattendue lors de la récupération des bundles : {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _isNotAlreadyNotified(bundle):
|
def _isNotAlreadyNotified(bundle):
|
||||||
return GameBundle.query.filter_by(url=bundle['url']).first() == None
|
return GameBundle.query.filter_by(url=bundle['url']).first() == None
|
||||||
@@ -54,14 +46,9 @@ async def checkHumbleBundleAndNotify(bot: Client):
|
|||||||
bundle = _findFirstNotNotified(bundles)
|
bundle = _findFirstNotNotified(bundles)
|
||||||
if bundle != None :
|
if bundle != None :
|
||||||
message = _formatMessage(bundle)
|
message = _formatMessage(bundle)
|
||||||
try:
|
await bot.get_channel(ConfigurationHelper().getIntValue('humble_bundle_channel')).send(message)
|
||||||
await asyncio.wait_for(bot.get_channel(ConfigurationHelper().getIntValue('humble_bundle_channel')).send(message), timeout=30.0)
|
db.session.add(GameBundle(url=bundle['url'], name=bundle['name'], json = json.dumps(bundle)))
|
||||||
db.session.add(GameBundle(url=bundle['url'], name=bundle['name'], json = json.dumps(bundle)))
|
db.session.commit()
|
||||||
db.session.commit()
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
logging.error(f'Timeout lors de l\'envoi du message Humble Bundle')
|
|
||||||
except Exception as send_error:
|
|
||||||
logging.error(f'Erreur lors de l\'envoi du message Humble Bundle : {send_error}')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Échec de la vérification des offres Humble Bundle : {e}")
|
logging.error(f"Échec de la vérification des offres Humble Bundle : {e}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
1392
discordbot/moderation.py
Normal file
1392
discordbot/moderation.py
Normal file
File diff suppressed because it is too large
Load Diff
192
discordbot/welcome.py
Normal file
192
discordbot/welcome.py
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import discord
|
||||||
|
import logging
|
||||||
|
from database.helpers import ConfigurationHelper
|
||||||
|
from discord import Member, TextChannel
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
invite_cache = {}
|
||||||
|
|
||||||
|
def replaceMessageVariables(message: str, member: Member) -> str:
|
||||||
|
replacements = {
|
||||||
|
'{member.mention}': member.mention,
|
||||||
|
'{member.name}': member.name,
|
||||||
|
'{member.display_name}': member.display_name,
|
||||||
|
'{member.id}': str(member.id),
|
||||||
|
'{server.name}': member.guild.name,
|
||||||
|
'{server.member_count}': str(member.guild.member_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
for variable, value in replacements.items():
|
||||||
|
message = message.replace(variable, value)
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
async def updateInviteCache(guild):
|
||||||
|
try:
|
||||||
|
invites = await guild.invites()
|
||||||
|
invite_cache[guild.id] = {invite.code: invite.uses for invite in invites}
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def getUsedInvite(guild):
|
||||||
|
try:
|
||||||
|
new_invites = await guild.invites()
|
||||||
|
for invite in new_invites:
|
||||||
|
old_uses = invite_cache.get(guild.id, {}).get(invite.code, 0)
|
||||||
|
if invite.uses > old_uses:
|
||||||
|
await updateInviteCache(guild)
|
||||||
|
invite_code = invite.code
|
||||||
|
inviter_name = invite.inviter.name if invite.inviter else None
|
||||||
|
display_text = f'`{invite_code}`'
|
||||||
|
if inviter_name:
|
||||||
|
display_text += f' (créée par {inviter_name})'
|
||||||
|
return (invite_code, inviter_name, display_text)
|
||||||
|
await updateInviteCache(guild)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return (None, None, 'Inconnue')
|
||||||
|
|
||||||
|
async def sendWelcomeMessage(bot: discord.Client, member: Member):
|
||||||
|
config = ConfigurationHelper()
|
||||||
|
|
||||||
|
if not config.getValue('welcome_enable'):
|
||||||
|
return
|
||||||
|
|
||||||
|
channel_id = config.getIntValue('welcome_channel_id')
|
||||||
|
if not channel_id:
|
||||||
|
logging.warning('Canal de bienvenue non configuré')
|
||||||
|
return
|
||||||
|
|
||||||
|
channel = bot.get_channel(channel_id)
|
||||||
|
if not channel or not isinstance(channel, TextChannel):
|
||||||
|
logging.error(f'Canal de bienvenue {channel_id} introuvable')
|
||||||
|
return
|
||||||
|
|
||||||
|
welcome_message = config.getValue('welcome_message')
|
||||||
|
if not welcome_message:
|
||||||
|
welcome_message = 'Bienvenue sur le serveur !'
|
||||||
|
|
||||||
|
welcome_message = replaceMessageVariables(welcome_message, member)
|
||||||
|
|
||||||
|
invite_code, inviter_name, invite_display = await getUsedInvite(member.guild)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from database import db
|
||||||
|
from sqlalchemy import text
|
||||||
|
db.session.execute(
|
||||||
|
text("INSERT INTO member_invites (user_id, guild_id, invite_code, inviter_name, join_date) VALUES (:user_id, :guild_id, :invite_code, :inviter_name, :join_date)"),
|
||||||
|
{
|
||||||
|
'user_id': str(member.id),
|
||||||
|
'guild_id': str(member.guild.id),
|
||||||
|
'invite_code': invite_code,
|
||||||
|
'inviter_name': inviter_name,
|
||||||
|
'join_date': datetime.now(timezone.utc)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f'Échec de la sauvegarde de l\'invitation : {e}')
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='🎉 Nouveau membre !',
|
||||||
|
description=welcome_message,
|
||||||
|
color=discord.Color.green()
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.set_thumbnail(url=member.display_avatar.url)
|
||||||
|
embed.add_field(name='Membre', value=member.mention, inline=True)
|
||||||
|
embed.add_field(name='Nombre de membres', value=str(member.guild.member_count), inline=True)
|
||||||
|
embed.add_field(name='Invitation utilisée', value=invite_display, inline=False)
|
||||||
|
embed.set_footer(text=f'ID: {member.id}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
logging.info(f'Message de bienvenue envoyé pour {member.name}')
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f'Échec de l\'envoi du message de bienvenue : {e}')
|
||||||
|
|
||||||
|
def formatDuration(seconds: int) -> str:
|
||||||
|
days = seconds // 86400
|
||||||
|
hours = (seconds % 86400) // 3600
|
||||||
|
minutes = (seconds % 3600) // 60
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
if days > 0:
|
||||||
|
parts.append(f'{days} jour{"s" if days > 1 else ""}')
|
||||||
|
if hours > 0:
|
||||||
|
parts.append(f'{hours} heure{"s" if hours > 1 else ""}')
|
||||||
|
if minutes > 0:
|
||||||
|
parts.append(f'{minutes} minute{"s" if minutes > 1 else ""}')
|
||||||
|
|
||||||
|
if not parts:
|
||||||
|
return 'moins d\'une minute'
|
||||||
|
|
||||||
|
return ' et '.join(parts)
|
||||||
|
|
||||||
|
async def sendLeaveMessage(bot: discord.Client, member: Member):
|
||||||
|
config = ConfigurationHelper()
|
||||||
|
|
||||||
|
if not config.getValue('leave_enable'):
|
||||||
|
return
|
||||||
|
|
||||||
|
channel_id = config.getIntValue('leave_channel_id')
|
||||||
|
if not channel_id:
|
||||||
|
logging.warning('Canal de départ non configuré')
|
||||||
|
return
|
||||||
|
|
||||||
|
channel = bot.get_channel(channel_id)
|
||||||
|
if not channel or not isinstance(channel, TextChannel):
|
||||||
|
logging.error(f'Canal de départ {channel_id} introuvable')
|
||||||
|
return
|
||||||
|
|
||||||
|
leave_message = config.getValue('leave_message')
|
||||||
|
if not leave_message:
|
||||||
|
leave_message = 'Un membre a quitté le serveur.'
|
||||||
|
|
||||||
|
leave_message = replaceMessageVariables(leave_message, member)
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
duration_seconds = int((now - member.joined_at).total_seconds()) if member.joined_at else 0
|
||||||
|
duration_text = formatDuration(duration_seconds)
|
||||||
|
|
||||||
|
reason = 'Départ volontaire'
|
||||||
|
try:
|
||||||
|
async for entry in member.guild.audit_logs(limit=5):
|
||||||
|
if not (entry.target and entry.target.id == member.id):
|
||||||
|
continue
|
||||||
|
|
||||||
|
time_diff = (now - entry.created_at).total_seconds()
|
||||||
|
if time_diff > 3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if entry.action == discord.AuditLogAction.kick:
|
||||||
|
reason = f'Expulsé par {entry.user.mention}'
|
||||||
|
if entry.reason:
|
||||||
|
reason += f' - Raison: {entry.reason}'
|
||||||
|
break
|
||||||
|
elif entry.action == discord.AuditLogAction.ban:
|
||||||
|
reason = f'Banni par {entry.user.mention}'
|
||||||
|
if entry.reason:
|
||||||
|
reason += f' - Raison: {entry.reason}'
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title='👋 Membre parti',
|
||||||
|
description=leave_message,
|
||||||
|
color=discord.Color.red()
|
||||||
|
)
|
||||||
|
|
||||||
|
embed.set_thumbnail(url=member.display_avatar.url)
|
||||||
|
embed.add_field(name='Membre', value=f'{member.mention} ({member.name})', inline=True)
|
||||||
|
embed.add_field(name='Nombre de membres', value=str(member.guild.member_count), inline=True)
|
||||||
|
embed.add_field(name='Temps sur le serveur', value=duration_text, inline=False)
|
||||||
|
embed.set_footer(text=f'ID: {member.id}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
logging.info(f'Message de départ envoyé pour {member.name}')
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f'Échec de l\'envoi du message de départ : {e}')
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
services:
|
services:
|
||||||
mamiehenriette:
|
mamiehenriette:
|
||||||
container_name: MamieHenriette # Nom du conteneur
|
container_name: MamieHenriette # Nom du conteneur
|
||||||
image: ghcr.io/skylanix/mamiehenriette:latest # Image hébergée sur GitHub Container Registry
|
|
||||||
restart: unless-stopped # Redémarre automatiquement sauf si arrêté manuellement
|
restart: unless-stopped # Redémarre automatiquement sauf si arrêté manuellement
|
||||||
|
|
||||||
# build: . # Build du conteneur à partir d'un Dockerfile local (décommentez si nécessaire)
|
# build: . # Build du conteneur à partir d'un Dockerfile local (décommentez si nécessaire)
|
||||||
# image: mamiehenriette # Build du conteneur à partir d'un Dockerfile local (décommentez si nécessaire)
|
# image: mamiehenriette # Build du conteneur à partir d'un Dockerfile local (décommentez si nécessaire)
|
||||||
|
image: ghcr.io/skylanix/mamiehenriette:latest # Image hébergée sur GitHub Container Registry (commentez si nécessaire)
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
TZ: Europe/Paris # Fuseau horaire
|
TZ: Europe/Paris # Fuseau horaire
|
||||||
@@ -45,4 +45,4 @@ services:
|
|||||||
# volumes:
|
# volumes:
|
||||||
# - ./instance/database.db:/data/database.db # Monte la base de données locale dans le conteneur
|
# - ./instance/database.db:/data/database.db # Monte la base de données locale dans le conteneur
|
||||||
# environment:
|
# environment:
|
||||||
# - SQLITE_DATABASE=/data/database.db # Chemin vers la base de données dans le conteneur
|
# - SQLITE_DATABASE=/data/database.db # Chemin vers la base de données dans le conteneur
|
||||||
@@ -1,45 +1,33 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from algoliasearch.search.client import SearchClientSync, SearchConfig
|
from algoliasearch.search.client import SearchClientSync, SearchConfig
|
||||||
|
from database import db
|
||||||
from database.helpers import ConfigurationHelper
|
from database.helpers import ConfigurationHelper
|
||||||
from database.models import GameAlias
|
from database.models import GameAlias, AntiCheatCache, Configuration
|
||||||
from sqlalchemy import desc,func
|
from sqlalchemy import desc, func
|
||||||
|
|
||||||
def _call_algoliasearch(search_name:str):
|
def _call_algoliasearch(search_name:str):
|
||||||
try:
|
config = SearchConfig(ConfigurationHelper().getValue('proton_db_api_id'),
|
||||||
config = SearchConfig(ConfigurationHelper().getValue('proton_db_api_id'),
|
ConfigurationHelper().getValue('proton_db_api_key'))
|
||||||
ConfigurationHelper().getValue('proton_db_api_key'))
|
config.set_default_hosts()
|
||||||
config.set_default_hosts()
|
client = SearchClientSync(config=config)
|
||||||
client = SearchClientSync(config=config)
|
return client.search_single_index(index_name="steamdb",
|
||||||
return client.search_single_index(index_name="steamdb",
|
search_params={
|
||||||
search_params={
|
"query":search_name,
|
||||||
"query":search_name,
|
"facetFilters":[["appType:Game"]],
|
||||||
"facetFilters":[["appType:Game"]],
|
"hitsPerPage":50},
|
||||||
"hitsPerPage":50},
|
request_options= {'headers':{'Referer':'https://www.protondb.com/'}})
|
||||||
request_options= {
|
|
||||||
'headers':{'Referer':'https://www.protondb.com/'},
|
|
||||||
'timeout': 30
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f'Erreur lors de la recherche Algolia pour "{search_name}" : {e}')
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _call_summary(id):
|
def _call_summary(id):
|
||||||
try:
|
response = requests.get(f'http://jazzy-starlight-aeea19.netlify.app/api/v1/reports/summaries/{id}.json')
|
||||||
response = requests.get(f'http://jazzy-starlight-aeea19.netlify.app/api/v1/reports/summaries/{id}.json', timeout=30)
|
if (response.status_code == 200) :
|
||||||
if (response.status_code == 200) :
|
return response.json()
|
||||||
return response.json()
|
logging.error(f'Échec de la récupération des données ProtonDB pour le jeu {id}. Code de statut HTTP : {response.status_code}')
|
||||||
logging.error(f'Échec de la récupération des données ProtonDB pour le jeu {id}. Code de statut HTTP : {response.status_code}')
|
return None
|
||||||
return None
|
|
||||||
except (requests.exceptions.SSLError, requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
|
|
||||||
logging.error(f'Erreur de connexion ProtonDB pour le jeu {id} : {e}')
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f'Erreur inattendue lors de la récupération ProtonDB pour le jeu {id} : {e}')
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _is_name_match(name:str, search_name:str) -> bool:
|
def _is_name_match(name:str, search_name:str) -> bool:
|
||||||
normalized_game_name = re.sub("[^a-z0-9]", "", name.lower())
|
normalized_game_name = re.sub("[^a-z0-9]", "", name.lower())
|
||||||
@@ -51,12 +39,131 @@ def _apply_game_aliases(search_name:str) -> str:
|
|||||||
search_name = re.sub(re.escape(alias.alias), alias.name, search_name, flags=re.IGNORECASE)
|
search_name = re.sub(re.escape(alias.alias), alias.name, search_name, flags=re.IGNORECASE)
|
||||||
return search_name
|
return search_name
|
||||||
|
|
||||||
def searhProtonDb(search_name:str):
|
def _should_update_anticheat_cache() -> bool:
|
||||||
|
try:
|
||||||
|
last_update_conf = Configuration.query.filter_by(key='anticheat_last_update').first()
|
||||||
|
if not last_update_conf:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
last_update = datetime.fromisoformat(last_update_conf.value)
|
||||||
|
return datetime.now() - last_update > timedelta(days=7)
|
||||||
|
except:
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f'Erreur lors de la vérification du cache anti-cheat: {e}')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _fetch_anticheat_data():
|
||||||
|
try:
|
||||||
|
url = 'https://raw.githubusercontent.com/AreWeAntiCheatYet/AreWeAntiCheatYet/master/games.json'
|
||||||
|
response = requests.get(url, timeout=10)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
logging.error(f'Échec de la récupération des données anti-cheat. Code HTTP: {response.status_code}')
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f'Erreur lors de la récupération des données anti-cheat: {e}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _update_anticheat_cache_if_needed():
|
||||||
|
try:
|
||||||
|
if not _should_update_anticheat_cache():
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.info('Mise à jour du cache anti-cheat...')
|
||||||
|
anticheat_data = _fetch_anticheat_data()
|
||||||
|
if not anticheat_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
for game in anticheat_data:
|
||||||
|
try:
|
||||||
|
steam_id = str(game.get('storeIds', {}).get('steam', ''))
|
||||||
|
if not steam_id or steam_id == '0':
|
||||||
|
continue
|
||||||
|
|
||||||
|
cache_entry = AntiCheatCache.query.filter_by(steam_id=steam_id).first()
|
||||||
|
|
||||||
|
status = game.get('status', 'Unknown')
|
||||||
|
anticheats_list = game.get('anticheats', [])
|
||||||
|
anticheats_str = json.dumps(anticheats_list) if anticheats_list else None
|
||||||
|
reference = game.get('reference', '')
|
||||||
|
notes_data = game.get('notes', '')
|
||||||
|
if isinstance(notes_data, list):
|
||||||
|
notes = json.dumps(notes_data)
|
||||||
|
else:
|
||||||
|
notes = str(notes_data) if notes_data else ''
|
||||||
|
game_name = game.get('name', '')
|
||||||
|
|
||||||
|
if cache_entry:
|
||||||
|
cache_entry.game_name = game_name
|
||||||
|
cache_entry.status = status
|
||||||
|
cache_entry.anticheats = anticheats_str
|
||||||
|
cache_entry.reference = reference
|
||||||
|
cache_entry.notes = notes
|
||||||
|
cache_entry.updated_at = datetime.now()
|
||||||
|
else:
|
||||||
|
cache_entry = AntiCheatCache(
|
||||||
|
steam_id=steam_id,
|
||||||
|
game_name=game_name,
|
||||||
|
status=status,
|
||||||
|
anticheats=anticheats_str,
|
||||||
|
reference=reference,
|
||||||
|
notes=notes,
|
||||||
|
updated_at=datetime.now()
|
||||||
|
)
|
||||||
|
db.session.add(cache_entry)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f'Erreur lors de la mise à jour du jeu {game.get("name")}: {e}')
|
||||||
|
continue
|
||||||
|
|
||||||
|
last_update_conf = Configuration.query.filter_by(key='anticheat_last_update').first()
|
||||||
|
if last_update_conf:
|
||||||
|
last_update_conf.value = datetime.now().isoformat()
|
||||||
|
else:
|
||||||
|
last_update_conf = Configuration(key='anticheat_last_update', value=datetime.now().isoformat())
|
||||||
|
db.session.add(last_update_conf)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
logging.info('Cache anti-cheat mis à jour avec succès')
|
||||||
|
except Exception as e:
|
||||||
|
try:
|
||||||
|
db.session.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
logging.error(f'Erreur lors de la mise à jour du cache anti-cheat: {e}')
|
||||||
|
|
||||||
|
def _get_anticheat_info(steam_id: str) -> dict:
|
||||||
|
try:
|
||||||
|
cache_entry = AntiCheatCache.query.filter_by(steam_id=steam_id).first()
|
||||||
|
if not cache_entry:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
anticheats = json.loads(cache_entry.anticheats) if cache_entry.anticheats else []
|
||||||
|
except:
|
||||||
|
anticheats = []
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': cache_entry.status,
|
||||||
|
'anticheats': anticheats,
|
||||||
|
'reference': cache_entry.reference,
|
||||||
|
'notes': cache_entry.notes
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f'Erreur lors de la récupération des infos anti-cheat pour {steam_id}: {e}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
def searhProtonDb(search_name:str):
|
||||||
results = []
|
results = []
|
||||||
search_name = _apply_game_aliases(search_name)
|
search_name = _apply_game_aliases(search_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_update_anticheat_cache_if_needed()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f'Erreur lors de la mise à jour du cache anti-cheat: {e}')
|
||||||
|
|
||||||
responses = _call_algoliasearch(search_name)
|
responses = _call_algoliasearch(search_name)
|
||||||
if responses is None:
|
|
||||||
return results
|
|
||||||
for hit in responses.model_dump().get('hits'):
|
for hit in responses.model_dump().get('hits'):
|
||||||
id = hit.get('object_id')
|
id = hit.get('object_id')
|
||||||
name:str = hit.get('name')
|
name:str = hit.get('name')
|
||||||
@@ -65,12 +172,27 @@ def searhProtonDb(search_name:str):
|
|||||||
summmary = _call_summary(id)
|
summmary = _call_summary(id)
|
||||||
if (summmary != None) :
|
if (summmary != None) :
|
||||||
tier = summmary.get('tier')
|
tier = summmary.get('tier')
|
||||||
results.append({
|
|
||||||
|
anticheat_info = None
|
||||||
|
try:
|
||||||
|
anticheat_info = _get_anticheat_info(str(id))
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f'Erreur lors de la récupération anti-cheat pour {name}: {e}')
|
||||||
|
|
||||||
|
result = {
|
||||||
'id':id,
|
'id':id,
|
||||||
'name' : name,
|
'name' : name,
|
||||||
'tier' : tier
|
'tier' : tier
|
||||||
})
|
}
|
||||||
logging.info(f'Trouvé {name}({id}) : {tier}')
|
|
||||||
|
if anticheat_info:
|
||||||
|
result['anticheat_status'] = anticheat_info.get('status')
|
||||||
|
result['anticheats'] = anticheat_info.get('anticheats', [])
|
||||||
|
result['anticheat_reference'] = anticheat_info.get('reference')
|
||||||
|
result['anticheat_notes'] = anticheat_info.get('notes')
|
||||||
|
|
||||||
|
results.append(result)
|
||||||
|
logging.info(f'Trouvé {name}({id}) : {tier}' + (f' [Anti-cheat: {anticheat_info.get("status")}]' if anticheat_info else ''))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f'Erreur lors du traitement du jeu {name} (ID: {id}) : {e}')
|
logging.error(f'Erreur lors du traitement du jeu {name} (ID: {id}) : {e}')
|
||||||
else:
|
else:
|
||||||
|
|||||||
40
run-web.py
40
run-web.py
@@ -1,6 +1,8 @@
|
|||||||
import locale
|
import locale
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
import os
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
from webapp import webapp
|
from webapp import webapp
|
||||||
from discordbot import bot
|
from discordbot import bot
|
||||||
@@ -23,12 +25,40 @@ def start_twitch_bot():
|
|||||||
twitchBot.begin()
|
twitchBot.begin()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
# Config logs (console + fichier avec rotation)
|
||||||
|
os.makedirs('logs', exist_ok=True)
|
||||||
|
log_formatter = logging.Formatter('%(asctime)s %(levelname)s [%(threadName)s] %(name)s: %(message)s')
|
||||||
|
handlers = []
|
||||||
|
stream_handler = logging.StreamHandler()
|
||||||
|
stream_handler.setFormatter(log_formatter)
|
||||||
|
handlers.append(stream_handler)
|
||||||
|
file_handler = RotatingFileHandler('logs/app.log', maxBytes=5*1024*1024, backupCount=5, encoding='utf-8')
|
||||||
|
file_handler.setFormatter(log_formatter)
|
||||||
|
handlers.append(file_handler)
|
||||||
|
logging.basicConfig(level=logging.INFO, handlers=handlers)
|
||||||
|
|
||||||
|
# Calmer les logs verbeux de certaines libs si besoin
|
||||||
|
logging.getLogger('werkzeug').setLevel(logging.WARNING)
|
||||||
|
logging.getLogger('discord').setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# Hook exceptions non-capturées (threads inclus)
|
||||||
|
def _log_uncaught(exc_type, exc, tb):
|
||||||
|
logging.exception('Exception non capturée', exc_info=(exc_type, exc, tb))
|
||||||
|
import sys
|
||||||
|
sys.excepthook = _log_uncaught
|
||||||
|
if hasattr(threading, 'excepthook'):
|
||||||
|
def _thread_excepthook(args):
|
||||||
|
logging.exception(f"Exception dans le thread {args.thread.name}", exc_info=(args.exc_type, args.exc_value, args.exc_traceback))
|
||||||
|
threading.excepthook = _thread_excepthook
|
||||||
|
|
||||||
locale.setlocale(locale.LC_TIME, 'fr_FR.UTF-8')
|
locale.setlocale(locale.LC_TIME, 'fr_FR.UTF-8')
|
||||||
|
|
||||||
jobs = []
|
jobs = []
|
||||||
jobs.append(threading.Thread(target=start_discord_bot))
|
jobs.append(threading.Thread(target=start_discord_bot, name='discord-bot'))
|
||||||
jobs.append(threading.Thread(target=start_server))
|
jobs.append(threading.Thread(target=start_server, name='web-server'))
|
||||||
jobs.append(threading.Thread(target=start_twitch_bot))
|
jobs.append(threading.Thread(target=start_twitch_bot, name='twitch-bot'))
|
||||||
|
|
||||||
for job in jobs: job.start()
|
for job in jobs:
|
||||||
for job in jobs: job.join()
|
job.start()
|
||||||
|
for job in jobs:
|
||||||
|
job.join()
|
||||||
|
|||||||
@@ -37,16 +37,14 @@ class TwitchBot() :
|
|||||||
if _isConfigured() :
|
if _isConfigured() :
|
||||||
try :
|
try :
|
||||||
helper = ConfigurationHelper()
|
helper = ConfigurationHelper()
|
||||||
self.twitch = await asyncio.wait_for(Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret')), timeout=30.0)
|
self.twitch = await Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret'))
|
||||||
await asyncio.wait_for(self.twitch.set_user_authentication(helper.getValue('twitch_access_token'), USER_SCOPE, helper.getValue('twitch_refresh_token')), timeout=30.0)
|
await self.twitch.set_user_authentication(helper.getValue('twitch_access_token'), USER_SCOPE, helper.getValue('twitch_refresh_token'))
|
||||||
self.chat = await asyncio.wait_for(Chat(self.twitch), timeout=30.0)
|
self.chat = await Chat(self.twitch)
|
||||||
self.chat.register_event(ChatEvent.READY, _onReady)
|
self.chat.register_event(ChatEvent.READY, _onReady)
|
||||||
self.chat.register_event(ChatEvent.MESSAGE, _onMessage)
|
self.chat.register_event(ChatEvent.MESSAGE, _onMessage)
|
||||||
# chat.register_event(ChatEvent.SUB, on_sub)
|
# chat.register_event(ChatEvent.SUB, on_sub)
|
||||||
self.chat.register_command('hello', _helloCommand)
|
self.chat.register_command('hello', _helloCommand)
|
||||||
self.chat.start()
|
self.chat.start()
|
||||||
except asyncio.TimeoutError:
|
|
||||||
logging.error('Timeout lors de la connexion à Twitch. Vérifiez votre connexion réseau.')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f'Échec de l\'authentification Twitch. Vérifiez vos identifiants et redémarrez après correction : {e}')
|
logging.error(f'Échec de l\'authentification Twitch. Vérifiez vos identifiants et redémarrez après correction : {e}')
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from twitchAPI.twitch import Twitch
|
from twitchAPI.twitch import Twitch
|
||||||
@@ -37,24 +36,14 @@ async def _notifyAlert(alert : LiveAlert, stream : Stream):
|
|||||||
|
|
||||||
async def _sendMessage(channel : int, message : str) :
|
async def _sendMessage(channel : int, message : str) :
|
||||||
logger.info(f'Envoi de notification : {message}')
|
logger.info(f'Envoi de notification : {message}')
|
||||||
try:
|
await bot.get_channel(channel).send(message)
|
||||||
await asyncio.wait_for(bot.get_channel(channel).send(message), timeout=30.0)
|
logger.info(f'Notification envoyé')
|
||||||
logger.info(f'Notification envoyée')
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
logger.error(f'Timeout lors de l\'envoi de notification live alert')
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f'Erreur lors de l\'envoi de notification live alert : {e}')
|
|
||||||
|
|
||||||
async def _retreiveStreams(twitch: Twitch, alerts : list[LiveAlert]) -> list[Stream] :
|
async def _retreiveStreams(twitch: Twitch, alerts : list[LiveAlert]) -> list[Stream] :
|
||||||
streams : list[Stream] = []
|
streams : list[Stream] = []
|
||||||
logger.info(f'Recherche de streams pour : {alerts}')
|
logger.info(f'Recherche de streams pour : {alerts}')
|
||||||
try:
|
async for stream in twitch.get_streams(user_login = [alert.login for alert in alerts]):
|
||||||
async for stream in asyncio.wait_for(twitch.get_streams(user_login = [alert.login for alert in alerts]), timeout=30.0):
|
streams.append(stream)
|
||||||
streams.append(stream)
|
logger.info(f'Ces streams sont en ligne : {streams}')
|
||||||
logger.info(f'Ces streams sont en ligne : {streams}')
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
logger.error('Timeout lors de la récupération des streams Twitch')
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f'Erreur lors de la récupération des streams Twitch : {e}')
|
|
||||||
return streams
|
return streams
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ from flask import Flask
|
|||||||
|
|
||||||
webapp = Flask(__name__)
|
webapp = Flask(__name__)
|
||||||
|
|
||||||
from webapp import commandes, configurations, index, humeurs, protondb, live_alert, twitch_auth
|
from webapp import commandes, configurations, index, humeurs, protondb, live_alert, twitch_auth, moderation
|
||||||
|
|||||||
@@ -6,17 +6,37 @@ from discordbot import bot
|
|||||||
|
|
||||||
@webapp.route("/configurations")
|
@webapp.route("/configurations")
|
||||||
def openConfigurations():
|
def openConfigurations():
|
||||||
return render_template("configurations.html", configuration = ConfigurationHelper(), channels = bot.getAllTextChannel())
|
return render_template("configurations.html", configuration = ConfigurationHelper(), channels = bot.getAllTextChannel(), roles = bot.getAllRoles())
|
||||||
|
|
||||||
@webapp.route("/configurations/update", methods=['POST'])
|
@webapp.route("/configurations/update", methods=['POST'])
|
||||||
def updateConfiguration():
|
def updateConfiguration():
|
||||||
for key in request.form :
|
checkboxes = {
|
||||||
ConfigurationHelper().createOrUpdate(key, request.form.get(key))
|
'humble_bundle_enable': 'humble_bundle_channel',
|
||||||
# Je fais ça car HTML n'envoie pas le paramètre de checkbox quand il est décoché
|
'proton_db_enable_enable': 'proton_db_api_id',
|
||||||
if (request.form.get("humble_bundle_channel") != None and request.form.get("humble_bundle_enable") == None) :
|
'moderation_enable': 'moderation_staff_role_ids',
|
||||||
ConfigurationHelper().createOrUpdate('humble_bundle_enable', False)
|
'moderation_ban_enable': 'moderation_staff_role_ids',
|
||||||
if (request.form.get("proton_db_api_id") != None and request.form.get("proton_db_enable_enable") == None) :
|
'moderation_kick_enable': 'moderation_staff_role_ids',
|
||||||
ConfigurationHelper().createOrUpdate('proton_db_enable_enable', False)
|
'welcome_enable': 'welcome_channel_id',
|
||||||
|
'leave_enable': 'leave_channel_id'
|
||||||
|
}
|
||||||
|
|
||||||
|
staff_roles = request.form.getlist('moderation_staff_role_ids')
|
||||||
|
if staff_roles:
|
||||||
|
ConfigurationHelper().createOrUpdate('moderation_staff_role_ids', ','.join(staff_roles))
|
||||||
|
else:
|
||||||
|
ConfigurationHelper().createOrUpdate('moderation_staff_role_ids', '')
|
||||||
|
|
||||||
|
for key in request.form:
|
||||||
|
if key == 'moderation_staff_role_ids':
|
||||||
|
continue
|
||||||
|
value = request.form.get(key)
|
||||||
|
if value and value.strip():
|
||||||
|
ConfigurationHelper().createOrUpdate(key, value)
|
||||||
|
|
||||||
|
for checkbox, reference_field in checkboxes.items():
|
||||||
|
if request.form.get(reference_field) is not None and request.form.get(checkbox) is None:
|
||||||
|
ConfigurationHelper().createOrUpdate(checkbox, False)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(request.referrer)
|
return redirect(request.referrer)
|
||||||
|
|
||||||
|
|||||||
30
webapp/moderation.py
Normal file
30
webapp/moderation.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from flask import render_template, request, redirect, url_for
|
||||||
|
from webapp import webapp
|
||||||
|
from database import db
|
||||||
|
from database.models import ModerationEvent
|
||||||
|
|
||||||
|
@webapp.route("/moderation")
|
||||||
|
def moderation():
|
||||||
|
events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all()
|
||||||
|
return render_template("moderation.html", events=events, event=None)
|
||||||
|
|
||||||
|
@webapp.route("/moderation/edit/<int:event_id>")
|
||||||
|
def open_edit_moderation_event(event_id):
|
||||||
|
event = ModerationEvent.query.get_or_404(event_id)
|
||||||
|
events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all()
|
||||||
|
return render_template("moderation.html", events=events, event=event)
|
||||||
|
|
||||||
|
@webapp.route("/moderation/update/<int:event_id>", methods=['POST'])
|
||||||
|
def update_moderation_event(event_id):
|
||||||
|
event = ModerationEvent.query.get_or_404(event_id)
|
||||||
|
event.reason = request.form.get('reason')
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for('moderation'))
|
||||||
|
|
||||||
|
@webapp.route("/moderation/delete/<int:event_id>")
|
||||||
|
def delete_moderation_event(event_id):
|
||||||
|
event = ModerationEvent.query.get_or_404(event_id)
|
||||||
|
db.session.delete(event)
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for('moderation'))
|
||||||
|
|
||||||
@@ -2,14 +2,192 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Configuration de Mamie</h1>
|
<h1>Configuration de Mamie</h1>
|
||||||
<p>Configurez les tokens Discord, les notifications Humble Bundle et l'API ProtonDB pour la commande !protondb.</p>
|
<p>Configurez les tokens Discord, les notifications Humble Bundle et l'API Twitch.</p>
|
||||||
|
|
||||||
<h2>API Discord</h2>
|
<h2>Discord</h2>
|
||||||
<form action="{{ url_for('updateConfiguration') }}" method="POST">
|
<form action="{{ url_for('updateConfiguration') }}" method="POST">
|
||||||
<label for="discord_token">API Discord (cachée)</label>
|
<fieldset>
|
||||||
<input name="discord_token" type="password" />
|
<legend>API Discord</legend>
|
||||||
<input type="Submit" value="Définir">
|
<label for="discord_token">Token Discord (caché)</label>
|
||||||
<p>Nécessite un redémarrage</p>
|
<input name="discord_token" type="password" placeholder="Votre token Discord" />
|
||||||
|
<small>Nécessite un redémarrage après modification</small>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Messages de bienvenue</legend>
|
||||||
|
<label for="welcome_enable">
|
||||||
|
<input type="checkbox" name="welcome_enable" {% if configuration.getValue('welcome_enable') %}checked="checked"{% endif %}>
|
||||||
|
Activer le message de bienvenue pour les nouveaux membres
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label for="welcome_channel_id">Canal de bienvenue</label>
|
||||||
|
<select name="welcome_channel_id">
|
||||||
|
{% for channel in channels %}
|
||||||
|
<option value="{{channel.id}}" {% if configuration.getIntValue('welcome_channel_id')==channel.id %}selected="selected"{% endif %}>
|
||||||
|
{{channel.name}}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="welcome_message">Message personnalisé de bienvenue</label>
|
||||||
|
<textarea name="welcome_message" rows="3" placeholder="Bienvenue {member.mention} sur le serveur !">{{ configuration.getValue('welcome_message') }}</textarea>
|
||||||
|
<small>
|
||||||
|
<strong>Syntaxes disponibles :</strong><br>
|
||||||
|
• <code>{member.mention}</code> - Mentionne l'utilisateur (@NomUtilisateur)<br>
|
||||||
|
• <code>{member.name}</code> - Nom d'utilisateur (sans mention)<br>
|
||||||
|
• <code>{member.display_name}</code> - Surnom sur le serveur<br>
|
||||||
|
• <code>{member.id}</code> - ID de l'utilisateur<br>
|
||||||
|
• <code>{server.name}</code> - Nom du serveur<br>
|
||||||
|
• <code>{server.member_count}</code> - Nombre total de membres<br>
|
||||||
|
• <code><#ID_DU_CHANNEL></code> - Mentionne un salon (ex: <#123456789012345678>)
|
||||||
|
</small>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Messages de départ</legend>
|
||||||
|
<label for="leave_enable">
|
||||||
|
<input type="checkbox" name="leave_enable" {% if configuration.getValue('leave_enable') %}checked="checked"{% endif %}>
|
||||||
|
Activer le message de départ quand un membre quitte le serveur
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label for="leave_channel_id">Canal de départ</label>
|
||||||
|
<select name="leave_channel_id">
|
||||||
|
{% for channel in channels %}
|
||||||
|
<option value="{{channel.id}}" {% if configuration.getIntValue('leave_channel_id')==channel.id %}selected="selected"{% endif %}>
|
||||||
|
{{channel.name}}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="leave_message">Message personnalisé de départ</label>
|
||||||
|
<textarea name="leave_message" rows="3" placeholder="{member.mention} a quitté le serveur.">{{ configuration.getValue('leave_message') }}</textarea>
|
||||||
|
<small>
|
||||||
|
<strong>Syntaxes disponibles :</strong><br>
|
||||||
|
• <code>{member.mention}</code> - Mentionne l'utilisateur (@NomUtilisateur)<br>
|
||||||
|
• <code>{member.name}</code> - Nom d'utilisateur (sans mention)<br>
|
||||||
|
• <code>{member.display_name}</code> - Surnom sur le serveur<br>
|
||||||
|
• <code>{member.id}</code> - ID de l'utilisateur<br>
|
||||||
|
• <code>{server.name}</code> - Nom du serveur<br>
|
||||||
|
• <code>{server.member_count}</code> - Nombre total de membres<br>
|
||||||
|
• <code><#ID_DU_CHANNEL></code> - Mentionne un salon (ex: <#123456789012345678>)
|
||||||
|
</small>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Modération</legend>
|
||||||
|
<label for="moderation_enable">
|
||||||
|
<input type="checkbox" name="moderation_enable" {% if configuration.getValue('moderation_enable') %}checked="checked"{% endif %}>
|
||||||
|
Activer les commandes d'avertissement (!warn, !unwarn, !inspect)
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label for="moderation_ban_enable">
|
||||||
|
<input type="checkbox" name="moderation_ban_enable" {% if configuration.getValue('moderation_ban_enable') %}checked="checked"{% endif %}>
|
||||||
|
Activer les commandes de bannissement (!ban, !unban)
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label for="moderation_kick_enable">
|
||||||
|
<input type="checkbox" name="moderation_kick_enable" {% if configuration.getValue('moderation_kick_enable') %}checked="checked"{% endif %}>
|
||||||
|
Activer la commande d'expulsion (!kick)
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label for="moderation_log_channel_id">Canal de logs de modération</label>
|
||||||
|
<select name="moderation_log_channel_id">
|
||||||
|
{% for channel in channels %}
|
||||||
|
<option value="{{channel.id}}" {% if configuration.getIntValue('moderation_log_channel_id')==channel.id %}selected="selected"{% endif %}>
|
||||||
|
{{channel.name}}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<small>Toutes les actions de modération seront notifiées dans ce canal</small>
|
||||||
|
|
||||||
|
<label>Rôles Staff autorisés</label>
|
||||||
|
{% set selected_roles = (configuration.getValue('moderation_staff_role_ids') or '').split(',') %}
|
||||||
|
|
||||||
|
{% if roles|length > 1 %}
|
||||||
|
<div class="tabs">
|
||||||
|
{% for guild_data in roles %}
|
||||||
|
<button type="button" class="tab-button" onclick="openTab(event, 'guild-{{guild_data.guild_id}}')" {% if loop.first %}id="defaultOpen"{% endif %}>
|
||||||
|
{{ guild_data.guild_name }}
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for guild_data in roles %}
|
||||||
|
<div id="guild-{{guild_data.guild_id}}" class="tab-content" {% if not loop.first %}style="display: none;"{% endif %}>
|
||||||
|
<div style="max-height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; border-radius: 5px;">
|
||||||
|
{% for role in guild_data.roles %}
|
||||||
|
<label style="display: block; margin: 5px 0;">
|
||||||
|
<input type="checkbox" name="moderation_staff_role_ids" value="{{role.id}}" {% if role.id|string in selected_roles %}checked="checked"{% endif %}>
|
||||||
|
{% if role.color.value != 0 %}
|
||||||
|
<span style="color:#{{ '%06x' % role.color.value }}">●</span>
|
||||||
|
{% else %}
|
||||||
|
<span>○</span>
|
||||||
|
{% endif %}
|
||||||
|
{{role.name}}
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<small>Sélectionnez un ou plusieurs rôles qui peuvent utiliser les commandes de modération</small>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openTab(evt, tabName) {
|
||||||
|
var i, tabcontent, tabbuttons;
|
||||||
|
tabcontent = document.getElementsByClassName("tab-content");
|
||||||
|
for (i = 0; i < tabcontent.length; i++) {
|
||||||
|
tabcontent[i].style.display = "none";
|
||||||
|
}
|
||||||
|
tabbuttons = document.getElementsByClassName("tab-button");
|
||||||
|
for (i = 0; i < tabbuttons.length; i++) {
|
||||||
|
tabbuttons[i].className = tabbuttons[i].className.replace(" active", "");
|
||||||
|
}
|
||||||
|
document.getElementById(tabName).style.display = "block";
|
||||||
|
evt.currentTarget.className += " active";
|
||||||
|
}
|
||||||
|
document.getElementById("defaultOpen")?.click();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tabs {
|
||||||
|
overflow: hidden;
|
||||||
|
border-bottom: 2px solid #ccc;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.tab-button {
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 10px 20px;
|
||||||
|
transition: 0.3s;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
.tab-button:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
.tab-button.active {
|
||||||
|
background-color: #ccc;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.tab-content {
|
||||||
|
animation: fadeEffect 0.3s;
|
||||||
|
}
|
||||||
|
@keyframes fadeEffect {
|
||||||
|
from {opacity: 0;}
|
||||||
|
to {opacity: 1;}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<label for="moderation_embed_delete_delay">Délai de suppression des embeds (en secondes)</label>
|
||||||
|
<input name="moderation_embed_delete_delay" type="number" value="{{ configuration.getValue('moderation_embed_delete_delay') or '0' }}" placeholder="0" min="0" />
|
||||||
|
<small>Mettre 0 pour ne pas supprimer automatiquement</small>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<input type="Submit" value="Enregistrer la configuration Discord">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h2>API Twitch</h2>
|
<h2>API Twitch</h2>
|
||||||
@@ -21,7 +199,7 @@
|
|||||||
<label for="twitch_channel">Chaîne à rejoindre</label>
|
<label for="twitch_channel">Chaîne à rejoindre</label>
|
||||||
<input name="twitch_channel" type="text" value="{{ configuration.getValue('twitch_channel') }}"
|
<input name="twitch_channel" type="text" value="{{ configuration.getValue('twitch_channel') }}"
|
||||||
placeholder="#machinTruc" />
|
placeholder="#machinTruc" />
|
||||||
<input type="Submit" value="Définir">
|
<input type="Submit" value="Enregistrer la configuration Twitch">
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url_for('twitchConfigurationHelp') }}">Aide</a>
|
<a href="{{ url_for('twitchConfigurationHelp') }}">Aide</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -41,18 +219,22 @@
|
|||||||
|
|
||||||
<h2>Humble Bundle</h2>
|
<h2>Humble Bundle</h2>
|
||||||
<form action="{{ url_for('updateConfiguration') }}" method="POST">
|
<form action="{{ url_for('updateConfiguration') }}" method="POST">
|
||||||
<label for="humble_bundle_enable">Activer</label>
|
<p>Humble Bundle propose régulièrement des bundles de jeux vidéo à des prix réduits. Activez les notifications pour recevoir automatiquement les nouveaux packs disponibles sur votre serveur Discord.</p>
|
||||||
<input type="checkbox" name="humble_bundle_enable" {% if configuration.getValue('humble_bundle_enable') %}
|
|
||||||
checked="checked" {% endif %}>
|
<label for="humble_bundle_enable">
|
||||||
<label>Activer les notifications Humble Bundle</label>
|
<input type="checkbox" name="humble_bundle_enable" {% if configuration.getValue('humble_bundle_enable') %}checked="checked"{% endif %}>
|
||||||
<label for="humble_bundle_channel">Canal de notification des packs Humble Bundle</label>
|
Activer les notifications Humble Bundle
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label for="humble_bundle_channel">Canal de notification</label>
|
||||||
<select name="humble_bundle_channel">
|
<select name="humble_bundle_channel">
|
||||||
{% for channel in channels %}
|
{% for channel in channels %}
|
||||||
<option value="{{channel.id}}" {% if configuration.getIntValue('humble_bundle_channel')==channel.id %}
|
<option value="{{channel.id}}" {% if configuration.getIntValue('humble_bundle_channel')==channel.id %}selected="selected"{% endif %}>
|
||||||
selected="selected" {% endif %}>
|
{{channel.name}}
|
||||||
{{channel.name}}</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<input type="Submit" value="Définir">
|
|
||||||
|
<input type="Submit" value="Enregistrer la configuration Humble Bundle">
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
110
webapp/templates/moderation.html
Normal file
110
webapp/templates/moderation.html
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
{% extends "template.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Modération Discord</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Historique des actions de modération effectuées sur le serveur Discord.
|
||||||
|
|
||||||
|
Le bot enregistre automatiquement les avertissements, exclusions et bannissements.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Commande</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><strong>!averto @utilisateur raison</strong><br><small>Alias : !warn, !av, !avertissement</small></td>
|
||||||
|
<td>Avertit un utilisateur et enregistre l'avertissement dans la base de données</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>!delaverto id</strong><br><small>Alias : !removewarn, !delwarn</small></td>
|
||||||
|
<td>Retire un avertissement en utilisant son numéro d'ID</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>!warnings</strong> ou <strong>!warnings @utilisateur</strong><br><small>Alias : !listevent, !listwarn</small></td>
|
||||||
|
<td>Affiche la liste des événements de modération (tous ou pour un utilisateur spécifique)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>!inspect @utilisateur</strong> ou <strong>!inspect id</strong></td>
|
||||||
|
<td>Affiche des informations détaillées sur un utilisateur : création du compte, date d'arrivée, historique de modération</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>!kick @utilisateur raison</strong></td>
|
||||||
|
<td>Expulse un utilisateur du serveur</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>!ban @utilisateur raison</strong></td>
|
||||||
|
<td>Bannit définitivement un utilisateur du serveur</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>!unban discord_id</strong> ou <strong>!unban #sanction_id raison</strong></td>
|
||||||
|
<td>Révoque le bannissement d'un utilisateur et lui envoie une invitation</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>!banlist</strong></td>
|
||||||
|
<td>Affiche la liste des utilisateurs actuellement bannis du serveur</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>!aide</strong><br><small>Alias : !help</small></td>
|
||||||
|
<td>Affiche l'aide avec toutes les commandes disponibles</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if not event %}
|
||||||
|
<h2>Événements de modération</h2>
|
||||||
|
<table class="moderation">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Utilisateur</th>
|
||||||
|
<th>Discord ID</th>
|
||||||
|
<th>Date & Heure</th>
|
||||||
|
<th>Raison</th>
|
||||||
|
<th>Staff</th>
|
||||||
|
<th>#</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for mod_event in events %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ mod_event.type }}</td>
|
||||||
|
<td>{{ mod_event.username }}</td>
|
||||||
|
<td>{{ mod_event.discord_id }}</td>
|
||||||
|
<td>{{ mod_event.created_at.strftime('%d/%m/%Y %H:%M') if mod_event.created_at else 'N/A' }}</td>
|
||||||
|
<td>{{ mod_event.reason }}</td>
|
||||||
|
<td>{{ mod_event.staff_name }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('open_edit_moderation_event', event_id = mod_event.id) }}" class="icon">✐</a>
|
||||||
|
<a href="{{ url_for('delete_moderation_event', event_id = mod_event.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')" class="icon">🗑</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if event %}
|
||||||
|
<h2>Editer un événement</h2>
|
||||||
|
<form action="{{ url_for('update_moderation_event', event_id = event.id) }}" method="POST">
|
||||||
|
<label for="type">Type</label>
|
||||||
|
<input name="type" type="text" value="{{ event.type }}" disabled />
|
||||||
|
<label for="username">Utilisateur</label>
|
||||||
|
<input name="username" type="text" value="{{ event.username }}" disabled />
|
||||||
|
<label for="discord_id">Discord ID</label>
|
||||||
|
<input name="discord_id" type="text" value="{{ event.discord_id }}" disabled />
|
||||||
|
<label for="reason">Raison</label>
|
||||||
|
<input name="reason" type="text" value="{{ event.reason }}" required="required" />
|
||||||
|
<label for="staff_name">Staff</label>
|
||||||
|
<input name="staff_name" type="text" value="{{ event.staff_name }}" disabled />
|
||||||
|
<input type="Submit" value="Modifier">
|
||||||
|
<a href="{{ url_for('moderation') }}">Annuler</a>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
<li><a href="/live-alert">Alerte live</a></li>
|
<li><a href="/live-alert">Alerte live</a></li>
|
||||||
<li><a href="/commandes">Commandes</a></li>
|
<li><a href="/commandes">Commandes</a></li>
|
||||||
<li><a href="/humeurs">Humeurs</a></li>
|
<li><a href="/humeurs">Humeurs</a></li>
|
||||||
|
<li><a href="/moderation">Modération</a></li>
|
||||||
<li><a href="/protondb">ProtonDB</a></li>
|
<li><a href="/protondb">ProtonDB</a></li>
|
||||||
<li><a href="/configurations">Configurations</a></li>
|
<li><a href="/configurations">Configurations</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -17,53 +17,34 @@ auth: UserAuthenticator
|
|||||||
def twitchConfigurationHelp():
|
def twitchConfigurationHelp():
|
||||||
return render_template("twitch-aide.html", token_redirect_url = _buildUrl())
|
return render_template("twitch-aide.html", token_redirect_url = _buildUrl())
|
||||||
|
|
||||||
@webapp.route("/configurations/twitch/request-token")
|
@webapp.route("/configurations/twitch/request-token")
|
||||||
async def twitchRequestToken():
|
async def twitchRequestToken():
|
||||||
global auth
|
global auth
|
||||||
try:
|
helper = ConfigurationHelper()
|
||||||
helper = ConfigurationHelper()
|
twitch = await Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret'))
|
||||||
import asyncio
|
auth = UserAuthenticator(twitch, USER_SCOPE, url=_buildUrl())
|
||||||
twitch = await asyncio.wait_for(
|
return redirect(auth.return_auth_url())
|
||||||
Twitch(helper.getValue('twitch_client_id'), helper.getValue('twitch_client_secret')),
|
|
||||||
timeout=30.0
|
|
||||||
)
|
|
||||||
auth = UserAuthenticator(twitch, USER_SCOPE, url=_buildUrl())
|
|
||||||
return redirect(auth.return_auth_url())
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
logging.error('Timeout lors de la connexion à Twitch API pour la demande de token')
|
|
||||||
return redirect(url_for('openConfigurations'))
|
|
||||||
except TwitchAPIException as e:
|
|
||||||
logging.error(f'Erreur API Twitch lors de la demande de token : {e}')
|
|
||||||
return redirect(url_for('openConfigurations'))
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f'Erreur inattendue lors de la demande de token Twitch : {e}')
|
|
||||||
return redirect(url_for('openConfigurations'))
|
|
||||||
|
|
||||||
@webapp.route("/configurations/twitch/receive-token")
|
@webapp.route("/configurations/twitch/receive-token")
|
||||||
async def twitchReceiveToken():
|
async def twitchReceiveToken():
|
||||||
global auth
|
global auth
|
||||||
state = request.args.get('state')
|
state = request.args.get('state')
|
||||||
code = request.args.get('code')
|
code = request.args.get('code')
|
||||||
if state != auth.state :
|
if state != auth.state :
|
||||||
logging.error('bad returned state')
|
logging('bad returned state')
|
||||||
return redirect(url_for('openConfigurations'))
|
return redirect(url_for('openConfigurations'))
|
||||||
if code == None :
|
if code == None :
|
||||||
logging.error('no returned code')
|
logging('no returned state')
|
||||||
return redirect(url_for('openConfigurations'))
|
return redirect(url_for('openConfigurations'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import asyncio
|
token, refresh = await auth.authenticate(user_token=code)
|
||||||
token, refresh = await asyncio.wait_for(auth.authenticate(user_token=code), timeout=30.0)
|
|
||||||
helper = ConfigurationHelper()
|
helper = ConfigurationHelper()
|
||||||
helper.createOrUpdate('twitch_access_token', token)
|
helper.createOrUpdate('twitch_access_token', token)
|
||||||
helper.createOrUpdate('twitch_refresh_token', refresh)
|
helper.createOrUpdate('twitch_refresh_token', refresh)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
except asyncio.TimeoutError:
|
|
||||||
logging.error('Timeout lors de l\'authentification Twitch')
|
|
||||||
except TwitchAPIException as e:
|
except TwitchAPIException as e:
|
||||||
logging.error(f'Erreur API Twitch lors de l\'authentification : {e}')
|
logging(e)
|
||||||
except Exception as e:
|
|
||||||
logging.error(f'Erreur inattendue lors de l\'authentification Twitch : {e}')
|
|
||||||
return redirect(url_for('openConfigurations'))
|
return redirect(url_for('openConfigurations'))
|
||||||
|
|
||||||
# hack pas fou mais on estime qu'on sera toujours en ssl en connecté
|
# hack pas fou mais on estime qu'on sera toujours en ssl en connecté
|
||||||
|
|||||||
Reference in New Issue
Block a user