implement post and comment restrictions, changes to views

This commit is contained in:
Yusur 2025-07-18 00:10:02 +02:00
parent a88b12e844
commit e7912ad88c
10 changed files with 53 additions and 17 deletions

View file

@ -13,6 +13,7 @@
+ Allowed operations: change display name, description, restriction status, and exile (guild-local ban) members + Allowed operations: change display name, description, restriction status, and exile (guild-local ban) members
+ Site administrators and guild owners can add moderators + Site administrators and guild owners can add moderators
- Administrators can claim ownership of abandoned guilds - Administrators can claim ownership of abandoned guilds
- Guilds can have restricted posting/commenting now. Unmoderated guilds always have.
- Implemented guild subscriptions (not as in $$$, yes as in the follow button) - Implemented guild subscriptions (not as in $$$, yes as in the follow button)
- Minimum karma requirement for creating a guild is now configurable via env variable `FREAK_CREATE_GUILD_THRESHOLD` (previously hardcoded at 15) - Minimum karma requirement for creating a guild is now configurable via env variable `FREAK_CREATE_GUILD_THRESHOLD` (previously hardcoded at 15)
- Users can now set their display name, biography and color theme in `/settings` - Users can now set their display name, biography and color theme in `/settings`

View file

@ -33,9 +33,9 @@ def username_availability(username: str):
'is_available': is_available 'is_available': is_available
} }
@bp.route('/guild_name_availability/<username>') @bp.route('/guild_name_availability/<name>')
def guild_name_availability(name: str): def guild_name_availability(name: str):
is_valid = re.fullmatch('[a-z0-9_-]+', name) is not None is_valid = username_is_legal(name)
if is_valid: if is_valid:
gd = db.session.execute(select(Guild).where(Guild.name == name)).scalar() gd = db.session.execute(select(Guild).where(Guild.name == name)).scalar()

View file

