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)