Merge pull request #1 from skylanix/HumbleBundle

Ajout de Humble Bundle, refonte de Docker ainsi que de la documentation
This commit is contained in:
skylanix
2025-08-12 15:15:10 +02:00
committed by GitHub
15 changed files with 307 additions and 328 deletions

View File

@@ -1,8 +1,3 @@
# Configuration Discord Bot
TOKEN=votre_token_discord
STATUS=online
INTERVAL=3600
# Configuration Zabbix (optionnel)
ENABLE_ZABBIX=false
ZABBIX_SERVER=zabbix-server.example.com

View File

@@ -3,9 +3,12 @@ FROM debian:trixie-slim
WORKDIR /app
ENV DEBIAN_FRONTEND=noninteractive
ENV LANG=fr_FR.UTF-8
ENV LC_ALL=fr_FR.UTF-8
RUN apt-get update && apt-get install -y --no-install-recommends \
apt-utils \
locales \
python3 \
python3-pip \
python3-venv \
@@ -14,16 +17,21 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& dpkg -i zabbix-release_latest_7.4+debian12_all.deb \
&& apt-get update \
&& apt-get install -y --no-install-recommends zabbix-agent2 \
&& sed -i 's/# fr_FR.UTF-8 UTF-8/fr_FR.UTF-8 UTF-8/' /etc/locale.gen \
&& locale-gen \
&& rm -rf /var/lib/apt/lists/* \
&& rm zabbix-release_latest_7.4+debian12_all.deb
COPY requirements.txt .
COPY bot.py .
COPY statuts.txt .
COPY run-web.py .
COPY ./webapp ./webapp
COPY ./discordbot ./discordbot
COPY ./database ./database
COPY zabbix_agent2.conf /etc/zabbix/zabbix_agent2.conf
COPY start.sh /start.sh
RUN pip3 install --no-cache-dir --break-system-packages --root-user-action=ignore -r requirements.txt && \
RUN python3 -m venv /app/venv && \
/app/venv/bin/pip install --no-cache-dir -r requirements.txt && \
chmod +x /start.sh
CMD ["/start.sh"]

273
README.md
View File

@@ -1,175 +1,188 @@
# MamieHenriette 👵
# 👵 Mamie Henriette - Discord Status Bot 🤖
**Bot multi-plateformes pour Discord, Twitch et YouTube Live**
## 📖 Description
Mamie Henriette est un bot Discord intelligent qui change automatiquement de statut, surveillant et gérant votre serveur avec une touche d'humour et de caractère.
## Vue d'ensemble
## ✨ Fonctionnalités
Mamie Henriette est un bot intelligent open-source développé spécifiquement pour les communautés de [STEvE](https://www.youtube.com/@STEvE_YT) sur YouTube, [Twitch](https://www.twitch.tv/steve_yt) et Discord.
- Changement cyclique automatique des statuts
- Configuration flexible via variables d'environnement
- Gestion des erreurs et logging
- Support multi-statuts Discord
- Déploiement simplifié avec Docker
- 📊 Surveillance optionnelle avec Zabbix
> ⚠️ **Statut** : En cours de développement
## 🛠 Prérequis
### Caractéristiques principales
- Docker et Docker Compose
- Compte Discord et Token du bot
- (Optionnel) Serveur Zabbix pour la surveillance
- Interface web d'administration complète
- Gestion multi-plateformes (Discord, Twitch, YouTube Live)
- Système de notifications automatiques
- Base de données intégrée pour la persistance
- Surveillance optionnelle avec Zabbix *(non testée)*
## 📦 Installation
## Fonctionnalités
### Discord
- **Statuts dynamiques** : Rotation automatique des humeurs (10 min)
- **Notifications Humble Bundle** : Surveillance et alertes automatiques (30 min)
- **Commandes personnalisées** : Gestion via interface web
- **Modération** : Outils intégrés
### Twitch *(en développement)*
- **Chat bot** : Commandes et interactions
- **Événements live** : Notifications de stream
### YouTube Live *(en développement)*
- **Chat bot** : Modération et commandes
- **Événements** : Notifications de diffusion
### Interface d'administration
- **Dashboard** : Vue d'ensemble et statistiques
- **Configuration** : Tokens, paramètres des plateformes
- **Gestion des humeurs** : Création et modification des statuts
- **Commandes** : Édition des commandes personnalisées
- **Modération** : Outils de gestion communautaire
### Surveillance
- **Zabbix Agent 2** : Monitoring avancé *(non testé)*
- **Métriques** : Santé du bot et uptime
## Installation
### Prérequis
- [Docker Engine](https://docs.docker.com/engine/install/) ou [Docker Desktop](https://docs.docker.com/desktop/)
- Token Discord pour le bot
### Démarrage rapide
1. Clonez le dépôt
```bash
git clone https://git.favrep.ch/lapatatedouce/MamieHenrriette
cd MamieHenrriette
# 1. Cloner le projet
git clone https://github.com/skylanix/MamieHenriette.git
```
2. Copiez le fichier de configuration
```bash
cp .env.example .env
cd MamieHenriette
```
3. Éditez le fichier `.env` avec vos paramètres
```bash
nano .env
# 2. Lancer avec Docker
docker compose up --build -d
```
4. Démarrez le conteneur Docker
### Configuration
1. **Interface web** : Accédez à http://localhost
2. **Token Discord** : Section "Configurations"
3. **Humeurs** : Définir les statuts du bot
4. **Canaux** : Configurer les notifications
> ⚠️ **Important** : Après avoir configuré le token Discord, les humeurs et autres fonctionnalités via l'interface web, **redémarrez le conteneur** pour que les changements soient pris en compte :
> ```bash
> docker compose restart mamiehenriette
> ```
### Commandes Docker utiles
**Mode développement (avec logs):**
```bash
docker-compose up --build
# Logs en temps réel
docker compose logs -f mamiehenriette
```
**Mode production (en arrière-plan):**
```bash
docker-compose up --build -d
# Logs d'un conteneur en cours d'exécution
docker logs -f mamiehenriette
```
**Voir les logs:**
```bash
docker-compose logs -f discord-bot
# Redémarrer
docker compose restart mamiehenriette
```
**Arrêter le conteneur:**
```bash
docker-compose down
# Arrêter
docker compose down
```
## 🔧 Configuration
## Configuration avancée
### Variables d'environnement principales
### Variables d'environnement
- `TOKEN`: Votre token Discord (obligatoire)
- `STATUS`: Statut initial (défaut: online)
- `INTERVAL`: Intervalle de changement de statut (défaut: 3600 secondes)
```yaml
environment:
- ENABLE_ZABBIX=false # Surveillance (non testée)
- ZABBIX_SERVER=localhost
- ZABBIX_HOSTNAME=MamieHenriette
```
### 📊 Configuration Zabbix (optionnelle)
### Interface d'administration
- `ENABLE_ZABBIX`: Activer la surveillance Zabbix (défaut: false)
- `ZABBIX_SERVER`: Adresse du serveur Zabbix
- `ZABBIX_HOSTNAME`: Nom d'hôte pour identifier le bot
- `ZABBIX_PORT`: Port d'exposition Zabbix (défaut: 10050)
| Section | Fonction |
|---------|----------|
| **Configurations** | Tokens et paramètres généraux |
| **Humeurs** | Gestion des statuts Discord |
| **Commandes** | Commandes personnalisées |
| **Modération** | Outils de gestion |
#### Métriques surveillées par Zabbix
## Architecture du projet
- Statut du bot Discord
- Temps de fonctionnement (uptime)
- Utilisation mémoire
- Erreurs et avertissements dans les logs
- Connectivité à Discord
### Structure des modules
#### Activation de Zabbix
```
├── database/ # Couche données
│ ├── models.py # Modèles ORM
│ ├── helpers.py # Utilitaires BDD
│ └── schema.sql # Structure initiale
├── discordbot/ # Module Discord
│ └── __init__.py # Bot et handlers
└── webapp/ # Interface d'administration
├── static/ # Assets statiques
├── templates/ # Vues HTML
└── *.py # Contrôleurs par section
```
Dans votre fichier `.env` :
### Composants principaux
| Fichier | Rôle |
|---------|------|
| `run-web.py` | Point d'entrée principal |
| `start.sh` | Script de démarrage Docker |
| `docker-compose.yml` | Configuration des services |
| `requirements.txt` | Dépendances Python |
## Spécifications techniques
### Base de données (SQLite)
- **Configuration** : Paramètres et tokens
- **Humeur** : Statuts Discord rotatifs
- **Message** : Messages périodiques *(planifié)*
- **GameBundle** : Historique Humble Bundle
### Architecture multi-thread
- **Thread 1** : Interface web Flask (port 5000)
- **Thread 2** : Bot Discord et tâches automatisées
### Dépendances principales
```
discord.py # API Discord
flask # Interface web
requests # Client HTTP
waitress # Serveur WSGI
```
## Développement
### Installation locale
```bash
ENABLE_ZABBIX=true
ZABBIX_SERVER=votre-serveur-zabbix.com
ZABBIX_HOSTNAME=MamieHenriette
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python run-web.py
```
### Fichier `statuts.txt`
Créez un fichier `statuts.txt` avec vos statuts, un par ligne.
Exemple :
```
Surveiller le serveur
Mamie est là !
En mode supervision
```
## 📋 Dépendances
- discord.py==2.3.2
- python-dotenv==1.0.0
### Contribution
1. Fork du projet
2. Branche feature
3. Pull Request
---
# 🖥️ Installation environnement de développement
## Installation des dépendances système
```bash
sudo apt install python3 python3-pip
```
## Création de l'environnement Python local
Dans le dossier du projet :
```bash
python3 -m venv .venv
```
Puis activer l'environnement :
```bash
source .venv/bin/activate
```
## Installation des dépendances Python
```bash
pip install -r requirements.txt
```
## Exécution
```bash
python3 run-web.py
```
# Structure du projet
```
.
|-- database : module de connexion à la BDD
| |-- __init.py__
| |-- models.py : contient les pojo représentant chaque table
| |-- schema.sql : contient un scrip sql d'initialisation de la bdd, celui-ci doit être réentrant
|
|-- discordbot : module de connexion à discord
| |-- __init.py__
|
|-- webapp : module du site web d'administration
| |-- static : Ressource fixe directement accessible par le navigateir
| | |-- css
| | |-- ...
| |
| |-- template : Fichier html
| | |-- template.html : structure globale du site
| | |-- commandes.html : page de gestion des commandes
| | |-- ...
| |
| |-- __init.py__
| |-- index.py : controller de la page d'acceuil
| |-- commandes.py : controller de gestion des commandes
| |-- ...
|
|-- run-web.py : launcher
```
*Mamie Henriette vous surveille ! 👵👀*

114
bot.py
View File

@@ -1,114 +0,0 @@
import discord
import json
import random
import asyncio
import logging
import os
class DiscordStatusBot:
def __init__(self):
# Configuration des logs
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
# Charger la configuration à partir des variables d'environnement
self.config = self.charger_configuration()
if not self.config:
logging.error("Impossible de charger la configuration")
exit(1)
# Configuration des intents
intents = discord.Intents.default()
intents.message_content = False
# Création du client
self.client = discord.Client(intents=intents)
# Événements
self.setup_events()
def charger_configuration(self):
"""Chargement de la configuration à partir des variables d'environnement"""
config = {
'token': os.getenv('TOKEN'),
'status': os.getenv('STATUS', 'online'),
'interval': int(os.getenv('INTERVAL', 60))
}
if not config['token']:
logging.error("Token non fourni")
return None
return config
def charger_statuts(self):
"""Chargement des statuts depuis le fichier"""
try:
with open('/app/statuts.txt', 'r', encoding='utf-8') as fichier:
return [ligne.strip() for ligne in fichier.readlines() if ligne.strip()]
except FileNotFoundError:
logging.error("Fichier de statuts non trouvé")
return []
def setup_events(self):
"""Configuration des événements du bot"""
@self.client.event
async def on_ready():
logging.info(f'Bot connecté : {self.client.user}')
self.client.loop.create_task(self.changer_statut())
# Déplacez changer_statut à l'extérieur de setup_events
async def changer_statut(self):
"""Changement cyclique du statut"""
await self.client.wait_until_ready()
statuts = self.charger_statuts()
if not statuts:
logging.warning("Aucun statut disponible")
return
# Mapping des status Discord
status_mapping = {
'online': discord.Status.online,
'idle': discord.Status.idle,
'dnd': discord.Status.dnd,
'invisible': discord.Status.invisible
}
# Récupérer le status depuis la configuration
status_discord = status_mapping.get(self.config.get('status', 'online'), discord.Status.online)
while not self.client.is_closed():
try:
# Sélection du statut
statut = random.choice(statuts)
# Changement de statut avec custom activity
await self.client.change_presence(
status=status_discord,
activity=discord.CustomActivity(name=statut)
)
logging.info(f"Statut changé : {statut}")
# Délai entre les changements
await asyncio.sleep(self.config.get('interval', 60))
except Exception as e:
logging.error(f"Erreur lors du changement de statut : {e}")
await asyncio.sleep(30) # Attente en cas d'erreur
def executer(self):
"""Lancement du bot"""
try:
if self.config and 'token' in self.config:
self.client.run(self.config['token'])
else:
logging.error("Token non trouvé dans la configuration")
except discord.LoginFailure:
logging.error("Échec de connexion - Vérifiez votre token")
except Exception as e:
logging.error(f"Erreur lors du lancement : {e}")
def main():
bot = DiscordStatusBot()
bot.executer()
if __name__ == "__main__":
main()

32
database/helpers.py Normal file
View File

@@ -0,0 +1,32 @@
from database import db
from database.models import Configuration
class ConfigurationHelper:
def getValue(self, key:str) :
conf = Configuration.query.filter_by(key=key).first()
if conf == None:
return None
if (key.endswith('_enable')) :
return conf.value in ['true', '1', 'yes', 'on']
return conf.value
def getIntValue(self, key:str) :
conf = Configuration.query.filter_by(key=key).first()
if conf == None:
return 0
return int(conf.value)
def createOrUpdate(self, key:str, value) :
conf = Configuration.query.filter_by(key=key).first()
if (key.endswith('_enable')) :
value = value in ['true', '1', 'yes', 'on']
if conf :
conf.value = value
else :
conf = Configuration(key = key, value = value)
db.session.add(conf)

View File

@@ -9,6 +9,11 @@ class Humeur(db.Model):
enable = db.Column(db.Boolean, default=True)
text = db.Column(db.String(256))
class GameBundle(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(256))
json = db.Column(db.String(1024))
class Message(db.Model):
id = db.Column(db.Integer, primary_key=True)
enable = db.Column(db.Boolean, default=False)

View File

@@ -4,15 +4,21 @@ CREATE TABLE IF NOT EXISTS `configuration` (
`value` VARCHAR(512) NOT NULL
);
CREATE TABLE IF NOT EXISTS `game_bundle` (
id INTEGER PRIMARY KEY,
name VARCHAR(256) NOT NULL,
json VARCHAR(1024) NOT NULL
);
CREATE TABLE IF NOT EXISTS `humeur` (
id INTEGER PRIMARY KEY AUTOINCREMENT,
`enable` BOOLEAN NOT NULL DEFAULT TRUE,
`text` VARCHAR(256) NULL
);
CREATE TABLE IF NOT EXISTS `message` (
id INTEGER PRIMARY KEY AUTOINCREMENT,
`enable` BOOLEAN NOT NULL DEFAULT FALSE,
`text` VARCHAR(256) NULL,
periodicity INTEGER NULL
);
CREATE TABLE IF NOT EXISTS `humeur` (
id INTEGER PRIMARY KEY AUTOINCREMENT,
`enable` BOOLEAN NOT NULL DEFAULT TRUE,
`text` VARCHAR(256) NULL
);

View File

@@ -1,34 +1,65 @@
import random
import discord
# import os
import logging
import asyncio
from webapp import webapp
from database.models import Configuration, Humeur
import datetime
import discord
import json
import logging
import random
import requests
from database import db
from database.helpers import ConfigurationHelper
from database.models import Configuration, GameBundle, Humeur
class DiscordBot(discord.Client):
async def on_ready(self):
logging.info(f'Logged in as {self.user} (ID: {self.user.id})')
for c in self.get_all_channels() :
logging.info(f'{c.id} {c.name}')
self.loop.create_task(self.updateStatus())
# await self.get_channel(1123512494468644984).send("essai en python")
self.loop.create_task(self.updateHumbleBundle())
async def updateStatus(self):
# from database.models import Humeur
humeur = random.choice(Humeur.query.all())
if humeur != None:
logging.info(f'changement de status {humeur.text}')
await self.change_presence(status = discord.Status.online, activity = discord.CustomActivity(humeur.text))
await asyncio.sleep(60)
while not self.is_closed():
humeurs = Humeur.query.all()
if len(humeurs)>0 :
humeur = random.choice(humeurs)
if humeur != None:
logging.info(f'changement de status {humeur.text}')
await self.change_presence(status = discord.Status.online, activity = discord.CustomActivity(humeur.text))
# 10 minutes TODO à rendre configurable
await asyncio.sleep(10*60)
async def updateHumbleBundle(self):
while not self.is_closed():
if ConfigurationHelper().getValue('humble_bundle_enable') and ConfigurationHelper().getIntValue('humble_bundle_channel') != 0 :
response = requests.get("http://hexas.shionn.org/humble-bundle/json", headers={ "Content-Type": "application/json" })
if response.status_code == 200:
bundle = response.json()
if GameBundle.query.filter_by(id=bundle['id']).first() == None :
choice = bundle['choices'][0]
date = datetime.datetime.fromtimestamp(bundle['endDate']/1000,datetime.UTC).strftime("%d %B %Y")
message = f"@here **Humble Bundle** propose un pack de jeu [{bundle['name']}]({bundle['url']}) contenant :\n"
for game in choice["games"]:
message += f"- {game}\n"
message += f"Pour {choice['price']}€, disponible jusqu'au {date}."
await self.get_channel(ConfigurationHelper().getIntValue('humble_bundle_channel')).send(message)
db.session.add(GameBundle(id=bundle['id'], name=bundle['name'], json = json.dumps(bundle)))
db.session.commit()
else:
logging.error(f"Erreur de connexion {response.status_code}")
else:
logging.info('Humble bundle est désactivé')
# toute les 30 minutes
await asyncio.sleep(30*60)
def begin(self) :
with webapp.app_context():
token = Configuration.query.filter_by(key='discord_token').first()
if token :
self.run(token.value)
else :
logging.error('pas de token on ne lance pas discord')
token = Configuration.query.filter_by(key='discord_token').first()
if token :
self.run(token.value)
else :
logging.error('pas de token on ne lance pas discord')
intents = discord.Intents.default()
bot = DiscordBot(intents=intents)

View File

@@ -1,16 +1,15 @@
services:
discord-bot:
mamiehenriette:
container_name: MamieHenriette
build: .
restart: on-failure
environment:
- TOKEN=VOTRE_TOKEN_DISCORD_ICI
- STATUS=online
- INTERVAL=3600
- ENABLE_ZABBIX=false
- ZABBIX_SERVER=zabbix-server.example.com
- ZABBIX_HOSTNAME=mamie-henriette-bot
volumes:
- ./statuts.txt:/app/statuts.txt
# ports:
- ./instance:/app/instance
ports:
- 80:5000
# - "10050:10050" # Décommentez si ENABLE_ZABBIX=true

View File

@@ -6,4 +6,5 @@ audioop-lts; python_version>='3.13'
flask>=2.3.2
flask-sqlalchemy>=3.0.3
waitress>=3.0.2
waitress>=3.0.2
requests>=2.32.4

View File

@@ -1,25 +1,28 @@
#
# import discordbot
import multiprocessing
import locale
import logging
import threading
from webapp import webapp
from discordbot import bot
def start_server():
logging.info("Start Web Serveur")
from webapp import webapp
from waitress import serve
serve(webapp, host="0.0.0.0", port=5000)
def start_discord_bot():
logging.info("Start Discord Bot")
from discordbot import bot
bot.begin()
with webapp.app_context():
bot.begin()
if __name__ == '__main__':
locale.setlocale(locale.LC_TIME, 'fr_FR.UTF-8')
jobs = []
jobs.append(multiprocessing.Process(target=start_server))
jobs.append(multiprocessing.Process(target=start_discord_bot))
jobs.append(threading.Thread(target=start_discord_bot))
jobs.append(threading.Thread(target=start_server))
for job in jobs: job.start()
for job in jobs: job.join()

View File

@@ -18,4 +18,4 @@ else
fi
echo "Démarrage du bot Discord..."
exec python3 bot.py
exec /app/venv/bin/python run-web.py

View File

@@ -1,22 +0,0 @@
STEvE galère un peu, mais moi je gère le bazar.
Sans mamie, vous seriez bien vite paumés.
Ici, cest mamie qui veille, alors écoutez un peu.
STEvE dort, moi je fais tourner la baraque.
Jaime râler, mais cest pour vous réveiller un peu.
Restez sages, sinon mamie va grogner un peu.
Les malins, faites-moi plaisir, écoutez mamie.
Jamais le bazar ne sinstalle, je veille au grain.
Cest moi qui tiens la maison.
Vous aimez STEvE ? Moi, je vous tolère.
Gardez votre calme, mamie veille sur vous.
Quand STEvE foire, je suis là pour arranger ça.
Suivez STEvE, mais surtout ne perdez pas mamie de vue.
STEvE rêve, moi je travaille en coulisses.
Ecoutez mamie, cest pour votre bien.
Pas dembrouille, ou mamie va devoir intervenir.
STEvE dort, mais mamie veille toujours.
Vous râlez ? Moi aussi, mais on fait avec.
STEvE prend son temps, moi je veille au grain.
Pas de bazar, mamie préfère le calme.
STEvE est parfois perdu, mais mamie est là.
Le serveur est calme, grâce à mamie.

View File

@@ -1,19 +1,24 @@
from flask import render_template, request, redirect, url_for
from webapp import webapp
from database import db
from database.models import Configuration
from database.helpers import ConfigurationHelper
from discordbot import bot
from discord import TextChannel
@webapp.route("/configurations")
def openConfigurations():
return render_template("configurations.html")
channels = []
for channel in bot.get_all_channels():
if isinstance(channel, TextChannel):
channels.append(channel)
return render_template("configurations.html", configuration = ConfigurationHelper(), channels = channels)
@webapp.route('/configurations/set/<key>', methods=['POST'])
def setConfiguration(key):
conf = Configuration.query.filter_by(key=key).first()
if conf :
conf.value = request.form['value']
else :
conf = Configuration(key = key, value = request.form['value'])
db.session.add(conf)
@webapp.route("/configurations/update", methods=['POST'])
def updateConfiguration():
for key in request.form :
ConfigurationHelper().createOrUpdate(key, request.form.get(key))
# Je fait ca car html n'envoi pas le parametre de checkbox quand il est décoché
if (request.form.get("humble_bundle_channel") != None and request.form.get("humble_bundle_enable") == None) :
ConfigurationHelper().createOrUpdate('humble_bundle_enable', False)
db.session.commit()
return redirect(url_for('openConfigurations'))

View File

@@ -3,10 +3,27 @@
{% block content %}
<h1>Configuration de Mamie.</h1>
<h2>Humble Bundle</h2>
<form action="{{ url_for('updateConfiguration') }}" method="POST">
<label for="humble_bundle_enable">Activer</label>
<input type="checkbox" name="humble_bundle_enable" {% if configuration.getValue('humble_bundle_enable') %}
checked="checked" {% endif %}>
<label>Activer les notifications Humble bundle</label>
<label for="humble_bundle_channel">Canal de notification des pack Humble Bundle</label>
<select name="humble_bundle_channel">
{% for channel in channels %}
<option value="{{channel.id}}" {% if configuration.getIntValue('humble_bundle_channel')==channel.id %}
selected="selected" {% endif %}>
{{channel.name}}</option>
{% endfor %}
</select>
<input type="Submit" value="Définir">
</form>
<h2>Api</h2>
<form action="{{ url_for('setConfiguration', key = 'discord_token') }}" method="POST">
<label for="value">Api Discord (cachée)</label>
<input name="value" type="password" />
<form action="{{ url_for('updateConfiguration') }}" method="POST">
<label for="discord_token">Api Discord (cachée)</label>
<input name="discord_token" type="password" />
<input type="Submit" value="Définir">
<p>Nécéssite un redémarrage</p>
</form>