add user exile to Mod Tools

This commit is contained in:
Yusur 2025-07-16 14:35:32 +02:00
parent 2214863496
commit f97e613f7a
6 changed files with 117 additions and 8 deletions

View file

@ -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`

View file

@ -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__))

View file

@ -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

View file

@ -14,12 +14,42 @@
<section class="card">
<h2>Community Identity</h2>
<div>
<label for="GS__display_name">Display name:</label>
<input type="text" name="display_name" id="GS__display_name" value="{{ gu.display_name or '' }}" />
<label>Display name:
<input type="text" name="display_name" value="{{ gu.display_name or '' }}" />
</label>
</div>
<div>
<label for="GS__description">Description:</label>
<textarea name="description" id="GS__description">{{ gu.description or '' }}</textarea>
<label>Description:
<textarea name="description">{{ gu.description or '' }}</textarea>
</label>
</div>
<div>
<button type="submit" class="primary">Save</button>
</div>
</section>
<section class="card">
<h2>Safety</h2>
<div>
<label>
<input type="checkbox" name="restricted" value="1" {{ checked_if(gu.is_restricted) }} />
Allow only approved members to post and comment
</label>
</div>
<div>
<label>
Ban user from participating in {{ gu.handle() }}:
<input type="text" name="exile_name" placeholder="username" />
</label>
<label>
<input type="checkbox" name="exile_reverse" value="1" />
Remove ban on given user
</label>
<small class="faint">
Bans (aka: exiles) are permanent and reversible.<br />
Banned (exiled) users are not allowed to post or comment on {{ gu.handle() }}.<br />
Reverse the ban by checking “Remove ban on given user”.
</small>
</div>
<div>
<button type="submit" class="primary">Save</button>

View file

@ -18,6 +18,31 @@
<a href="{{ gu.url() }}/settings"><button class="card">{{ icon('settings') }} Mod Tools</button></a>
{% endif %}
{{ subscribe_button(gu, gu.has_subscriber(current_user)) }}
{% if not gu.owner %}
<aside class="card">
<p class="centered">{{ gu.handle() }} is currently unmoderated</p>
</aside>
{% elif gu.has_exiled(current_user) %}
<aside class="card">
<p class="centered">Moderator list is hidden because you are banned.</p>
<!-- TODO appeal button -->
</aside>
{% else %}
<aside class="card">
<h3>Moderators of {{ gu.handle() }}</h3>
<div>
<ul>
{% for moder in gu.moderators() %}
<li><a href="{{ moder.user.url() }}">{{ moder.user.handle() }}</a>
{% if moder.is_owner %}
<span>{{ icon('mod_mode') }} <small>Owner</small></span>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
</aside>
{% endif %}
{% endif %}
{% endmacro %}

View file

@ -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)