File size: 9,617 Bytes
c5d8c3f d1dc645 c5d8c3f d1dc645 c5d8c3f d1dc645 c5d8c3f d1dc645 c5d8c3f d1dc645 c5d8c3f d1dc645 c5d8c3f d1dc645 c5d8c3f d1dc645 c5d8c3f d1dc645 c5d8c3f d1dc645 c5d8c3f d1dc645 c5d8c3f d1dc645 c5d8c3f |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
from flask import Flask, request, render_template, redirect, url_for, flash, jsonify, abort, make_response
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime, timedelta, timezone
from apscheduler.schedulers.background import BackgroundScheduler
import markdown
import uuid # Importation du module UUID
import random # Importation du module random
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///forum.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = 'change_this_secret_key' # À modifier pour la production
db = SQLAlchemy(app)
# -------------------------------
# Ajout d'un filtre escapejs pour Jinja2
# -------------------------------
def escapejs_filter(s):
if s is None:
return ""
return (s.replace('\\', '\\\\')
.replace("'", "\\'")
.replace('"', '\\"')
.replace('\n', '\\n')
.replace('\r', '\\r'))
app.jinja_env.filters['escapejs'] = escapejs_filter
# -------------------------------
# Modèles de données
# -------------------------------
class Thread(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
timestamp = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
messages = db.relationship('Message', backref='thread', lazy=True, cascade="all, delete-orphan")
class Message(db.Model):
id = db.Column(db.Integer, primary_key=True)
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'), nullable=False)
content = db.Column(db.Text, nullable=False)
timestamp = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
vote_count = db.Column(db.Integer, default=0)
reports = db.Column(db.Integer, default=0)
removed = db.Column(db.Boolean, default=False)
user_id = db.Column(db.String(36), nullable=False) # Ajout de la colonne user_id
# -------------------------------
# Fonction pour obtenir ou créer l'ID utilisateur
# -------------------------------
def get_user_id():
user_id = request.cookies.get('user_id')
if not user_id:
user_id = str(uuid.uuid4()) # Génère un nouvel UUID
return user_id
# -------------------------------
# Fonction pour obtenir un nom d'utilisateur aléatoire
# -------------------------------
def generate_username(user_id):
# Liste de couleurs
colors = ["Red", "Green", "Blue", "Yellow", "Purple", "Orange", "Pink", "Brown", "Gray", "Cyan"]
# Liste d'animaux
animals = ["Lion", "Tiger", "Elephant", "Panda", "Monkey", "Giraffe", "Zebra", "Wolf", "Fox", "Bear"]
# Utilisation de l'UUID pour générer des index "aléatoires" mais constants pour un même utilisateur
random.seed(user_id)
color = random.choice(colors)
animal = random.choice(animals)
random.seed() # Reinitialiser le seed pour le reste
return f"{color} {animal}"
# -------------------------------
# Routes de base et fonctionnalités
# -------------------------------
# Accueil : liste des fils de discussion
@app.route('/')
def index():
threads = Thread.query.order_by(Thread.timestamp.desc()).all()
user_id = get_user_id() # Obtenir l'ID utilisateur
username = generate_username(user_id) # Obtenir un nom d'utilisateur
resp = make_response(render_template('index.html', threads=threads, username=username)) # Créer une réponse
resp.set_cookie('user_id', user_id, max_age=60*60*24*365*2) # Définir le cookie, expire dans 2 ans
return resp
# Création d'un nouveau fil de discussion
@app.route('/new_thread', methods=['GET', 'POST'])
def new_thread():
user_id = get_user_id() # Obtenir l'ID utilisateur
username = generate_username(user_id) # Obtenir un nom d'utilisateur
if request.method == 'POST':
title = request.form.get('title', '').strip()
content = request.form.get('content', '').strip()
if not title or not content:
flash("Le titre et le contenu initial sont obligatoires.", "error")
return redirect(url_for('new_thread'))
# Création du thread
thread = Thread(title=title)
db.session.add(thread)
db.session.commit()
# Message initial (avec user_id)
message = Message(thread_id=thread.id, content=content, user_id=user_id)
db.session.add(message)
db.session.commit()
flash("Fil de discussion créé.", "success")
return redirect(url_for('thread', thread_id=thread.id))
resp = make_response(render_template('new_thread.html', username=username)) # Créer une réponse
resp.set_cookie('user_id', user_id, max_age=60*60*24*365*2) # Définir le cookie
return resp
# Visualisation d'un fil et réponse
@app.route('/thread/<int:thread_id>', methods=['GET', 'POST'])
def thread(thread_id):
thread_obj = Thread.query.get_or_404(thread_id)
user_id = get_user_id() # Obtenir l'ID utilisateur
username = generate_username(user_id) # Obtenir un nom d'utilisateur
if request.method == 'POST':
content = request.form.get('content', '').strip()
if not content:
flash("Le contenu du message ne peut pas être vide.", "error")
return redirect(url_for('thread', thread_id=thread_id))
# Création du message (avec user_id)
message = Message(thread_id=thread_id, content=content, user_id=user_id)
db.session.add(message)
db.session.commit()
flash("Réponse postée.", "success")
return redirect(url_for('thread', thread_id=thread_id))
# Afficher uniquement les messages de moins de 72h et non supprimés
expiration_threshold = datetime.now(timezone.utc) - timedelta(hours=72)
messages = Message.query.filter(
Message.thread_id == thread_id,
Message.timestamp >= expiration_threshold,
Message.removed == False
).order_by(Message.timestamp.asc()).all()
# Préparer un dictionnaire pour mapper les user_id aux noms d'utilisateur
user_id_to_username = {}
for msg in messages:
if msg.user_id not in user_id_to_username:
user_id_to_username[msg.user_id] = generate_username(msg.user_id)
resp = make_response(render_template('thread.html', thread=thread_obj, messages=messages, user_id_to_username=user_id_to_username, username=username)) # Créer une réponse avec le username
resp.set_cookie('user_id', user_id, max_age=60*60*24*365*2) # Définir le cookie
return resp
# Système de vote (upvote/downvote)
@app.route('/vote/<int:message_id>/<action>', methods=['POST'])
def vote(message_id, action):
message = Message.query.get_or_404(message_id)
if action == 'up':
message.vote_count += 1
elif action == 'down':
message.vote_count -= 1
else:
abort(400)
db.session.commit()
flash("Vote enregistré.", "success")
return redirect(request.referrer or url_for('index'))
# Signalement d'un message
@app.route('/report/<int:message_id>', methods=['POST'])
def report(message_id):
message = Message.query.get_or_404(message_id)
message.reports += 1
db.session.commit()
flash("Message signalé.", "success")
return redirect(request.referrer or url_for('index'))
# Prévisualisation d'un message en Markdown
@app.route('/preview', methods=['POST'])
def preview():
content = request.form.get('content', '')
rendered = markdown.markdown(content)
return jsonify({'preview': rendered})
# Recherche dans les threads et messages
@app.route('/search')
def search():
query = request.args.get('q', '').strip()
if not query:
flash("Veuillez entrer un terme de recherche.", "error")
return redirect(url_for('index'))
threads = Thread.query.filter(Thread.title.ilike(f'%{query}%')).all()
# Modification pour inclure l'ID de l'utilisateur dans la recherche des messages
messages = Message.query.filter(Message.content.ilike(f'%{query}%')).all()
return render_template('search.html', query=query, threads=threads, messages=messages)
# Page de modération : affichage des messages signalés
@app.route('/moderate')
def moderate():
reported_messages = Message.query.filter(
Message.reports >= 3, Message.removed == False
).order_by(Message.reports.desc()).all()
return render_template('moderate.html', messages=reported_messages)
# Action de modération : retirer un message
@app.route('/remove/<int:message_id>', methods=['POST'])
def remove_message(message_id):
message = Message.query.get_or_404(message_id)
message.removed = True
db.session.commit()
flash("Message retiré.", "success")
return redirect(url_for('moderate'))
# -------------------------------
# Suppression automatique des messages de plus de 72 heures
# -------------------------------
def delete_old_messages():
with app.app_context():
expiration_threshold = datetime.now(timezone.utc) - timedelta(hours=72)
old_messages = Message.query.filter(Message.timestamp < expiration_threshold).all()
count = len(old_messages)
for msg in old_messages:
db.session.delete(msg)
db.session.commit()
if count:
print(f"{count} messages supprimés définitivement.")
scheduler = BackgroundScheduler(daemon=True)
scheduler.add_job(func=delete_old_messages, trigger="interval", hours=1)
scheduler.start()
# -------------------------------
# Lancement de l'application
# -------------------------------
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True) |