From f97e613f7a714ccd105c2fc4431536d65d4353c5 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Wed, 16 Jul 2025 14:35:32 +0200 Subject: [PATCH] add user exile to Mod Tools --- CHANGELOG.md | 1 + freak/__init__.py | 2 +- freak/models.py | 36 ++++++++++++++++++++++++++-- freak/templates/guildsettings.html | 38 ++++++++++++++++++++++++++---- freak/templates/macros/nav.html | 25 ++++++++++++++++++++ freak/website/moderation.py | 23 +++++++++++++++++- 6 files changed, 117 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8924a33..6d7ac82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Added user strikes: a strike logs the content of a removed message for future use - Posts may now be deleted by author. If it has comments, comments are not spared - Moderators (and admins) have now access to mod tools + + Allowed operations: change display name, description, restriction status, and exile (guild-local ban) members - Implemented guild subscriptions - Added ✨color themes✨ - Users can now set their display name, biography and color theme in `/settings` diff --git a/freak/__init__.py b/freak/__init__.py index 7590ada..653388a 100644 --- a/freak/__init__.py +++ b/freak/__init__.py @@ -23,7 +23,7 @@ from suou.configparse import ConfigOptions, ConfigValue from .colors import color_themes, theme_classes -__version__ = '0.4.0-dev27' +__version__ = '0.4.0-dev28' APP_BASE_DIR = os.path.dirname(os.path.dirname(__file__)) diff --git a/freak/models.py b/freak/models.py index e52aa3a..9349519 100644 --- a/freak/models.py +++ b/freak/models.py @@ -8,7 +8,7 @@ from functools import partial from operator import or_ import re from threading import Lock -from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, text, \ +from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint, insert, text, \ CheckConstraint, Date, DateTime, Boolean, func, BigInteger, \ SmallInteger, select, update, Table from sqlalchemy.orm import Relationship, relationship @@ -78,7 +78,7 @@ ILLEGAL_USERNAMES = ( 'pedophile', 'lolicon', 'giphy', 'tenor', 'csam', 'cp', 'pedobear', 'lolita', 'loli', 'kkk', 'pnf', 'adl', 'cop', 'tranny', 'google', 'trustandsafety', 'safety', 'ice', ## VVVVIP - 'potus', 'realdonaldtrump', 'elonmusk', 'teddysphotos', 'mrbeast', 'jkrowling' + 'potus', 'realdonaldtrump', 'elonmusk', 'teddysphotos', 'mrbeast', 'jkrowling', 'pewdiepie' ) def username_is_legal(username: str) -> bool: @@ -308,6 +308,8 @@ class User(Base): ## END User +ModeratorInfo = namedtuple('ModeratorInfo', 'user is_owner') + class Guild(Base): __tablename__ = 'freak_topic' __table_args__ = ( @@ -340,6 +342,7 @@ class Guild(Base): return db.session.execute(select(func.count('*')).select_from(Member).where(Member.guild == self, Member.is_subscribed == True)).scalar() # utilities + owner = relationship(User, foreign_keys=owner_id) posts = relationship('Post', back_populates='guild') def has_subscriber(self, other: User) -> bool: @@ -347,6 +350,35 @@ class Guild(Base): return False return bool(db.session.execute(select(Member).where(Member.user_id == other.id, Member.guild_id == self.id, Member.is_subscribed == True)).scalar()) + def has_exiled(self, other: User) -> bool: + if other is None or not other.is_authenticated: + return False + u = db.session.execute(select(Member).where(Member.user_id == other.id, Member.guild_id == self.id)).scalar() + return u.is_banned if u else False + + def moderators(self): + if self.owner: + yield ModeratorInfo(self.owner, True) + for mem in db.session.execute(select(Member).where(Member.guild_id == self.id, Member.is_moderator == True)).scalars(): + if mem.user != self.owner and not mem.user.is_banned: + yield ModeratorInfo(mem.user, False) + + def update_member(self, u: User | Member, /, **values): + if isinstance(u, User): + m = db.session.execute(select(Member).where(Member.user_id == u.id, Member.guild_id == self.id)).scalar() + if m is None: + return db.session.execute(insert(Member).values( + guild_id = self.id, + user_id = u.id, + **values + ).returning(Member)).scalar() + else: + m = u + if len(values): + db.session.execute(update(Member).where(Member.user_id == u.id, Member.guild_id == self.id).values(**values)) + return m + + Topic = deprecated('renamed to Guild')(Guild) ## END Guild diff --git a/freak/templates/guildsettings.html b/freak/templates/guildsettings.html index cda0e85..99e9e2e 100644 --- a/freak/templates/guildsettings.html +++ b/freak/templates/guildsettings.html @@ -14,12 +14,42 @@

Community Identity

- - +
- - + +
+
+ +
+
+ +
+

Safety

+
+ +
+
+ + + + Bans (aka: exiles) are permanent and reversible.
+ Banned (exiled) users are not allowed to post or comment on {{ gu.handle() }}.
+ Reverse the ban by checking “Remove ban on given user”. +
diff --git a/freak/templates/macros/nav.html b/freak/templates/macros/nav.html index 97a8e4d..4db9f60 100644 --- a/freak/templates/macros/nav.html +++ b/freak/templates/macros/nav.html @@ -18,6 +18,31 @@ {% endif %} {{ subscribe_button(gu, gu.has_subscriber(current_user)) }} + {% if not gu.owner %} + + {% elif gu.has_exiled(current_user) %} + + {% else %} + + {% endif %} {% endif %} {% endmacro %} diff --git a/freak/website/moderation.py b/freak/website/moderation.py index c57c84f..3563ebd 100644 --- a/freak/website/moderation.py +++ b/freak/website/moderation.py @@ -2,8 +2,9 @@ from flask import Blueprint, abort, flash, render_template, request from flask_login import current_user, login_required from sqlalchemy import select +import datetime -from ..models import db, User, Guild +from ..models import Member, db, User, Guild current_user: User @@ -21,11 +22,31 @@ def guild_settings(name: str): changes = False display_name = request.form.get('display_name') description = request.form.get('description') + exile_name = request.form.get('exile_name') + exile_reverse = 'exile_reverse' in request.form + restricted = 'restricted' in request.form if description and description != gu.description: changes, gu.description = True, description.strip() if display_name and display_name != gu.display_name: changes, gu.display_name = True, display_name.strip() + if exile_name: + exile_user = db.session.execute(select(User).where(User.username == exile_name)).scalar() + if exile_user: + if exile_reverse: + mem = gu.update_member(exile_user, banned_at = None, banned_by_id = None) + if mem.banned_at == None: + flash(f'Removed ban on {exile_user.handle()}') + changes = True + else: + mem = gu.update_member(exile_user, banned_at = datetime.datetime.now(), banned_by_id = current_user.id) + if mem.banned_at != None: + flash(f'{exile_user.handle()} has been exiled') + changes = True + else: + flash(f'User \'{exile_name}\' not found, can\'t exile') + if restricted and restricted != gu.is_restricted: + changes, gu.is_restricted = True, restricted if changes: db.session.add(gu)