Files
MamieHenriette/database/__init__.py
T
Mow910andCursor b274dcaa51 Ajout de fonctionnalités majeures Twitch, Discord et interface web
Nouvelles fonctionnalités :
- Système de modération Twitch complet (bans, timeouts, avertissements)
- Filtre de liens intelligent pour Twitch avec whitelist/blacklist
- Notifications d'événements Twitch (follows, subs, raids, etc.)
- Système Freeloot Discord avec flux RSS dédié
- Salons automatiques Discord (création/suppression dynamique)
- Authentification utilisateur pour l'interface web (login/register)
- Gestion des utilisateurs et permissions
- Interface de modération Twitch dans la webapp
- Interface de gestion des événements Twitch
- Configuration des paramètres utilisateur
- Migration BDD pour les mots bannis

Améliorations de l'interface :
- Refonte complète des templates (configurations, commandes, humeurs, etc.)
- Nouvelle page de settings utilisateur
- Page de gestion des utilisateurs (admin)
- Page d'erreur 403 personnalisée
- Amélioration de la navigation et du design global
- Intégration de nouvelles sections dans le menu principal

Modifications techniques :
- Ajout de nouveaux modèles en base de données
- Extension des helpers database
- Mise à jour des dépendances (requirements.txt)
- Amélioration de la gestion des annonces Twitch
- Refactorisation du code pour meilleure maintenabilité

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 22:37:22 +01:00

299 lines
11 KiB
Python

import logging
import json
import os
from sqlalchemy import event
from sqlalchemy.engine import Engine
from flask_sqlalchemy import SQLAlchemy
from sqlite3 import Cursor, Connection
from webapp import webapp
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")}'
# 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)
# 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 _tableExists(table_name: str, cursor: Cursor) -> bool:
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
return cursor.fetchone() is not None
def _tableHaveColumn(table_name:str, column_name:str, cursor:Cursor) -> bool:
if not _tableExists(table_name, cursor):
return False
cursor.execute(f'PRAGMA table_info({table_name})')
columns = cursor.fetchall()
return any(col[1] == column_name for col in columns)
def _tableEmpty(table:str, cursor:Cursor) -> bool:
return cursor.execute(f'SELECT COUNT(*) FROM {table}').fetchone()[0] == 0
def _renameTable(old_name:str, new_name:str, cursor:Cursor) :
cursor.execute(f'ALTER TABLE {old_name} RENAME TO {new_name}')
def _dropTable(table_name:str, cursor:Cursor) :
cursor.execute(f'DROP TABLE {table_name}')
def _doPreImportMigration(cursor:Cursor):
if _tableHaveColumn('game_bundle', 'id', cursor) :
logging.info("Table game_bundle détécté, rennomage en game_bundle_old")
_renameTable('game_bundle', 'game_bundle_old', cursor)
def _doPostImportMigration(cursor:Cursor):
if _tableEmpty('game_bundle', cursor) and _tableExists('game_bundle_old', cursor):
logging.info("remplir game_bundle avec game_bundle_old")
bundles = cursor.execute('SELECT * FROM game_bundle_old').fetchall()
for bundle in bundles :
name = bundle[1]
json_data = json.loads(bundle[2])
url = json_data['url']
logging.info(f'import du bundle {name}, {url}')
cursor.execute('INSERT INTO game_bundle(url, name, json) VALUES (?, ?, ?)', (url, name, json.dumps(json_data)))
logging.info("suppression de la table temporaire game_bundle_old")
_dropTable('game_bundle_old', cursor)
if _tableExists('youtube_notification', cursor):
embed_columns = [
('embed_title', 'VARCHAR(256)'),
('embed_description', 'VARCHAR(2000)'),
('embed_color', 'VARCHAR(8) DEFAULT "FF0000"'),
('embed_footer', 'VARCHAR(2048)'),
('embed_author_name', 'VARCHAR(256)'),
('embed_author_icon', 'VARCHAR(512)'),
('embed_thumbnail', 'BOOLEAN DEFAULT 1'),
('embed_image', 'BOOLEAN DEFAULT 1'),
]
for col_name, col_type in embed_columns:
if not _tableHaveColumn('youtube_notification', col_name, cursor):
try:
cursor.execute(f'ALTER TABLE youtube_notification ADD COLUMN {col_name} {col_type}')
logging.info(f"Colonne {col_name} ajoutée à youtube_notification")
except Exception as e:
logging.warning(f"Colonne youtube_notification.{col_name}: {e}")
if _tableExists('commande', cursor) and not _tableHaveColumn('commande', 'twitch_permission', cursor):
try:
cursor.execute("ALTER TABLE commande ADD COLUMN twitch_permission VARCHAR(16) DEFAULT 'viewer'")
logging.info("Colonne twitch_permission ajoutée à commande")
except Exception as e:
logging.warning(f"Colonne commande.twitch_permission: {e}")
def _doAddColumnMigrations(cursor: Cursor):
"""Migrations d'ajout de colonnes. Exécutées à part pour ne pas dépendre du script principal."""
if _tableExists('commande', cursor) and not _tableHaveColumn('commande', 'twitch_permission', cursor):
try:
cursor.execute("ALTER TABLE commande ADD COLUMN twitch_permission VARCHAR(16) DEFAULT 'viewer'")
logging.info("Colonne twitch_permission ajoutée à commande")
except Exception as e:
logging.warning(f"Colonne commande.twitch_permission: {e}")
if _tableExists('live_alert', cursor) and not _tableHaveColumn('live_alert', 'watch_activity', cursor):
try:
cursor.execute("ALTER TABLE live_alert ADD COLUMN watch_activity BOOLEAN NOT NULL DEFAULT 0")
logging.info("Colonne watch_activity ajoutée à live_alert")
except Exception as e:
logging.warning(f"Colonne live_alert.watch_activity: {e}")
# Colonnes embed pour live_alert (message par défaut en embed)
if _tableExists('live_alert', cursor):
live_alert_embed_columns = [
('embed_title', 'VARCHAR(256)'),
('embed_description', 'VARCHAR(2000)'),
('embed_color', 'VARCHAR(8) DEFAULT "9146FF"'),
('embed_footer', 'VARCHAR(2048)'),
('embed_author_name', 'VARCHAR(256)'),
('embed_author_icon', 'VARCHAR(512)'),
('embed_thumbnail', 'BOOLEAN DEFAULT 1'),
('embed_image', 'BOOLEAN DEFAULT 1'),
]
for col_name, col_type in live_alert_embed_columns:
if not _tableHaveColumn('live_alert', col_name, cursor):
try:
cursor.execute(f'ALTER TABLE live_alert ADD COLUMN {col_name} {col_type}')
logging.info(f"Colonne {col_name} ajoutée à live_alert")
except Exception as e:
logging.warning(f"Colonne live_alert.{col_name}: {e}")
# Table twitch_event_notification + seed des 4 types
if not _tableExists('twitch_event_notification', cursor):
try:
cursor.execute("""
CREATE TABLE twitch_event_notification (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_type VARCHAR(32) UNIQUE NOT NULL,
enable BOOLEAN NOT NULL DEFAULT 1,
notify_twitch_chat BOOLEAN NOT NULL DEFAULT 1,
notify_discord BOOLEAN NOT NULL DEFAULT 0,
discord_channel_id INTEGER NULL,
message_twitch VARCHAR(500) NOT NULL DEFAULT '',
message_discord VARCHAR(2000) NULL,
embed_color VARCHAR(8) DEFAULT '9146FF',
embed_title VARCHAR(256) NULL,
embed_description VARCHAR(2000) NULL,
embed_thumbnail BOOLEAN NOT NULL DEFAULT 1,
last_clip_id VARCHAR(128) NULL
)
""")
logging.info("Table twitch_event_notification créée")
except Exception as e:
logging.warning(f"Table twitch_event_notification: {e}")
if _tableExists('twitch_event_notification', cursor) and _tableEmpty('twitch_event_notification', cursor):
for ev in ('sub', 'follow', 'raid', 'clip'):
try:
cursor.execute(
"INSERT INTO twitch_event_notification (event_type, message_twitch) VALUES (?, ?)",
(ev, 'Merci {user} !' if ev != 'raid' else 'Bienvenue aux viewers de {from_broadcaster_name} !'),
)
except Exception as e:
logging.warning(f"Seed twitch_event_notification {ev}: {e}")
# Table webapp_user (auth)
if not _tableExists('webapp_user', cursor):
try:
cursor.execute("""
CREATE TABLE webapp_user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(64) UNIQUE NOT NULL,
email VARCHAR(256) UNIQUE NOT NULL,
password_hash VARCHAR(256) NOT NULL,
role VARCHAR(64) NOT NULL DEFAULT 'viewer_twitch',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)
""")
logging.info("Table webapp_user créée")
except Exception as e:
logging.warning(f"Table webapp_user: {e}")
# Tables webapp_role et webapp_page_permission
if not _tableExists('webapp_role', cursor):
try:
cursor.execute("""
CREATE TABLE webapp_role (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(64) UNIQUE NOT NULL,
level INTEGER NOT NULL DEFAULT 0
)
""")
logging.info("Table webapp_role créée")
except Exception as e:
logging.warning(f"Table webapp_role: {e}")
if not _tableExists('webapp_page_permission', cursor):
try:
cursor.execute("""
CREATE TABLE webapp_page_permission (
id INTEGER PRIMARY KEY AUTOINCREMENT,
page_key VARCHAR(64) UNIQUE NOT NULL,
min_level INTEGER NOT NULL DEFAULT 0,
write_level INTEGER NULL
)
""")
logging.info("Table webapp_page_permission créée")
except Exception as e:
logging.warning(f"Table webapp_page_permission: {e}")
def _doSeedAuth(cursor: Cursor):
"""Seed rôles par défaut et permissions des pages si vides."""
from database.models import ROLE_ORDER
if not _tableExists('webapp_role', cursor):
return
if cursor.execute("SELECT COUNT(*) FROM webapp_role").fetchone()[0] > 0:
return
default_roles = [
("viewer_twitch", 0),
("utilisateur_discord", 1),
("moderateur_discord", 2),
("expert_discord", 3),
("moderateur_twitch", 4),
("super_administrateur", 5),
]
for name, level in default_roles:
try:
cursor.execute("INSERT INTO webapp_role (name, level) VALUES (?, ?)", (name, level))
except Exception as e:
logging.warning(f"Seed role {name}: {e}")
logging.info("Rôles par défaut insérés")
if not _tableExists('webapp_page_permission', cursor):
return
if cursor.execute("SELECT COUNT(*) FROM webapp_page_permission").fetchone()[0] > 0:
return
# page_key, min_level, write_level (NULL = même que min_level)
default_pages = [
("index", 0, None),
("configurations", 5, 5),
("commandes", 1, 2),
("humeurs", 1, 2),
("live_alert", 0, 4),
("announcements", 0, 4),
("twitch_moderation", 0, 4),
("link_filter", 0, 4),
("twitch_events", 0, 4),
("youtube", 1, 2),
("protondb", 1, 2),
("freeloot", 1, 2),
("moderation", 1, 2),
("users", 5, 5),
("settings", 5, 5),
]
for page_key, min_level, wl in default_pages:
try:
cursor.execute(
"INSERT INTO webapp_page_permission (page_key, min_level, write_level) VALUES (?, ?, ?)",
(page_key, min_level, wl),
)
except Exception as e:
logging.warning(f"Seed page {page_key}: {e}")
logging.info("Permissions pages par défaut insérées")
# Inscriptions activées par défaut
if _tableExists("configuration", cursor):
try:
cursor.execute(
"INSERT OR IGNORE INTO configuration (key, value) VALUES ('registration_enabled', 'true')"
)
except Exception as e:
logging.warning(f"Config registration_enabled: {e}")
with webapp.app_context():
with open('database/schema.sql', 'r') as f:
sql = f.read()
connection: Connection = db.session.connection().connection
try:
cursor = connection.cursor()
_doPreImportMigration(cursor)
cursor.executescript(sql)
_doPostImportMigration(cursor)
connection.commit()
except Exception as e:
logging.error(f"lors de l'import de la bdd : {e}")
finally:
try:
cursor = connection.cursor()
_doAddColumnMigrations(cursor)
_doSeedAuth(cursor)
connection.commit()
except Exception as e:
logging.warning(f"migrations colonnes : {e}")
connection.close()