communities that are local-only, w access control for posting and voting

This commit is contained in:
rimu 2024-01-02 19:41:00 +13:00
parent 520db4a924
commit 74cc2d17c0
13 changed files with 202 additions and 63 deletions

View file

@ -18,7 +18,8 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti
user_removed_from_remote_server, create_post, create_post_reply, update_post_reply_from_activity, \ user_removed_from_remote_server, create_post, create_post_reply, update_post_reply_from_activity, \
update_post_from_activity, undo_vote, undo_downvote update_post_from_activity, undo_vote, undo_downvote
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \ from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \
domain_from_url, markdown_to_html, community_membership, ap_datetime, markdown_to_text, ip_address domain_from_url, markdown_to_html, community_membership, ap_datetime, markdown_to_text, ip_address, can_downvote, \
can_upvote, can_create
import werkzeug.exceptions import werkzeug.exceptions
@ -247,7 +248,9 @@ def community_profile(actor):
"moderators": f"https://{server}/c/{actor}/moderators", "moderators": f"https://{server}/c/{actor}/moderators",
"featured": f"https://{server}/c/{actor}/featured", "featured": f"https://{server}/c/{actor}/featured",
"attributedTo": f"https://{server}/c/{actor}/moderators", "attributedTo": f"https://{server}/c/{actor}/moderators",
"postingRestrictedToMods": community.restricted_to_mods, "postingRestrictedToMods": community.restricted_to_mods or community.local_only,
"newModsWanted": community.new_mods_wanted,
"privateMods": community.private_mods,
"url": f"https://{server}/c/{actor}", "url": f"https://{server}/c/{actor}",
"publicKey": { "publicKey": {
"id": f"https://{server}/c/{actor}#main-key", "id": f"https://{server}/c/{actor}#main-key",
@ -403,6 +406,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
if object_type in new_content_types: # create a new post if object_type in new_content_types: # create a new post
in_reply_to = request_json['object']['object']['inReplyTo'] if 'inReplyTo' in \ in_reply_to = request_json['object']['object']['inReplyTo'] if 'inReplyTo' in \
request_json['object']['object'] else None request_json['object']['object'] else None
if can_create(user, community):
if not in_reply_to: if not in_reply_to:
post = create_post(activity_log, community, request_json['object'], user, announce_id=request_json['id']) post = create_post(activity_log, community, request_json['object'], user, announce_id=request_json['id'])
else: else:
@ -421,8 +425,14 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
user_ap_id = request_json['object']['actor'] user_ap_id = request_json['object']['actor']
liked_ap_id = request_json['object']['object'] liked_ap_id = request_json['object']['object']
user = find_actor_or_create(user_ap_id) user = find_actor_or_create(user_ap_id)
if user and not user.is_local():
liked = find_liked_object(liked_ap_id) liked = find_liked_object(liked_ap_id)
if user is None:
activity_log.exception_message = 'Blocked or unfound user'
elif user.is_local():
activity_log.exception_message = 'Activity about local content which is already present'
activity_log.result = 'ignored'
elif can_upvote(user, liked):
# insert into voted table # insert into voted table
if liked is None: if liked is None:
activity_log.exception_message = 'Liked object not found' activity_log.exception_message = 'Liked object not found'
@ -439,10 +449,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
# todo: recalculate 'hotness' of liked post/reply # todo: recalculate 'hotness' of liked post/reply
# todo: if vote was on content in local community, federate the vote out to followers # todo: if vote was on content in local community, federate the vote out to followers
else: else:
if user is None: activity_log.exception_message = 'Cannot upvote this'
activity_log.exception_message = 'Blocked or unfound user'
if user and user.is_local():
activity_log.exception_message = 'Activity about local content which is already present'
activity_log.result = 'ignored' activity_log.result = 'ignored'
elif request_json['object']['type'] == 'Dislike': elif request_json['object']['type'] == 'Dislike':
@ -453,27 +460,28 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
user_ap_id = request_json['object']['actor'] user_ap_id = request_json['object']['actor']
liked_ap_id = request_json['object']['object'] liked_ap_id = request_json['object']['object']
user = find_actor_or_create(user_ap_id) user = find_actor_or_create(user_ap_id)
if user and not user.is_local():
disliked = find_liked_object(liked_ap_id) disliked = find_liked_object(liked_ap_id)
if user is None:
activity_log.exception_message = 'Blocked or unfound user'
elif user.is_local():
activity_log.exception_message = 'Activity about local content which is already present'
activity_log.result = 'ignored'
elif can_downvote(user, disliked, site):
# insert into voted table # insert into voted table
if disliked is None: if disliked is None:
activity_log.exception_message = 'Liked object not found' activity_log.exception_message = 'Liked object not found'
elif disliked is not None and isinstance(disliked, Post): elif isinstance(disliked, (Post, PostReply)):
if isinstance(disliked, Post):
downvote_post(disliked, user) downvote_post(disliked, user)
activity_log.result = 'success' elif isinstance(disliked, PostReply):
elif disliked is not None and isinstance(disliked, PostReply):
downvote_post_reply(disliked, user) downvote_post_reply(disliked, user)
activity_log.result = 'success' activity_log.result = 'success'
# todo: recalculate 'hotness' of liked post/reply
# todo: if vote was on content in the local community, federate the vote out to followers
else: else:
activity_log.exception_message = 'Could not detect type of like' activity_log.exception_message = 'Could not detect type of like'
if activity_log.result == 'success':
... # todo: recalculate 'hotness' of liked post/reply
# todo: if vote was on content in local community, federate the vote out to followers
else: else:
if user is None: activity_log.exception_message = 'Cannot downvote this'
activity_log.exception_message = 'Blocked or unfound user'
if user and user.is_local():
activity_log.exception_message = 'Activity about local content which is already present'
activity_log.result = 'ignored' activity_log.result = 'ignored'
elif request_json['object']['type'] == 'Delete': elif request_json['object']['type'] == 'Delete':
activity_log.activity_type = request_json['object']['type'] activity_log.activity_type = request_json['object']['type']
@ -634,12 +642,14 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
comment = PostReply.query.filter_by(ap_id=target_ap_id).first() comment = PostReply.query.filter_by(ap_id=target_ap_id).first()
if '/post/' in target_ap_id: if '/post/' in target_ap_id:
post = Post.query.filter_by(ap_id=target_ap_id).first() post = Post.query.filter_by(ap_id=target_ap_id).first()
if (user and not user.is_local()) and post: if (user and not user.is_local()) and post and can_upvote(user, post):
upvote_post(post, user) upvote_post(post, user)
activity_log.result = 'success' activity_log.result = 'success'
elif (user and not user.is_local()) and comment: elif (user and not user.is_local()) and comment and can_upvote(user, comment):
upvote_post_reply(comment, user) upvote_post_reply(comment, user)
activity_log.result = 'success' activity_log.result = 'success'
else:
activity_log.exception_message = 'Could not find user or content for vote'
elif request_json['type'] == 'Dislike': # Downvote elif request_json['type'] == 'Dislike': # Downvote
if get_setting('allow_dislike', True) is False: if get_setting('allow_dislike', True) is False:
@ -655,10 +665,10 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
comment = PostReply.query.filter_by(ap_id=target_ap_id).first() comment = PostReply.query.filter_by(ap_id=target_ap_id).first()
if '/post/' in target_ap_id: if '/post/' in target_ap_id:
post = Post.query.filter_by(ap_id=target_ap_id).first() post = Post.query.filter_by(ap_id=target_ap_id).first()
if (user and not user.is_local()) and comment: if (user and not user.is_local()) and comment and can_downvote(user, comment, site):
downvote_post_reply(comment, user) downvote_post_reply(comment, user)
activity_log.result = 'success' activity_log.result = 'success'
elif (user and not user.is_local()) and post: elif (user and not user.is_local()) and post and can_downvote(user, post, site):
downvote_post(post, user) downvote_post(post, user)
activity_log.result = 'success' activity_log.result = 'success'
else: else:

View file

@ -371,6 +371,8 @@ def actor_json_to_model(activity_json, address, server):
rules_html=markdown_to_html(activity_json['rules'] if 'rules' in activity_json else ''), rules_html=markdown_to_html(activity_json['rules'] if 'rules' in activity_json else ''),
nsfw=activity_json['sensitive'], nsfw=activity_json['sensitive'],
restricted_to_mods=activity_json['postingRestrictedToMods'], restricted_to_mods=activity_json['postingRestrictedToMods'],
new_mods_wanted=activity_json['newModsWanted'] if 'newModsWanted' in activity_json else False,
private_mods=activity_json['privateMods'] if 'privateMods' in activity_json else False,
created_at=activity_json['published'] if 'published' in activity_json else utcnow(), created_at=activity_json['published'] if 'published' in activity_json else utcnow(),
last_active=activity_json['updated'] if 'updated' in activity_json else utcnow(), last_active=activity_json['updated'] if 'updated' in activity_json else utcnow(),
ap_id=f"{address[1:]}", ap_id=f"{address[1:]}",

View file

@ -47,6 +47,9 @@ class EditCommunityForm(FlaskForm):
banner_file = FileField(_('Banner image')) banner_file = FileField(_('Banner image'))
rules = TextAreaField(_l('Rules')) rules = TextAreaField(_l('Rules'))
nsfw = BooleanField('Porn community') nsfw = BooleanField('Porn community')
local_only = BooleanField('Only accept posts from current instance')
restricted_to_mods = BooleanField('Only moderators can post')
new_mods_wanted = BooleanField('New moderators wanted')
show_home = BooleanField('Posts show on home page') show_home = BooleanField('Posts show on home page')
show_popular = BooleanField('Posts can be popular') show_popular = BooleanField('Posts can be popular')
show_all = BooleanField('Posts show in All list') show_all = BooleanField('Posts show in All list')

View file

@ -194,6 +194,9 @@ def admin_community_edit(community_id):
community.description = form.description.data community.description = form.description.data
community.rules = form.rules.data community.rules = form.rules.data
community.nsfw = form.nsfw.data community.nsfw = form.nsfw.data
community.local_only = form.local_only.data
community.restricted_to_mods = form.restricted_to_mods.data
community.new_mods_wanted = form.new_mods_wanted.data
community.show_home = form.show_home.data community.show_home = form.show_home.data
community.show_popular = form.show_popular.data community.show_popular = form.show_popular.data
community.show_all = form.show_all.data community.show_all = form.show_all.data
@ -224,6 +227,9 @@ def admin_community_edit(community_id):
form.description.data = community.description form.description.data = community.description
form.rules.data = community.rules form.rules.data = community.rules
form.nsfw.data = community.nsfw form.nsfw.data = community.nsfw
form.local_only.data = community.local_only
form.new_mods_wanted.data = community.new_mods_wanted
form.restricted_to_mods.data = community.restricted_to_mods
form.show_home.data = community.show_home form.show_home.data = community.show_home
form.show_popular.data = community.show_popular form.show_popular.data = community.show_popular
form.show_all.data = community.show_all form.show_all.data = community.show_all

View file

@ -17,7 +17,7 @@ from app.models import User, Community, CommunityMember, CommunityJoinRequest, C
from app.community import bp from app.community import bp
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \ from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, community_membership, ap_datetime, \ shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, community_membership, ap_datetime, \
request_etag_matches, return_304, instance_banned request_etag_matches, return_304, instance_banned, can_create
from feedgen.feed import FeedGenerator from feedgen.feed import FeedGenerator
from datetime import timezone from datetime import timezone
@ -313,8 +313,13 @@ def add_post(actor):
form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()] form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
if not can_create(current_user, community):
abort(401)
if form.validate_on_submit(): if form.validate_on_submit():
community = Community.query.get_or_404(form.communities.data) community = Community.query.get_or_404(form.communities.data)
if not can_create(current_user, community):
abort(401)
post = Post(user_id=current_user.id, community_id=form.communities.data, instance_id=1) post = Post(user_id=current_user.id, community_id=form.communities.data, instance_id=1)
save_post(form, post) save_post(form, post)
community.post_count += 1 community.post_count += 1

View file

@ -105,6 +105,7 @@ class Community(db.Model):
banned = db.Column(db.Boolean, default=False) banned = db.Column(db.Boolean, default=False)
restricted_to_mods = db.Column(db.Boolean, default=False) restricted_to_mods = db.Column(db.Boolean, default=False)
local_only = db.Column(db.Boolean, default=False) # only users on this instance can post
new_mods_wanted = db.Column(db.Boolean, default=False) new_mods_wanted = db.Column(db.Boolean, default=False)
searchable = db.Column(db.Boolean, default=True) searchable = db.Column(db.Boolean, default=True)
private_mods = db.Column(db.Boolean, default=False) private_mods = db.Column(db.Boolean, default=False)
@ -116,8 +117,8 @@ class Community(db.Model):
search_vector = db.Column(TSVectorType('name', 'title', 'description', 'rules')) search_vector = db.Column(TSVectorType('name', 'title', 'description', 'rules'))
posts = db.relationship('Post', backref='community', lazy='dynamic', cascade="all, delete-orphan") posts = db.relationship('Post', lazy='dynamic', cascade="all, delete-orphan")
replies = db.relationship('PostReply', backref='community', lazy='dynamic', cascade="all, delete-orphan") replies = db.relationship('PostReply', lazy='dynamic', cascade="all, delete-orphan")
icon = db.relationship('File', foreign_keys=[icon_id], single_parent=True, backref='community', cascade="all, delete-orphan") icon = db.relationship('File', foreign_keys=[icon_id], single_parent=True, backref='community', cascade="all, delete-orphan")
image = db.relationship('File', foreign_keys=[image_id], single_parent=True, cascade="all, delete-orphan") image = db.relationship('File', foreign_keys=[image_id], single_parent=True, cascade="all, delete-orphan")
@ -574,6 +575,7 @@ class Post(db.Model):
image = db.relationship(File, lazy='joined', foreign_keys=[image_id], cascade="all, delete") image = db.relationship(File, lazy='joined', foreign_keys=[image_id], cascade="all, delete")
domain = db.relationship('Domain', lazy='joined', foreign_keys=[domain_id]) domain = db.relationship('Domain', lazy='joined', foreign_keys=[domain_id])
author = db.relationship('User', lazy='joined', overlaps='posts', foreign_keys=[user_id]) author = db.relationship('User', lazy='joined', overlaps='posts', foreign_keys=[user_id])
community = db.relationship('Community', lazy='joined', overlaps='posts', foreign_keys=[community_id])
replies = db.relationship('PostReply', lazy='dynamic', backref='post') replies = db.relationship('PostReply', lazy='dynamic', backref='post')
def is_local(self): def is_local(self):
@ -647,6 +649,7 @@ class PostReply(db.Model):
search_vector = db.Column(TSVectorType('body')) search_vector = db.Column(TSVectorType('body'))
author = db.relationship('User', lazy='joined', foreign_keys=[user_id], single_parent=True, overlaps="post_replies") author = db.relationship('User', lazy='joined', foreign_keys=[user_id], single_parent=True, overlaps="post_replies")
community = db.relationship('Community', lazy='joined', overlaps='replies', foreign_keys=[community_id])
def is_local(self): def is_local(self):
return self.ap_id is None or self.ap_id.startswith('https://' + current_app.config['SERVER_NAME']) return self.ap_id is None or self.ap_id.startswith('https://' + current_app.config['SERVER_NAME'])

View file

@ -33,10 +33,13 @@
<small class="field_hint">Provide a wide image - letterbox orientation.</small> <small class="field_hint">Provide a wide image - letterbox orientation.</small>
{{ render_field(form.rules) }} {{ render_field(form.rules) }}
{{ render_field(form.nsfw) }} {{ render_field(form.nsfw) }}
{{ render_field(form.restricted_to_mods) }}
{% if not community.is_local() %} {% if not community.is_local() %}
<fieldset class="border pl-2 pt-2 mb-4"> <fieldset class="border pl-2 pt-2 mb-4">
<legend>{{ _('Will not be overwritten by remote server') }}</legend> <legend>{{ _('Will not be overwritten by remote server') }}</legend>
{% endif %} {% endif %}
{{ render_field(form.local_only) }}
{{ render_field(form.new_mods_wanted) }}
{{ render_field(form.show_home) }} {{ render_field(form.show_home) }}
{{ render_field(form.show_popular) }} {{ render_field(form.show_popular) }}
{{ render_field(form.show_all) }} {{ render_field(form.show_all) }}

View file

@ -1,16 +1,20 @@
{% if current_user.is_authenticated and current_user.verified %} {% if current_user.is_authenticated and current_user.verified %}
<div class="upvote_button digits_{{ digits(post.up_votes) }} {{ upvoted_class }}" {% if can_upvote(current_user, post) %}
<div class="upvote_button digits_{{ digits(post.up_votes) }} {{ upvoted_class }}"
hx-post="/post/{{ post.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons"> hx-post="/post/{{ post.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
<span class="fe fe-arrow-up"></span> <span class="fe fe-arrow-up"></span>
{{ post.up_votes }} {{ post.up_votes }}
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;"> <img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
</div> </div>
<div class="downvote_button digits_{{ digits(post.down_votes) }} {{ downvoted_class }}" {% endif %}
{% if can_downvote(current_user, post) %}
<div class="downvote_button digits_{{ digits(post.down_votes) }} {{ downvoted_class }}"
hx-post="/post/{{ post.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons"> hx-post="/post/{{ post.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
<span class="fe fe-arrow-down"></span> <span class="fe fe-arrow-down"></span>
{{ post.down_votes }} {{ post.down_votes }}
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;"> <img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
</div> </div>
{% endif %}
{% else %} {% else %}
<div class="upvote_button digits_{{ digits(post.up_votes) }} {{ upvoted_class }}"> <div class="upvote_button digits_{{ digits(post.up_votes) }} {{ upvoted_class }}">
<span class="fe fe-arrow-up"></span> <span class="fe fe-arrow-up"></span>

View file

@ -1,16 +1,20 @@
{% if current_user.is_authenticated and current_user.verified %} {% if current_user.is_authenticated and current_user.verified %}
<div class="upvote_button digits_{{ digits(comment.up_votes) }} {{ upvoted_class }}" {% if can_upvote(current_user, comment) %}
<div class="upvote_button digits_{{ digits(comment.up_votes) }} {{ upvoted_class }}"
hx-post="/comment/{{ comment.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons"> hx-post="/comment/{{ comment.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
<span class="fe fe-arrow-up"></span> <span class="fe fe-arrow-up"></span>
{{ comment.up_votes }} {{ comment.up_votes }}
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;"> <img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
</div> </div>
<div class="downvote_button digits_{{ digits(comment.down_votes) }} {{ downvoted_class }}" {% endif %}
{% if can_downvote(current_user, comment) %}
<div class="downvote_button digits_{{ digits(comment.down_votes) }} {{ downvoted_class }}"
hx-post="/comment/{{ comment.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons"> hx-post="/comment/{{ comment.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
<span class="fe fe-arrow-down"></span> <span class="fe fe-arrow-down"></span>
{{ comment.down_votes }} {{ comment.down_votes }}
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;"> <img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
</div> </div>
{% endif %}
{% else %} {% else %}
<div class="upvote_button digits_{{ digits(comment.up_votes) }} {{ upvoted_class }}"> <div class="upvote_button digits_{{ digits(comment.up_votes) }} {{ upvoted_class }}">
<span class="fe fe-arrow-up"></span> <span class="fe fe-arrow-up"></span>

View file

@ -10,6 +10,7 @@
<div class="card-body"> <div class="card-body">
<p>{{ _('If you wish to de-escalate the discussion on your post and now feel like it was a mistake, click the button below.') }}</p> <p>{{ _('If you wish to de-escalate the discussion on your post and now feel like it was a mistake, click the button below.') }}</p>
<p>{{ _('No further comments will be posted and a message saying you made a mistake in this post will be displayed.') }}</p> <p>{{ _('No further comments will be posted and a message saying you made a mistake in this post will be displayed.') }}</p>
<!-- <p>{{ _('The effect of downvotes on your reputation score will be removed.') }}</p> -->
<p><a href="https://nickpunt.com/blog/deescalating-social-media/" target="_blank">More about this</a></p> <p><a href="https://nickpunt.com/blog/deescalating-social-media/" target="_blank">More about this</a></p>
{{ render_form(form) }} {{ render_form(form) }}
</div> </div>

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import random import random
from datetime import datetime from datetime import datetime
from typing import List, Literal from typing import List, Literal, Union
import markdown2 import markdown2
import math import math
@ -14,14 +14,15 @@ from bs4 import BeautifulSoup
import requests import requests
import os import os
import imghdr import imghdr
from flask import current_app, json, redirect, url_for, request, make_response, Response from flask import current_app, json, redirect, url_for, request, make_response, Response, g
from flask_login import current_user from flask_login import current_user
from sqlalchemy import text from sqlalchemy import text
from wtforms.fields import SelectField, SelectMultipleField from wtforms.fields import SelectField, SelectMultipleField
from wtforms.widgets import Select, html_params, ListWidget, CheckboxInput from wtforms.widgets import Select, html_params, ListWidget, CheckboxInput
from app import db, cache from app import db, cache
from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan, \
Site, Post, PostReply
# Flask's render_template function, with support for themes added # Flask's render_template function, with support for themes added
@ -374,3 +375,64 @@ def user_cookie_banned() -> bool:
def banned_ip_addresses() -> List[str]: def banned_ip_addresses() -> List[str]:
ips = IpBan.query.all() ips = IpBan.query.all()
return [ip.ip_address for ip in ips] return [ip.ip_address for ip in ips]
def can_downvote(user, content: Union[Post, PostReply], site=None) -> bool:
if user is None or content is None or user.banned:
return False
if site is None:
try:
site = g.site
except:
site = Site.query.get(1)
if not site.enable_downvotes and content.community.is_local():
return False
if content.community.is_moderator(user) or user.is_admin():
return True
if content.community.local_only and not user.is_local():
return False
return True
def can_upvote(user, content: Union[Post, PostReply]) -> bool:
if user is None or content is None or user.banned:
return False
if content.community.is_moderator(user) or user.is_admin():
return True
return True
def can_create(user, content: Union[Community, Post, PostReply]) -> bool:
if user is None or content is None or user.banned:
return False
if isinstance(content, Community):
if content.is_moderator(user) or user.is_admin():
return True
if content.restricted_to_mods:
return False
if content.local_only and not user.is_local():
return False
else:
if content.community.is_moderator(user) or user.is_admin():
return True
if content.community.restricted_to_mods and isinstance(content, Post):
return False
if content.community.local_only and not user.is_local():
return False
if isinstance(content, PostReply) and content.post.comments_enabled is False:
return False
return True

View file

@ -0,0 +1,32 @@
"""local only communities
Revision ID: 328c9990e53e
Revises: b18ea0b841fe
Create Date: 2024-01-02 16:10:04.201183
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '328c9990e53e'
down_revision = 'b18ea0b841fe'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('community', schema=None) as batch_op:
batch_op.add_column(sa.Column('local_only', sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('community', schema=None) as batch_op:
batch_op.drop_column('local_only')
# ### end Alembic commands ###

View file

@ -7,7 +7,8 @@ import os, click
from flask import session, g, json from flask import session, g, json
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE
from app.models import Site from app.models import Site
from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access, community_membership from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access, community_membership, \
can_create, can_upvote, can_downvote
app = create_app() app = create_app()
cli.register(app) cli.register(app)
@ -33,6 +34,9 @@ with app.app_context():
app.jinja_env.globals['community_membership'] = community_membership app.jinja_env.globals['community_membership'] = community_membership
app.jinja_env.globals['json_loads'] = json.loads app.jinja_env.globals['json_loads'] = json.loads
app.jinja_env.globals['user_access'] = user_access app.jinja_env.globals['user_access'] = user_access
app.jinja_env.globals['can_create'] = can_create
app.jinja_env.globals['can_upvote'] = can_upvote
app.jinja_env.globals['can_downvote'] = can_downvote
app.jinja_env.filters['shorten'] = shorten_string app.jinja_env.filters['shorten'] = shorten_string
app.jinja_env.filters['shorten_url'] = shorten_url app.jinja_env.filters['shorten_url'] = shorten_url