Fix #10: Persist bans and mutes to database

- Add Ban and Mute models to models.py
- Load persisted bans/mutes from DB on app startup in create_app()
- Persist ban to DB on mod_ban and mod_kickban
- Persist/delete mute to DB on mod_mute toggle
- Bans and mutes now survive server restarts
This commit is contained in:
3nd3r 2026-04-12 12:59:20 -05:00
parent 496701c713
commit 9570283ad8
2 changed files with 51 additions and 3 deletions

34
app.py
View File

@ -53,7 +53,7 @@ from flask import Flask, request, send_from_directory
from flask_socketio import SocketIO, emit, join_room, disconnect
from database import db, init_db
from models import User, Message, UserIgnore
from models import User, Message, UserIgnore, Ban, Mute
from config import (
SECRET_KEY, ADMIN_PASSWORD, DATABASE_URL, CORS_ORIGINS,
MAX_MSG_LEN, LOBBY, AI_FREE_LIMIT, AI_BOT_NAME,
@ -304,6 +304,15 @@ def create_app() -> Flask:
init_db(app)
_app_ref = app
# Load persisted bans and mutes from the database
with app.app_context():
for ban in Ban.query.all():
banned_usernames.add(ban.username.lower())
if ban.ip:
banned_ips.add(ban.ip)
for mute in Mute.query.all():
muted_users.add(mute.username.lower())
msg_queue = (
os.environ.get("SOCKETIO_MESSAGE_QUEUE")
or os.environ.get("REDIS_URL")
@ -659,13 +668,19 @@ def on_ban(data):
target = str(data.get("target", "")).strip()
lower = target.lower()
banned_usernames.add(lower)
ip = None
target_sid = username_to_sid.get(lower)
if target_sid:
info = connected_users.get(target_sid, {})
if info.get("ip"):
banned_ips.add(info["ip"])
ip = info["ip"]
socketio.emit("kicked", {"msg": "You have been banned."}, to=target_sid)
eventlet.spawn_after(0.5, _do_disconnect, target_sid)
# Persist to DB
if not Ban.query.filter_by(username=lower).first():
db.session.add(Ban(username=lower, ip=ip))
db.session.commit()
socketio.emit("system", {"msg": f"🔨 **{target}** was banned.", "ts": _ts()}, to=LOBBY)
@ -675,9 +690,16 @@ def on_mute(data):
target = str(data.get("target", "")).strip()
lower = target.lower()
if lower in muted_users:
muted_users.discard(lower); action = "unmuted"
muted_users.discard(lower)
Mute.query.filter_by(username=lower).delete()
db.session.commit()
action = "unmuted"
else:
muted_users.add(lower); action = "muted"
muted_users.add(lower)
if not Mute.query.filter_by(username=lower).first():
db.session.add(Mute(username=lower))
db.session.commit()
action = "muted"
emit("system", {"msg": f"🔇 **{target}** was {action}.", "ts": _ts()}, to=LOBBY)
@ -688,13 +710,19 @@ def on_kickban(data):
lower = target.lower()
# Ban
banned_usernames.add(lower)
ip = None
target_sid = username_to_sid.get(lower)
if target_sid:
info = connected_users.get(target_sid, {})
if info.get("ip"):
banned_ips.add(info["ip"])
ip = info["ip"]
socketio.emit("kicked", {"msg": "You have been banned."}, to=target_sid)
eventlet.spawn_after(0.5, _do_disconnect, target_sid)
# Persist to DB
if not Ban.query.filter_by(username=lower).first():
db.session.add(Ban(username=lower, ip=ip))
db.session.commit()
# Announce
socketio.emit("system", {"msg": f"💀 **{target}** was kickbanned.", "ts": _ts()}, to=LOBBY)

View File

@ -80,3 +80,23 @@ class Message(db.Model):
def __repr__(self):
return f"<Message {self.sender_id}{self.recipient_id} @ {self.timestamp}>"
class Ban(db.Model):
"""Persisted ban entry survives server restarts."""
__tablename__ = "bans"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), nullable=False, index=True)
ip = db.Column(db.String(45), nullable=True, index=True)
reason = db.Column(db.String(255), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
class Mute(db.Model):
"""Persisted mute entry survives server restarts."""
__tablename__ = "mutes"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False, index=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)