mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
communities that are local-only, w access control for posting and voting
This commit is contained in:
parent
520db4a924
commit
74cc2d17c0
13 changed files with 202 additions and 63 deletions
|
@ -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,10 +406,11 @@ 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 not in_reply_to:
|
if can_create(user, community):
|
||||||
post = create_post(activity_log, community, request_json['object'], user, announce_id=request_json['id'])
|
if not in_reply_to:
|
||||||
else:
|
post = create_post(activity_log, community, request_json['object'], user, announce_id=request_json['id'])
|
||||||
post = create_post_reply(activity_log, community, in_reply_to, request_json['object'], user, announce_id=request_json['id'])
|
else:
|
||||||
|
post = create_post_reply(activity_log, community, in_reply_to, request_json['object'], user, announce_id=request_json['id'])
|
||||||
else:
|
else:
|
||||||
activity_log.exception_message = 'Unacceptable type: ' + object_type
|
activity_log.exception_message = 'Unacceptable type: ' + object_type
|
||||||
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,11 +449,8 @@ 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'
|
activity_log.result = 'ignored'
|
||||||
if user and user.is_local():
|
|
||||||
activity_log.exception_message = 'Activity about local content which is already present'
|
|
||||||
activity_log.result = 'ignored'
|
|
||||||
|
|
||||||
elif request_json['object']['type'] == 'Dislike':
|
elif request_json['object']['type'] == 'Dislike':
|
||||||
activity_log.activity_type = request_json['object']['type']
|
activity_log.activity_type = request_json['object']['type']
|
||||||
|
@ -453,28 +460,29 @@ 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)):
|
||||||
downvote_post(disliked, user)
|
if isinstance(disliked, Post):
|
||||||
activity_log.result = 'success'
|
downvote_post(disliked, user)
|
||||||
elif disliked is not None and isinstance(disliked, PostReply):
|
elif 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'
|
activity_log.result = 'ignored'
|
||||||
if user and user.is_local():
|
|
||||||
activity_log.exception_message = 'Activity about local content which is already present'
|
|
||||||
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']
|
||||||
user_ap_id = request_json['object']['actor']
|
user_ap_id = request_json['object']['actor']
|
||||||
|
@ -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:
|
||||||
|
|
|
@ -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:]}",
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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) }}
|
||||||
|
|
|
@ -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) %}
|
||||||
hx-post="/post/{{ post.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
<div class="upvote_button digits_{{ digits(post.up_votes) }} {{ upvoted_class }}"
|
||||||
<span class="fe fe-arrow-up"></span>
|
hx-post="/post/{{ post.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||||
{{ post.up_votes }}
|
<span class="fe fe-arrow-up"></span>
|
||||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
{{ post.up_votes }}
|
||||||
</div>
|
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||||
<div class="downvote_button digits_{{ digits(post.down_votes) }} {{ downvoted_class }}"
|
</div>
|
||||||
hx-post="/post/{{ post.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
{% endif %}
|
||||||
<span class="fe fe-arrow-down"></span>
|
{% if can_downvote(current_user, post) %}
|
||||||
{{ post.down_votes }}
|
<div class="downvote_button digits_{{ digits(post.down_votes) }} {{ downvoted_class }}"
|
||||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
hx-post="/post/{{ post.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||||
</div>
|
<span class="fe fe-arrow-down"></span>
|
||||||
|
{{ post.down_votes }}
|
||||||
|
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||||
|
</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>
|
||||||
|
|
|
@ -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) %}
|
||||||
hx-post="/comment/{{ comment.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
<div class="upvote_button digits_{{ digits(comment.up_votes) }} {{ upvoted_class }}"
|
||||||
<span class="fe fe-arrow-up"></span>
|
hx-post="/comment/{{ comment.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||||
{{ comment.up_votes }}
|
<span class="fe fe-arrow-up"></span>
|
||||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
{{ comment.up_votes }}
|
||||||
</div>
|
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||||
<div class="downvote_button digits_{{ digits(comment.down_votes) }} {{ downvoted_class }}"
|
</div>
|
||||||
hx-post="/comment/{{ comment.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
{% endif %}
|
||||||
<span class="fe fe-arrow-down"></span>
|
{% if can_downvote(current_user, comment) %}
|
||||||
{{ comment.down_votes }}
|
<div class="downvote_button digits_{{ digits(comment.down_votes) }} {{ downvoted_class }}"
|
||||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
hx-post="/comment/{{ comment.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||||
</div>
|
<span class="fe fe-arrow-down"></span>
|
||||||
|
{{ comment.down_votes }}
|
||||||
|
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||||
|
</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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
68
app/utils.py
68
app/utils.py
|
@ -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
|
||||||
|
|
32
migrations/versions/328c9990e53e_local_only_communities.py
Normal file
32
migrations/versions/328c9990e53e_local_only_communities.py
Normal 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 ###
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue