mirror of
https://github.com/skylanix/MamieHenriette.git
synced 2026-02-06 14:50:34 +01:00
Compare commits
5 Commits
front-tail
...
10906bdae3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10906bdae3 | ||
|
|
48531690fd | ||
|
|
9bbfa1fade | ||
|
|
a8d2a0e063 | ||
|
|
f2cd19a053 |
@@ -33,7 +33,13 @@ def _set_sqlite_pragma(dbapi_connection, connection_record):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _tableHaveColumn(table_name:str, column_name:str, cursor:Cursor) -> bool:
|
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})')
|
cursor.execute(f'PRAGMA table_info({table_name})')
|
||||||
columns = cursor.fetchall()
|
columns = cursor.fetchall()
|
||||||
return any(col[1] == column_name for col in columns)
|
return any(col[1] == column_name for col in columns)
|
||||||
@@ -65,6 +71,25 @@ def _doPostImportMigration(cursor:Cursor):
|
|||||||
logging.info("suppression de la table temporaire game_bundle_old")
|
logging.info("suppression de la table temporaire game_bundle_old")
|
||||||
_dropTable('game_bundle_old', cursor)
|
_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}")
|
||||||
|
|
||||||
with webapp.app_context():
|
with webapp.app_context():
|
||||||
with open('database/schema.sql', 'r') as f:
|
with open('database/schema.sql', 'r') as f:
|
||||||
sql = f.read()
|
sql = f.read()
|
||||||
|
|||||||
@@ -61,3 +61,22 @@ class AntiCheatCache(db.Model):
|
|||||||
notes = db.Column(db.String(1024))
|
notes = db.Column(db.String(1024))
|
||||||
updated_at = db.Column(db.DateTime)
|
updated_at = db.Column(db.DateTime)
|
||||||
|
|
||||||
|
|
||||||
|
class YouTubeNotification(db.Model):
|
||||||
|
__tablename__ = 'youtube_notification'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
enable = db.Column(db.Boolean, default=True)
|
||||||
|
channel_id = db.Column(db.String(128))
|
||||||
|
notify_channel = db.Column(db.Integer)
|
||||||
|
message = db.Column(db.String(2000))
|
||||||
|
video_type = db.Column(db.String(16), default='all')
|
||||||
|
last_video_id = db.Column(db.String(128))
|
||||||
|
embed_title = db.Column(db.String(256))
|
||||||
|
embed_description = db.Column(db.String(2000))
|
||||||
|
embed_color = db.Column(db.String(8), default='FF0000')
|
||||||
|
embed_footer = db.Column(db.String(2048))
|
||||||
|
embed_author_name = db.Column(db.String(256))
|
||||||
|
embed_author_icon = db.Column(db.String(512))
|
||||||
|
embed_thumbnail = db.Column(db.Boolean, default=True)
|
||||||
|
embed_image = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
|
|||||||
@@ -76,3 +76,21 @@ CREATE TABLE IF NOT EXISTS `member_invites` (
|
|||||||
`inviter_name` VARCHAR(256),
|
`inviter_name` VARCHAR(256),
|
||||||
`join_date` DATETIME NOT NULL
|
`join_date` DATETIME NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `youtube_notification` (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
`enable` BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
`channel_id` VARCHAR(128) NOT NULL,
|
||||||
|
`notify_channel` INTEGER NOT NULL,
|
||||||
|
`message` VARCHAR(2000) NOT NULL,
|
||||||
|
`video_type` VARCHAR(16) NOT NULL DEFAULT 'all',
|
||||||
|
`last_video_id` VARCHAR(128),
|
||||||
|
`embed_title` VARCHAR(256),
|
||||||
|
`embed_description` VARCHAR(2000),
|
||||||
|
`embed_color` VARCHAR(8) NOT NULL DEFAULT 'FF0000',
|
||||||
|
`embed_footer` VARCHAR(2048),
|
||||||
|
`embed_author_name` VARCHAR(256),
|
||||||
|
`embed_author_icon` VARCHAR(512),
|
||||||
|
`embed_thumbnail` BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
`embed_image` BOOLEAN NOT NULL DEFAULT TRUE
|
||||||
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import discord
|
|||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
from webapp import webapp
|
||||||
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
|
||||||
@@ -22,11 +23,14 @@ from discordbot.moderation import (
|
|||||||
handle_say_command
|
handle_say_command
|
||||||
)
|
)
|
||||||
from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache
|
from discordbot.welcome import sendWelcomeMessage, sendLeaveMessage, updateInviteCache
|
||||||
|
from discordbot.youtube import checkYouTubeVideos
|
||||||
from protondb import searhProtonDb
|
from protondb import searhProtonDb
|
||||||
|
|
||||||
class DiscordBot(discord.Client):
|
class DiscordBot(discord.Client):
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
logging.info(f'Connecté en tant que {self.user} (ID: {self.user.id})')
|
logging.info(f'Connecté en tant que {self.user} (ID: {self.user.id})')
|
||||||
|
webapp.config["BOT_STATUS"]["discord_connected"] = True
|
||||||
|
webapp.config["BOT_STATUS"]["discord_guild_count"] = len(self.guilds)
|
||||||
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}')
|
||||||
|
|
||||||
@@ -35,6 +39,10 @@ class DiscordBot(discord.Client):
|
|||||||
|
|
||||||
self.loop.create_task(self.updateStatus())
|
self.loop.create_task(self.updateStatus())
|
||||||
self.loop.create_task(self.updateHumbleBundle())
|
self.loop.create_task(self.updateHumbleBundle())
|
||||||
|
self.loop.create_task(self.updateYouTube())
|
||||||
|
|
||||||
|
async def on_disconnect(self):
|
||||||
|
webapp.config["BOT_STATUS"]["discord_connected"] = False
|
||||||
|
|
||||||
async def updateStatus(self):
|
async def updateStatus(self):
|
||||||
while not self.is_closed():
|
while not self.is_closed():
|
||||||
@@ -51,6 +59,11 @@ class DiscordBot(discord.Client):
|
|||||||
await checkHumbleBundleAndNotify(self)
|
await checkHumbleBundleAndNotify(self)
|
||||||
await asyncio.sleep(30*60)
|
await asyncio.sleep(30*60)
|
||||||
|
|
||||||
|
async def updateYouTube(self):
|
||||||
|
while not self.is_closed():
|
||||||
|
await checkYouTubeVideos()
|
||||||
|
await asyncio.sleep(5*60)
|
||||||
|
|
||||||
def getAllTextChannel(self) -> list[TextChannel]:
|
def getAllTextChannel(self) -> list[TextChannel]:
|
||||||
channels = []
|
channels = []
|
||||||
for channel in self.get_all_channels():
|
for channel in self.get_all_channels():
|
||||||
|
|||||||
225
discordbot/youtube.py
Normal file
225
discordbot/youtube.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from database import db
|
||||||
|
from database.models import YouTubeNotification
|
||||||
|
from webapp import webapp
|
||||||
|
|
||||||
|
logger = logging.getLogger('youtube-notification')
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
async def checkYouTubeVideos():
|
||||||
|
with webapp.app_context():
|
||||||
|
try:
|
||||||
|
notifications: list[YouTubeNotification] = YouTubeNotification.query.filter_by(enable=True).all()
|
||||||
|
|
||||||
|
for notification in notifications:
|
||||||
|
try:
|
||||||
|
await _checkChannelVideos(notification)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lors de la vérification de la chaîne {notification.channel_id}: {e}")
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lors de la vérification YouTube: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _checkChannelVideos(notification: YouTubeNotification):
|
||||||
|
try:
|
||||||
|
channel_id = notification.channel_id
|
||||||
|
|
||||||
|
rss_url = f"https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}"
|
||||||
|
|
||||||
|
response = await asyncio.to_thread(requests.get, rss_url, timeout=10)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.error(f"Erreur HTTP {response.status_code} lors de la récupération du RSS pour {channel_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
root = ET.fromstring(response.content)
|
||||||
|
|
||||||
|
ns = {'atom': 'http://www.w3.org/2005/Atom', 'yt': 'http://www.youtube.com/xml/schemas/2015', 'media': 'http://search.yahoo.com/mrss/'}
|
||||||
|
|
||||||
|
entries = root.findall('atom:entry', ns)
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
logger.warning(f"Aucune vidéo trouvée dans le RSS pour {channel_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
videos = []
|
||||||
|
for entry in entries:
|
||||||
|
video_id = entry.find('yt:videoId', ns)
|
||||||
|
if video_id is None:
|
||||||
|
continue
|
||||||
|
video_id = video_id.text
|
||||||
|
|
||||||
|
title_elem = entry.find('atom:title', ns)
|
||||||
|
video_title = title_elem.text if title_elem is not None else 'Sans titre'
|
||||||
|
|
||||||
|
link_elem = entry.find('atom:link', ns)
|
||||||
|
video_url = link_elem.get('href') if link_elem is not None else f"https://www.youtube.com/watch?v={video_id}"
|
||||||
|
|
||||||
|
published_elem = entry.find('atom:published', ns)
|
||||||
|
published_at = published_elem.text if published_elem is not None else ''
|
||||||
|
|
||||||
|
author_elem = entry.find('atom:author/atom:name', ns)
|
||||||
|
channel_name = author_elem.text if author_elem is not None else 'Inconnu'
|
||||||
|
|
||||||
|
thumbnail = None
|
||||||
|
media_thumbnail = entry.find('media:group/media:thumbnail', ns)
|
||||||
|
if media_thumbnail is not None:
|
||||||
|
thumbnail = media_thumbnail.get('url')
|
||||||
|
|
||||||
|
is_short = False
|
||||||
|
if video_title and ('#shorts' in video_title.lower() or '#short' in video_title.lower()):
|
||||||
|
is_short = True
|
||||||
|
|
||||||
|
if notification.video_type == 'all':
|
||||||
|
videos.append((video_id, {
|
||||||
|
'title': video_title,
|
||||||
|
'url': video_url,
|
||||||
|
'published': published_at,
|
||||||
|
'channel_name': channel_name,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'is_short': is_short
|
||||||
|
}))
|
||||||
|
elif notification.video_type == 'short' and is_short:
|
||||||
|
videos.append((video_id, {
|
||||||
|
'title': video_title,
|
||||||
|
'url': video_url,
|
||||||
|
'published': published_at,
|
||||||
|
'channel_name': channel_name,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'is_short': is_short
|
||||||
|
}))
|
||||||
|
elif notification.video_type == 'video' and not is_short:
|
||||||
|
videos.append((video_id, {
|
||||||
|
'title': video_title,
|
||||||
|
'url': video_url,
|
||||||
|
'published': published_at,
|
||||||
|
'channel_name': channel_name,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'is_short': is_short
|
||||||
|
}))
|
||||||
|
|
||||||
|
videos.sort(key=lambda x: x[1]['published'], reverse=True)
|
||||||
|
|
||||||
|
if videos:
|
||||||
|
latest_video_id, latest_video = videos[0]
|
||||||
|
|
||||||
|
if not notification.last_video_id:
|
||||||
|
notification.last_video_id = latest_video_id
|
||||||
|
db.session.commit()
|
||||||
|
return
|
||||||
|
|
||||||
|
if latest_video_id != notification.last_video_id:
|
||||||
|
logger.info(f"Nouvelle vidéo détectée: {latest_video_id} pour la chaîne {notification.channel_id}")
|
||||||
|
await _notifyVideo(notification, latest_video, latest_video_id)
|
||||||
|
notification.last_video_id = latest_video_id
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lors de la vérification des vidéos: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _notifyVideo(notification: YouTubeNotification, video_data: dict, video_id: str):
|
||||||
|
from discordbot import bot
|
||||||
|
try:
|
||||||
|
channel_name = video_data.get('channel_name', 'Inconnu')
|
||||||
|
video_title = video_data.get('title', 'Sans titre')
|
||||||
|
video_url = video_data.get('url', f"https://www.youtube.com/watch?v={video_id}")
|
||||||
|
thumbnail = video_data.get('thumbnail', '')
|
||||||
|
published_at = video_data.get('published', '')
|
||||||
|
is_short = video_data.get('is_short', False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = notification.message.format(
|
||||||
|
channel_name=channel_name or 'Inconnu',
|
||||||
|
video_title=video_title or 'Sans titre',
|
||||||
|
video_url=video_url,
|
||||||
|
video_id=video_id,
|
||||||
|
thumbnail=thumbnail or '',
|
||||||
|
published_at=published_at or '',
|
||||||
|
is_short=is_short
|
||||||
|
)
|
||||||
|
except KeyError as e:
|
||||||
|
logger.error(f"Variable manquante dans le message de notification: {e}")
|
||||||
|
message = f"🎥 Nouvelle vidéo de {channel_name}: [{video_title}]({video_url})"
|
||||||
|
|
||||||
|
logger.info(f"Envoi de notification YouTube: {message}")
|
||||||
|
bot.loop.create_task(_sendMessage(notification, message, video_url, thumbnail, video_title, channel_name, video_id, published_at, is_short))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lors de la notification: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _format_embed_text(text: str, channel_name: str, video_title: str, video_url: str, video_id: str, thumbnail: str, published_at: str, is_short: bool) -> str:
|
||||||
|
"""Formate un texte d'embed avec les variables disponibles"""
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return text.format(
|
||||||
|
channel_name=channel_name or 'Inconnu',
|
||||||
|
video_title=video_title or 'Sans titre',
|
||||||
|
video_url=video_url,
|
||||||
|
video_id=video_id,
|
||||||
|
thumbnail=thumbnail or '',
|
||||||
|
published_at=published_at or '',
|
||||||
|
is_short=is_short
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
async def _sendMessage(notification: YouTubeNotification, message: str, video_url: str, thumbnail: str, video_title: str, channel_name: str, video_id: str, published_at: str, is_short: bool):
|
||||||
|
from discordbot import bot
|
||||||
|
try:
|
||||||
|
discord_channel = bot.get_channel(notification.notify_channel)
|
||||||
|
if not discord_channel:
|
||||||
|
logger.error(f"Canal Discord {notification.notify_channel} introuvable")
|
||||||
|
return
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
embed_title = _format_embed_text(notification.embed_title, channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short) if notification.embed_title else video_title
|
||||||
|
embed_description = _format_embed_text(notification.embed_description, channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short) if notification.embed_description else None
|
||||||
|
|
||||||
|
try:
|
||||||
|
embed_color = int(notification.embed_color or 'FF0000', 16)
|
||||||
|
except ValueError:
|
||||||
|
embed_color = 0xFF0000
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=embed_title,
|
||||||
|
url=video_url,
|
||||||
|
color=embed_color
|
||||||
|
)
|
||||||
|
|
||||||
|
if embed_description:
|
||||||
|
embed.description = embed_description
|
||||||
|
|
||||||
|
author_name = _format_embed_text(notification.embed_author_name, channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short) if notification.embed_author_name else channel_name
|
||||||
|
author_icon = notification.embed_author_icon if notification.embed_author_icon else "https://www.youtube.com/img/desktop/yt_1200.png"
|
||||||
|
embed.set_author(name=author_name, icon_url=author_icon)
|
||||||
|
|
||||||
|
if notification.embed_thumbnail and thumbnail:
|
||||||
|
embed.set_thumbnail(url=thumbnail)
|
||||||
|
|
||||||
|
if notification.embed_image and thumbnail:
|
||||||
|
embed.set_image(url=thumbnail)
|
||||||
|
|
||||||
|
if notification.embed_footer:
|
||||||
|
footer_text = _format_embed_text(notification.embed_footer, channel_name, video_title, video_url, video_id, thumbnail, published_at, is_short)
|
||||||
|
if footer_text:
|
||||||
|
embed.set_footer(text=footer_text)
|
||||||
|
|
||||||
|
if message and message.strip():
|
||||||
|
await discord_channel.send(message, embed=embed)
|
||||||
|
else:
|
||||||
|
await discord_channel.send(embed=embed)
|
||||||
|
logger.info(f"Notification YouTube envoyée avec succès")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lors de l'envoi du message Discord: {e}")
|
||||||
@@ -14,8 +14,11 @@ USER_SCOPE = [AuthScope.CHAT_READ, AuthScope.CHAT_EDIT]
|
|||||||
|
|
||||||
async def _onReady(ready_event: EventData):
|
async def _onReady(ready_event: EventData):
|
||||||
logging.info('Bot Twitch prêt')
|
logging.info('Bot Twitch prêt')
|
||||||
|
channel = ConfigurationHelper().getValue('twitch_channel')
|
||||||
|
webapp.config["BOT_STATUS"]["twitch_connected"] = True
|
||||||
|
webapp.config["BOT_STATUS"]["twitch_channel_name"] = channel
|
||||||
with webapp.app_context():
|
with webapp.app_context():
|
||||||
await ready_event.chat.join_room(ConfigurationHelper().getValue('twitch_channel'))
|
await ready_event.chat.join_room(channel)
|
||||||
asyncio.get_event_loop().create_task(twitchBot._checkOnlineStreamers())
|
asyncio.get_event_loop().create_task(twitchBot._checkOnlineStreamers())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,12 @@ from flask import Flask
|
|||||||
|
|
||||||
webapp = Flask(__name__)
|
webapp = Flask(__name__)
|
||||||
|
|
||||||
from webapp import commandes, configurations, index, humeurs, protondb, live_alert, twitch_auth, moderation
|
# État des bots (mis à jour par les bots, lu par le panneau)
|
||||||
|
webapp.config["BOT_STATUS"] = {
|
||||||
|
"discord_connected": False,
|
||||||
|
"discord_guild_count": 0,
|
||||||
|
"twitch_connected": False,
|
||||||
|
"twitch_channel_name": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
from webapp import commandes, configurations, index, humeurs, protondb, live_alert, twitch_auth, moderation, youtube
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
from flask import render_template
|
from flask import render_template
|
||||||
from webapp import webapp
|
from webapp import webapp
|
||||||
|
from database.models import ModerationEvent
|
||||||
|
|
||||||
@webapp.route("/")
|
@webapp.route("/")
|
||||||
def index():
|
def index():
|
||||||
return render_template("index.html")
|
status = webapp.config["BOT_STATUS"]
|
||||||
|
sanctions_count = ModerationEvent.query.count()
|
||||||
|
return render_template(
|
||||||
|
"index.html",
|
||||||
|
discord_connected=status["discord_connected"],
|
||||||
|
discord_guild_count=status["discord_guild_count"],
|
||||||
|
sanctions_count=sanctions_count,
|
||||||
|
twitch_connected=status["twitch_connected"],
|
||||||
|
twitch_channel_name=status["twitch_channel_name"],
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,16 +3,58 @@ from webapp import webapp
|
|||||||
from database import db
|
from database import db
|
||||||
from database.models import ModerationEvent
|
from database.models import ModerationEvent
|
||||||
|
|
||||||
|
def _top_sanctioned():
|
||||||
|
return (
|
||||||
|
db.session.query(
|
||||||
|
ModerationEvent.discord_id,
|
||||||
|
db.func.max(ModerationEvent.username).label("username"),
|
||||||
|
db.func.count(ModerationEvent.id).label("count"),
|
||||||
|
)
|
||||||
|
.group_by(ModerationEvent.discord_id)
|
||||||
|
.order_by(db.func.count(ModerationEvent.id).desc())
|
||||||
|
.limit(3)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _top_moderators():
|
||||||
|
return (
|
||||||
|
db.session.query(
|
||||||
|
ModerationEvent.staff_id,
|
||||||
|
db.func.max(ModerationEvent.staff_name).label("staff_name"),
|
||||||
|
db.func.count(ModerationEvent.id).label("count"),
|
||||||
|
)
|
||||||
|
.group_by(ModerationEvent.staff_id)
|
||||||
|
.order_by(db.func.count(ModerationEvent.id).desc())
|
||||||
|
.limit(3)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
@webapp.route("/moderation")
|
@webapp.route("/moderation")
|
||||||
def moderation():
|
def moderation():
|
||||||
events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all()
|
events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all()
|
||||||
return render_template("moderation.html", events=events, event=None)
|
top_sanctioned = _top_sanctioned()
|
||||||
|
top_moderators = _top_moderators()
|
||||||
|
return render_template(
|
||||||
|
"moderation.html",
|
||||||
|
events=events,
|
||||||
|
event=None,
|
||||||
|
top_sanctioned=top_sanctioned,
|
||||||
|
top_moderators=top_moderators,
|
||||||
|
)
|
||||||
|
|
||||||
@webapp.route("/moderation/edit/<int:event_id>")
|
@webapp.route("/moderation/edit/<int:event_id>")
|
||||||
def open_edit_moderation_event(event_id):
|
def open_edit_moderation_event(event_id):
|
||||||
event = ModerationEvent.query.get_or_404(event_id)
|
event = ModerationEvent.query.get_or_404(event_id)
|
||||||
events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all()
|
events = ModerationEvent.query.order_by(ModerationEvent.created_at.desc()).all()
|
||||||
return render_template("moderation.html", events=events, event=event)
|
top_sanctioned = _top_sanctioned()
|
||||||
|
top_moderators = _top_moderators()
|
||||||
|
return render_template(
|
||||||
|
"moderation.html",
|
||||||
|
events=events,
|
||||||
|
event=event,
|
||||||
|
top_sanctioned=top_sanctioned,
|
||||||
|
top_moderators=top_moderators,
|
||||||
|
)
|
||||||
|
|
||||||
@webapp.route("/moderation/update/<int:event_id>", methods=['POST'])
|
@webapp.route("/moderation/update/<int:event_id>", methods=['POST'])
|
||||||
def update_moderation_event(event_id):
|
def update_moderation_event(event_id):
|
||||||
|
|||||||
@@ -10,84 +10,64 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-10">
|
{# Zone Discord #}
|
||||||
<a href="/live-alert" class="group bg-white dark:bg-slate-800 rounded-lg p-5 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md transition-all">
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden mb-8">
|
||||||
<div class="flex items-center gap-4">
|
<div class="p-4 sm:p-6 border-b border-slate-200 dark:border-slate-700 flex flex-wrap items-center justify-between gap-4">
|
||||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
|
<div class="flex items-center gap-3">
|
||||||
<svg class="w-5 h-5 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
<span class="inline-flex items-center justify-center w-3 h-3 rounded-full {% if discord_connected %}bg-emerald-500 ring-4 ring-emerald-500/30{% else %}bg-slate-400 ring-4 ring-slate-400/30{% endif %}" title="{% if discord_connected %}Bot Discord connecté{% else %}Bot Discord déconnecté{% endif %}"></span>
|
||||||
</div>
|
<h2 class="text-xl font-semibold text-slate-800 dark:text-white">Discord</h2>
|
||||||
<div>
|
|
||||||
<h3 class="font-medium text-slate-800 dark:text-white">Alertes Live</h3>
|
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">Notifications Twitch</p>
|
|
||||||
</div>
|
|
||||||
<svg class="w-5 h-5 text-slate-400 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
<span class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
{% if discord_connected %}Connecté{% else %}Déconnecté{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 sm:p-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
|
||||||
|
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Serveurs connectés</p>
|
||||||
|
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">{{ discord_guild_count }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
|
||||||
|
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Sanctions enregistrées</p>
|
||||||
|
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">{{ sanctions_count }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600 sm:col-span-2 lg:col-span-2 flex items-center justify-center">
|
||||||
|
<div class="flex flex-wrap gap-3 justify-center">
|
||||||
|
<a href="/live-alert" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Alertes Live</a>
|
||||||
|
<a href="/youtube" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Notification YouTube</a>
|
||||||
|
<a href="/humeurs" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Humeurs</a>
|
||||||
|
<a href="/protondb" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">ProtonDB</a>
|
||||||
|
<a href="/commandes" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Commandes</a>
|
||||||
|
<a href="/moderation" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Modération</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a href="/commandes" class="group bg-white dark:bg-slate-800 rounded-lg p-5 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md transition-all">
|
{# Zone Twitch #}
|
||||||
<div class="flex items-center gap-4">
|
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden mb-8">
|
||||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
|
<div class="p-4 sm:p-6 border-b border-slate-200 dark:border-slate-700 flex flex-wrap items-center justify-between gap-4">
|
||||||
<svg class="w-5 h-5 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
<div class="flex items-center gap-3">
|
||||||
</div>
|
<span class="inline-flex items-center justify-center w-3 h-3 rounded-full {% if twitch_connected %}bg-emerald-500 ring-4 ring-emerald-500/30{% else %}bg-slate-400 ring-4 ring-slate-400/30{% endif %}" title="{% if twitch_connected %}Bot Twitch connecté{% else %}Bot Twitch déconnecté{% endif %}"></span>
|
||||||
<div>
|
<h2 class="text-xl font-semibold text-slate-800 dark:text-white">Twitch</h2>
|
||||||
<h3 class="font-medium text-slate-800 dark:text-white">Commandes</h3>
|
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">Commandes personnalisées</p>
|
|
||||||
</div>
|
|
||||||
<svg class="w-5 h-5 text-slate-400 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
<span class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
{% if twitch_connected %}Connecté{% else %}Déconnecté{% endif %}
|
||||||
<a href="/humeurs" class="group bg-white dark:bg-slate-800 rounded-lg p-5 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md transition-all">
|
</span>
|
||||||
<div class="flex items-center gap-4">
|
</div>
|
||||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
|
<div class="p-4 sm:p-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<svg class="w-5 h-5 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
|
||||||
</div>
|
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Canal connecté</p>
|
||||||
<div>
|
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">{% if twitch_channel_name %}{{ twitch_channel_name }}{% else %}—{% endif %}</p>
|
||||||
<h3 class="font-medium text-slate-800 dark:text-white">Humeurs</h3>
|
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">Statuts Discord rotatifs</p>
|
|
||||||
</div>
|
|
||||||
<svg class="w-5 h-5 text-slate-400 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
|
||||||
|
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Sanctions</p>
|
||||||
<a href="/moderation" class="group bg-white dark:bg-slate-800 rounded-lg p-5 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md transition-all">
|
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">—</p>
|
||||||
<div class="flex items-center gap-4">
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">À venir</p>
|
||||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
|
|
||||||
<svg class="w-5 h-5 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-medium text-slate-800 dark:text-white">Modération</h3>
|
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">Historique et actions</p>
|
|
||||||
</div>
|
|
||||||
<svg class="w-5 h-5 text-slate-400 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600 sm:col-span-2 lg:col-span-2 flex items-center justify-center">
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">Intégrations du bot Twitch à venir.</p>
|
||||||
<a href="/protondb" class="group bg-white dark:bg-slate-800 rounded-lg p-5 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md transition-all">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
|
|
||||||
<svg class="w-5 h-5 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"></path></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-medium text-slate-800 dark:text-white">ProtonDB</h3>
|
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">Compatibilité Linux</p>
|
|
||||||
</div>
|
|
||||||
<svg class="w-5 h-5 text-slate-400 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
|
|
||||||
<a href="/configurations" class="group bg-white dark:bg-slate-800 rounded-lg p-5 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md transition-all">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
|
|
||||||
<svg class="w-5 h-5 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-medium text-slate-800 dark:text-white">Configurations</h3>
|
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">Paramètres du bot</p>
|
|
||||||
</div>
|
|
||||||
<svg class="w-5 h-5 text-slate-400 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||||
@@ -98,7 +78,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3 class="font-medium text-slate-800 dark:text-white mb-2">À propos</h3>
|
<h3 class="font-medium text-slate-800 dark:text-white mb-2">À propos</h3>
|
||||||
<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">
|
<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">
|
||||||
Mamie Henriette est un bot open source pour Discord et Twitch, développé par la communauté.
|
Mamie Henriette est un bot open source pour Discord et Twitch, développé par la communauté.
|
||||||
Cette interface vous permet de configurer et gérer toutes les fonctionnalités.
|
Cette interface vous permet de configurer et gérer toutes les fonctionnalités.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="flex flex-wrap gap-3">
|
||||||
|
|||||||
@@ -8,6 +8,53 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||||
|
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||||
|
<h2 class="text-lg font-medium text-slate-800 dark:text-white">Top 3 sanctions</h2>
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Utilisateurs les plus sanctionnés</p>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||||
|
{% for row in top_sanctioned %}
|
||||||
|
<div class="px-5 py-3 flex items-center justify-between gap-3">
|
||||||
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
|
<span class="flex-shrink-0 w-7 h-7 rounded-full bg-slate-200 dark:bg-slate-600 flex items-center justify-center text-sm font-bold text-slate-700 dark:text-slate-300">{{ loop.index }}</span>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<span class="block text-sm font-medium text-slate-800 dark:text-white truncate">{{ row.username or '—' }}</span>
|
||||||
|
<span class="block text-xs text-slate-500 dark:text-slate-400 font-mono truncate">{{ row.discord_id }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="flex-shrink-0 text-sm font-semibold text-slate-600 dark:text-slate-300">{{ row.count }} sanction{{ 's' if row.count > 1 else '' }}</span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="px-5 py-6 text-center text-sm text-slate-500 dark:text-slate-400">Aucune sanction enregistrée</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||||
|
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||||
|
<h2 class="text-lg font-medium text-slate-800 dark:text-white">Top 3 modérateurs</h2>
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Staff ayant effectué le plus d'actions</p>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||||
|
{% for row in top_moderators %}
|
||||||
|
<div class="px-5 py-3 flex items-center justify-between gap-3">
|
||||||
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
|
<span class="flex-shrink-0 w-7 h-7 rounded-full bg-slate-200 dark:bg-slate-600 flex items-center justify-center text-sm font-bold text-slate-700 dark:text-slate-300">{{ loop.index }}</span>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<span class="block text-sm font-medium text-slate-800 dark:text-white truncate">{{ row.staff_name or '—' }}</span>
|
||||||
|
<span class="block text-xs text-slate-500 dark:text-slate-400 font-mono truncate">{{ row.staff_id }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="flex-shrink-0 text-sm font-semibold text-slate-600 dark:text-slate-300">{{ row.count }} action{{ 's' if row.count > 1 else '' }}</span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="px-5 py-6 text-center text-sm text-slate-500 dark:text-slate-400">Aucune action enregistrée</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
|
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
|
||||||
<details class="group">
|
<details class="group">
|
||||||
<summary class="flex items-center justify-between px-5 py-4 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors">
|
<summary class="flex items-center justify-between px-5 py-4 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors">
|
||||||
|
|||||||
@@ -77,41 +77,68 @@
|
|||||||
|
|
||||||
<!-- Navigation Desktop -->
|
<!-- Navigation Desktop -->
|
||||||
<div class="hidden md:flex items-center gap-1">
|
<div class="hidden md:flex items-center gap-1">
|
||||||
<a href="/live-alert" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all">
|
<!-- Discord (sous-menus) -->
|
||||||
<span class="flex items-center gap-2">
|
<div class="relative group">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
<button type="button" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all flex items-center gap-1.5">
|
||||||
Alerte live
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
|
||||||
</span>
|
Discord
|
||||||
</a>
|
<svg class="w-4 h-4 transition-transform group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||||
<a href="/commandes" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all">
|
</button>
|
||||||
<span class="flex items-center gap-2">
|
<div class="absolute left-0 top-full pt-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 min-w-[200px]">
|
||||||
Commandes
|
<a href="/humeurs" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||||
</span>
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||||
</a>
|
Humeur
|
||||||
<a href="/humeurs" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all">
|
</a>
|
||||||
<span class="flex items-center gap-2">
|
<a href="/live-alert" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
||||||
Humeurs
|
Notification Twitch
|
||||||
</span>
|
</a>
|
||||||
</a>
|
<a href="/youtube" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||||
<a href="/moderation" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all">
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
|
||||||
<span class="flex items-center gap-2">
|
Notification YouTube
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
|
</a>
|
||||||
Modération
|
<a href="/protondb" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||||
</span>
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"></path></svg>
|
||||||
</a>
|
ProtonDB
|
||||||
<a href="/protondb" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all">
|
</a>
|
||||||
<span class="flex items-center gap-2">
|
<div class="border-t border-gray-200 dark:border-gray-700 my-1"></div>
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"></path></svg>
|
<a href="/commandes" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||||
ProtonDB
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
||||||
</span>
|
Commandes
|
||||||
</a>
|
</a>
|
||||||
<a href="/configurations" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all">
|
<a href="/moderation" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||||
<span class="flex items-center gap-2">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
Modération
|
||||||
Configurations
|
</a>
|
||||||
</span>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Twitch (futur bot) -->
|
||||||
|
<div class="relative group">
|
||||||
|
<button type="button" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all flex items-center gap-1.5">
|
||||||
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M11.571 4.714h1.715v5.143H11.57l-.002-5.143zm3.43 0H16.714v5.143H15V4.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0H6zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714v9.429z"/></svg>
|
||||||
|
Twitch
|
||||||
|
<svg class="w-4 h-4 transition-transform group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||||
|
</button>
|
||||||
|
<div class="absolute left-0 top-full pt-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 min-w-[200px]">
|
||||||
|
<a href="/live-alert" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
||||||
|
Alerte live
|
||||||
|
</a>
|
||||||
|
<span class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-400 dark:text-gray-500 italic">
|
||||||
|
Bot Twitch — à venir
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configuration locale -->
|
||||||
|
<a href="/configurations" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||||
|
Configuration
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -138,6 +165,10 @@
|
|||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
||||||
Alerte live
|
Alerte live
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/youtube" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
||||||
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
|
||||||
|
YouTube
|
||||||
|
</a>
|
||||||
<a href="/commandes" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
<a href="/commandes" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
||||||
Commandes
|
Commandes
|
||||||
|
|||||||
245
webapp/templates/youtube.html
Normal file
245
webapp/templates/youtube.html
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
{% extends "template.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Notifications YouTube</h1>
|
||||||
|
|
||||||
|
{% if msg %}
|
||||||
|
<div id="alert-msg" class="alert alert-{{ msg_type }}" style="padding: 10px; margin: 10px 0; border: 1px solid {{ '#f00' if msg_type == 'error' else '#0f0' }}; background-color: {{ '#ffe0e0' if msg_type == 'error' else '#e0ffe0' }};">
|
||||||
|
{{ msg }}
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
setTimeout(function() {
|
||||||
|
var el = document.getElementById('alert-msg');
|
||||||
|
if (el) el.style.display = 'none';
|
||||||
|
}, 5000);
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Liste des chaînes YouTube surveillées pour les notifications de nouvelles vidéos.
|
||||||
|
|
||||||
|
Le bot vérifie toutes les 5 minutes les nouvelles vidéos des chaînes en dessous.
|
||||||
|
Quand une nouvelle vidéo est détectée, le bot enverra une notification sur Discord.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if not notification %}
|
||||||
|
<h2>Notifications</h2>
|
||||||
|
<table class="live-alert">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Chaîne YouTube</th>
|
||||||
|
<th>Canal Discord</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Message</th>
|
||||||
|
<th>#</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for notification in notifications %}
|
||||||
|
<tr>
|
||||||
|
<td>{{notification.channel_id}}</td>
|
||||||
|
<td>{{notification.notify_channel_name}}</td>
|
||||||
|
<td>
|
||||||
|
{% if notification.video_type == 'all' %}
|
||||||
|
Toutes
|
||||||
|
{% elif notification.video_type == 'video' %}
|
||||||
|
Vidéos uniquement
|
||||||
|
{% elif notification.video_type == 'short' %}
|
||||||
|
Shorts uniquement
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{notification.message}}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('toggleYouTube', id = notification.id) }}" class="icon">{{ '✅' if notification.enable else '❌' }}</a>
|
||||||
|
<a href="{{ url_for('openEditYouTube', id = notification.id) }}" class="icon">✐</a>
|
||||||
|
<a href="{{ url_for('delYouTube', id = notification.id) }}"
|
||||||
|
onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette notification ?')" class="icon">🗑</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h2>{{ 'Editer une notification' if notification else 'Ajouter une notification YouTube' }}</h2>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
|
||||||
|
<div>
|
||||||
|
<form id="youtube-form" action="{{ url_for('submitEditYouTube', id = notification.id) if notification else url_for('addYouTube') }}" method="POST">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Configuration de base</legend>
|
||||||
|
<label for="channel_id">Lien ou ID de la chaîne YouTube</label>
|
||||||
|
<input name="channel_id" id="channel_id" type="text" maxlength="256" required="required" value="{{notification.channel_id if notification}}" placeholder="https://www.youtube.com/@513v3 ou https://www.youtube.com/channel/UC... ou UC..."/>
|
||||||
|
|
||||||
|
<label for="notify_channel">Canal de Notification Discord</label>
|
||||||
|
<select name="notify_channel" id="notify_channel">
|
||||||
|
{% for channel in channels %}
|
||||||
|
<option value="{{channel.id}}"{% if notification and notification.notify_channel == channel.id %}
|
||||||
|
selected="selected" {% endif %}>{{channel.name}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="video_type">Type de vidéo à notifier</label>
|
||||||
|
<select name="video_type" id="video_type">
|
||||||
|
<option value="all"{% if notification and notification.video_type == 'all' %} selected="selected" {% endif %}>Toutes (vidéos + shorts)</option>
|
||||||
|
<option value="video"{% if notification and notification.video_type == 'video' %} selected="selected" {% endif %}>Vidéos uniquement</option>
|
||||||
|
<option value="short"{% if notification and notification.video_type == 'short' %} selected="selected" {% endif %}>Shorts uniquement</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="message">Message (optionnel, envoyé avant l'embed)</label>
|
||||||
|
<textarea name="message" id="message" rows="3" cols="50">{{notification.message if notification}}</textarea>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Personnalisation de l'embed Discord</legend>
|
||||||
|
|
||||||
|
<label for="embed_title">Titre de l'embed</label>
|
||||||
|
<input name="embed_title" id="embed_title" type="text" maxlength="256" value="{{notification.embed_title if notification}}" placeholder="{video_title} (par défaut: titre de la vidéo)"/>
|
||||||
|
<small>Variables: {video_title}, {channel_name}, {video_url}, {video_id}</small>
|
||||||
|
|
||||||
|
<label for="embed_description">Description de l'embed</label>
|
||||||
|
<textarea name="embed_description" id="embed_description" rows="4" cols="50" placeholder="Description optionnelle de l'embed">{{notification.embed_description if notification}}</textarea>
|
||||||
|
<small>Variables: {video_title}, {channel_name}, {video_url}, {published_at}, {is_short}</small>
|
||||||
|
|
||||||
|
<label for="embed_color">Couleur de l'embed (hexadécimal)</label>
|
||||||
|
<input name="embed_color" id="embed_color" type="color" value="#{{notification.embed_color if notification else 'FF0000'}}" style="width: 100px; height: 40px;"/>
|
||||||
|
<input type="text" id="embed_color_text" value="{{notification.embed_color if notification else 'FF0000'}}" placeholder="FF0000" style="width: 100px; margin-left: 10px;" maxlength="6"/>
|
||||||
|
<small>Format: FF0000 (rouge YouTube par défaut)</small>
|
||||||
|
|
||||||
|
<label for="embed_author_name">Nom de l'auteur</label>
|
||||||
|
<input name="embed_author_name" id="embed_author_name" type="text" maxlength="256" value="{{notification.embed_author_name if notification}}" placeholder="{channel_name} (par défaut: nom de la chaîne)"/>
|
||||||
|
|
||||||
|
<label for="embed_author_icon">Icône de l'auteur (URL)</label>
|
||||||
|
<input name="embed_author_icon" id="embed_author_icon" type="text" maxlength="512" value="{{notification.embed_author_icon if notification}}" placeholder="https://www.youtube.com/img/desktop/yt_1200.png"/>
|
||||||
|
|
||||||
|
<label for="embed_footer">Pied de page</label>
|
||||||
|
<input name="embed_footer" id="embed_footer" type="text" maxlength="2048" value="{{notification.embed_footer if notification}}" placeholder="Texte optionnel en bas de l'embed"/>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="embed_thumbnail" id="embed_thumbnail" {% if not notification or notification.embed_thumbnail %}checked="checked"{% endif %}>
|
||||||
|
Afficher la miniature (thumbnail) en haut à droite
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="embed_image" id="embed_image" {% if not notification or notification.embed_image %}checked="checked"{% endif %}>
|
||||||
|
Afficher l'image principale (image de la vidéo)
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<input type="Submit" value="{{ 'Modifier' if notification else 'Ajouter' }}">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>Prévisualisation de l'embed Discord</h3>
|
||||||
|
<div id="embed-preview" style="background-color: #2f3136; border-radius: 4px; padding: 16px; font-family: 'Whitney', 'Helvetica Neue', Helvetica, Arial, sans-serif; color: #dcddde; max-width: 520px; border-left: 4px solid #FF0000;">
|
||||||
|
<div id="embed-author" style="display: flex; align-items: center; margin-bottom: 8px; font-size: 14px;">
|
||||||
|
<img id="embed-author-icon" src="https://www.youtube.com/img/desktop/yt_1200.png" style="width: 20px; height: 20px; border-radius: 50%; margin-right: 8px;" onerror="this.style.display='none'"/>
|
||||||
|
<span id="embed-author-name" style="font-weight: 600;">Nom de la chaîne</span>
|
||||||
|
</div>
|
||||||
|
<a id="embed-title" href="#" style="color: #00aff4; text-decoration: none; font-size: 16px; font-weight: 600; display: block; margin-bottom: 8px;">Titre de la vidéo</a>
|
||||||
|
<div id="embed-description" style="font-size: 14px; line-height: 1.375; margin-bottom: 8px; color: #dcddde;"></div>
|
||||||
|
<div id="embed-thumbnail-container" style="margin: 8px 0;">
|
||||||
|
<img id="embed-thumbnail" src="" style="max-width: 80px; max-height: 80px; border-radius: 4px; float: right; margin-left: 16px; display: none;"/>
|
||||||
|
</div>
|
||||||
|
<div id="embed-image-container" style="margin-top: 16px;">
|
||||||
|
<img id="embed-image" src="https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" style="max-width: 100%; border-radius: 4px; display: none;"/>
|
||||||
|
</div>
|
||||||
|
<div id="embed-footer" style="margin-top: 8px; font-size: 12px; color: #72767d;"></div>
|
||||||
|
</div>
|
||||||
|
<small style="color: #666;">Cette prévisualisation est approximative. L'apparence réelle sur Discord peut varier légèrement.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function formatText(text, vars) {
|
||||||
|
if (!text) return '';
|
||||||
|
return text.replace(/\{(\w+)\}/g, function(match, key) {
|
||||||
|
return vars[key] || match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePreview() {
|
||||||
|
const embedTitle = document.getElementById('embed_title').value || '{video_title}';
|
||||||
|
const embedDescription = document.getElementById('embed_description').value || '';
|
||||||
|
const embedColor = document.getElementById('embed_color_text').value || 'FF0000';
|
||||||
|
const embedAuthorName = document.getElementById('embed_author_name').value || '{channel_name}';
|
||||||
|
const embedAuthorIcon = document.getElementById('embed_author_icon').value || 'https://www.youtube.com/img/desktop/yt_1200.png';
|
||||||
|
const embedFooter = document.getElementById('embed_footer').value || '';
|
||||||
|
const embedThumbnail = document.getElementById('embed_thumbnail').checked;
|
||||||
|
const embedImage = document.getElementById('embed_image').checked;
|
||||||
|
|
||||||
|
const vars = {
|
||||||
|
video_title: 'Nouvelle vidéo de test',
|
||||||
|
channel_name: 'Ma Chaîne YouTube',
|
||||||
|
video_url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||||
|
video_id: 'dQw4w9WgXcQ',
|
||||||
|
thumbnail: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg',
|
||||||
|
published_at: '2026-01-25T12:00:00Z',
|
||||||
|
is_short: false
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('embed-title').textContent = formatText(embedTitle, vars);
|
||||||
|
document.getElementById('embed-title').href = vars.video_url;
|
||||||
|
document.getElementById('embed-description').textContent = formatText(embedDescription, vars);
|
||||||
|
document.getElementById('embed-author-name').textContent = formatText(embedAuthorName, vars);
|
||||||
|
document.getElementById('embed-author-icon').src = embedAuthorIcon;
|
||||||
|
document.getElementById('embed-footer').textContent = formatText(embedFooter, vars);
|
||||||
|
|
||||||
|
document.getElementById('embed-preview').style.borderLeftColor = '#' + embedColor;
|
||||||
|
|
||||||
|
if (embedThumbnail) {
|
||||||
|
document.getElementById('embed-thumbnail').src = vars.thumbnail;
|
||||||
|
document.getElementById('embed-thumbnail').style.display = 'block';
|
||||||
|
} else {
|
||||||
|
document.getElementById('embed-thumbnail').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (embedImage) {
|
||||||
|
document.getElementById('embed-image').src = vars.thumbnail;
|
||||||
|
document.getElementById('embed-image').style.display = 'block';
|
||||||
|
} else {
|
||||||
|
document.getElementById('embed-image').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('embed_color').addEventListener('input', function(e) {
|
||||||
|
document.getElementById('embed_color_text').value = e.target.value.substring(1).toUpperCase();
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('embed_color_text').addEventListener('input', function(e) {
|
||||||
|
const val = e.target.value.replace(/[^0-9A-Fa-f]/g, '').substring(0, 6);
|
||||||
|
e.target.value = val;
|
||||||
|
if (val.length === 6) {
|
||||||
|
document.getElementById('embed_color').value = '#' + val;
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const formFields = ['embed_title', 'embed_description', 'embed_author_name', 'embed_author_icon', 'embed_footer', 'embed_thumbnail', 'embed_image'];
|
||||||
|
formFields.forEach(field => {
|
||||||
|
const el = document.getElementById(field);
|
||||||
|
if (el) {
|
||||||
|
el.addEventListener('input', updatePreview);
|
||||||
|
el.addEventListener('change', updatePreview);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updatePreview();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Variables disponibles pour l'embed :</strong>
|
||||||
|
<ul>
|
||||||
|
<li><code>{channel_name}</code> : nom de la chaîne YouTube</li>
|
||||||
|
<li><code>{video_title}</code> : titre de la vidéo</li>
|
||||||
|
<li><code>{video_url}</code> : lien vers la vidéo</li>
|
||||||
|
<li><code>{video_id}</code> : ID de la vidéo</li>
|
||||||
|
<li><code>{thumbnail}</code> : URL de la miniature</li>
|
||||||
|
<li><code>{published_at}</code> : date de publication</li>
|
||||||
|
<li><code>{is_short}</code> : True si c'est un short, False sinon</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
181
webapp/youtube.py
Normal file
181
webapp/youtube.py
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import re
|
||||||
|
import requests
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from flask import render_template, request, redirect, url_for
|
||||||
|
|
||||||
|
from webapp import webapp
|
||||||
|
from database import db
|
||||||
|
from database.models import YouTubeNotification
|
||||||
|
from discordbot import bot
|
||||||
|
|
||||||
|
|
||||||
|
def extract_channel_id(channel_input: str) -> str:
|
||||||
|
"""Extrait l'ID de la chaîne YouTube depuis différents formats"""
|
||||||
|
if not channel_input:
|
||||||
|
return None
|
||||||
|
|
||||||
|
channel_input = channel_input.strip()
|
||||||
|
|
||||||
|
if channel_input.startswith('UC') and len(channel_input) == 24:
|
||||||
|
return channel_input
|
||||||
|
|
||||||
|
if '/channel/' in channel_input:
|
||||||
|
match = re.search(r'/channel/([a-zA-Z0-9_-]{24})', channel_input)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
if '/c/' in channel_input or '/user/' in channel_input:
|
||||||
|
parts = channel_input.split('/')
|
||||||
|
for i, part in enumerate(parts):
|
||||||
|
if part in ['c', 'user'] and i + 1 < len(parts):
|
||||||
|
handle = parts[i + 1].split('?')[0].split('&')[0]
|
||||||
|
channel_id = _get_channel_id_from_handle(handle)
|
||||||
|
if channel_id:
|
||||||
|
return channel_id
|
||||||
|
|
||||||
|
if '@' in channel_input:
|
||||||
|
handle = re.search(r'@([a-zA-Z0-9_-]+)', channel_input)
|
||||||
|
if handle:
|
||||||
|
channel_id = _get_channel_id_from_handle(handle.group(1))
|
||||||
|
if channel_id:
|
||||||
|
return channel_id
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_channel_id_from_handle(handle: str) -> str:
|
||||||
|
"""Récupère l'ID de la chaîne depuis un handle en utilisant le flux RSS"""
|
||||||
|
try:
|
||||||
|
url = f"https://www.youtube.com/@{handle}"
|
||||||
|
response = requests.get(url, timeout=10, allow_redirects=True)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
channel_id_match = re.search(r'"channelId":"([^"]{24})"', response.text)
|
||||||
|
if channel_id_match:
|
||||||
|
return channel_id_match.group(1)
|
||||||
|
|
||||||
|
canonical_match = re.search(r'<link rel="canonical" href="https://www\.youtube\.com/channel/([^"]{24})"', response.text)
|
||||||
|
if canonical_match:
|
||||||
|
return canonical_match.group(1)
|
||||||
|
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route("/youtube")
|
||||||
|
def openYouTube():
|
||||||
|
notifications: list[YouTubeNotification] = YouTubeNotification.query.all()
|
||||||
|
channels = bot.getAllTextChannel()
|
||||||
|
for notification in notifications:
|
||||||
|
for channel in channels:
|
||||||
|
if notification.notify_channel == channel.id:
|
||||||
|
notification.notify_channel_name = channel.name
|
||||||
|
msg = request.args.get('msg')
|
||||||
|
msg_type = request.args.get('type', 'info')
|
||||||
|
return render_template("youtube.html", notifications=notifications, channels=channels, msg=msg, msg_type=msg_type)
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route("/youtube/add", methods=['POST'])
|
||||||
|
def addYouTube():
|
||||||
|
channel_input = request.form.get('channel_id', '').strip()
|
||||||
|
channel_id = extract_channel_id(channel_input)
|
||||||
|
|
||||||
|
if not channel_id:
|
||||||
|
return redirect(url_for("openYouTube") + "?" + urlencode({'msg': f"Impossible d'extraire l'ID de la chaîne depuis : {channel_input}. Veuillez vérifier le lien.", 'type': 'error'}))
|
||||||
|
|
||||||
|
notify_channel_str = request.form.get('notify_channel')
|
||||||
|
if not notify_channel_str:
|
||||||
|
return redirect(url_for("openYouTube") + "?" + urlencode({'msg': "Veuillez sélectionner un canal Discord. Assurez-vous que le bot Discord est connecté.", 'type': 'error'}))
|
||||||
|
|
||||||
|
try:
|
||||||
|
notify_channel = int(notify_channel_str)
|
||||||
|
except ValueError:
|
||||||
|
return redirect(url_for("openYouTube") + "?" + urlencode({'msg': "Canal Discord invalide.", 'type': 'error'}))
|
||||||
|
|
||||||
|
embed_color = request.form.get('embed_color', 'FF0000').strip().lstrip('#')
|
||||||
|
if len(embed_color) != 6:
|
||||||
|
embed_color = 'FF0000'
|
||||||
|
|
||||||
|
notification = YouTubeNotification(
|
||||||
|
enable=True,
|
||||||
|
channel_id=channel_id,
|
||||||
|
notify_channel=notify_channel,
|
||||||
|
message=request.form.get('message'),
|
||||||
|
video_type=request.form.get('video_type', 'all'),
|
||||||
|
embed_title=request.form.get('embed_title') or None,
|
||||||
|
embed_description=request.form.get('embed_description') or None,
|
||||||
|
embed_color=embed_color,
|
||||||
|
embed_footer=request.form.get('embed_footer') or None,
|
||||||
|
embed_author_name=request.form.get('embed_author_name') or None,
|
||||||
|
embed_author_icon=request.form.get('embed_author_icon') or None,
|
||||||
|
embed_thumbnail=request.form.get('embed_thumbnail') == 'on',
|
||||||
|
embed_image=request.form.get('embed_image') == 'on'
|
||||||
|
)
|
||||||
|
db.session.add(notification)
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for("openYouTube") + "?" + urlencode({'msg': f"Notification ajoutée avec succès pour la chaîne {channel_id}", 'type': 'success'}))
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route("/youtube/toggle/<int:id>")
|
||||||
|
def toggleYouTube(id):
|
||||||
|
notification: YouTubeNotification = YouTubeNotification.query.get_or_404(id)
|
||||||
|
notification.enable = not notification.enable
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for("openYouTube"))
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route("/youtube/edit/<int:id>")
|
||||||
|
def openEditYouTube(id):
|
||||||
|
notification = YouTubeNotification.query.get_or_404(id)
|
||||||
|
channels = bot.getAllTextChannel()
|
||||||
|
msg = request.args.get('msg')
|
||||||
|
msg_type = request.args.get('type', 'info')
|
||||||
|
return render_template("youtube.html", notification=notification, channels=channels, notifications=YouTubeNotification.query.all(), msg=msg, msg_type=msg_type)
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route("/youtube/edit/<int:id>", methods=['POST'])
|
||||||
|
def submitEditYouTube(id):
|
||||||
|
notification: YouTubeNotification = YouTubeNotification.query.get_or_404(id)
|
||||||
|
|
||||||
|
channel_input = request.form.get('channel_id', '').strip()
|
||||||
|
channel_id = extract_channel_id(channel_input)
|
||||||
|
|
||||||
|
if not channel_id:
|
||||||
|
return redirect(url_for("openEditYouTube", id=id) + "?" + urlencode({'msg': f"Impossible d'extraire l'ID de la chaîne depuis : {channel_input}. Veuillez vérifier le lien.", 'type': 'error'}))
|
||||||
|
|
||||||
|
notify_channel_str = request.form.get('notify_channel')
|
||||||
|
if not notify_channel_str:
|
||||||
|
return redirect(url_for("openEditYouTube", id=id) + "?" + urlencode({'msg': "Veuillez sélectionner un canal Discord. Assurez-vous que le bot Discord est connecté.", 'type': 'error'}))
|
||||||
|
|
||||||
|
try:
|
||||||
|
notify_channel = int(notify_channel_str)
|
||||||
|
except ValueError:
|
||||||
|
return redirect(url_for("openEditYouTube", id=id) + "?" + urlencode({'msg': "Canal Discord invalide.", 'type': 'error'}))
|
||||||
|
|
||||||
|
embed_color = request.form.get('embed_color', 'FF0000').strip().lstrip('#')
|
||||||
|
if len(embed_color) != 6:
|
||||||
|
embed_color = 'FF0000'
|
||||||
|
|
||||||
|
notification.channel_id = channel_id
|
||||||
|
notification.notify_channel = notify_channel
|
||||||
|
notification.message = request.form.get('message')
|
||||||
|
notification.video_type = request.form.get('video_type', 'all')
|
||||||
|
notification.embed_title = request.form.get('embed_title') or None
|
||||||
|
notification.embed_description = request.form.get('embed_description') or None
|
||||||
|
notification.embed_color = embed_color
|
||||||
|
notification.embed_footer = request.form.get('embed_footer') or None
|
||||||
|
notification.embed_author_name = request.form.get('embed_author_name') or None
|
||||||
|
notification.embed_author_icon = request.form.get('embed_author_icon') or None
|
||||||
|
notification.embed_thumbnail = request.form.get('embed_thumbnail') == 'on'
|
||||||
|
notification.embed_image = request.form.get('embed_image') == 'on'
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for("openYouTube") + "?" + urlencode({'msg': "Notification modifiée avec succès", 'type': 'success'}))
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route("/youtube/del/<int:id>")
|
||||||
|
def delYouTube(id):
|
||||||
|
notification = YouTubeNotification.query.get_or_404(id)
|
||||||
|
db.session.delete(notification)
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for("openYouTube"))
|
||||||
Reference in New Issue
Block a user