@ -42,9 +42,10 @@ post_report_reasons = [
ReportReason(180, 'impersonation', 'Impersonation'), ReportReason(180, 'impersonation', 'Impersonation'),
ReportReason(141, 'doxing', 'Diffusion of PII (personally identifiable information)'), ReportReason(141, 'doxing', 'Diffusion of PII (personally identifiable information)'),
## less urgent ## less urgent
ReportReason(140, 'bullying', 'Harassment, bullying, or suicide incitement'),
ReportReason(112, 'lgbt_hate', 'Hate speech against LGBTQ+ or women'), ReportReason(112, 'lgbt_hate', 'Hate speech against LGBTQ+ or women'),
ReportReason(140, 'bullying', 'Harassment, bullying, or suicide incitement'),
ReportReason(150, 'security_exploit', 'Dangerous security exploit or violation'), ReportReason(150, 'security_exploit', 'Dangerous security exploit or violation'),
ReportReason(160, 'spam', 'Unsolicited advertising'),
ReportReason(190, 'false_information', 'False or deceiving information'), ReportReason(190, 'false_information', 'False or deceiving information'),
ReportReason(123, 'copyviol', 'This is my creation and someone else is using it without my permission/license'), ReportReason(123, 'copyviol', 'This is my creation and someone else is using it without my permission/license'),
## minor (unironically) ## minor (unironically)
@ -168,7 +169,7 @@ class User(Base):
## XXX posts and comments relationships are temporarily disabled because they make ## XXX posts and comments relationships are temporarily disabled because they make
## SQLAlchemy fail initialization of models — bricking the app. ## SQLAlchemy fail initialization of models — bricking the app.
## Posts are queried manually anyway ## Posts are queried manually anyway
#posts = relationship("Post", back_populates='author', ) #posts = relationship("Post", primaryjoin=lambda: #back_populates='author', pr)
upvoted_posts = relationship("Post", secondary=PostUpvote, back_populates='upvoters') upvoted_posts = relationship("Post", secondary=PostUpvote, back_populates='upvoters')
#comments = relationship("Comment", back_populates='author') #comments = relationship("Comment", back_populates='author')
@ -364,8 +365,10 @@ class Guild(Base):
mem: Member | None = db.session.execute(select(Member).where(Member.user_id == other.id, Member.guild_id == self.id)).scalar() if other else None mem: Member | None = db.session.execute(select(Member).where(Member.user_id == other.id, Member.guild_id == self.id)).scalar() if other else None
if mem and mem.is_banned: if mem and mem.is_banned:
return False return False
if other.moderates(self):
return True
if self.is_restricted: if self.is_restricted:
return mem and mem.is_approved return (mem and mem.is_approved)
return True return True
@ -447,7 +450,7 @@ class Post(Base):
title = Column(String(256), nullable=False) title = Column(String(256), nullable=False)
post_type = Column(SmallInteger, server_default=text('0')) post_type = Column(SmallInteger, server_default=text('0'))
author_id = Column(BigInteger, ForeignKey('freak_user.id', name='post_author_id'), nullable=True) author_id = Column(BigInteger, ForeignKey('freak_user.id', name='post_author_id'), nullable=True)
topic_id = Column(BigInteger, ForeignKey('freak_topic.id', name='post_topic_id'), nullable=True) topic_id = Column('topic_id', BigInteger, ForeignKey('freak_topic.id', name='post_topic_id'), nullable=True)
created_at = Column(DateTime, server_default=func.current_timestamp()) created_at = Column(DateTime, server_default=func.current_timestamp())
created_ip = Column(String(64), default=get_remote_addr, nullable=False) created_ip = Column(String(64), default=get_remote_addr, nullable=False)
updated_at = Column(DateTime, nullable=True) updated_at = Column(DateTime, nullable=True)
@ -465,7 +468,7 @@ class Post(Base):
# utilities # utilities
author: Relationship[User] = relationship("User", lazy='selectin', foreign_keys=[author_id])#, back_populates="posts") author: Relationship[User] = relationship("User", lazy='selectin', foreign_keys=[author_id])#, back_populates="posts")
guild = relationship("Guild", back_populates="posts", lazy='selectin') guild: Relationship[Guild] = relationship("Guild", back_populates="posts", lazy='selectin')
comments = relationship("Comment", back_populates="parent_post") comments = relationship("Comment", back_populates="parent_post")
upvoters = relationship("User", secondary=PostUpvote, back_populates='upvoted_posts') upvoters = relationship("User", secondary=PostUpvote, back_populates='upvoted_posts')

View file

@ -27,21 +27,30 @@
usernameInputMessage.className = 'username-input-message error'; usernameInputMessage.className = 'username-input-message error';
return; return;
} }
if (value.length >= 100) {
usernameInputMessage.innerHTML = 'Your username must be shorter.';
usernameInputMessage.className = 'username-input-message error';
return;
}
if(/^[01]/.test(value)) { if(/^[01]/.test(value)) {
usernameInputMessage.innerHTML = 'Your username cannot start with 0 or 1.'; usernameInputMessage.innerHTML = 'Your username cannot start with 0 or 1.';
usernameInputMessage.className = 'username-input-message error'; usernameInputMessage.className = 'username-input-message error';
return; return;
} }
usernameInputMessage.innerHTML = 'Checking username...'; usernameInputMessage.innerHTML = 'Checking username...';
usernameInputMessage.className = 'username-input-message checking'; usernameInputMessage.className = 'username-input-message checking faint';
requestUsernameAvailability(value, endpoint).then((resp) => { requestUsernameAvailability(value, endpoint).then((resp) => {
if (['ok', void 0].indexOf(resp.status) < 0){ if (['ok', void 0].indexOf(resp.status) < 0){
usernameInputMessage.innerHTML = 'Sorry, there was an unknown error.'; usernameInputMessage.innerHTML = 'Sorry, there was an unknown error.';
usernameInputMessage.className = 'username-input-message error'; usernameInputMessage.className = 'username-input-message error';
return; return;
} }
if (resp.is_available){ if (!resp.is_legal) {
usernameInputMessage.innerHTML = "The username @" + value + " is available!"; usernameInputMessage.innerHTML = "You can't use this username.";
usernameInputMessage.className = 'username-input-message error';
return;
} else if (resp.is_available){
usernameInputMessage.innerHTML = `The username @${value} is available!`;
usernameInputMessage.className = 'username-input-message success'; usernameInputMessage.className = 'username-input-message success';
return; return;
} else { } else {

View file

@ -47,7 +47,7 @@
<!-- no user --> <!-- no user -->
{% elif current_user.is_authenticated %} {% elif current_user.is_authenticated %}
<li class="nomobile"> <li class="nomobile">
<a class="round border-accent" href="/create" title="Create a post" aria-label="Create a post"> <a class="round border-accent" href="{{ url_for('create.create', on=current_guild.name) if current_guild else '/create/' }}" title="Create a post" aria-label="Create a post">
{{ icon('add') }} {{ icon('add') }}
<span>New post</span> <span>New post</span>
</a> </a>

View file

@ -16,7 +16,7 @@
<form action="{{ url_for('create.createguild') }}" method="POST" enctype="multipart/form-data" class="boundaryless"> <form action="{{ url_for('create.createguild') }}" method="POST" enctype="multipart/form-data" class="boundaryless">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div> <div>
<p>URL of the guild: <strong>+</strong><input type="text" class="username-input" name="name" required="true" data-endpoint="guild_name_availability/$1" /></p> <p>URL of the guild: <strong>+</strong><input type="text" class="username-input" name="name" required="true" data-endpoint="/guild_name_availability/$1" /></p>
<p><small class="faint">Must be alphanumeric and unique. <strong>May not be changed later</strong>: choose wisely!</small></p> <p><small class="faint">Must be alphanumeric and unique. <strong>May not be changed later</strong>: choose wisely!</small></p>
</div> </div>
<div> <div>

View file

@ -22,9 +22,16 @@ disabled=""
</ul> </ul>
{% endmacro %} {% endmacro %}
{% macro comment_area(url) %} {% macro comment_area(p) %}
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<form id="comment-area" class="boundaryless" action="{{ url }}" method="POST" enctype="multipart/form-data"> {% if current_user.is_disabled %}
<div class="centered">Your account is suspended</div>
{% elif current_guild and not current_guild.allows_posting(current_user) %}
<div class="centered">This community allows only its members to post and comment</div>
{% elif p.is_locked %}
<div class="centered">Comments are closed</div>
{% else %}
<form id="comment-area" class="boundaryless" action="{{ p.url() }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" /> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="reply_to" value="" /> <input type="hidden" name="reply_to" value="" />
<div> <div>
@ -35,6 +42,7 @@ disabled=""
<button type="submit" class="primary">Publish</button> <button type="submit" class="primary">Publish</button>
</div> </div>
</form> </form>
{% endif %}
{% else %} {% else %}
<div class="centered"><a href="/login">Log in</a> to leave a comment</div> <div class="centered"><a href="/login">Log in</a> to leave a comment</div>
{% endif %} {% endif %}

View file

@ -66,7 +66,7 @@
</ul> </ul>
</div> </div>
{{ comment_area(p.url()) }} {{ comment_area(p) }}
<div class="comment-section"> <div class="comment-section">
<ul> <ul>
{% for comment in comments %} {% for comment in comments %}

View file

@ -40,6 +40,20 @@ def user_profile_s(username):
def single_post_post_hook(p: Post): def single_post_post_hook(p: Post):
if p.guild is not None:
gu = p.guild
if gu.has_exiled(current_user):
flash(f'You have been banned from {gu.handle()}')
return
if not gu.allows_posting(current_user):
flash(f'You can\'t post in {gu.handle()}')
return
if p.is_locked:
flash(f'You can\'t comment on locked posts')
return
if 'reply_to' in request.form: if 'reply_to' in request.form:
reply_to_id = request.form['reply_to'] reply_to_id = request.form['reply_to']
text = request.form['text'] text = request.form['text']
@ -100,7 +114,7 @@ def guild_post_detail(gname, id, slug=''):
if request.method == 'POST': if request.method == 'POST':
single_post_post_hook(post) single_post_post_hook(post)
return render_template('singlepost.html', p=post, comments=comments_of(post)) return render_template('singlepost.html', p=post, comments=comments_of(post), current_guild = post.guild)

View file

@ -41,7 +41,8 @@ def guild_feed(name):
posts = db.paginate(topic_timeline(name)) posts = db.paginate(topic_timeline(name))
return render_template( return render_template(
'feed.html', feed_type='guild', feed_title=f'{guild.display_name} (+{guild.name})', l=posts, guild=guild) 'feed.html', feed_type='guild', feed_title=f'{guild.display_name} (+{guild.name})', l=posts, guild=guild,
current_guild=guild)
@bp.route('/r/<name>/') @bp.route('/r/<name>/')
def guild_feed_r(name): def guild_feed_r(name):