mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-24 03:43:42 -08:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
373eade750
75 changed files with 13520 additions and 2493 deletions
|
@ -16,3 +16,4 @@ To build a federated discussion and link aggregation platform, similar to Reddit
|
|||
- [Screencast: overview of the PieFed codebase](https://join.piefed.social/2024/01/22/an-introduction-to-the-piefed-codebase/)
|
||||
- [Database / entity relationship diagram](https://join.piefed.social/wp-content/uploads/2024/02/PieFed-entity-relationships.png)
|
||||
- see [INSTALL.md](INSTALL.md)
|
||||
- see docs/project_management/* for a project roadmap, contributing guide and much more.
|
||||
|
|
|
@ -20,7 +20,8 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti
|
|||
lemmy_site_data, instance_weight, is_activitypub_request, downvote_post_reply, downvote_post, upvote_post_reply, \
|
||||
upvote_post, delete_post_or_comment, community_members, \
|
||||
user_removed_from_remote_server, create_post, create_post_reply, update_post_reply_from_activity, \
|
||||
update_post_from_activity, undo_vote, undo_downvote, post_to_page, get_redis_connection
|
||||
update_post_from_activity, undo_vote, undo_downvote, post_to_page, get_redis_connection, find_reported_object, \
|
||||
process_report
|
||||
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, render_template, \
|
||||
domain_from_url, markdown_to_html, community_membership, ap_datetime, ip_address, can_downvote, \
|
||||
can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply, sha256_digest, \
|
||||
|
@ -373,6 +374,7 @@ def shared_inbox():
|
|||
|
||||
redis_client.set(request_json['id'], 1, ex=90) # Save the activity ID into redis, to avoid duplicate activities that Lemmy sometimes sends
|
||||
activity_log.activity_id = request_json['id']
|
||||
g.site = Site.query.get(1) # g.site is not initialized by @app.before_request when request.path == '/inbox'
|
||||
if g.site.log_activitypub_json:
|
||||
activity_log.activity_json = json.dumps(request_json)
|
||||
activity_log.result = 'processing'
|
||||
|
@ -990,7 +992,19 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
else:
|
||||
activity_log.exception_message = 'Cannot downvote this'
|
||||
activity_log.result = 'ignored'
|
||||
# Flush the caches of any major object that was created. To be sure.
|
||||
elif request_json['type'] == 'Flag': # Reported content
|
||||
activity_log.activity_type = 'Report'
|
||||
user_ap_id = request_json['actor']
|
||||
user = find_actor_or_create(user_ap_id)
|
||||
target_ap_id = request_json['object']
|
||||
reported = find_reported_object(target_ap_id)
|
||||
if user and reported:
|
||||
process_report(user, reported, request_json, activity_log)
|
||||
activity_log.result = 'success'
|
||||
else:
|
||||
activity_log.exception_message = 'Report ignored due to missing user or content'
|
||||
|
||||
# Flush the caches of any major object that was created. To be sure.
|
||||
if 'user' in vars() and user is not None:
|
||||
user.flush_cache()
|
||||
if user.instance_id and user.instance_id != 1:
|
||||
|
@ -1053,6 +1067,9 @@ def process_delete_request(request_json, activitypublog_id, ip_address):
|
|||
|
||||
|
||||
def announce_activity_to_followers(community, creator, activity):
|
||||
# remove context from what will be inner object
|
||||
del activity["@context"]
|
||||
|
||||
announce_activity = {
|
||||
'@context': default_context(),
|
||||
"actor": community.profile_id(),
|
||||
|
|
|
@ -12,7 +12,7 @@ from flask_babel import _
|
|||
from sqlalchemy import text, func
|
||||
from app import db, cache, constants, celery
|
||||
from app.models import User, Post, Community, BannedInstances, File, PostReply, AllowedInstances, Instance, utcnow, \
|
||||
PostVote, PostReplyVote, ActivityPubLog, Notification, Site, CommunityMember, InstanceRole
|
||||
PostVote, PostReplyVote, ActivityPubLog, Notification, Site, CommunityMember, InstanceRole, Report, Conversation
|
||||
import time
|
||||
import base64
|
||||
import requests
|
||||
|
@ -223,6 +223,8 @@ def banned_user_agents():
|
|||
|
||||
@cache.memoize(150)
|
||||
def instance_blocked(host: str) -> bool: # see also utils.instance_banned()
|
||||
if host is None or host == '':
|
||||
return True
|
||||
host = host.lower()
|
||||
if 'https://' in host or 'http://' in host:
|
||||
host = urlparse(host).hostname
|
||||
|
@ -232,6 +234,8 @@ def instance_blocked(host: str) -> bool: # see also utils.instance_banned
|
|||
|
||||
@cache.memoize(150)
|
||||
def instance_allowed(host: str) -> bool:
|
||||
if host is None or host == '':
|
||||
return True
|
||||
host = host.lower()
|
||||
if 'https://' in host or 'http://' in host:
|
||||
host = urlparse(host).hostname
|
||||
|
@ -561,11 +565,11 @@ def actor_json_to_model(activity_json, address, server):
|
|||
current_app.logger.error(f'KeyError for {address}@{server} while parsing ' + str(activity_json))
|
||||
return None
|
||||
|
||||
if 'icon' in activity_json:
|
||||
if 'icon' in activity_json and activity_json['icon'] is not None and 'url' in activity_json['icon']:
|
||||
avatar = File(source_url=activity_json['icon']['url'])
|
||||
user.avatar = avatar
|
||||
db.session.add(avatar)
|
||||
if 'image' in activity_json:
|
||||
if 'image' in activity_json and activity_json['image'] is not None and 'url' in activity_json['image']:
|
||||
cover = File(source_url=activity_json['image']['url'])
|
||||
user.cover = cover
|
||||
db.session.add(cover)
|
||||
|
@ -625,11 +629,11 @@ def actor_json_to_model(activity_json, address, server):
|
|||
elif 'content' in activity_json:
|
||||
community.description_html = allowlist_html(activity_json['content'])
|
||||
community.description = ''
|
||||
if 'icon' in activity_json:
|
||||
if 'icon' in activity_json and activity_json['icon'] is not None and 'url' in activity_json['icon']:
|
||||
icon = File(source_url=activity_json['icon']['url'])
|
||||
community.icon = icon
|
||||
db.session.add(icon)
|
||||
if 'image' in activity_json:
|
||||
if 'image' in activity_json and activity_json['image'] is not None and 'url' in activity_json['image']:
|
||||
image = File(source_url=activity_json['image']['url'])
|
||||
community.image = image
|
||||
db.session.add(image)
|
||||
|
@ -702,12 +706,12 @@ def post_json_to_model(activity_log, post_json, user, community) -> Post:
|
|||
if not domain.banned:
|
||||
domain.post_count += 1
|
||||
post.domain = domain
|
||||
if 'image' in post_json and post.image is None:
|
||||
image = File(source_url=post_json['image']['url'])
|
||||
db.session.add(image)
|
||||
post.image = image
|
||||
|
||||
if post is not None:
|
||||
if 'image' in post_json and post.image is None:
|
||||
image = File(source_url=post_json['image']['url'])
|
||||
db.session.add(image)
|
||||
post.image = image
|
||||
db.session.add(post)
|
||||
community.post_count += 1
|
||||
activity_log.result = 'success'
|
||||
|
@ -793,18 +797,19 @@ def make_image_sizes_async(file_id, thumbnail_width, medium_width, directory):
|
|||
db.session.commit()
|
||||
|
||||
# Alert regarding fascist meme content
|
||||
try:
|
||||
image_text = pytesseract.image_to_string(Image.open(BytesIO(source_image)).convert('L'), timeout=30)
|
||||
except FileNotFoundError as e:
|
||||
image_text = ''
|
||||
if 'Anonymous' in image_text and ('No.' in image_text or ' N0' in image_text): # chan posts usually contain the text 'Anonymous' and ' No.12345'
|
||||
post = Post.query.filter_by(image_id=file.id).first()
|
||||
notification = Notification(title='Review this',
|
||||
user_id=1,
|
||||
author_id=post.user_id,
|
||||
url=url_for('activitypub.post_ap', post_id=post.id))
|
||||
db.session.add(notification)
|
||||
db.session.commit()
|
||||
if img_width < 2000: # images > 2000px tend to be real photos instead of 4chan screenshots.
|
||||
try:
|
||||
image_text = pytesseract.image_to_string(Image.open(BytesIO(source_image)).convert('L'), timeout=30)
|
||||
except FileNotFoundError as e:
|
||||
image_text = ''
|
||||
if 'Anonymous' in image_text and ('No.' in image_text or ' N0' in image_text): # chan posts usually contain the text 'Anonymous' and ' No.12345'
|
||||
post = Post.query.filter_by(image_id=file.id).first()
|
||||
notification = Notification(title='Review this',
|
||||
user_id=1,
|
||||
author_id=post.user_id,
|
||||
url=url_for('activitypub.post_ap', post_id=post.id))
|
||||
db.session.add(notification)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# create a summary from markdown if present, otherwise use html if available
|
||||
|
@ -895,6 +900,21 @@ def find_liked_object(ap_id) -> Union[Post, PostReply, None]:
|
|||
return None
|
||||
|
||||
|
||||
def find_reported_object(ap_id) -> Union[User, Post, PostReply, None]:
|
||||
post = Post.get_by_ap_id(ap_id)
|
||||
if post:
|
||||
return post
|
||||
else:
|
||||
post_reply = PostReply.get_by_ap_id(ap_id)
|
||||
if post_reply:
|
||||
return post_reply
|
||||
else:
|
||||
user = find_actor_or_create(ap_id, create_if_not_found=False)
|
||||
if user:
|
||||
return user
|
||||
return None
|
||||
|
||||
|
||||
def find_instance_id(server):
|
||||
server = server.strip()
|
||||
instance = Instance.query.filter_by(domain=server).first()
|
||||
|
@ -1017,7 +1037,13 @@ def downvote_post(post, user):
|
|||
if not existing_vote:
|
||||
effect = -1.0
|
||||
post.down_votes += 1
|
||||
post.score -= 1.0
|
||||
# Make 'hot' sort more spicy by amplifying the effect of early downvotes
|
||||
if post.up_votes + post.down_votes <= 30:
|
||||
post.score -= 5.0
|
||||
elif post.up_votes + post.down_votes <= 60:
|
||||
post.score -= 2.0
|
||||
else:
|
||||
post.score -= 1.0
|
||||
vote = PostVote(user_id=user.id, post_id=post.id, author_id=post.author.id,
|
||||
effect=effect)
|
||||
post.author.reputation += effect
|
||||
|
@ -1119,10 +1145,18 @@ def upvote_post(post, user):
|
|||
user.last_seen = utcnow()
|
||||
user.recalculate_attitude()
|
||||
effect = instance_weight(user.ap_domain)
|
||||
# Make 'hot' sort more spicy by amplifying the effect of early upvotes
|
||||
spicy_effect = effect
|
||||
if post.up_votes + post.down_votes <= 10:
|
||||
spicy_effect = effect * 10
|
||||
elif post.up_votes + post.down_votes <= 30:
|
||||
spicy_effect = effect * 5
|
||||
elif post.up_votes + post.down_votes <= 60:
|
||||
spicy_effect = effect * 2
|
||||
existing_vote = PostVote.query.filter_by(user_id=user.id, post_id=post.id).first()
|
||||
if not existing_vote:
|
||||
post.up_votes += 1
|
||||
post.score += effect
|
||||
post.score += spicy_effect
|
||||
vote = PostVote(user_id=user.id, post_id=post.id, author_id=post.author.id,
|
||||
effect=effect)
|
||||
if post.community.low_quality and effect > 0:
|
||||
|
@ -1533,7 +1567,7 @@ def update_post_from_activity(post: Post, request_json: dict):
|
|||
old_cross_posts = Post.query.filter(Post.id.in_(post.cross_posts)).all()
|
||||
post.cross_posts.clear()
|
||||
for ocp in old_cross_posts:
|
||||
if ocp.cross_posts is not None:
|
||||
if ocp.cross_posts is not None and post.id in ocp.cross_posts:
|
||||
ocp.cross_posts.remove(post.id)
|
||||
|
||||
if post is not None:
|
||||
|
@ -1623,6 +1657,77 @@ def undo_vote(activity_log, comment, post, target_ap_id, user):
|
|||
return post
|
||||
|
||||
|
||||
def process_report(user, reported, request_json, activity_log):
|
||||
if len(request_json['summary']) < 15:
|
||||
reasons = request_json['summary']
|
||||
description = ''
|
||||
else:
|
||||
reasons = request_json['summary'][:15]
|
||||
description = request_json['summary'][15:]
|
||||
if isinstance(reported, User):
|
||||
if reported.reports == -1:
|
||||
return
|
||||
type = 0
|
||||
report = Report(reasons=reasons, description=description,
|
||||
type=type, reporter_id=user.id, suspect_user_id=reported.id, source_instance_id=user.instance_id)
|
||||
db.session.add(report)
|
||||
|
||||
# Notify site admin
|
||||
already_notified = set()
|
||||
for admin in Site.admins():
|
||||
if admin.id not in already_notified:
|
||||
notify = Notification(title='Reported user', url='/admin/reports', user_id=admin.id,
|
||||
author_id=user.id)
|
||||
db.session.add(notify)
|
||||
admin.unread_notifications += 1
|
||||
reported.reports += 1
|
||||
db.session.commit()
|
||||
elif isinstance(reported, Post):
|
||||
if reported.reports == -1:
|
||||
return
|
||||
type = 1
|
||||
report = Report(reasons=reasons, description=description, type=type, reporter_id=user.id,
|
||||
suspect_user_id=reported.author.id, suspect_post_id=reported.id,
|
||||
suspect_community_id=reported.community.id, in_community_id=reported.community.id,
|
||||
source_instance_id=user.instance_id)
|
||||
db.session.add(report)
|
||||
|
||||
already_notified = set()
|
||||
for mod in reported.community.moderators():
|
||||
notification = Notification(user_id=mod.user_id, title=_('A post has been reported'),
|
||||
url=f"https://{current_app.config['SERVER_NAME']}/post/{reported.id}",
|
||||
author_id=user.id)
|
||||
db.session.add(notification)
|
||||
already_notified.add(mod.user_id)
|
||||
reported.reports += 1
|
||||
db.session.commit()
|
||||
elif isinstance(reported, PostReply):
|
||||
if reported.reports == -1:
|
||||
return
|
||||
type = 2
|
||||
post = Post.query.get(reported.post_id)
|
||||
report = Report(reasons=reasons, description=description, type=type, reporter_id=user.id, suspect_post_id=post.id,
|
||||
suspect_community_id=post.community.id,
|
||||
suspect_user_id=reported.author.id, suspect_post_reply_id=reported.id,
|
||||
in_community_id=post.community.id,
|
||||
source_instance_id=user.instance_id)
|
||||
db.session.add(report)
|
||||
# Notify moderators
|
||||
already_notified = set()
|
||||
for mod in post.community.moderators():
|
||||
notification = Notification(user_id=mod.user_id, title=_('A comment has been reported'),
|
||||
url=f"https://{current_app.config['SERVER_NAME']}/comment/{reported.id}",
|
||||
author_id=user.id)
|
||||
db.session.add(notification)
|
||||
already_notified.add(mod.user_id)
|
||||
reported.reports += 1
|
||||
db.session.commit()
|
||||
elif isinstance(reported, Community):
|
||||
...
|
||||
elif isinstance(reported, Conversation):
|
||||
...
|
||||
|
||||
|
||||
def get_redis_connection() -> redis.Redis:
|
||||
connection_string = current_app.config['CACHE_REDIS_URL']
|
||||
if connection_string.startswith('unix://'):
|
||||
|
|
|
@ -4,6 +4,7 @@ from time import sleep
|
|||
from flask import request, flash, json, url_for, current_app, redirect, g
|
||||
from flask_login import login_required, current_user
|
||||
from flask_babel import _
|
||||
from slugify import slugify
|
||||
from sqlalchemy import text, desc, or_
|
||||
|
||||
from app import db, celery, cache
|
||||
|
@ -13,13 +14,14 @@ from app.activitypub.util import default_context, instance_allowed, instance_blo
|
|||
from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm, EditCommunityForm, EditUserForm, \
|
||||
EditTopicForm, SendNewsletterForm, AddUserForm
|
||||
from app.admin.util import unsubscribe_from_everything_then_delete, unsubscribe_from_community, send_newsletter, \
|
||||
topic_tree, topics_for_form
|
||||
topics_for_form
|
||||
from app.community.util import save_icon_file, save_banner_file
|
||||
from app.constants import REPORT_STATE_NEW, REPORT_STATE_ESCALATED
|
||||
from app.models import AllowedInstances, BannedInstances, ActivityPubLog, utcnow, Site, Community, CommunityMember, \
|
||||
User, Instance, File, Report, Topic, UserRegistration, Role, Post
|
||||
from app.utils import render_template, permission_required, set_setting, get_setting, gibberish, markdown_to_html, \
|
||||
moderating_communities, joined_communities, finalize_user_setup, theme_list, blocked_phrases, blocked_referrers
|
||||
moderating_communities, joined_communities, finalize_user_setup, theme_list, blocked_phrases, blocked_referrers, \
|
||||
topic_tree
|
||||
from app.admin import bp
|
||||
|
||||
|
||||
|
@ -357,7 +359,7 @@ def admin_topic_add():
|
|||
form = EditTopicForm()
|
||||
form.parent_id.choices = topics_for_form(0)
|
||||
if form.validate_on_submit():
|
||||
topic = Topic(name=form.name.data, machine_name=form.machine_name.data, num_communities=0)
|
||||
topic = Topic(name=form.name.data, machine_name=slugify(form.machine_name.data.strip()), num_communities=0)
|
||||
if form.parent_id.data:
|
||||
topic.parent_id = form.parent_id.data
|
||||
else:
|
||||
|
@ -456,6 +458,7 @@ def admin_users_trash():
|
|||
page = request.args.get('page', 1, type=int)
|
||||
search = request.args.get('search', '')
|
||||
local_remote = request.args.get('local_remote', '')
|
||||
type = request.args.get('type', 'bad_rep')
|
||||
|
||||
users = User.query.filter_by(deleted=False)
|
||||
if local_remote == 'local':
|
||||
|
@ -464,14 +467,19 @@ def admin_users_trash():
|
|||
users = users.filter(User.ap_id != None)
|
||||
if search:
|
||||
users = users.filter(User.email.ilike(f"%{search}%"))
|
||||
users = users.filter(User.reputation < -10)
|
||||
users = users.order_by(User.reputation).paginate(page=page, per_page=1000, error_out=False)
|
||||
|
||||
if type == '' or type == 'bad_rep':
|
||||
users = users.filter(User.reputation < -10)
|
||||
users = users.order_by(User.reputation).paginate(page=page, per_page=1000, error_out=False)
|
||||
elif type == 'bad_attitude':
|
||||
users = users.filter(User.attitude < 0.0)
|
||||
users = users.order_by(-User.attitude).paginate(page=page, per_page=1000, error_out=False)
|
||||
|
||||
next_url = url_for('admin.admin_users_trash', page=users.next_num) if users.has_next else None
|
||||
prev_url = url_for('admin.admin_users_trash', page=users.prev_num) if users.has_prev and page != 1 else None
|
||||
|
||||
return render_template('admin/users.html', title=_('Problematic users'), next_url=next_url, prev_url=prev_url, users=users,
|
||||
local_remote=local_remote, search=search,
|
||||
local_remote=local_remote, search=search, type=type,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
site=g.site
|
||||
|
|
|
@ -9,7 +9,7 @@ from app import db, cache, celery
|
|||
from app.activitypub.signature import post_request
|
||||
from app.activitypub.util import default_context
|
||||
from app.models import User, Community, Instance, Site, ActivityPubLog, CommunityMember, Topic
|
||||
from app.utils import gibberish
|
||||
from app.utils import gibberish, topic_tree
|
||||
|
||||
|
||||
def unsubscribe_from_everything_then_delete(user_id):
|
||||
|
@ -106,21 +106,6 @@ def send_newsletter(form):
|
|||
break
|
||||
|
||||
|
||||
# replies to a post, in a tree, sorted by a variety of methods
|
||||
def topic_tree() -> List:
|
||||
topics = Topic.query.order_by(Topic.name)
|
||||
|
||||
topics_dict = {topic.id: {'topic': topic, 'children': []} for topic in topics.all()}
|
||||
|
||||
for topic in topics:
|
||||
if topic.parent_id is not None:
|
||||
parent_comment = topics_dict.get(topic.parent_id)
|
||||
if parent_comment:
|
||||
parent_comment['children'].append(topics_dict[topic.id])
|
||||
|
||||
return [topic for topic in topics_dict.values() if topic['topic'].parent_id is None]
|
||||
|
||||
|
||||
def topics_for_form(current_topic: int) -> List[Tuple[int, str]]:
|
||||
result = [(0, _('None'))]
|
||||
topics = topic_tree()
|
||||
|
|
|
@ -105,7 +105,7 @@ def empty():
|
|||
@login_required
|
||||
def chat_options(conversation_id):
|
||||
conversation = Conversation.query.get_or_404(conversation_id)
|
||||
if current_user.is_admin() or current_user.is_member(current_user):
|
||||
if current_user.is_admin() or conversation.is_member(current_user):
|
||||
return render_template('chat/chat_options.html', conversation=conversation,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
|
|
|
@ -85,21 +85,23 @@ class BanUserCommunityForm(FlaskForm):
|
|||
submit = SubmitField(_l('Ban'))
|
||||
|
||||
|
||||
class CreatePostForm(FlaskForm):
|
||||
class CreateDiscussionForm(FlaskForm):
|
||||
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
|
||||
post_type = HiddenField() # https://getbootstrap.com/docs/4.6/components/navs/#tabs
|
||||
discussion_title = StringField(_l('Title'), validators=[Optional(), Length(min=3, max=255)])
|
||||
discussion_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'placeholder': 'Text (optional)'})
|
||||
link_title = StringField(_l('Title'), validators=[Optional(), Length(min=3, max=255)])
|
||||
link_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)],
|
||||
render_kw={'placeholder': 'Text (optional)'})
|
||||
link_url = StringField(_l('URL'), validators=[Optional(), Regexp(r'^https?://', message='Submitted links need to start with "http://"" or "https://"')], render_kw={'placeholder': 'https://...'})
|
||||
image_title = StringField(_l('Title'), validators=[Optional(), Length(min=3, max=255)])
|
||||
image_alt_text = StringField(_l('Alt text'), validators=[Optional(), Length(min=3, max=255)])
|
||||
image_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)],
|
||||
render_kw={'placeholder': 'Text (optional)'})
|
||||
image_file = FileField(_('Image'))
|
||||
# flair = SelectField(_l('Flair'), coerce=int)
|
||||
discussion_title = StringField(_l('Title'), validators=[DataRequired(), Length(min=3, max=255)])
|
||||
discussion_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
||||
sticky = BooleanField(_l('Sticky'))
|
||||
nsfw = BooleanField(_l('NSFW'))
|
||||
nsfl = BooleanField(_l('Gore/gross'))
|
||||
notify_author = BooleanField(_l('Notify about replies'))
|
||||
submit = SubmitField(_l('Save'))
|
||||
|
||||
|
||||
class CreateLinkForm(FlaskForm):
|
||||
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
|
||||
link_title = StringField(_l('Title'), validators=[DataRequired(), Length(min=3, max=255)])
|
||||
link_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
||||
link_url = StringField(_l('URL'), validators=[DataRequired(), Regexp(r'^https?://', message='Submitted links need to start with "http://"" or "https://"')],
|
||||
render_kw={'placeholder': 'https://...'})
|
||||
sticky = BooleanField(_l('Sticky'))
|
||||
nsfw = BooleanField(_l('NSFW'))
|
||||
nsfl = BooleanField(_l('Gore/gross'))
|
||||
|
@ -107,53 +109,45 @@ class CreatePostForm(FlaskForm):
|
|||
submit = SubmitField(_l('Save'))
|
||||
|
||||
def validate(self, extra_validators=None) -> bool:
|
||||
if not super().validate():
|
||||
domain = domain_from_url(self.link_url.data, create=False)
|
||||
if domain and domain.banned:
|
||||
self.link_url.errors.append(_("Links to %(domain)s are not allowed.", domain=domain.name))
|
||||
return False
|
||||
if self.post_type.data is None or self.post_type.data == '':
|
||||
self.post_type.data = 'discussion'
|
||||
return True
|
||||
|
||||
if self.post_type.data == 'discussion':
|
||||
if self.discussion_title.data == '':
|
||||
self.discussion_title.errors.append(_('Title is required.'))
|
||||
|
||||
class CreateImageForm(FlaskForm):
|
||||
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
|
||||
image_title = StringField(_l('Title'), validators=[DataRequired(), Length(min=3, max=255)])
|
||||
image_alt_text = StringField(_l('Alt text'), validators=[Optional(), Length(min=3, max=255)])
|
||||
image_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
||||
image_file = FileField(_('Image'), validators=[DataRequired()])
|
||||
sticky = BooleanField(_l('Sticky'))
|
||||
nsfw = BooleanField(_l('NSFW'))
|
||||
nsfl = BooleanField(_l('Gore/gross'))
|
||||
notify_author = BooleanField(_l('Notify about replies'))
|
||||
submit = SubmitField(_l('Save'))
|
||||
|
||||
def validate(self, extra_validators=None) -> bool:
|
||||
uploaded_file = request.files['image_file']
|
||||
if uploaded_file and uploaded_file.filename != '':
|
||||
Image.MAX_IMAGE_PIXELS = 89478485
|
||||
# Do not allow fascist meme content
|
||||
try:
|
||||
image_text = pytesseract.image_to_string(Image.open(BytesIO(uploaded_file.read())).convert('L'))
|
||||
except FileNotFoundError as e:
|
||||
image_text = ''
|
||||
if 'Anonymous' in image_text and (
|
||||
'No.' in image_text or ' N0' in image_text): # chan posts usually contain the text 'Anonymous' and ' No.12345'
|
||||
self.image_file.errors.append(
|
||||
f"This image is an invalid file type.") # deliberately misleading error message
|
||||
current_user.reputation -= 1
|
||||
db.session.commit()
|
||||
return False
|
||||
elif self.post_type.data == 'link':
|
||||
if self.link_title.data == '':
|
||||
self.link_title.errors.append(_('Title is required.'))
|
||||
return False
|
||||
if self.link_url.data == '':
|
||||
self.link_url.errors.append(_('URL is required.'))
|
||||
return False
|
||||
domain = domain_from_url(self.link_url.data, create=False)
|
||||
if domain and domain.banned:
|
||||
self.link_url.errors.append(_("Links to %(domain)s are not allowed.", domain=domain.name))
|
||||
return False
|
||||
elif self.post_type.data == 'image':
|
||||
if self.image_title.data == '':
|
||||
self.image_title.errors.append(_('Title is required.'))
|
||||
return False
|
||||
if self.image_file.data == '':
|
||||
self.image_file.errors.append(_('File is required.'))
|
||||
return False
|
||||
uploaded_file = request.files['image_file']
|
||||
if uploaded_file and uploaded_file.filename != '':
|
||||
Image.MAX_IMAGE_PIXELS = 89478485
|
||||
# Do not allow fascist meme content
|
||||
try:
|
||||
image_text = pytesseract.image_to_string(Image.open(BytesIO(uploaded_file.read())).convert('L'))
|
||||
except FileNotFoundError as e:
|
||||
image_text = ''
|
||||
if 'Anonymous' in image_text and ('No.' in image_text or ' N0' in image_text): # chan posts usually contain the text 'Anonymous' and ' No.12345'
|
||||
self.image_file.errors.append(f"This image is an invalid file type.") # deliberately misleading error message
|
||||
current_user.reputation -= 1
|
||||
db.session.commit()
|
||||
return False
|
||||
if self.communities:
|
||||
community = Community.query.get(self.communities.data)
|
||||
if community.is_local() and g.site.allow_local_image_posts is False:
|
||||
self.communities.errors.append(_('Images cannot be posted to local communities.'))
|
||||
elif self.post_type.data == 'poll':
|
||||
self.discussion_title.errors.append(_('Poll not implemented yet.'))
|
||||
return False
|
||||
if self.communities:
|
||||
community = Community.query.get(self.communities.data)
|
||||
if community.is_local() and g.site.allow_local_image_posts is False:
|
||||
self.communities.errors.append(_('Images cannot be posted to local communities.'))
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
@ -12,14 +12,15 @@ from app import db, constants, cache
|
|||
from app.activitypub.signature import RsaKeys, post_request
|
||||
from app.activitypub.util import default_context, notify_about_post, find_actor_or_create, make_image_sizes
|
||||
from app.chat.util import send_message
|
||||
from app.community.forms import SearchRemoteCommunity, CreatePostForm, ReportCommunityForm, \
|
||||
from app.community.forms import SearchRemoteCommunity, CreateDiscussionForm, CreateImageForm, CreateLinkForm, ReportCommunityForm, \
|
||||
DeleteCommunityForm, AddCommunityForm, EditCommunityForm, AddModeratorForm, BanUserCommunityForm, \
|
||||
EscalateReportForm, ResolveReportForm
|
||||
from app.community.util import search_for_community, community_url_exists, actor_to_community, \
|
||||
opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file, send_to_remote_instance, \
|
||||
delete_post_from_community, delete_post_reply_from_community
|
||||
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, \
|
||||
SUBSCRIPTION_PENDING, SUBSCRIPTION_MODERATOR, REPORT_STATE_NEW, REPORT_STATE_ESCALATED, REPORT_STATE_RESOLVED
|
||||
SUBSCRIPTION_PENDING, SUBSCRIPTION_MODERATOR, REPORT_STATE_NEW, REPORT_STATE_ESCALATED, REPORT_STATE_RESOLVED, \
|
||||
REPORT_STATE_DISCARDED
|
||||
from app.inoculation import inoculation
|
||||
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
|
||||
File, PostVote, utcnow, Report, Notification, InstanceBlock, ActivityPubLog, Topic, Conversation, PostReply
|
||||
|
@ -29,7 +30,8 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_
|
|||
shorten_string, gibberish, community_membership, ap_datetime, \
|
||||
request_etag_matches, return_304, instance_banned, can_create_post, can_upvote, can_downvote, user_filters_posts, \
|
||||
joined_communities, moderating_communities, blocked_domains, mimetype_from_url, blocked_instances, \
|
||||
community_moderators, communities_banned_from, show_ban_message
|
||||
community_moderators, communities_banned_from, show_ban_message, recently_upvoted_posts, recently_downvoted_posts, \
|
||||
blocked_users
|
||||
from feedgen.feed import FeedGenerator
|
||||
from datetime import timezone, timedelta
|
||||
|
||||
|
@ -180,10 +182,15 @@ def show_community(community: Community):
|
|||
if instance_ids:
|
||||
posts = posts.filter(or_(Post.instance_id.not_in(instance_ids), Post.instance_id == None))
|
||||
|
||||
# filter blocked users
|
||||
blocked_accounts = blocked_users(current_user.id)
|
||||
if blocked_accounts:
|
||||
posts = posts.filter(Post.user_id.not_in(blocked_accounts))
|
||||
|
||||
if sort == '' or sort == 'hot':
|
||||
posts = posts.order_by(desc(Post.sticky)).order_by(desc(Post.ranking)).order_by(desc(Post.posted_at))
|
||||
elif sort == 'top':
|
||||
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=7)).order_by(desc(Post.sticky)).order_by(desc(Post.score))
|
||||
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=7)).order_by(desc(Post.sticky)).order_by(desc(Post.up_votes - Post.down_votes))
|
||||
elif sort == 'new':
|
||||
posts = posts.order_by(desc(Post.posted_at))
|
||||
elif sort == 'active':
|
||||
|
@ -240,12 +247,21 @@ def show_community(community: Community):
|
|||
prev_url = url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name,
|
||||
page=posts.prev_num, sort=sort, layout=post_layout) if posts.has_prev and page != 1 else None
|
||||
|
||||
# Voting history
|
||||
if current_user.is_authenticated:
|
||||
recently_upvoted = recently_upvoted_posts(current_user.id)
|
||||
recently_downvoted = recently_downvoted_posts(current_user.id)
|
||||
else:
|
||||
recently_upvoted = []
|
||||
recently_downvoted = []
|
||||
|
||||
return render_template('community/community.html', community=community, title=community.title, breadcrumbs=breadcrumbs,
|
||||
is_moderator=is_moderator, is_owner=is_owner, is_admin=is_admin, mods=mod_list, posts=posts, description=description,
|
||||
og_image=og_image, POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK, SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING,
|
||||
SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR,
|
||||
etag=f"{community.id}{sort}{post_layout}_{hash(community.last_active)}", related_communities=related_communities,
|
||||
next_url=next_url, prev_url=prev_url, low_bandwidth=low_bandwidth,
|
||||
recently_upvoted=recently_upvoted, recently_downvoted=recently_downvoted,
|
||||
rss_feed=f"https://{current_app.config['SERVER_NAME']}/community/{community.link()}/feed", rss_feed_name=f"{community.title} on PieFed",
|
||||
content_filters=content_filters, moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()), sort=sort,
|
||||
|
@ -435,7 +451,7 @@ def join_then_add(actor):
|
|||
db.session.commit()
|
||||
flash('You joined ' + community.title)
|
||||
if not community.user_is_banned(current_user):
|
||||
return redirect(url_for('community.add_post', actor=community.link()))
|
||||
return redirect(url_for('community.add_discussion_post', actor=community.link()))
|
||||
else:
|
||||
abort(401)
|
||||
|
||||
|
@ -443,11 +459,13 @@ def join_then_add(actor):
|
|||
@bp.route('/<actor>/submit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@validation_required
|
||||
def add_post(actor):
|
||||
def add_discussion_post(actor):
|
||||
if current_user.banned:
|
||||
return show_ban_message()
|
||||
community = actor_to_community(actor)
|
||||
form = CreatePostForm()
|
||||
|
||||
form = CreateDiscussionForm()
|
||||
|
||||
if g.site.enable_nsfl is False:
|
||||
form.nsfl.render_kw = {'disabled': True}
|
||||
if community.nsfw:
|
||||
|
@ -469,7 +487,63 @@ def add_post(actor):
|
|||
if not can_create_post(current_user, community):
|
||||
abort(401)
|
||||
post = Post(user_id=current_user.id, community_id=form.communities.data, instance_id=1)
|
||||
save_post(form, post)
|
||||
save_post(form, post, 'discussion')
|
||||
community.post_count += 1
|
||||
community.last_active = g.site.last_active = utcnow()
|
||||
db.session.commit()
|
||||
post.ap_id = f"https://{current_app.config['SERVER_NAME']}/post/{post.id}"
|
||||
db.session.commit()
|
||||
|
||||
notify_about_post(post)
|
||||
|
||||
if not community.local_only:
|
||||
federate_post(community, post)
|
||||
|
||||
return redirect(f"/c/{community.link()}")
|
||||
else:
|
||||
form.communities.data = community.id
|
||||
form.notify_author.data = True
|
||||
|
||||
return render_template('community/add_discussion_post.html', title=_('Add post to community'), form=form, community=community,
|
||||
markdown_editor=current_user.markdown_editor, low_bandwidth=False, actor=actor,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.id),
|
||||
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/<actor>/submit_image', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@validation_required
|
||||
def add_image_post(actor):
|
||||
if current_user.banned:
|
||||
return show_ban_message()
|
||||
community = actor_to_community(actor)
|
||||
|
||||
form = CreateImageForm()
|
||||
|
||||
if g.site.enable_nsfl is False:
|
||||
form.nsfl.render_kw = {'disabled': True}
|
||||
if community.nsfw:
|
||||
form.nsfw.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
if community.nsfl:
|
||||
form.nsfl.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
if not(community.is_moderator() or community.is_owner() or current_user.is_admin()):
|
||||
form.sticky.render_kw = {'disabled': True}
|
||||
|
||||
form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
|
||||
|
||||
if not can_create_post(current_user, community):
|
||||
abort(401)
|
||||
|
||||
if form.validate_on_submit():
|
||||
community = Community.query.get_or_404(form.communities.data)
|
||||
if not can_create_post(current_user, community):
|
||||
abort(401)
|
||||
post = Post(user_id=current_user.id, community_id=form.communities.data, instance_id=1)
|
||||
save_post(form, post, 'image')
|
||||
community.post_count += 1
|
||||
community.last_active = g.site.last_active = utcnow()
|
||||
db.session.commit()
|
||||
|
@ -496,105 +570,183 @@ def add_post(actor):
|
|||
notify_about_post(post)
|
||||
|
||||
if not community.local_only:
|
||||
page = {
|
||||
'type': 'Page',
|
||||
'id': post.ap_id,
|
||||
'attributedTo': current_user.ap_profile_id,
|
||||
'to': [
|
||||
community.ap_profile_id,
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'name': post.title,
|
||||
'cc': [],
|
||||
'content': post.body_html if post.body_html else '',
|
||||
'mediaType': 'text/html',
|
||||
'source': {
|
||||
'content': post.body if post.body else '',
|
||||
'mediaType': 'text/markdown'
|
||||
},
|
||||
'attachment': [],
|
||||
'commentsEnabled': post.comments_enabled,
|
||||
'sensitive': post.nsfw,
|
||||
'nsfl': post.nsfl,
|
||||
'stickied': post.sticky,
|
||||
'published': ap_datetime(utcnow()),
|
||||
'audience': community.ap_profile_id
|
||||
}
|
||||
create = {
|
||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
|
||||
"actor": current_user.ap_profile_id,
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"cc": [
|
||||
community.ap_profile_id
|
||||
],
|
||||
"type": "Create",
|
||||
"audience": community.ap_profile_id,
|
||||
"object": page,
|
||||
'@context': default_context()
|
||||
}
|
||||
if post.type == POST_TYPE_LINK:
|
||||
page['attachment'] = [{'href': post.url, 'type': 'Link'}]
|
||||
elif post.image_id:
|
||||
if post.image.file_path:
|
||||
image_url = post.image.file_path.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/")
|
||||
elif post.image.thumbnail_path:
|
||||
image_url = post.image.thumbnail_path.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/")
|
||||
else:
|
||||
image_url = post.image.source_url
|
||||
# NB image is a dict while attachment is a list of dicts (usually just one dict in the list)
|
||||
page['image'] = {'type': 'Image', 'url': image_url}
|
||||
if post.type == POST_TYPE_IMAGE:
|
||||
page['attachment'] = [{'type': 'Link', 'href': post.image.source_url}] # source_url is always a https link, no need for .replace() as done above
|
||||
if not community.is_local(): # this is a remote community - send the post to the instance that hosts it
|
||||
success = post_request(community.ap_inbox_url, create, current_user.private_key,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
if success:
|
||||
flash(_('Your post to %(name)s has been made.', name=community.title))
|
||||
else:
|
||||
flash('There was a problem making your post to ' + community.title)
|
||||
else: # local community - send (announce) post out to followers
|
||||
announce = {
|
||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}",
|
||||
"type": 'Announce',
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"actor": community.ap_profile_id,
|
||||
"cc": [
|
||||
community.ap_followers_url
|
||||
],
|
||||
'@context': default_context(),
|
||||
'object': create
|
||||
}
|
||||
|
||||
sent_to = 0
|
||||
for instance in community.following_instances():
|
||||
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
|
||||
send_to_remote_instance(instance.id, community.id, announce)
|
||||
sent_to += 1
|
||||
if sent_to:
|
||||
flash(_('Your post to %(name)s has been made.', name=community.title))
|
||||
else:
|
||||
flash(_('Your post to %(name)s has been made.', name=community.title))
|
||||
federate_post(community, post)
|
||||
|
||||
return redirect(f"/c/{community.link()}")
|
||||
else:
|
||||
# when request.form has some data in it, it means form validation failed. Set the post_type so the correct tab is shown. See setupPostTypeTabs() in scripts.js
|
||||
if request.form.get('post_type', None):
|
||||
form.post_type.data = request.form.get('post_type')
|
||||
form.communities.data = community.id
|
||||
form.notify_author.data = True
|
||||
|
||||
return render_template('community/add_post.html', title=_('Add post to community'), form=form, community=community,
|
||||
markdown_editor=current_user.markdown_editor, low_bandwidth=False,
|
||||
return render_template('community/add_image_post.html', title=_('Add post to community'), form=form, community=community,
|
||||
markdown_editor=current_user.markdown_editor, low_bandwidth=False, actor=actor,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.id),
|
||||
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/<actor>/submit_link', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@validation_required
|
||||
def add_link_post(actor):
|
||||
if current_user.banned:
|
||||
return show_ban_message()
|
||||
community = actor_to_community(actor)
|
||||
|
||||
form = CreateLinkForm()
|
||||
|
||||
if g.site.enable_nsfl is False:
|
||||
form.nsfl.render_kw = {'disabled': True}
|
||||
if community.nsfw:
|
||||
form.nsfw.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
if community.nsfl:
|
||||
form.nsfl.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
if not(community.is_moderator() or community.is_owner() or current_user.is_admin()):
|
||||
form.sticky.render_kw = {'disabled': True}
|
||||
|
||||
form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
|
||||
|
||||
if not can_create_post(current_user, community):
|
||||
abort(401)
|
||||
|
||||
if form.validate_on_submit():
|
||||
community = Community.query.get_or_404(form.communities.data)
|
||||
if not can_create_post(current_user, community):
|
||||
abort(401)
|
||||
post = Post(user_id=current_user.id, community_id=form.communities.data, instance_id=1)
|
||||
save_post(form, post, 'link')
|
||||
community.post_count += 1
|
||||
community.last_active = g.site.last_active = utcnow()
|
||||
db.session.commit()
|
||||
post.ap_id = f"https://{current_app.config['SERVER_NAME']}/post/{post.id}"
|
||||
db.session.commit()
|
||||
if post.image_id and post.image.file_path is None:
|
||||
make_image_sizes(post.image_id, 150, 512, 'posts') # the 512 sized image is for masonry view
|
||||
|
||||
# Update list of cross posts
|
||||
if post.url:
|
||||
other_posts = Post.query.filter(Post.id != post.id, Post.url == post.url,
|
||||
Post.posted_at > post.posted_at - timedelta(days=6)).all()
|
||||
for op in other_posts:
|
||||
if op.cross_posts is None:
|
||||
op.cross_posts = [post.id]
|
||||
else:
|
||||
op.cross_posts.append(post.id)
|
||||
if post.cross_posts is None:
|
||||
post.cross_posts = [op.id]
|
||||
else:
|
||||
post.cross_posts.append(op.id)
|
||||
db.session.commit()
|
||||
|
||||
notify_about_post(post)
|
||||
|
||||
if not community.local_only:
|
||||
federate_post(community, post)
|
||||
|
||||
return redirect(f"/c/{community.link()}")
|
||||
else:
|
||||
form.communities.data = community.id
|
||||
form.notify_author.data = True
|
||||
|
||||
return render_template('community/add_link_post.html', title=_('Add post to community'), form=form, community=community,
|
||||
markdown_editor=current_user.markdown_editor, low_bandwidth=False, actor=actor,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.id),
|
||||
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||
)
|
||||
|
||||
|
||||
def federate_post(community, post):
|
||||
page = {
|
||||
'type': 'Page',
|
||||
'id': post.ap_id,
|
||||
'attributedTo': current_user.ap_profile_id,
|
||||
'to': [
|
||||
community.ap_profile_id,
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'name': post.title,
|
||||
'cc': [],
|
||||
'content': post.body_html if post.body_html else '',
|
||||
'mediaType': 'text/html',
|
||||
'source': {
|
||||
'content': post.body if post.body else '',
|
||||
'mediaType': 'text/markdown'
|
||||
},
|
||||
'attachment': [],
|
||||
'commentsEnabled': post.comments_enabled,
|
||||
'sensitive': post.nsfw,
|
||||
'nsfl': post.nsfl,
|
||||
'stickied': post.sticky,
|
||||
'published': ap_datetime(utcnow()),
|
||||
'audience': community.ap_profile_id
|
||||
}
|
||||
create = {
|
||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
|
||||
"actor": current_user.ap_profile_id,
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"cc": [
|
||||
community.ap_profile_id
|
||||
],
|
||||
"type": "Create",
|
||||
"audience": community.ap_profile_id,
|
||||
"object": page,
|
||||
'@context': default_context()
|
||||
}
|
||||
if post.type == POST_TYPE_LINK:
|
||||
page['attachment'] = [{'href': post.url, 'type': 'Link'}]
|
||||
elif post.image_id:
|
||||
if post.image.file_path:
|
||||
image_url = post.image.file_path.replace('app/static/',
|
||||
f"https://{current_app.config['SERVER_NAME']}/static/")
|
||||
elif post.image.thumbnail_path:
|
||||
image_url = post.image.thumbnail_path.replace('app/static/',
|
||||
f"https://{current_app.config['SERVER_NAME']}/static/")
|
||||
else:
|
||||
image_url = post.image.source_url
|
||||
# NB image is a dict while attachment is a list of dicts (usually just one dict in the list)
|
||||
page['image'] = {'type': 'Image', 'url': image_url}
|
||||
if post.type == POST_TYPE_IMAGE:
|
||||
page['attachment'] = [{'type': 'Link',
|
||||
'href': post.image.source_url}] # source_url is always a https link, no need for .replace() as done above
|
||||
if not community.is_local(): # this is a remote community - send the post to the instance that hosts it
|
||||
success = post_request(community.ap_inbox_url, create, current_user.private_key,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
if success:
|
||||
flash(_('Your post to %(name)s has been made.', name=community.title))
|
||||
else:
|
||||
flash('There was a problem making your post to ' + community.title)
|
||||
else: # local community - send (announce) post out to followers
|
||||
announce = {
|
||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}",
|
||||
"type": 'Announce',
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"actor": community.ap_profile_id,
|
||||
"cc": [
|
||||
community.ap_followers_url
|
||||
],
|
||||
'@context': default_context(),
|
||||
'object': create
|
||||
}
|
||||
|
||||
sent_to = 0
|
||||
for instance in community.following_instances():
|
||||
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(
|
||||
instance.domain):
|
||||
send_to_remote_instance(instance.id, community.id, announce)
|
||||
sent_to += 1
|
||||
if sent_to:
|
||||
flash(_('Your post to %(name)s has been made.', name=community.title))
|
||||
else:
|
||||
flash(_('Your post to %(name)s has been made.', name=community.title))
|
||||
|
||||
|
||||
@bp.route('/community/<int:community_id>/report', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def community_report(community_id: int):
|
||||
|
@ -1051,7 +1203,19 @@ def community_moderate_report_resolve(community_id, report_id):
|
|||
form = ResolveReportForm()
|
||||
if form.validate_on_submit():
|
||||
report.status = REPORT_STATE_RESOLVED
|
||||
|
||||
# Reset the 'reports' counter on the comment, post or user
|
||||
if report.suspect_post_reply_id:
|
||||
post_reply = PostReply.query.get(report.suspect_post_reply_id)
|
||||
post_reply.reports = 0
|
||||
elif report.suspect_post_id:
|
||||
post = Post.query.get(report.suspect_post_id)
|
||||
post.reports = 0
|
||||
elif report.suspect_user_id:
|
||||
user = User.query.get(report.suspect_user_id)
|
||||
user.reports = 0
|
||||
db.session.commit()
|
||||
|
||||
# todo: remove unread notifications about this report
|
||||
# todo: append to mod log
|
||||
if form.also_resolve_others.data:
|
||||
|
@ -1072,6 +1236,44 @@ def community_moderate_report_resolve(community_id, report_id):
|
|||
return render_template('community/community_moderate_report_resolve.html', form=form)
|
||||
|
||||
|
||||
@bp.route('/community/<int:community_id>/moderate_report/<int:report_id>/ignore', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def community_moderate_report_ignore(community_id, report_id):
|
||||
community = Community.query.get_or_404(community_id)
|
||||
if community.is_moderator() or current_user.is_admin():
|
||||
report = Report.query.filter_by(in_community_id=community.id, id=report_id).first()
|
||||
if report:
|
||||
# Set the 'reports' counter on the comment, post or user to -1 to ignore all future reports
|
||||
if report.suspect_post_reply_id:
|
||||
post_reply = PostReply.query.get(report.suspect_post_reply_id)
|
||||
post_reply.reports = -1
|
||||
elif report.suspect_post_id:
|
||||
post = Post.query.get(report.suspect_post_id)
|
||||
post.reports = -1
|
||||
elif report.suspect_user_id:
|
||||
user = User.query.get(report.suspect_user_id)
|
||||
user.reports = -1
|
||||
db.session.commit()
|
||||
|
||||
# todo: append to mod log
|
||||
|
||||
if report.suspect_post_reply_id:
|
||||
db.session.execute(text('UPDATE "report" SET status = :new_status WHERE suspect_post_reply_id = :suspect_post_reply_id'),
|
||||
{'new_status': REPORT_STATE_DISCARDED,
|
||||
'suspect_post_reply_id': report.suspect_post_reply_id})
|
||||
# todo: remove unread notifications about these reports
|
||||
elif report.suspect_post_id:
|
||||
db.session.execute(text('UPDATE "report" SET status = :new_status WHERE suspect_post_id = :suspect_post_id'),
|
||||
{'new_status': REPORT_STATE_DISCARDED,
|
||||
'suspect_post_id': report.suspect_post_id})
|
||||
# todo: remove unread notifications about these reports
|
||||
db.session.commit()
|
||||
flash(_('Report ignored.'))
|
||||
return redirect(url_for('community.community_moderate', actor=community.link()))
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
|
||||
@bp.route('/lookup/<community>/<domain>')
|
||||
def lookup(community, domain):
|
||||
if domain == current_app.config['SERVER_NAME']:
|
||||
|
|
|
@ -196,18 +196,18 @@ def url_to_thumbnail_file(filename) -> File:
|
|||
source_url=filename)
|
||||
|
||||
|
||||
def save_post(form, post: Post):
|
||||
def save_post(form, post: Post, type: str):
|
||||
post.indexable = current_user.indexable
|
||||
post.sticky = form.sticky.data
|
||||
post.nsfw = form.nsfw.data
|
||||
post.nsfl = form.nsfl.data
|
||||
post.notify_author = form.notify_author.data
|
||||
if form.post_type.data == '' or form.post_type.data == 'discussion':
|
||||
if type == '' or type == 'discussion':
|
||||
post.title = form.discussion_title.data
|
||||
post.body = form.discussion_body.data
|
||||
post.body_html = markdown_to_html(post.body)
|
||||
post.type = POST_TYPE_ARTICLE
|
||||
elif form.post_type.data == 'link':
|
||||
elif type == 'link':
|
||||
post.title = form.link_title.data
|
||||
post.body = form.link_body.data
|
||||
post.body_html = markdown_to_html(post.body)
|
||||
|
@ -244,7 +244,7 @@ def save_post(form, post: Post):
|
|||
post.image = file
|
||||
db.session.add(file)
|
||||
|
||||
elif form.post_type.data == 'image':
|
||||
elif type == 'image':
|
||||
post.title = form.image_title.data
|
||||
post.body = form.image_body.data
|
||||
post.body_html = markdown_to_html(post.body)
|
||||
|
@ -304,7 +304,7 @@ def save_post(form, post: Post):
|
|||
post.image = file
|
||||
db.session.add(file)
|
||||
|
||||
elif form.post_type.data == 'poll':
|
||||
elif type == 'poll':
|
||||
...
|
||||
else:
|
||||
raise Exception('invalid post type')
|
||||
|
|
|
@ -3,9 +3,11 @@ from app import db
|
|||
from app.errors import bp
|
||||
|
||||
|
||||
@bp.app_errorhandler(404)
|
||||
def not_found_error(error):
|
||||
return render_template('errors/404.html'), 404
|
||||
# 404 error handler removed because a lot of 404s are just images in /static/* and it doesn't make sense to waste cpu cycles presenting a nice page.
|
||||
# Also rendering a page requires populating g.site which means hitting the DB.
|
||||
# @bp.app_errorhandler(404)
|
||||
# def not_found_error(error):
|
||||
# return render_template('errors/404.html'), 404
|
||||
|
||||
|
||||
@bp.app_errorhandler(500)
|
||||
|
|
|
@ -25,7 +25,7 @@ from sqlalchemy_searchable import search
|
|||
from app.utils import render_template, get_setting, gibberish, request_etag_matches, return_304, blocked_domains, \
|
||||
ap_datetime, ip_address, retrieve_block_list, shorten_string, markdown_to_text, user_filters_home, \
|
||||
joined_communities, moderating_communities, parse_page, theme_list, get_request, markdown_to_html, allowlist_html, \
|
||||
blocked_instances, communities_banned_from
|
||||
blocked_instances, communities_banned_from, topic_tree, recently_upvoted_posts, recently_downvoted_posts
|
||||
from app.models import Community, CommunityMember, Post, Site, User, utcnow, Domain, Topic, File, Instance, \
|
||||
InstanceRole, Notification
|
||||
from PIL import Image
|
||||
|
@ -114,7 +114,7 @@ def home_page(type, sort):
|
|||
if sort == 'hot':
|
||||
posts = posts.order_by(desc(Post.ranking)).order_by(desc(Post.posted_at))
|
||||
elif sort == 'top':
|
||||
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=1)).order_by(desc(Post.score))
|
||||
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=1)).order_by(desc(Post.up_votes - Post.down_votes))
|
||||
elif sort == 'new':
|
||||
posts = posts.order_by(desc(Post.posted_at))
|
||||
elif sort == 'active':
|
||||
|
@ -140,9 +140,18 @@ def home_page(type, sort):
|
|||
active_communities = active_communities.filter(Community.id.not_in(banned_from))
|
||||
active_communities = active_communities.order_by(desc(Community.last_active)).limit(5).all()
|
||||
|
||||
# Voting history
|
||||
if current_user.is_authenticated:
|
||||
recently_upvoted = recently_upvoted_posts(current_user.id)
|
||||
recently_downvoted = recently_downvoted_posts(current_user.id)
|
||||
else:
|
||||
recently_upvoted = []
|
||||
recently_downvoted = []
|
||||
|
||||
return render_template('index.html', posts=posts, active_communities=active_communities, show_post_community=True,
|
||||
POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK,
|
||||
low_bandwidth=low_bandwidth,
|
||||
low_bandwidth=low_bandwidth, recently_upvoted=recently_upvoted,
|
||||
recently_downvoted=recently_downvoted,
|
||||
SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
|
||||
etag=f"{type}_{sort}_{hash(str(g.site.last_active))}", next_url=next_url, prev_url=prev_url,
|
||||
#rss_feed=f"https://{current_app.config['SERVER_NAME']}/feed",
|
||||
|
@ -158,7 +167,7 @@ def home_page(type, sort):
|
|||
@bp.route('/topics', methods=['GET'])
|
||||
def list_topics():
|
||||
verification_warning()
|
||||
topics = Topic.query.filter_by(parent_id=None).order_by(Topic.name).all()
|
||||
topics = topic_tree()
|
||||
|
||||
return render_template('list_topics.html', topics=topics, title=_('Browse by topic'),
|
||||
low_bandwidth=request.cookies.get('low_bandwidth', '0') == '1',
|
||||
|
@ -294,7 +303,9 @@ def list_files(directory):
|
|||
|
||||
@bp.route('/test')
|
||||
def test():
|
||||
return ''
|
||||
md = "::: spoiler I'm all for ya having fun and your right to hurt yourself.\n\nI am a former racer, commuter, and professional Buyer for a chain of bike shops. I'm also disabled from the crash involving the 6th and 7th cars that have hit me in the last 170k+ miles of riding. I only barely survived what I simplify as a \"broken neck and back.\" Cars making U-turns are what will get you if you ride long enough, \n\nespecially commuting. It will look like just another person turning in front of you, you'll compensate like usual, and before your brain can even register what is really happening, what was your normal escape route will close and you're going to crash really hard. It is the only kind of crash that your intuition is useless against.\n:::"
|
||||
|
||||
return markdown_to_html(md)
|
||||
|
||||
users_to_notify = User.query.join(Notification, User.id == Notification.user_id).filter(
|
||||
User.ap_id == None,
|
||||
|
@ -349,6 +360,36 @@ def test_email():
|
|||
return f'Email sent to {current_user.email}.'
|
||||
|
||||
|
||||
@bp.route('/find_voters')
|
||||
def find_voters():
|
||||
user_ids = db.session.execute(text('SELECT id from "user" ORDER BY last_seen DESC LIMIT 5000')).scalars()
|
||||
voters = {}
|
||||
for user_id in user_ids:
|
||||
recently_downvoted = recently_downvoted_posts(user_id)
|
||||
if len(recently_downvoted) > 10:
|
||||
voters[user_id] = str(recently_downvoted)
|
||||
|
||||
return str(find_duplicate_values(voters))
|
||||
|
||||
|
||||
def find_duplicate_values(dictionary):
|
||||
# Create a dictionary to store the keys for each value
|
||||
value_to_keys = {}
|
||||
|
||||
# Iterate through the input dictionary
|
||||
for key, value in dictionary.items():
|
||||
# If the value is not already in the dictionary, add it
|
||||
if value not in value_to_keys:
|
||||
value_to_keys[value] = [key]
|
||||
else:
|
||||
# If the value is already in the dictionary, append the key to the list
|
||||
value_to_keys[value].append(key)
|
||||
|
||||
# Filter out the values that have only one key (i.e., unique values)
|
||||
duplicates = {value: keys for value, keys in value_to_keys.items() if len(keys) > 1}
|
||||
|
||||
return duplicates
|
||||
|
||||
def verification_warning():
|
||||
if hasattr(current_user, 'verified') and current_user.verified is False:
|
||||
flash(_('Please click the link in your email inbox to verify your account.'), 'warning')
|
||||
|
|
|
@ -208,14 +208,23 @@ class File(db.Model):
|
|||
def delete_from_disk(self):
|
||||
purge_from_cache = []
|
||||
if self.file_path and os.path.isfile(self.file_path):
|
||||
os.unlink(self.file_path)
|
||||
try:
|
||||
os.unlink(self.file_path)
|
||||
except FileNotFoundError as e:
|
||||
...
|
||||
purge_from_cache.append(self.file_path.replace('app/', f"https://{current_app.config['SERVER_NAME']}/"))
|
||||
if self.thumbnail_path and os.path.isfile(self.thumbnail_path):
|
||||
os.unlink(self.thumbnail_path)
|
||||
try:
|
||||
os.unlink(self.thumbnail_path)
|
||||
except FileNotFoundError as e:
|
||||
...
|
||||
purge_from_cache.append(self.thumbnail_path.replace('app/', f"https://{current_app.config['SERVER_NAME']}/"))
|
||||
if self.source_url and self.source_url.startswith('http') and current_app.config['SERVER_NAME'] in self.source_url:
|
||||
# self.source_url is always a url rather than a file path, which makes deleting the file a bit fiddly
|
||||
os.unlink(self.source_url.replace(f"https://{current_app.config['SERVER_NAME']}/", 'app/'))
|
||||
try:
|
||||
os.unlink(self.source_url.replace(f"https://{current_app.config['SERVER_NAME']}/", 'app/'))
|
||||
except FileNotFoundError as e:
|
||||
...
|
||||
purge_from_cache.append(self.source_url) # otoh it makes purging the cdn cache super easy.
|
||||
|
||||
if purge_from_cache:
|
||||
|
@ -283,6 +292,18 @@ class Topic(db.Model):
|
|||
parent_id = db.Column(db.Integer)
|
||||
communities = db.relationship('Community', lazy='dynamic', backref='topic', cascade="all, delete-orphan")
|
||||
|
||||
def path(self):
|
||||
return_value = [self.machine_name]
|
||||
parent_id = self.parent_id
|
||||
while parent_id is not None:
|
||||
parent_topic = Topic.query.get(parent_id)
|
||||
if parent_topic is None:
|
||||
break
|
||||
return_value.append(parent_topic.machine_name)
|
||||
parent_id = parent_topic.parent_id
|
||||
return_value = list(reversed(return_value))
|
||||
return '/'.join(return_value)
|
||||
|
||||
|
||||
class Community(db.Model):
|
||||
query_class = FullTextSearchQuery
|
||||
|
@ -405,8 +426,8 @@ class Community(db.Model):
|
|||
(or_(
|
||||
CommunityMember.is_owner,
|
||||
CommunityMember.is_moderator
|
||||
)) & CommunityMember.is_banned == False
|
||||
).all()
|
||||
))
|
||||
).filter(CommunityMember.is_banned == False).all()
|
||||
|
||||
def is_moderator(self, user=None):
|
||||
if user is None:
|
||||
|
@ -1257,6 +1278,7 @@ class Site(db.Model):
|
|||
last_active = db.Column(db.DateTime, default=utcnow)
|
||||
log_activitypub_json = db.Column(db.Boolean, default=False)
|
||||
default_theme = db.Column(db.String(20), default='')
|
||||
contact_email = db.Column(db.String(255), default='')
|
||||
|
||||
@staticmethod
|
||||
def admins() -> List[User]:
|
||||
|
|
|
@ -7,7 +7,7 @@ from app.utils import MultiCheckboxField
|
|||
|
||||
|
||||
class NewReplyForm(FlaskForm):
|
||||
body = TextAreaField(_l('Body'), render_kw={'placeholder': 'What are your thoughts?', 'rows': 3}, validators={DataRequired(), Length(min=3, max=5000)})
|
||||
body = TextAreaField(_l('Body'), render_kw={'placeholder': 'What are your thoughts?', 'rows': 5}, validators={DataRequired(), Length(min=3, max=5000)})
|
||||
notify_author = BooleanField(_l('Notify about replies'))
|
||||
submit = SubmitField(_l('Comment'))
|
||||
|
||||
|
|
|
@ -13,18 +13,19 @@ from app.activitypub.util import default_context
|
|||
from app.community.util import save_post, send_to_remote_instance
|
||||
from app.inoculation import inoculation
|
||||
from app.post.forms import NewReplyForm, ReportPostForm, MeaCulpaForm
|
||||
from app.community.forms import CreatePostForm
|
||||
from app.community.forms import CreateLinkForm, CreateImageForm, CreateDiscussionForm
|
||||
from app.post.util import post_replies, get_comment_branch, post_reply_count
|
||||
from app.constants import SUBSCRIPTION_MEMBER, POST_TYPE_LINK, POST_TYPE_IMAGE
|
||||
from app.constants import SUBSCRIPTION_MEMBER, POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE
|
||||
from app.models import Post, PostReply, \
|
||||
PostReplyVote, PostVote, Notification, utcnow, UserBlock, DomainBlock, InstanceBlock, Report, Site, Community, \
|
||||
Topic, User
|
||||
Topic, User, Instance
|
||||
from app.post import bp
|
||||
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
||||
shorten_string, markdown_to_text, gibberish, ap_datetime, return_304, \
|
||||
request_etag_matches, ip_address, user_ip_banned, instance_banned, can_downvote, can_upvote, post_ranking, \
|
||||
reply_already_exists, reply_is_just_link_to_gif_reaction, confidence, moderating_communities, joined_communities, \
|
||||
blocked_instances, blocked_domains, community_moderators, blocked_phrases, show_ban_message
|
||||
blocked_instances, blocked_domains, community_moderators, blocked_phrases, show_ban_message, recently_upvoted_posts, \
|
||||
recently_downvoted_posts, recently_upvoted_post_replies, recently_downvoted_post_replies
|
||||
|
||||
|
||||
def show_post(post_id: int):
|
||||
|
@ -239,12 +240,26 @@ def show_post(post_id: int):
|
|||
breadcrumb.url = '/communities'
|
||||
breadcrumbs.append(breadcrumb)
|
||||
|
||||
# Voting history
|
||||
if current_user.is_authenticated:
|
||||
recently_upvoted = recently_upvoted_posts(current_user.id)
|
||||
recently_downvoted = recently_downvoted_posts(current_user.id)
|
||||
recently_upvoted_replies = recently_upvoted_post_replies(current_user.id)
|
||||
recently_downvoted_replies = recently_downvoted_post_replies(current_user.id)
|
||||
else:
|
||||
recently_upvoted = []
|
||||
recently_downvoted = []
|
||||
recently_upvoted_replies = []
|
||||
recently_downvoted_replies = []
|
||||
|
||||
response = render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator, community=post.community,
|
||||
breadcrumbs=breadcrumbs, related_communities=related_communities, mods=mod_list,
|
||||
canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH,
|
||||
description=description, og_image=og_image, POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE,
|
||||
POST_TYPE_LINK=constants.POST_TYPE_LINK, POST_TYPE_ARTICLE=constants.POST_TYPE_ARTICLE,
|
||||
noindex=not post.author.indexable,
|
||||
recently_upvoted=recently_upvoted, recently_downvoted=recently_downvoted,
|
||||
recently_upvoted_replies=recently_upvoted_replies, recently_downvoted_replies=recently_downvoted_replies,
|
||||
etag=f"{post.id}{sort}_{hash(post.last_active)}", markdown_editor=current_user.is_authenticated and current_user.markdown_editor,
|
||||
low_bandwidth=request.cookies.get('low_bandwidth', '0') == '1', SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
|
@ -259,7 +274,6 @@ def show_post(post_id: int):
|
|||
@login_required
|
||||
@validation_required
|
||||
def post_vote(post_id: int, vote_direction):
|
||||
upvoted_class = downvoted_class = ''
|
||||
post = Post.query.get_or_404(post_id)
|
||||
existing_vote = PostVote.query.filter_by(user_id=current_user.id, post_id=post.id).first()
|
||||
if existing_vote:
|
||||
|
@ -275,7 +289,6 @@ def post_vote(post_id: int, vote_direction):
|
|||
post.up_votes -= 1
|
||||
post.down_votes += 1
|
||||
post.score -= 2
|
||||
downvoted_class = 'voted_down'
|
||||
else: # previous vote was down
|
||||
if vote_direction == 'downvote': # new vote is also down, so remove it
|
||||
db.session.delete(existing_vote)
|
||||
|
@ -286,18 +299,28 @@ def post_vote(post_id: int, vote_direction):
|
|||
post.up_votes += 1
|
||||
post.down_votes -= 1
|
||||
post.score += 2
|
||||
upvoted_class = 'voted_up'
|
||||
else:
|
||||
if vote_direction == 'upvote':
|
||||
effect = 1
|
||||
post.up_votes += 1
|
||||
post.score += 1
|
||||
upvoted_class = 'voted_up'
|
||||
# Make 'hot' sort more spicy by amplifying the effect of early upvotes
|
||||
if post.up_votes + post.down_votes <= 10:
|
||||
post.score += 10
|
||||
elif post.up_votes + post.down_votes <= 30:
|
||||
post.score += 5
|
||||
elif post.up_votes + post.down_votes <= 60:
|
||||
post.score += 2
|
||||
else:
|
||||
post.score += 1
|
||||
else:
|
||||
effect = -1
|
||||
post.down_votes += 1
|
||||
post.score -= 1
|
||||
downvoted_class = 'voted_down'
|
||||
if post.up_votes + post.down_votes <= 30:
|
||||
post.score -= 5
|
||||
elif post.up_votes + post.down_votes <= 60:
|
||||
post.score -= 2
|
||||
else:
|
||||
post.score -= 1
|
||||
vote = PostVote(user_id=current_user.id, post_id=post.id, author_id=post.author.id,
|
||||
effect=effect)
|
||||
# upvotes do not increase reputation in low quality communities
|
||||
|
@ -346,17 +369,25 @@ def post_vote(post_id: int, vote_direction):
|
|||
current_user.recalculate_attitude()
|
||||
db.session.commit()
|
||||
post.flush_cache()
|
||||
|
||||
recently_upvoted = []
|
||||
recently_downvoted = []
|
||||
if vote_direction == 'upvote':
|
||||
recently_upvoted = [post_id]
|
||||
elif vote_direction == 'downvote':
|
||||
recently_downvoted = [post_id]
|
||||
cache.delete_memoized(recently_upvoted_posts, current_user.id)
|
||||
cache.delete_memoized(recently_downvoted_posts, current_user.id)
|
||||
|
||||
template = 'post/_post_voting_buttons.html' if request.args.get('style', '') == '' else 'post/_post_voting_buttons_masonry.html'
|
||||
return render_template(template, post=post, community=post.community,
|
||||
upvoted_class=upvoted_class,
|
||||
downvoted_class=downvoted_class)
|
||||
return render_template(template, post=post, community=post.community, recently_upvoted=recently_upvoted,
|
||||
recently_downvoted=recently_downvoted)
|
||||
|
||||
|
||||
@bp.route('/comment/<int:comment_id>/<vote_direction>', methods=['POST'])
|
||||
@login_required
|
||||
@validation_required
|
||||
def comment_vote(comment_id, vote_direction):
|
||||
upvoted_class = downvoted_class = ''
|
||||
comment = PostReply.query.get_or_404(comment_id)
|
||||
existing_vote = PostReplyVote.query.filter_by(user_id=current_user.id, post_reply_id=comment.id).first()
|
||||
if existing_vote:
|
||||
|
@ -423,9 +454,20 @@ def comment_vote(comment_id, vote_direction):
|
|||
db.session.commit()
|
||||
|
||||
comment.post.flush_cache()
|
||||
|
||||
recently_upvoted = []
|
||||
recently_downvoted = []
|
||||
if vote_direction == 'upvote':
|
||||
recently_upvoted = [comment_id]
|
||||
elif vote_direction == 'downvote':
|
||||
recently_downvoted = [comment_id]
|
||||
cache.delete_memoized(recently_upvoted_post_replies, current_user.id)
|
||||
cache.delete_memoized(recently_downvoted_post_replies, current_user.id)
|
||||
|
||||
return render_template('post/_comment_voting_buttons.html', comment=comment,
|
||||
upvoted_class=upvoted_class,
|
||||
downvoted_class=downvoted_class, community=comment.community)
|
||||
recently_upvoted_replies=recently_upvoted,
|
||||
recently_downvoted_replies=recently_downvoted,
|
||||
community=comment.community)
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/comment/<int:comment_id>')
|
||||
|
@ -654,11 +696,82 @@ def post_reply_options(post_id: int, comment_id: int):
|
|||
)
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/edit', methods=['GET', 'POST'])
|
||||
@bp.route('/post/<int:post_id>/edit', methods=['GET'])
|
||||
@login_required
|
||||
def post_edit(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
form = CreatePostForm()
|
||||
if post.type == POST_TYPE_ARTICLE:
|
||||
return redirect(url_for('post.post_edit_discussion_post', post_id=post_id))
|
||||
elif post.type == POST_TYPE_LINK:
|
||||
return redirect(url_for('post.post_edit_link_post', post_id=post_id))
|
||||
elif post.type == POST_TYPE_IMAGE:
|
||||
return redirect(url_for('post.post_edit_image_post', post_id=post_id))
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/edit_discussion', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def post_edit_discussion_post(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
form = CreateDiscussionForm()
|
||||
del form.communities
|
||||
|
||||
mods = post.community.moderators()
|
||||
if post.community.private_mods:
|
||||
mod_list = []
|
||||
else:
|
||||
mod_user_ids = [mod.user_id for mod in mods]
|
||||
mod_list = User.query.filter(User.id.in_(mod_user_ids)).all()
|
||||
|
||||
if post.user_id == current_user.id or post.community.is_moderator() or current_user.is_admin():
|
||||
if g.site.enable_nsfl is False:
|
||||
form.nsfl.render_kw = {'disabled': True}
|
||||
if post.community.nsfw:
|
||||
form.nsfw.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
if post.community.nsfl:
|
||||
form.nsfl.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
|
||||
if form.validate_on_submit():
|
||||
save_post(form, post, 'discussion')
|
||||
post.community.last_active = utcnow()
|
||||
post.edited_at = utcnow()
|
||||
db.session.commit()
|
||||
|
||||
post.flush_cache()
|
||||
flash(_('Your changes have been saved.'), 'success')
|
||||
|
||||
# federate edit
|
||||
if not post.community.local_only:
|
||||
federate_post_update(post)
|
||||
|
||||
return redirect(url_for('activitypub.post_ap', post_id=post.id))
|
||||
else:
|
||||
form.discussion_title.data = post.title
|
||||
form.discussion_body.data = post.body
|
||||
form.notify_author.data = post.notify_author
|
||||
form.nsfw.data = post.nsfw
|
||||
form.nsfl.data = post.nsfl
|
||||
form.sticky.data = post.sticky
|
||||
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
|
||||
form.sticky.render_kw = {'disabled': True}
|
||||
return render_template('post/post_edit_discussion.html', title=_('Edit post'), form=form, post=post,
|
||||
markdown_editor=current_user.markdown_editor, mods=mod_list,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||
)
|
||||
else:
|
||||
abort(401)
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/edit_image', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def post_edit_image_post(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
form = CreateImageForm()
|
||||
del form.communities
|
||||
|
||||
mods = post.community.moderators()
|
||||
|
@ -678,11 +791,10 @@ def post_edit(post_id: int):
|
|||
form.nsfl.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
|
||||
#form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
|
||||
old_url = post.url
|
||||
|
||||
if form.validate_on_submit():
|
||||
save_post(form, post)
|
||||
save_post(form, post, 'image')
|
||||
post.community.last_active = utcnow()
|
||||
post.edited_at = utcnow()
|
||||
db.session.commit()
|
||||
|
@ -714,104 +826,20 @@ def post_edit(post_id: int):
|
|||
# federate edit
|
||||
|
||||
if not post.community.local_only:
|
||||
page_json = {
|
||||
'type': 'Page',
|
||||
'id': post.ap_id,
|
||||
'attributedTo': current_user.ap_profile_id,
|
||||
'to': [
|
||||
post.community.ap_profile_id,
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'name': post.title,
|
||||
'cc': [],
|
||||
'content': post.body_html if post.body_html else '',
|
||||
'mediaType': 'text/html',
|
||||
'source': {
|
||||
'content': post.body if post.body else '',
|
||||
'mediaType': 'text/markdown'
|
||||
},
|
||||
'attachment': [],
|
||||
'commentsEnabled': post.comments_enabled,
|
||||
'sensitive': post.nsfw,
|
||||
'nsfl': post.nsfl,
|
||||
'stickied': post.sticky,
|
||||
'published': ap_datetime(post.posted_at),
|
||||
'updated': ap_datetime(post.edited_at),
|
||||
'audience': post.community.ap_profile_id
|
||||
}
|
||||
update_json = {
|
||||
'id': f"https://{current_app.config['SERVER_NAME']}/activities/update/{gibberish(15)}",
|
||||
'type': 'Update',
|
||||
'actor': current_user.profile_id(),
|
||||
'audience': post.community.profile_id(),
|
||||
'to': [post.community.profile_id(), 'https://www.w3.org/ns/activitystreams#Public'],
|
||||
'published': ap_datetime(utcnow()),
|
||||
'cc': [
|
||||
current_user.followers_url()
|
||||
],
|
||||
'object': page_json,
|
||||
}
|
||||
if post.type == POST_TYPE_LINK:
|
||||
page_json['attachment'] = [{'href': post.url, 'type': 'Link'}]
|
||||
elif post.image_id:
|
||||
if post.image.file_path:
|
||||
image_url = post.image.file_path.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/")
|
||||
elif post.image.thumbnail_path:
|
||||
image_url = post.image.thumbnail_path.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/")
|
||||
else:
|
||||
image_url = post.image.source_url
|
||||
# NB image is a dict while attachment is a list of dicts (usually just one dict in the list)
|
||||
page_json['image'] = {'type': 'Image', 'url': image_url}
|
||||
if post.type == POST_TYPE_IMAGE:
|
||||
page_json['attachment'] = [{'type': 'Link', 'href': post.image.source_url}] # source_url is always a https link, no need for .replace() as done above
|
||||
|
||||
if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
|
||||
success = post_request(post.community.ap_inbox_url, update_json, current_user.private_key,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
if not success:
|
||||
flash('Failed to send edit to remote server', 'error')
|
||||
else: # local community - send it to followers on remote instances
|
||||
announce = {
|
||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}",
|
||||
"type": 'Announce',
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"actor": post.community.ap_profile_id,
|
||||
"cc": [
|
||||
post.community.ap_followers_url
|
||||
],
|
||||
'@context': default_context(),
|
||||
'object': update_json
|
||||
}
|
||||
|
||||
for instance in post.community.following_instances():
|
||||
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
|
||||
send_to_remote_instance(instance.id, post.community.id, announce)
|
||||
federate_post_update(post)
|
||||
|
||||
return redirect(url_for('activitypub.post_ap', post_id=post.id))
|
||||
else:
|
||||
if post.type == constants.POST_TYPE_ARTICLE:
|
||||
form.post_type.data = 'discussion'
|
||||
form.discussion_title.data = post.title
|
||||
form.discussion_body.data = post.body
|
||||
elif post.type == constants.POST_TYPE_LINK:
|
||||
form.post_type.data = 'link'
|
||||
form.link_title.data = post.title
|
||||
form.link_body.data = post.body
|
||||
form.link_url.data = post.url
|
||||
elif post.type == constants.POST_TYPE_IMAGE:
|
||||
form.post_type.data = 'image'
|
||||
form.image_title.data = post.title
|
||||
form.image_body.data = post.body
|
||||
form.image_alt_text.data = post.image.alt_text
|
||||
form.image_title.data = post.title
|
||||
form.image_body.data = post.body
|
||||
form.image_alt_text.data = post.image.alt_text
|
||||
form.notify_author.data = post.notify_author
|
||||
form.nsfw.data = post.nsfw
|
||||
form.nsfl.data = post.nsfl
|
||||
form.sticky.data = post.sticky
|
||||
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
|
||||
form.sticky.render_kw = {'disabled': True}
|
||||
return render_template('post/post_edit.html', title=_('Edit post'), form=form, post=post,
|
||||
return render_template('post/post_edit_image.html', title=_('Edit post'), form=form, post=post,
|
||||
markdown_editor=current_user.markdown_editor, mods=mod_list,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
|
@ -821,6 +849,168 @@ def post_edit(post_id: int):
|
|||
abort(401)
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/edit_link', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def post_edit_link_post(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
form = CreateLinkForm()
|
||||
del form.communities
|
||||
|
||||
mods = post.community.moderators()
|
||||
if post.community.private_mods:
|
||||
mod_list = []
|
||||
else:
|
||||
mod_user_ids = [mod.user_id for mod in mods]
|
||||
mod_list = User.query.filter(User.id.in_(mod_user_ids)).all()
|
||||
|
||||
if post.user_id == current_user.id or post.community.is_moderator() or current_user.is_admin():
|
||||
if g.site.enable_nsfl is False:
|
||||
form.nsfl.render_kw = {'disabled': True}
|
||||
if post.community.nsfw:
|
||||
form.nsfw.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
if post.community.nsfl:
|
||||
form.nsfl.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
|
||||
old_url = post.url
|
||||
|
||||
if form.validate_on_submit():
|
||||
save_post(form, post, 'link')
|
||||
post.community.last_active = utcnow()
|
||||
post.edited_at = utcnow()
|
||||
db.session.commit()
|
||||
|
||||
if post.url != old_url:
|
||||
if post.cross_posts is not None:
|
||||
old_cross_posts = Post.query.filter(Post.id.in_(post.cross_posts)).all()
|
||||
post.cross_posts.clear()
|
||||
for ocp in old_cross_posts:
|
||||
if ocp.cross_posts is not None:
|
||||
ocp.cross_posts.remove(post.id)
|
||||
|
||||
new_cross_posts = Post.query.filter(Post.id != post.id, Post.url == post.url,
|
||||
Post.posted_at > post.edited_at - timedelta(days=6)).all()
|
||||
for ncp in new_cross_posts:
|
||||
if ncp.cross_posts is None:
|
||||
ncp.cross_posts = [post.id]
|
||||
else:
|
||||
ncp.cross_posts.append(post.id)
|
||||
if post.cross_posts is None:
|
||||
post.cross_posts = [ncp.id]
|
||||
else:
|
||||
post.cross_posts.append(ncp.id)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
post.flush_cache()
|
||||
flash(_('Your changes have been saved.'), 'success')
|
||||
# federate edit
|
||||
|
||||
if not post.community.local_only:
|
||||
federate_post_update(post)
|
||||
|
||||
return redirect(url_for('activitypub.post_ap', post_id=post.id))
|
||||
else:
|
||||
form.link_title.data = post.title
|
||||
form.link_body.data = post.body
|
||||
form.link_url.data = post.url
|
||||
form.notify_author.data = post.notify_author
|
||||
form.nsfw.data = post.nsfw
|
||||
form.nsfl.data = post.nsfl
|
||||
form.sticky.data = post.sticky
|
||||
if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()):
|
||||
form.sticky.render_kw = {'disabled': True}
|
||||
return render_template('post/post_edit_link.html', title=_('Edit post'), form=form, post=post,
|
||||
markdown_editor=current_user.markdown_editor, mods=mod_list,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||
)
|
||||
else:
|
||||
abort(401)
|
||||
|
||||
|
||||
def federate_post_update(post):
|
||||
page_json = {
|
||||
'type': 'Page',
|
||||
'id': post.ap_id,
|
||||
'attributedTo': current_user.ap_profile_id,
|
||||
'to': [
|
||||
post.community.ap_profile_id,
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'name': post.title,
|
||||
'cc': [],
|
||||
'content': post.body_html if post.body_html else '',
|
||||
'mediaType': 'text/html',
|
||||
'source': {
|
||||
'content': post.body if post.body else '',
|
||||
'mediaType': 'text/markdown'
|
||||
},
|
||||
'attachment': [],
|
||||
'commentsEnabled': post.comments_enabled,
|
||||
'sensitive': post.nsfw,
|
||||
'nsfl': post.nsfl,
|
||||
'stickied': post.sticky,
|
||||
'published': ap_datetime(post.posted_at),
|
||||
'updated': ap_datetime(post.edited_at),
|
||||
'audience': post.community.ap_profile_id
|
||||
}
|
||||
update_json = {
|
||||
'id': f"https://{current_app.config['SERVER_NAME']}/activities/update/{gibberish(15)}",
|
||||
'type': 'Update',
|
||||
'actor': current_user.profile_id(),
|
||||
'audience': post.community.profile_id(),
|
||||
'to': [post.community.profile_id(), 'https://www.w3.org/ns/activitystreams#Public'],
|
||||
'published': ap_datetime(utcnow()),
|
||||
'cc': [
|
||||
current_user.followers_url()
|
||||
],
|
||||
'object': page_json,
|
||||
}
|
||||
if post.type == POST_TYPE_LINK:
|
||||
page_json['attachment'] = [{'href': post.url, 'type': 'Link'}]
|
||||
elif post.image_id:
|
||||
if post.image.file_path:
|
||||
image_url = post.image.file_path.replace('app/static/',
|
||||
f"https://{current_app.config['SERVER_NAME']}/static/")
|
||||
elif post.image.thumbnail_path:
|
||||
image_url = post.image.thumbnail_path.replace('app/static/',
|
||||
f"https://{current_app.config['SERVER_NAME']}/static/")
|
||||
else:
|
||||
image_url = post.image.source_url
|
||||
# NB image is a dict while attachment is a list of dicts (usually just one dict in the list)
|
||||
page_json['image'] = {'type': 'Image', 'url': image_url}
|
||||
if post.type == POST_TYPE_IMAGE:
|
||||
page_json['attachment'] = [{'type': 'Link',
|
||||
'href': post.image.source_url}] # source_url is always a https link, no need for .replace() as done above
|
||||
if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
|
||||
success = post_request(post.community.ap_inbox_url, update_json, current_user.private_key,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
if not success:
|
||||
flash('Failed to send edit to remote server', 'error')
|
||||
else: # local community - send it to followers on remote instances
|
||||
announce = {
|
||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}",
|
||||
"type": 'Announce',
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"actor": post.community.ap_profile_id,
|
||||
"cc": [
|
||||
post.community.ap_followers_url
|
||||
],
|
||||
'@context': default_context(),
|
||||
'object': update_json
|
||||
}
|
||||
|
||||
for instance in post.community.following_instances():
|
||||
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(
|
||||
instance.domain):
|
||||
send_to_remote_instance(instance.id, post.community.id, announce)
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/delete', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def post_delete(post_id: int):
|
||||
|
@ -888,7 +1078,12 @@ def post_delete(post_id: int):
|
|||
def post_report(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
form = ReportPostForm()
|
||||
if post.reports == -1: # When a mod decides to ignore future reports, post.reports is set to -1
|
||||
flash(_('Moderators have already assessed reports regarding this post, no further reports are necessary.'), 'warning')
|
||||
if form.validate_on_submit():
|
||||
if post.reports == -1:
|
||||
flash(_('Post has already been reported, thank you!'))
|
||||
return redirect(post.community.local_url())
|
||||
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
||||
type=1, reporter_id=current_user.id, suspect_user_id=post.author.id, suspect_post_id=post.id,
|
||||
suspect_community_id=post.community.id, in_community_id=post.community.id, source_instance_id=1)
|
||||
|
@ -911,9 +1106,29 @@ def post_report(post_id: int):
|
|||
admin.unread_notifications += 1
|
||||
db.session.commit()
|
||||
|
||||
# todo: federate report to originating instance
|
||||
# federate report to community instance
|
||||
if not post.community.is_local() and form.report_remote.data:
|
||||
...
|
||||
summary = form.reasons_to_string(form.reasons.data)
|
||||
if form.description.data:
|
||||
summary += ' - ' + form.description.data
|
||||
report_json = {
|
||||
"actor": current_user.profile_id(),
|
||||
"audience": post.community.profile_id(),
|
||||
"content": None,
|
||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/flag/{gibberish(15)}",
|
||||
"object": post.ap_id,
|
||||
"summary": summary,
|
||||
"to": [
|
||||
post.community.profile_id()
|
||||
],
|
||||
"type": "Flag"
|
||||
}
|
||||
instance = Instance.query.get(post.community.instance_id)
|
||||
if post.community.ap_inbox_url and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
|
||||
success = post_request(post.community.ap_inbox_url, report_json, current_user.private_key,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
if not success:
|
||||
flash('Failed to send report to remote server', 'error')
|
||||
|
||||
flash(_('Post has been reported, thank you!'))
|
||||
return redirect(post.community.local_url())
|
||||
|
@ -992,7 +1207,16 @@ def post_reply_report(post_id: int, comment_id: int):
|
|||
post = Post.query.get_or_404(post_id)
|
||||
post_reply = PostReply.query.get_or_404(comment_id)
|
||||
form = ReportPostForm()
|
||||
|
||||
if post_reply.reports == -1: # When a mod decides to ignore future reports, post_reply.reports is set to -1
|
||||
flash(_('Moderators have already assessed reports regarding this comment, no further reports are necessary.'), 'warning')
|
||||
|
||||
if form.validate_on_submit():
|
||||
|
||||
if post_reply.reports == -1:
|
||||
flash(_('Comment has already been reported, thank you!'))
|
||||
return redirect(post.community.local_url())
|
||||
|
||||
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
||||
type=2, reporter_id=current_user.id, suspect_post_id=post.id, suspect_community_id=post.community.id,
|
||||
suspect_user_id=post_reply.author.id, suspect_post_reply_id=post_reply.id, in_community_id=post.community.id,
|
||||
|
@ -1006,7 +1230,7 @@ def post_reply_report(post_id: int, comment_id: int):
|
|||
url=f"https://{current_app.config['SERVER_NAME']}/comment/{post_reply.id}",
|
||||
author_id=current_user.id)
|
||||
db.session.add(notification)
|
||||
already_notified.add(mod.id)
|
||||
already_notified.add(mod.user_id)
|
||||
post_reply.reports += 1
|
||||
# todo: only notify admins for certain types of report
|
||||
for admin in Site.admins():
|
||||
|
@ -1016,9 +1240,30 @@ def post_reply_report(post_id: int, comment_id: int):
|
|||
admin.unread_notifications += 1
|
||||
db.session.commit()
|
||||
|
||||
# todo: federate report to originating instance
|
||||
# federate report to originating instance
|
||||
if not post.community.is_local() and form.report_remote.data:
|
||||
...
|
||||
summary = form.reasons_to_string(form.reasons.data)
|
||||
if form.description.data:
|
||||
summary += ' - ' + form.description.data
|
||||
report_json = {
|
||||
"actor": current_user.profile_id(),
|
||||
"audience": post.community.profile_id(),
|
||||
"content": None,
|
||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/flag/{gibberish(15)}",
|
||||
"object": post_reply.ap_id,
|
||||
"summary": summary,
|
||||
"to": [
|
||||
post.community.profile_id()
|
||||
],
|
||||
"type": "Flag"
|
||||
}
|
||||
instance = Instance.query.get(post.community.instance_id)
|
||||
if post.community.ap_inbox_url and not current_user.has_blocked_instance(
|
||||
instance.id) and not instance_banned(instance.domain):
|
||||
success = post_request(post.community.ap_inbox_url, report_json, current_user.private_key,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
if not success:
|
||||
flash('Failed to send report to remote server', 'error')
|
||||
|
||||
flash(_('Comment has been reported, thank you!'))
|
||||
return redirect(post.community.local_url())
|
||||
|
|
|
@ -5,7 +5,7 @@ from sqlalchemy import desc, text, or_
|
|||
|
||||
from app import db
|
||||
from app.models import PostReply
|
||||
from app.utils import blocked_instances
|
||||
from app.utils import blocked_instances, blocked_users
|
||||
|
||||
|
||||
# replies to a post, in a tree, sorted by a variety of methods
|
||||
|
@ -17,6 +17,9 @@ def post_replies(post_id: int, sort_by: str, show_first: int = 0) -> List[PostRe
|
|||
comments = comments.filter(or_(PostReply.instance_id.not_in(instance_ids), PostReply.instance_id == None))
|
||||
if current_user.ignore_bots:
|
||||
comments = comments.filter(PostReply.from_bot == False)
|
||||
blocked_accounts = blocked_users(current_user.id)
|
||||
if blocked_accounts:
|
||||
comments = comments.filter(PostReply.user_id.not_in(blocked_accounts))
|
||||
if sort_by == 'hot':
|
||||
comments = comments.order_by(desc(PostReply.ranking))
|
||||
elif sort_by == 'top':
|
||||
|
|
|
@ -6,7 +6,7 @@ from sqlalchemy import or_
|
|||
from app.models import Post
|
||||
from app.search import bp
|
||||
from app.utils import moderating_communities, joined_communities, render_template, blocked_domains, blocked_instances, \
|
||||
communities_banned_from
|
||||
communities_banned_from, recently_upvoted_posts, recently_downvoted_posts, blocked_users
|
||||
|
||||
|
||||
@bp.route('/search', methods=['GET', 'POST'])
|
||||
|
@ -30,6 +30,10 @@ def run_search():
|
|||
instance_ids = blocked_instances(current_user.id)
|
||||
if instance_ids:
|
||||
posts = posts.filter(or_(Post.instance_id.not_in(instance_ids), Post.instance_id == None))
|
||||
# filter blocked users
|
||||
blocked_accounts = blocked_users(current_user.id)
|
||||
if blocked_accounts:
|
||||
posts = posts.filter(Post.user_id.not_in(blocked_accounts))
|
||||
banned_from = communities_banned_from(current_user.id)
|
||||
if banned_from:
|
||||
posts = posts.filter(Post.community_id.not_in(banned_from))
|
||||
|
@ -46,8 +50,18 @@ def run_search():
|
|||
next_url = url_for('search.run_search', page=posts.next_num, q=q) if posts.has_next else None
|
||||
prev_url = url_for('search.run_search', page=posts.prev_num, q=q) if posts.has_prev and page != 1 else None
|
||||
|
||||
# Voting history
|
||||
if current_user.is_authenticated:
|
||||
recently_upvoted = recently_upvoted_posts(current_user.id)
|
||||
recently_downvoted = recently_downvoted_posts(current_user.id)
|
||||
else:
|
||||
recently_upvoted = []
|
||||
recently_downvoted = []
|
||||
|
||||
return render_template('search/results.html', title=_('Search results for %(q)s', q=q), posts=posts, q=q,
|
||||
next_url=next_url, prev_url=prev_url, show_post_community=True,
|
||||
recently_upvoted=recently_upvoted,
|
||||
recently_downvoted=recently_downvoted,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id()),
|
||||
site=g.site)
|
||||
|
|
|
@ -501,6 +501,14 @@ fieldset legend {
|
|||
.form-group {
|
||||
margin-bottom: 1.1rem;
|
||||
}
|
||||
.form-group.required label:after {
|
||||
content: "*";
|
||||
color: red;
|
||||
margin-left: 2px;
|
||||
font-size: 80%;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.card {
|
||||
max-width: 350px;
|
||||
|
@ -977,7 +985,8 @@ fieldset legend {
|
|||
}
|
||||
.voting_buttons_new .upvote_button, .voting_buttons_new .downvote_button {
|
||||
display: inline-block;
|
||||
padding: 5px 15px;
|
||||
padding: 5px 0 5px 3px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
|
@ -1004,11 +1013,6 @@ fieldset legend {
|
|||
.voting_buttons_new .upvote_button {
|
||||
top: 1px;
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.voting_buttons_new .upvote_button {
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
.voting_buttons_new .upvote_button .htmx-indicator {
|
||||
left: 13px;
|
||||
top: 7px;
|
||||
|
@ -1018,14 +1022,7 @@ fieldset legend {
|
|||
}
|
||||
.voting_buttons_new .downvote_button .htmx-indicator {
|
||||
left: 12px;
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.voting_buttons_new .downvote_button {
|
||||
padding-left: 5px;
|
||||
}
|
||||
.voting_buttons_new .downvote_button .htmx-indicator {
|
||||
left: 2px;
|
||||
}
|
||||
top: 5px;
|
||||
}
|
||||
.voting_buttons_new .htmx-indicator {
|
||||
position: absolute;
|
||||
|
@ -1117,7 +1114,6 @@ fieldset legend {
|
|||
|
||||
.comment {
|
||||
clear: both;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 15px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
@ -1170,7 +1166,7 @@ fieldset legend {
|
|||
}
|
||||
.comment .comment_actions a {
|
||||
text-decoration: none;
|
||||
padding: 5px 0;
|
||||
padding: 0;
|
||||
}
|
||||
.comment .comment_actions .hide_button {
|
||||
display: inline-block;
|
||||
|
|
|
@ -65,6 +65,19 @@ html {
|
|||
|
||||
.form-group {
|
||||
margin-bottom: 1.1rem;
|
||||
|
||||
&.required {
|
||||
label {
|
||||
&:after {
|
||||
content: '*';
|
||||
color: red;
|
||||
margin-left: 2px;
|
||||
font-size: 80%;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
|
@ -610,7 +623,8 @@ html {
|
|||
|
||||
.upvote_button, .downvote_button {
|
||||
display: inline-block;
|
||||
padding: 5px 15px;
|
||||
padding: 5px 0 5px 3px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
color: rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));
|
||||
|
@ -641,9 +655,7 @@ html {
|
|||
|
||||
.upvote_button {
|
||||
top: 1px;
|
||||
@include breakpoint(laptop) {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.htmx-indicator {
|
||||
left: 13px;
|
||||
top: 7px;
|
||||
|
@ -654,14 +666,8 @@ html {
|
|||
top: 1px;
|
||||
.htmx-indicator {
|
||||
left: 12px;
|
||||
top: 5px;
|
||||
}
|
||||
@include breakpoint(laptop) {
|
||||
padding-left: 5px;
|
||||
.htmx-indicator {
|
||||
left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.htmx-indicator{
|
||||
|
@ -763,7 +769,6 @@ html {
|
|||
|
||||
.comment {
|
||||
clear: both;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 15px;
|
||||
padding-top: 8px;
|
||||
|
||||
|
@ -823,7 +828,7 @@ html {
|
|||
position: relative;
|
||||
a {
|
||||
text-decoration: none;
|
||||
padding: 5px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.hide_button {
|
||||
|
|
|
@ -693,7 +693,7 @@ div.navbar {
|
|||
.comment_actions_link {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: 3px;
|
||||
right: -16px;
|
||||
width: 41px;
|
||||
text-decoration: none;
|
||||
|
|
|
@ -284,7 +284,7 @@ div.navbar {
|
|||
.comment_actions_link {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
top: 3px;
|
||||
right: -16px;
|
||||
width: 41px;
|
||||
text-decoration: none;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<nav class="mb-4">
|
||||
<nav class="mb-1">
|
||||
<h2 class="visually-hidden">{{ _('Admin navigation') }}</h2>
|
||||
<a href="{{ url_for('admin.admin_home') }}">{{ _('Admin home') }}</a> |
|
||||
<a href="{{ url_for('admin.admin_site') }}">{{ _('Site profile') }}</a> |
|
||||
|
|
|
@ -4,16 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_activities' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Activities') }}</h1>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>When</th>
|
||||
|
@ -62,4 +58,11 @@
|
|||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
||||
|
|
|
@ -4,20 +4,24 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_activities' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Activity JSON') }}</h1>
|
||||
<code><pre>{{ activity_json_data | tojson(indent=2) }}</pre></code>
|
||||
{% if current_app.debug %}
|
||||
<p><a class="btn btn-warning" href="{{ url_for('admin.activity_replay', activity_id=activity.id) }}">Re-submit this activity</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,17 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
{% set active_child = 'admin_users' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
<h3>{{ _('Add new user') }}</h3>
|
||||
<h1>{{ _('Add new user') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" id="add_local_user_form">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.user_name) }}
|
||||
|
@ -44,4 +39,11 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,16 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_approve_registrations' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Registrations') }}</h1>
|
||||
{% if registrations %}
|
||||
<p>{{ _('When registering, people are asked "%(question)s".', question=site.application_question) }} </p>
|
||||
<form method="get">
|
||||
|
@ -52,4 +48,11 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,16 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_communities' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Communities') }}</h1>
|
||||
<form method="get">
|
||||
<input type="search" name="search"> <input type="submit" name="submit" value="Search">
|
||||
</form>
|
||||
|
@ -59,4 +55,11 @@
|
|||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,17 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
{% set active_child = 'admin_communities' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
<h3>{{ _('Edit %(community_name)s', community_name=community.display_name()) }}</h3>
|
||||
<h1>{{ _('Edit %(community_name)s', community_name=community.display_name()) }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" id="add_local_community_form">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.title) }}
|
||||
|
@ -59,4 +54,11 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,20 +4,22 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_topics' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
{% if topic %}
|
||||
<h1>{{ _('Edit %(topic_name)s', topic_name=topic.name) }}</h1>
|
||||
{% endif %}
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
{% if topic %}
|
||||
<h3>{{ _('Edit %(topic_name)s', topic_name=topic.name) }}</h3>
|
||||
{% endif %}
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,17 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
{% set active_child = 'admin_users' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-login mx-auto">
|
||||
<h3>{{ _('Edit %(user_name)s (%(display_name)s)', user_name=user.user_name, display_name=user.display_name()) }}</h3>
|
||||
<h1>{{ _('Edit %(user_name)s (%(display_name)s)', user_name=user.user_name, display_name=user.display_name()) }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" id="add_local_user_form">
|
||||
{{ form.csrf_token() }}
|
||||
{{ user.about_html|safe if user.about_html }}
|
||||
|
@ -48,4 +43,11 @@
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,19 +4,21 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_federation' %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Federation') }}</h1>
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,18 +4,20 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_misc' %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Misc settings') }}</h1>
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -1,17 +1,20 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_newsletter' %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Newsletter') }}</h1>
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,17 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_content_trash' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Most downvoted in the last 3 days') }}</h1>
|
||||
<h1>{{ _('Most downvoted posts in the last 3 days') }}</h1>
|
||||
<div class="post_list">
|
||||
{% for post in posts.items %}
|
||||
{% include 'post/_post_teaser.html' %}
|
||||
|
@ -34,4 +29,11 @@
|
|||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,16 +4,12 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_reports' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Reports') }}</h1>
|
||||
<form method="get">
|
||||
<input type="search" name="search" value="{{ search }}">
|
||||
<input type="radio" name="local_remote" value="local" id="local_remote_local" {{ 'checked' if local_remote == 'local' }}><label for="local_remote_local"> Local</label>
|
||||
|
@ -66,4 +62,11 @@
|
|||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,17 +4,13 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
{% set active_child = 'admin_site' %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Site profile') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.name) }}
|
||||
|
@ -28,4 +24,11 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,13 +4,9 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_topics' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% macro render_topic(topic, depth) %}
|
||||
<tr>
|
||||
<td nowrap="nowrap">{{ '--' * depth }} {{ topic['topic'].name }}</td>
|
||||
|
@ -32,7 +28,7 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<p><a href="{{ url_for('admin.admin_topic_add') }}" class="btn btn-primary">{{ _('Add topic') }}</a></p>
|
||||
<h1><a href="{{ url_for('admin.admin_topic_add') }}" class="btn btn-primary" style="float: right;">{{ _('Add topic') }}</a>{{ _('Topics') }}</h1>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
|
@ -45,4 +41,11 @@
|
|||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -4,21 +4,19 @@
|
|||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% set active_child = 'admin_users' %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{{ _('Users') }}</h1>
|
||||
<a class="btn btn-primary" href="{{ url_for('admin.admin_users_add') }}" style="float: right;">{{ _('Add local user') }}</a>
|
||||
<form method="get">
|
||||
<input type="search" name="search" value="{{ search }}">
|
||||
<input type="radio" name="local_remote" value="local" id="local_remote_local" {{ 'checked' if local_remote == 'local' }}><label for="local_remote_local"> Local</label>
|
||||
<input type="radio" name="local_remote" value="remote" id="local_remote_remote" {{ 'checked' if local_remote == 'remote' }}><label for="local_remote_remote"> Remote</label>
|
||||
<input type="radio" name="type" value="bad_rep" id="type_bad_rep" {{ 'checked' if type == 'bad_rep' }}><label for="type_bad_rep"> Bad rep</label>
|
||||
<input type="radio" name="type" value="bad_attitude" id="type_bad_attitude" {{ 'checked' if type == 'bad_attitude' }}><label for="type_bad_attitude"> Bad attitude</label>
|
||||
<input type="submit" name="submit" value="Search" class="btn btn-primary">
|
||||
</form>
|
||||
<table class="table table-striped mt-1">
|
||||
|
@ -77,4 +75,11 @@
|
|||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'admin/_nav.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endblock %}
|
|
@ -136,8 +136,8 @@
|
|||
<li class="nav-item dropdown{% if active_parent == 'communities' %} active{% endif %}">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="/topics" aria-haspopup="true" aria-expanded="false">{{ _('Topics') }}</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item{% if active_child == 'list_communities' %} active{% endif %}" href="/topics">{{ _('Browse by topic') }}</a></li>
|
||||
<li><a class="dropdown-item{% if active_child == 'list_topics' %} active{% endif %}" href="/communities">{{ _('All communities') }}</a></li>
|
||||
<li><a class="dropdown-item{% if active_child == 'list_topics' %} active{% endif %}" href="/topics">{{ _('Browse by topic') }}</a></li>
|
||||
<li><a class="dropdown-item{% if active_child == 'list_communities' %} active{% endif %}" href="/communities">{{ _('All communities') }}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item"><a class="nav-link" href="/auth/login">{{ _('Log in') }}</a></li>
|
||||
|
@ -182,7 +182,26 @@
|
|||
</li>
|
||||
<li class="nav-item"><a class="nav-link" href="/donate">{{ _('Donate') }}</a></li>
|
||||
{% if user_access('change instance settings', current_user.id) %}
|
||||
<li class="nav-item"><a class="nav-link" href="/admin/">{{ _('Admin') }}</a></li>
|
||||
<li class="nav-item dropdown{% if active_parent == 'admin' %} active{% endif %}">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="/admin/" aria-haspopup="true" aria-expanded="false">{{ _('Admin') }}</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_site' }}" href="{{ url_for('admin.admin_site') }}">{{ _('Site profile') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_misc' }}" href="{{ url_for('admin.admin_misc') }}">{{ _('Misc settings') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_communities' }}" href="{{ url_for('admin.admin_communities') }}">{{ _('Communities') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_topics' }}" href="{{ url_for('admin.admin_topics') }}">{{ _('Topics') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_users' }}" href="{{ url_for('admin.admin_users', local_remote='local') }}">{{ _('Users') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_users_trash' }}" href="{{ url_for('admin.admin_users_trash', local_remote='local') }}">{{ _('Monitoring - users') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_content_trash' }}" href="{{ url_for('admin.admin_content_trash') }}">{{ _('Monitoring - content') }}</a></li>
|
||||
{% if g.site.registration_mode == 'RequireApplication' %}
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_approve_registrations' }}" href="{{ url_for('admin.admin_approve_registrations') }}">{{ _('Registration applications') }}</a></li>
|
||||
{% endif %}
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_reports' }}" href="{{ url_for('admin.admin_reports') }}">{{ _('Moderation') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_federation' }}" href="{{ url_for('admin.admin_federation') }}">{{ _('Federation') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_newsletter' }}" href="{{ url_for('admin.newsletter') }}">{{ _('Newsletter') }}</a></li>
|
||||
<li><a class="dropdown-item{{ ' active' if active_child == 'admin_activities' }}" href="{{ url_for('admin.admin_activities') }}">{{ _('Activities') }}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
{% endif %}
|
||||
<li class="nav-item"><a class="nav-link" href="/auth/logout">{{ _('Log out') }}</a></li>
|
||||
<li class="nav-item d-none d-md-inline-block">
|
||||
|
|
95
app/templates/community/add_discussion_post.html
Normal file
95
app/templates/community/add_discussion_post.html
Normal file
|
@ -0,0 +1,95 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<h1>{{ _('Create post') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" role="form">
|
||||
{{ form.csrf_token() }}
|
||||
<div class="form-group">
|
||||
<label class="form-control-label" for="type_of_post">
|
||||
{{ _('Type of post') }}
|
||||
</label>
|
||||
<div id="type_of_post" class="btn-group flex-wrap" role="navigation">
|
||||
<a href="{{ url_for('community.add_discussion_post', actor=actor) }}" class="btn btn-primary" aria-label="{{ _('Start a discussion') }}">{{ _('Discussion') }}</a>
|
||||
<a href="{{ url_for('community.add_link_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share a link') }}">{{ _('Link') }}</a>
|
||||
<a href="{{ url_for('community.add_image_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share an image') }}">{{ _('Image') }}</a>
|
||||
<!-- <a href="#" class="btn" aria-label="{{ _('Create a poll') }}">{{ _('Poll') }}</a>
|
||||
<a href="#" class="btn" aria-label="{{ _('Create an event') }}">{{ _('Event') }}</a> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_field(form.communities) }}
|
||||
{{ render_field(form.discussion_title) }}
|
||||
{{ render_field(form.discussion_body) }}
|
||||
{% if not low_bandwidth %}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#discussion_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
});
|
||||
setupAutoResize('discussion_body');
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="discussion_body">{{ _('Enable markdown editor') }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3">
|
||||
{{ render_field(form.notify_author) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.sticky) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfw) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfl) }}
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<aside id="side_pane" class="col-12 col-md-4 side_pane" role="complementary">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ community.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ community.description_html|safe if community.description_html else '' }}</p>
|
||||
<p>{{ community.rules_html|safe if community.rules_html else '' }}</p>
|
||||
{% if len(mods) > 0 and not community.private_mods %}
|
||||
<h3>Moderators</h3>
|
||||
<ul class="moderator_list">
|
||||
{% for mod in mods %}
|
||||
<li>{{ render_username(mod) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if rss_feed %}
|
||||
<p class="mt-4">
|
||||
<a class="no-underline" href="{{ rss_feed }}" rel="nofollow"><span class="fe fe-rss"></span> RSS feed</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "_inoculation_links.html" %}
|
||||
</aside>
|
||||
</div>
|
||||
{% endblock %}
|
97
app/templates/community/add_image_post.html
Normal file
97
app/templates/community/add_image_post.html
Normal file
|
@ -0,0 +1,97 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<h1>{{ _('Create post') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" role="form">
|
||||
{{ form.csrf_token() }}
|
||||
<div class="form-group">
|
||||
<label class="form-control-label" for="type_of_post">
|
||||
{{ _('Type of post') }}
|
||||
</label>
|
||||
<div id="type_of_post" class="btn-group flex-wrap" role="navigation">
|
||||
<a href="{{ url_for('community.add_discussion_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Start a discussion') }}">{{ _('Discussion') }}</a>
|
||||
<a href="{{ url_for('community.add_link_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share a link') }}">{{ _('Link') }}</a>
|
||||
<a href="{{ url_for('community.add_image_post', actor=actor) }}" class="btn btn-primary" aria-label="{{ _('Share an image') }}">{{ _('Image') }}</a>
|
||||
<!-- <a href="#" class="btn" aria-label="{{ _('Create a poll') }}">{{ _('Poll') }}</a>
|
||||
<a href="#" class="btn" aria-label="{{ _('Create an event') }}">{{ _('Event') }}</a> -->
|
||||
</div>
|
||||
</div>
|
||||
{{ render_field(form.communities) }}
|
||||
{{ render_field(form.image_title) }}
|
||||
{{ render_field(form.image_file) }}
|
||||
{{ render_field(form.image_alt_text) }}
|
||||
<small class="field_hint">{{ _('Describe the image, to help visually impaired people.') }}</small>
|
||||
{{ render_field(form.image_body) }}
|
||||
{% if not low_bandwidth %}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#image_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
});
|
||||
setupAutoResize('image_body');
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="image_body">{{ _('Enable markdown editor') }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3">
|
||||
{{ render_field(form.notify_author) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.sticky) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfw) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfl) }}
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<aside id="side_pane" class="col-12 col-md-4 side_pane" role="complementary">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ community.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ community.description_html|safe if community.description_html else '' }}</p>
|
||||
<p>{{ community.rules_html|safe if community.rules_html else '' }}</p>
|
||||
{% if len(mods) > 0 and not community.private_mods %}
|
||||
<h3>Moderators</h3>
|
||||
<ul class="moderator_list">
|
||||
{% for mod in mods %}
|
||||
<li>{{ render_username(mod) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if rss_feed %}
|
||||
<p class="mt-4">
|
||||
<a class="no-underline" href="{{ rss_feed }}" rel="nofollow"><span class="fe fe-rss"></span> RSS feed</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "_inoculation_links.html" %}
|
||||
</aside>
|
||||
</div>
|
||||
{% endblock %}
|
96
app/templates/community/add_link_post.html
Normal file
96
app/templates/community/add_link_post.html
Normal file
|
@ -0,0 +1,96 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<h1>{{ _('Create post') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" role="form">
|
||||
{{ form.csrf_token() }}
|
||||
<div class="form-group">
|
||||
<label class="form-control-label" for="type_of_post">
|
||||
{{ _('Type of post') }}
|
||||
</label>
|
||||
<div id="type_of_post" class="btn-group flex-wrap" role="navigation">
|
||||
<a href="{{ url_for('community.add_discussion_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Start a discussion') }}">{{ _('Discussion') }}</a>
|
||||
<a href="{{ url_for('community.add_link_post', actor=actor) }}" class="btn btn-primary" aria-label="{{ _('Share a link') }}">{{ _('Link') }}</a>
|
||||
<a href="{{ url_for('community.add_image_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share an image') }}">{{ _('Image') }}</a>
|
||||
<!-- <a href="#" class="btn" aria-label="{{ _('Create a poll') }}">{{ _('Poll') }}</a>
|
||||
<a href="#" class="btn" aria-label="{{ _('Create an event') }}">{{ _('Event') }}</a> -->
|
||||
</div>
|
||||
</div>
|
||||
{{ render_field(form.communities) }}
|
||||
|
||||
{{ render_field(form.link_title) }}
|
||||
{{ render_field(form.link_url) }}
|
||||
{{ render_field(form.link_body) }}
|
||||
{% if not low_bandwidth %}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#link_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
});
|
||||
setupAutoResize('link_body');
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="link_body">{{ _('Enable markdown editor') }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3">
|
||||
{{ render_field(form.notify_author) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.sticky) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfw) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfl) }}
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<aside id="side_pane" class="col-12 col-md-4 side_pane" role="complementary">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ community.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ community.description_html|safe if community.description_html else '' }}</p>
|
||||
<p>{{ community.rules_html|safe if community.rules_html else '' }}</p>
|
||||
{% if len(mods) > 0 and not community.private_mods %}
|
||||
<h3>Moderators</h3>
|
||||
<ul class="moderator_list">
|
||||
{% for mod in mods %}
|
||||
<li>{{ render_username(mod) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if rss_feed %}
|
||||
<p class="mt-4">
|
||||
<a class="no-underline" href="{{ rss_feed }}" rel="nofollow"><span class="fe fe-rss"></span> RSS feed</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "_inoculation_links.html" %}
|
||||
</aside>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,145 +0,0 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<h1>{{ _('Create post') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" role="form">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.communities) }}
|
||||
<nav id="post_type_chooser">
|
||||
<div class="nav nav-tabs nav-justified" id="typeTab" role="tablist">
|
||||
<button class="nav-link active" id="discussion-tab" data-bs-toggle="tab" data-bs-target="#discussion-tab-pane"
|
||||
type="button" role="tab" aria-controls="discussion-tab-pane" aria-selected="true">Discussion</button>
|
||||
<button class="nav-link" id="link-tab" data-bs-toggle="tab" data-bs-target="#link-tab-pane"
|
||||
type="button" role="tab" aria-controls="link-tab-pane" aria-selected="false">Link</button>
|
||||
<button class="nav-link" id="image-tab" data-bs-toggle="tab" data-bs-target="#image-tab-pane"
|
||||
type="button" role="tab" aria-controls="image-tab-pane" aria-selected="false">Image</button>
|
||||
<button class="nav-link" id="poll-tab" data-bs-toggle="tab" data-bs-target="#poll-tab-pane"
|
||||
type="button" role="tab" aria-controls="poll-tab-pane" aria-selected="false" disabled>Poll</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="discussion-tab-pane" role="tabpanel" aria-labelledby="home-tab" tabindex="0">
|
||||
{{ render_field(form.discussion_title) }}
|
||||
{{ render_field(form.discussion_body) }}
|
||||
{% if not low_bandwidth %}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#discussion_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
});
|
||||
setupAutoResize('discussion_body');
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="discussion_body">{{ _('Enable markdown editor') }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="link-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">
|
||||
{{ render_field(form.link_title) }}
|
||||
{{ render_field(form.link_url) }}
|
||||
{{ render_field(form.link_body) }}
|
||||
{% if not low_bandwidth %}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#link_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
});
|
||||
setupAutoResize('link_body');
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="link_body">{{ _('Enable markdown editor') }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="image-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">
|
||||
{{ render_field(form.image_title) }}
|
||||
{{ render_field(form.image_file) }}
|
||||
{{ render_field(form.image_alt_text) }}
|
||||
<small class="field_hint">{{ _('Describe the image, to help visually impaired people.') }}</small>
|
||||
{{ render_field(form.image_body) }}
|
||||
{% if not low_bandwidth %}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#image_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
});
|
||||
setupAutoResize('image_body');
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="image_body">{{ _('Enable markdown editor') }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="poll-tab-pane" role="tabpanel" aria-labelledby="disabled-tab" tabindex="0">
|
||||
Poll
|
||||
</div>
|
||||
</div>
|
||||
{{ render_field(form.post_type) }}
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3">
|
||||
{{ render_field(form.notify_author) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.sticky) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfw) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfl) }}
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<aside id="side_pane" class="col-12 col-md-4 side_pane" role="complementary">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ community.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ community.description_html|safe if community.description_html else '' }}</p>
|
||||
<p>{{ community.rules_html|safe if community.rules_html else '' }}</p>
|
||||
{% if len(mods) > 0 and not community.private_mods %}
|
||||
<h3>Moderators</h3>
|
||||
<ul class="moderator_list">
|
||||
{% for mod in mods %}
|
||||
<li>{{ render_username(mod) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if rss_feed %}
|
||||
<p class="mt-4">
|
||||
<a class="no-underline" href="{{ rss_feed }}" rel="nofollow"><span class="fe fe-rss"></span> RSS feed</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "_inoculation_links.html" %}
|
||||
</aside>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -22,7 +22,7 @@
|
|||
<h1 class="mt-2">{{ _('Moderators for %(community)s', community=community.display_name()) }}</h1>
|
||||
</div>
|
||||
<p>{{ _('See and change who moderates this community') }}</p>
|
||||
<div class="col-12 col-md-2 text-right">
|
||||
<div class="col-12 text-right">
|
||||
<a class="btn btn-primary" href="{{ url_for('community.community_add_moderator', community_id=community.id) }}">{{ _('Add moderator') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<input type="search" name="search" value="{{ search }}">
|
||||
<input type="radio" name="local_remote" value="local" id="local_remote_local" {{ 'checked' if local_remote == 'local' }}><label for="local_remote_local"> Local</label>
|
||||
<input type="radio" name="local_remote" value="remote" id="local_remote_remote" {{ 'checked' if local_remote == 'remote' }}><label for="local_remote_remote"> Remote</label>
|
||||
<input type="submit" name="submit" value="Search" class="btn btn-primary">
|
||||
<input type="submit" name="submit" value="Search" class="btn btn-primary btn-sm">
|
||||
</form>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
|
@ -49,17 +49,27 @@
|
|||
<td>{{ report.type_text() }}</td>
|
||||
<td>{{ moment(report.created_at).fromNow() }}</td>
|
||||
<td>
|
||||
{% if report.suspect_conversation_id %}
|
||||
<a href="/chat/{{ report.suspect_conversation_id }}#message">View</a>
|
||||
{% elif report.suspect_post_reply_id %}
|
||||
<a href="/post/{{ report.suspect_post_id }}#comment_{{ report.suspect_post_reply_id }}">View</a>
|
||||
{% elif report.suspect_post_id %}
|
||||
<a href="/post/{{ report.suspect_post_id }}">View</a>
|
||||
{% elif report.suspect_user_id %}
|
||||
<a href="/user/{{ report.suspect_user_id }}">View</a>
|
||||
{% endif %} |
|
||||
<a href="{{ url_for('community.community_moderate_report_escalate', community_id=community.id, report_id=report.id) }}">{{ _('Escalate') }}</a> |
|
||||
<a href="{{ url_for('community.community_moderate_report_resolve', community_id=community.id, report_id=report.id) }}">{{ _('Resolve') }}</a>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-primary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% if report.suspect_conversation_id %}
|
||||
<li><a class="dropdown-item" href="/chat/{{ report.suspect_conversation_id }}#message">View</a></li>
|
||||
{% elif report.suspect_post_reply_id %}
|
||||
<li><a class="dropdown-item" href="/post/{{ report.suspect_post_id }}#comment_{{ report.suspect_post_reply_id }}">View</a></li>
|
||||
{% elif report.suspect_post_id %}
|
||||
<li><a class="dropdown-item" href="/post/{{ report.suspect_post_id }}">View</a></li>
|
||||
{% elif report.suspect_user_id %}
|
||||
<li><a class="dropdown-item" href="/user/{{ report.suspect_user_id }}">View</a></li>
|
||||
{% endif %}
|
||||
<div class="dropdown-divider"></div>
|
||||
<li><a class="dropdown-item" href="{{ url_for('community.community_moderate_report_escalate', community_id=community.id, report_id=report.id) }}">{{ _('Escalate') }}</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('community.community_moderate_report_resolve', community_id=community.id, report_id=report.id) }}">{{ _('Resolve') }}</a></li>
|
||||
<div class="dropdown-divider"></div>
|
||||
<li><a class="confirm_first dropdown-item" href="{{ url_for('community.community_moderate_report_ignore', community_id=community.id, report_id=report.id) }}">{{ _('Ignore') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
@ -8,14 +8,26 @@
|
|||
|
||||
{% block app_content %}
|
||||
{% if len(topics) > 0 %}
|
||||
{% macro render_topic(topic, depth) %}
|
||||
<tr>
|
||||
<td nowrap="nowrap">{{ '--' * depth }}
|
||||
{% if depth == 0 %}<strong>{% endif %}
|
||||
<a href="/topic/{{ topic['topic'].path() }}">{{ topic['topic'].name }}</a>
|
||||
{% if depth == 0 %}</strong>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if topic['children'] %}
|
||||
{% for topic in topic['children'] %}
|
||||
{{ render_topic(topic, depth + 1)|safe }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
<h1>{{ _('Choose a topic') }}</h1>
|
||||
<div class="table-responsive-md mt-4">
|
||||
<table class="communities_table table table-striped table-hover w-100">
|
||||
<tbody>
|
||||
{% for topic in topics %}
|
||||
<tr>
|
||||
<th class="pl-2"><a href="/topic/{{ topic.machine_name }}">{{ topic.name }}</a></th>
|
||||
</tr>
|
||||
{{ render_topic(topic, 0)|safe }}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% if current_user.is_authenticated and current_user.verified %}
|
||||
{% if can_upvote(current_user, community) %}
|
||||
<div class="upvote_button {{ upvoted_class }}" role="button" aria-label="{{ _('UpVote button.') }}" aria-live="assertive"
|
||||
<div class="upvote_button {{ 'voted_up' if in_sorted_list(recently_upvoted_replies, comment.id) }}" role="button" aria-label="{{ _('UpVote button.') }}" aria-live="assertive"
|
||||
hx-post="/comment/{{ comment.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons_new" tabindex="0">
|
||||
<span class="fe fe-arrow-up"></span>
|
||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||
|
@ -8,7 +8,7 @@
|
|||
{% endif %}
|
||||
<span title="{{ comment.up_votes }}, {{ comment.down_votes }}" aria-live="assertive" aria-label="{{ _('Score: ') }}{{ comment.up_votes - comment.down_votes }}.">{{ comment.up_votes - comment.down_votes }}</span>
|
||||
{% if can_downvote(current_user, community) %}
|
||||
<div class="downvote_button {{ downvoted_class }}" role="button" aria-label="{{ _('DownVote button.') }}" aria-live="assertive"
|
||||
<div class="downvote_button {{ 'voted_down' if in_sorted_list(recently_downvoted_replies, comment.id) }}" role="button" aria-label="{{ _('DownVote button.') }}" aria-live="assertive"
|
||||
hx-post="/comment/{{ comment.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons_new" tabindex="0">
|
||||
<span class="fe fe-arrow-down"></span>
|
||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<p><a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="Go to image">{{ post.url|shorten_url }}
|
||||
<span class="fe fe-external"></span></a></p>
|
||||
{% endif %}
|
||||
<p>{% if post.reports and current_user.is_authenticated and post.community.is_moderator(current_user) %}
|
||||
<p>{% if post.reports > 0 and current_user.is_authenticated and post.community.is_moderator(current_user) %}
|
||||
<span class="red fe fe-report" title="{{ _('Reported. Check post for issues.') }}"></span>
|
||||
{% endif %}<small>submitted {{ moment(post.posted_at).fromNow() }} by {{ render_username(post.author) }}
|
||||
{% if post.edited_at %} edited {{ moment(post.edited_at).fromNow() }}{% endif %}
|
||||
|
@ -72,7 +72,7 @@
|
|||
width="{{ post.image.thumbnail_width }}" height="{{ post.image.thumbnail_height }}" loading="lazy" /></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<p>{% if post.reports and current_user.is_authenticated and post.community.is_moderator(current_user) %}
|
||||
<p>{% if post.reports > 0 and current_user.is_authenticated and post.community.is_moderator(current_user) %}
|
||||
<span class="red fe fe-report" title="{{ _('Reported. Check post for issues.') }}"></span>
|
||||
{% endif %}<small>submitted {{ moment(post.posted_at).fromNow() }} by
|
||||
{{ render_username(post.author) }}
|
||||
|
@ -85,6 +85,7 @@
|
|||
<p><audio controls preload="{{ 'none' if low_bandwidth else 'metadata' }}" src="{{ post.url }}"></audio></p>
|
||||
{% endif %}
|
||||
{% if 'youtube.com' in post.url %}
|
||||
<p><a href="https://piped.video/watch?v={{ post.youtube_embed() }}">{{ _('Watch on piped.video') }} <span class="fe fe-external"></span></a></p>
|
||||
<div style="padding-bottom: 56.25%; position: relative;"><iframe style="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%;" src="https://www.youtube.com/embed/{{ post.youtube_embed() }}?rel=0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; fullscreen" width="100%" height="100%" frameborder="0"></iframe></div>
|
||||
{% endif %}
|
||||
{% elif post.type == POST_TYPE_IMAGE %}
|
||||
|
|
|
@ -2,15 +2,17 @@
|
|||
{% if content_blocked and content_blocked == '-1' %}
|
||||
{# do nothing - blocked by keyword filter #}
|
||||
{% else %}
|
||||
<div class="post_teaser type_{{ post.type }}{{ ' reported' if post.reports and current_user.is_authenticated and post.community.is_moderator() }}{{ ' blocked' if content_blocked }}"
|
||||
<div class="post_teaser type_{{ post.type }}{{ ' reported' if post.reports > 0 and current_user.is_authenticated and post.community.is_moderator() }}{{ ' blocked' if content_blocked }}"
|
||||
{% if content_blocked %} title="{{ _('Filtered: ') }}{{ content_blocked }}"{% endif %} tabindex="0">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="row main_row">
|
||||
<div class="col">
|
||||
{% if not hide_vote_buttons %}
|
||||
<div class="voting_buttons" aria-hidden="true">
|
||||
{% include "post/_post_voting_buttons.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if post.image_id %}
|
||||
<div class="thumbnail{{ ' lbw' if low_bandwidth }}" aria-hidden="true">
|
||||
{% if low_bandwidth %}
|
||||
|
@ -55,7 +57,7 @@
|
|||
{% endif %}
|
||||
{% if post.nsfw %}<span class="warning_badge nsfw" title="{{ _('Not safe for work') }}">nsfw</span>{% endif %}
|
||||
{% if post.nsfl %}<span class="warning_badge nsfl" title="{{ _('Potentially emotionally scarring content') }}">nsfl</span>{% endif %}
|
||||
{% if post.reports and current_user.is_authenticated and post.community.is_moderator(current_user) %}
|
||||
{% if post.reports > 0 and current_user.is_authenticated and post.community.is_moderator(current_user) %}
|
||||
<span class="red fe fe-report" title="{{ _('Reported. Check post for issues.') }}"></span>
|
||||
{% endif %}
|
||||
{% if post.sticky %}<span class="fe fe-sticky-right"></span>{% endif %}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% if content_blocked and content_blocked == '-1' %}
|
||||
{# do nothing - blocked by keyword filter #}
|
||||
{% else %}
|
||||
<div class="post_teaser{{ ' reported' if post.reports and current_user.is_authenticated and post.community.is_moderator() }}{{ ' blocked' if content_blocked }}"
|
||||
<div class="post_teaser{{ ' reported' if post.reports > 0 and current_user.is_authenticated and post.community.is_moderator() }}{{ ' blocked' if content_blocked }}"
|
||||
{% if content_blocked %} title="{{ _('Filtered: ') }}{{ content_blocked }}"{% endif %}>
|
||||
{% if post.image_id %}
|
||||
{% if post_layout == 'masonry' or low_bandwidth %}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% if current_user.is_authenticated and current_user.verified %}
|
||||
{% if can_upvote(current_user, post.community) %}
|
||||
<div class="upvote_button {{ upvoted_class }}" role="button" aria-label="{{ _('UpVote button, %(count)d upvotes so far.', count=post.up_votes) }}" aria-live="assertive"
|
||||
<div class="upvote_button {{ 'voted_up' if in_sorted_list(recently_upvoted, post.id) }}" role="button" aria-label="{{ _('UpVote button, %(count)d upvotes so far.', count=post.up_votes) }}" aria-live="assertive"
|
||||
hx-post="/post/{{ post.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons" tabindex="0">
|
||||
<span class="fe fe-arrow-up"></span>
|
||||
{{ shorten_number(post.up_votes) }}
|
||||
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% if can_downvote(current_user, post.community) %}
|
||||
<div class="downvote_button {{ downvoted_class }}" role="button" aria-label="{{ _('DownVote button, %(count)d downvotes so far.', count=post.down_votes) }}" aria-live="assertive"
|
||||
<div class="downvote_button {{ 'voted_down' if in_sorted_list(recently_downvoted, post.id) }}" role="button" aria-label="{{ _('DownVote button, %(count)d downvotes so far.', count=post.down_votes) }}" aria-live="assertive"
|
||||
hx-post="/post/{{ post.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons" tabindex="0">
|
||||
<span class="fe fe-arrow-down"></span>
|
||||
{{ shorten_number(post.down_votes) }}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{% if current_user.is_authenticated and current_user.verified %}
|
||||
{% if can_upvote(current_user, post.community) %}
|
||||
<div class="upvote_button {{ upvoted_class }}" role="button" aria-label="{{ _('UpVote') }}" aria-live="assertive"
|
||||
<div class="upvote_button {{ 'voted_up' if in_sorted_list(recently_upvoted, post.id) }}" role="button" aria-label="{{ _('UpVote') }}" aria-live="assertive"
|
||||
hx-post="/post/{{ post.id }}/upvote?style=masonry" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons_masonry" tabindex="0" title="{{ post.up_votes }} upvotes">
|
||||
<span class="fe fe-arrow-up"></span>
|
||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if can_downvote(current_user, post.community) %}
|
||||
<div class="downvote_button {{ downvoted_class }}" role="button" aria-label="{{ _('DownVote') }}" aria-live="assertive"
|
||||
<div class="downvote_button {{ 'voted_down' if in_sorted_list(recently_downvoted, post.id) }}" role="button" aria-label="{{ _('DownVote') }}" aria-live="assertive"
|
||||
hx-post="/post/{{ post.id }}/downvote?style=masonry" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons_masonry" tabindex="0" title="{{ post.down_votes }} downvotes">
|
||||
<span class="fe fe-arrow-down"></span>
|
||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||
|
|
|
@ -1,164 +0,0 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form, render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<script nonce="{{ session['nonce'] }}" type="text/javascript">
|
||||
window.addEventListener("load", function () {
|
||||
var type = document.forms[0].elements['type'].value;
|
||||
var toClick = undefined;
|
||||
switch(type) {
|
||||
case '':
|
||||
case 'discussion':
|
||||
toClick = document.getElementById('discussion-tab');
|
||||
break;
|
||||
case 'link':
|
||||
toClick = document.getElementById('link-tab');
|
||||
break;
|
||||
case 'image':
|
||||
toClick = document.getElementById('image-tab');
|
||||
break;
|
||||
case 'poll':
|
||||
toClick = document.getElementById('poll-tab');
|
||||
break;
|
||||
}
|
||||
if(toClick) {
|
||||
toClick.click();
|
||||
}
|
||||
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#discussion_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
value: {{ form.discussion_body.data | tojson | safe }}
|
||||
});
|
||||
setupAutoResize('discussion_body');
|
||||
});
|
||||
</script>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<h1>{{ _('Edit post') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" role="form">
|
||||
{{ form.csrf_token() }}
|
||||
<nav id="post_type_chooser">
|
||||
<div class="nav nav-tabs nav-justified" id="typeTab" role="tablist">
|
||||
<button class="nav-link active" id="discussion-tab" data-bs-toggle="tab" data-bs-target="#discussion-tab-pane"
|
||||
type="button" role="tab" aria-controls="discussion-tab-pane" aria-selected="true">Discussion</button>
|
||||
<button class="nav-link" id="link-tab" data-bs-toggle="tab" data-bs-target="#link-tab-pane"
|
||||
type="button" role="tab" aria-controls="link-tab-pane" aria-selected="false">Link</button>
|
||||
<button class="nav-link" id="image-tab" data-bs-toggle="tab" data-bs-target="#image-tab-pane"
|
||||
type="button" role="tab" aria-controls="image-tab-pane" aria-selected="false">Image</button>
|
||||
<button class="nav-link" id="poll-tab" data-bs-toggle="tab" data-bs-target="#poll-tab-pane"
|
||||
type="button" role="tab" aria-controls="poll-tab-pane" aria-selected="false" disabled>Poll</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="discussion-tab-pane" role="tabpanel" aria-labelledby="home-tab" tabindex="0">
|
||||
{{ render_field(form.discussion_title) }}
|
||||
{{ render_field(form.discussion_body) }}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#discussion_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
value: {{ form.discussion_body.data | tojson | safe }},
|
||||
});
|
||||
setupAutoResize('discussion_body');
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="link-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">
|
||||
{{ render_field(form.link_title) }}
|
||||
{{ render_field(form.link_url) }}
|
||||
{{ render_field(form.link_body) }}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#link_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
value: {{ form.link_body.data | tojson | safe }},
|
||||
});
|
||||
setupAutoResize('link_body');
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="image-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">
|
||||
{{ render_field(form.image_title) }}
|
||||
{{ render_field(form.image_file) }}
|
||||
{{ render_field(form.image_alt_text) }}
|
||||
<small class="field_hint">{{ _('Describe the image, to help visually impaired people.') }}</small>
|
||||
{{ render_field(form.image_body) }}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#image_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
value: {{ form.image_body.data | tojson | safe }},
|
||||
});
|
||||
setupAutoResize('image_body');
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="poll-tab-pane" role="tabpanel" aria-labelledby="disabled-tab" tabindex="0">
|
||||
Poll
|
||||
</div>
|
||||
</div>
|
||||
{{ render_field(form.post_type) }}
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3">
|
||||
{{ render_field(form.notify_author) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.sticky) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfw) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfl) }}
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<aside id="side_pane" class="col-12 col-md-4 side_pane" role="complementary">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ post.community.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ post.community.description_html|safe if post.community.description_html else '' }}</p>
|
||||
<p>{{ post.community.rules_html|safe if post.community.rules_html else '' }}</p>
|
||||
{% if len(mods) > 0 and not post.community.private_mods %}
|
||||
<h3>Moderators</h3>
|
||||
<ul class="moderator_list">
|
||||
{% for mod in mods %}
|
||||
<li><a href="/u/{{ mod.link() }}">{{ mod.display_name() }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "_inoculation_links.html" %}
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
75
app/templates/post/post_edit_discussion.html
Normal file
75
app/templates/post/post_edit_discussion.html
Normal file
|
@ -0,0 +1,75 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form, render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<h1>{{ _('Edit post') }}</h1>
|
||||
<form method="post" role="form">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.discussion_title) }}
|
||||
{{ render_field(form.discussion_body) }}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#discussion_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
value: {{ form.discussion_body.data | tojson | safe }},
|
||||
});
|
||||
setupAutoResize('discussion_body');
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3">
|
||||
{{ render_field(form.notify_author) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.sticky) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfw) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfl) }}
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<aside id="side_pane" class="col-12 col-md-4 side_pane" role="complementary">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ post.community.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ post.community.description_html|safe if post.community.description_html else '' }}</p>
|
||||
<p>{{ post.community.rules_html|safe if post.community.rules_html else '' }}</p>
|
||||
{% if len(mods) > 0 and not post.community.private_mods %}
|
||||
<h3>Moderators</h3>
|
||||
<ul class="moderator_list">
|
||||
{% for mod in mods %}
|
||||
<li><a href="/u/{{ mod.link() }}">{{ mod.display_name() }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "_inoculation_links.html" %}
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
78
app/templates/post/post_edit_image.html
Normal file
78
app/templates/post/post_edit_image.html
Normal file
|
@ -0,0 +1,78 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form, render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<h1>{{ _('Edit post') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" role="form">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.image_title) }}
|
||||
{{ render_field(form.image_file) }}
|
||||
{{ render_field(form.image_alt_text) }}
|
||||
<small class="field_hint">{{ _('Describe the image, to help visually impaired people.') }}</small>
|
||||
{{ render_field(form.image_body) }}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#image_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
value: {{ form.image_body.data | tojson | safe }},
|
||||
});
|
||||
setupAutoResize('image_body');
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3">
|
||||
{{ render_field(form.notify_author) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.sticky) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfw) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfl) }}
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<aside id="side_pane" class="col-12 col-md-4 side_pane" role="complementary">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ post.community.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ post.community.description_html|safe if post.community.description_html else '' }}</p>
|
||||
<p>{{ post.community.rules_html|safe if post.community.rules_html else '' }}</p>
|
||||
{% if len(mods) > 0 and not post.community.private_mods %}
|
||||
<h3>Moderators</h3>
|
||||
<ul class="moderator_list">
|
||||
{% for mod in mods %}
|
||||
<li><a href="/u/{{ mod.link() }}">{{ mod.display_name() }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "_inoculation_links.html" %}
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
75
app/templates/post/post_edit_link.html
Normal file
75
app/templates/post/post_edit_link.html
Normal file
|
@ -0,0 +1,75 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_form, render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<h1>{{ _('Edit post') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" role="form">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.link_title) }}
|
||||
{{ render_field(form.link_url) }}
|
||||
{{ render_field(form.link_body) }}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#link_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
value: {{ form.link_body.data | tojson | safe }},
|
||||
});
|
||||
setupAutoResize('link_body');
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3">
|
||||
{{ render_field(form.notify_author) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.sticky) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfw) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfl) }}
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<aside id="side_pane" class="col-12 col-md-4 side_pane" role="complementary">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ post.community.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ post.community.description_html|safe if post.community.description_html else '' }}</p>
|
||||
<p>{{ post.community.rules_html|safe if post.community.rules_html else '' }}</p>
|
||||
{% if len(mods) > 0 and not post.community.private_mods %}
|
||||
<h3>Moderators</h3>
|
||||
<ul class="moderator_list">
|
||||
{% for mod in mods %}
|
||||
<li><a href="/u/{{ mod.link() }}">{{ mod.display_name() }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "_inoculation_links.html" %}
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -12,3 +12,18 @@ User-Agent: *
|
|||
Disallow: /d/
|
||||
Disallow: /static/media/users/
|
||||
Disallow: /post/*/options
|
||||
|
||||
User-agent: GPTBot
|
||||
Disallow: /
|
||||
|
||||
User-agent: AhrefsBot
|
||||
Disallow: /
|
||||
|
||||
User-agent: SemrushBot
|
||||
Disallow: /
|
||||
|
||||
User-agent: DotBot
|
||||
Disallow: /
|
||||
|
||||
User-agent: SeznamBot
|
||||
Disallow: /
|
||||
|
|
|
@ -36,6 +36,10 @@
|
|||
--bs-btn-color: white;
|
||||
}
|
||||
|
||||
.post_body a, .comment_body a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.post_list .post_teaser {
|
||||
border-bottom: solid 1px black;
|
||||
}
|
||||
|
@ -83,4 +87,4 @@ div.navbar {
|
|||
}
|
||||
[data-bs-theme="dark"] .coolfieldset.collapsed legend, [data-bs-theme="dark"] .coolfieldset legend, [data-bs-theme="dark"] .coolfieldset.expanded legend {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ from app import db, celery, cache
|
|||
from app.topic.forms import ChooseTopicsForm
|
||||
from app.utils import render_template, user_filters_posts, moderating_communities, joined_communities, \
|
||||
community_membership, blocked_domains, validation_required, mimetype_from_url, blocked_instances, \
|
||||
communities_banned_from
|
||||
communities_banned_from, blocked_users
|
||||
|
||||
|
||||
@bp.route('/topic/<path:topic_path>', methods=['GET'])
|
||||
|
@ -63,12 +63,18 @@ def show_topic(topic_path):
|
|||
posts = posts.filter(Post.nsfw == False)
|
||||
content_filters = user_filters_posts(current_user.id)
|
||||
|
||||
# filter blocked domains and instances
|
||||
domains_ids = blocked_domains(current_user.id)
|
||||
if domains_ids:
|
||||
posts = posts.filter(or_(Post.domain_id.not_in(domains_ids), Post.domain_id == None))
|
||||
instance_ids = blocked_instances(current_user.id)
|
||||
if instance_ids:
|
||||
posts = posts.filter(or_(Post.instance_id.not_in(instance_ids), Post.instance_id == None))
|
||||
# filter blocked users
|
||||
blocked_accounts = blocked_users(current_user.id)
|
||||
if blocked_accounts:
|
||||
posts = posts.filter(Post.user_id.not_in(blocked_accounts))
|
||||
|
||||
banned_from = communities_banned_from(current_user.id)
|
||||
if banned_from:
|
||||
posts = posts.filter(Post.community_id.not_in(banned_from))
|
||||
|
@ -77,7 +83,7 @@ def show_topic(topic_path):
|
|||
if sort == '' or sort == 'hot':
|
||||
posts = posts.order_by(desc(Post.ranking)).order_by(desc(Post.posted_at))
|
||||
elif sort == 'top':
|
||||
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=7)).order_by(desc(Post.score))
|
||||
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=7)).order_by(desc(Post.up_votes - Post.down_votes))
|
||||
elif sort == 'new':
|
||||
posts = posts.order_by(desc(Post.posted_at))
|
||||
elif sort == 'active':
|
||||
|
|
2962
app/translations/ca/LC_MESSAGES/messages.po
Normal file
2962
app/translations/ca/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
2962
app/translations/es/LC_MESSAGES/messages.po
Normal file
2962
app/translations/es/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
2607
app/translations/lt/LC_MESSAGES/messages.po
Normal file
2607
app/translations/lt/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -14,7 +14,7 @@ class ProfileForm(FlaskForm):
|
|||
email = EmailField(_l('Email address'), validators=[Email(), DataRequired(), Length(min=5, max=255)])
|
||||
password_field = PasswordField(_l('Set new password'), validators=[Optional(), Length(min=1, max=50)],
|
||||
render_kw={"autocomplete": 'new-password'})
|
||||
about = TextAreaField(_l('Bio'), validators=[Optional(), Length(min=3, max=5000)])
|
||||
about = TextAreaField(_l('Bio'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
||||
matrixuserid = StringField(_l('Matrix User ID'), validators=[Optional(), Length(max=255)], render_kw={'autocomplete': 'off'})
|
||||
profile_file = FileField(_('Avatar image'))
|
||||
banner_file = FileField(_('Top banner image'))
|
||||
|
|
|
@ -19,7 +19,7 @@ from app.user.utils import purge_user_then_delete
|
|||
from app.utils import get_setting, render_template, markdown_to_html, user_access, markdown_to_text, shorten_string, \
|
||||
is_image_url, ensure_directory_exists, gibberish, file_get_contents, community_membership, user_filters_home, \
|
||||
user_filters_posts, user_filters_replies, moderating_communities, joined_communities, theme_list, blocked_instances, \
|
||||
allowlist_html
|
||||
allowlist_html, recently_upvoted_posts, recently_downvoted_posts, blocked_users
|
||||
from sqlalchemy import desc, or_, text
|
||||
import os
|
||||
|
||||
|
@ -85,7 +85,7 @@ def show_profile(user):
|
|||
description=description, subscribed=subscribed, upvoted=upvoted,
|
||||
post_next_url=post_next_url, post_prev_url=post_prev_url,
|
||||
replies_next_url=replies_next_url, replies_prev_url=replies_prev_url,
|
||||
noindex=not user.indexable, show_post_community=True,
|
||||
noindex=not user.indexable, show_post_community=True, hide_vote_buttons=True,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.get_id())
|
||||
)
|
||||
|
@ -294,6 +294,7 @@ def block_profile(actor):
|
|||
# federate block
|
||||
|
||||
flash(f'{actor} has been blocked.')
|
||||
cache.delete_memoized(blocked_users, current_user.id)
|
||||
|
||||
goto = request.args.get('redirect') if 'redirect' in request.args else f'/u/{actor}'
|
||||
return redirect(goto)
|
||||
|
@ -322,6 +323,7 @@ def unblock_profile(actor):
|
|||
# federate unblock
|
||||
|
||||
flash(f'{actor} has been unblocked.')
|
||||
cache.delete_memoized(blocked_users, current_user.id)
|
||||
|
||||
goto = request.args.get('redirect') if 'redirect' in request.args else f'/u/{actor}'
|
||||
return redirect(goto)
|
||||
|
@ -335,8 +337,18 @@ def report_profile(actor):
|
|||
else:
|
||||
user: User = User.query.filter_by(user_name=actor, deleted=False, ap_id=None).first()
|
||||
form = ReportUserForm()
|
||||
|
||||
if user and user.reports == -1: # When a mod decides to ignore future reports, user.reports is set to -1
|
||||
flash(_('Moderators have already assessed reports regarding this person, no further reports are necessary.'), 'warning')
|
||||
|
||||
if user and not user.banned:
|
||||
if form.validate_on_submit():
|
||||
|
||||
if user.reports == -1:
|
||||
flash(_('%(user_name)s has already been reported, thank you!', user_name=actor))
|
||||
goto = request.args.get('redirect') if 'redirect' in request.args else f'/u/{actor}'
|
||||
return redirect(goto)
|
||||
|
||||
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
||||
type=0, reporter_id=current_user.id, suspect_user_id=user.id, source_instance_id=1)
|
||||
db.session.add(report)
|
||||
|
|
80
app/utils.py
80
app/utils.py
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import bisect
|
||||
import hashlib
|
||||
import mimetypes
|
||||
import random
|
||||
|
@ -28,7 +29,7 @@ import re
|
|||
|
||||
from app.email import send_welcome_email
|
||||
from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan, \
|
||||
Site, Post, PostReply, utcnow, Filter, CommunityMember, InstanceBlock, CommunityBan
|
||||
Site, Post, PostReply, utcnow, Filter, CommunityMember, InstanceBlock, CommunityBan, Topic, UserBlock
|
||||
|
||||
|
||||
# Flask's render_template function, with support for themes added
|
||||
|
@ -169,7 +170,7 @@ def allowlist_html(html: str) -> str:
|
|||
if html is None or html == '':
|
||||
return ''
|
||||
allowed_tags = ['p', 'strong', 'a', 'ul', 'ol', 'li', 'em', 'blockquote', 'cite', 'br', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre',
|
||||
'code', 'img', 'details', 'summary', 'table', 'tr', 'td', 'th', 'tbody', 'thead']
|
||||
'code', 'img', 'details', 'summary', 'table', 'tr', 'td', 'th', 'tbody', 'thead', 'hr']
|
||||
# Parse the HTML using BeautifulSoup
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
|
||||
|
@ -211,13 +212,17 @@ def allowlist_html(html: str) -> str:
|
|||
if tag.name == 'table':
|
||||
tag.attrs['class'] = 'table'
|
||||
|
||||
return str(soup)
|
||||
# avoid returning empty anchors
|
||||
re_empty_anchor = re.compile(r'<a href="(.*?)" rel="nofollow ugc" target="_blank"><\/a>')
|
||||
return re_empty_anchor.sub(r'<a href="\1" rel="nofollow ugc" target="_blank">\1</a>', str(soup))
|
||||
|
||||
|
||||
def markdown_to_html(markdown_text) -> str:
|
||||
if markdown_text:
|
||||
raw_html = markdown2.markdown(markdown_text, safe_mode=True, extras={'middle-word-em': False, 'tables': True, 'fenced-code-blocks': True, 'strike': True})
|
||||
# todo: in raw_html, replace lemmy spoiler tokens with appropriate html tags instead.
|
||||
# replace lemmy spoiler tokens with appropriate html tags instead. (until possibly added as extra to markdown2)
|
||||
re_spoiler = re.compile(r':{3} spoiler\s+?(\S.+?)(?:\n|</p>)(.+?)(?:\n|<p>):{3}', re.S)
|
||||
raw_html = re_spoiler.sub(r'<details><summary>\1</summary><p>\2</p></details>', raw_html)
|
||||
return allowlist_html(raw_html)
|
||||
else:
|
||||
return ''
|
||||
|
@ -240,6 +245,8 @@ def microblog_content_to_title(html: str) -> str:
|
|||
continue
|
||||
else:
|
||||
tag = tag.extract()
|
||||
else:
|
||||
tag = tag.extract()
|
||||
|
||||
if title_found:
|
||||
result = soup.text
|
||||
|
@ -328,6 +335,12 @@ def blocked_instances(user_id) -> List[int]:
|
|||
return [block.instance_id for block in blocks]
|
||||
|
||||
|
||||
@cache.memoize(timeout=86400)
|
||||
def blocked_users(user_id) -> List[int]:
|
||||
blocks = UserBlock.query.filter_by(blocker_id=user_id)
|
||||
return [block.blocked_id for block in blocks]
|
||||
|
||||
|
||||
@cache.memoize(timeout=86400)
|
||||
def blocked_phrases() -> List[str]:
|
||||
site = Site.query.get(1)
|
||||
|
@ -447,6 +460,8 @@ def user_ip_banned() -> bool:
|
|||
|
||||
@cache.memoize(timeout=30)
|
||||
def instance_banned(domain: str) -> bool: # see also activitypub.util.instance_blocked()
|
||||
if domain is None or domain == '':
|
||||
return False
|
||||
banned = BannedInstances.query.filter_by(domain=domain).first()
|
||||
return banned is not None
|
||||
|
||||
|
@ -675,6 +690,21 @@ def finalize_user_setup(user, application_required=False):
|
|||
send_welcome_email(user, application_required)
|
||||
|
||||
|
||||
# topics, in a tree
|
||||
def topic_tree() -> List:
|
||||
topics = Topic.query.order_by(Topic.name)
|
||||
|
||||
topics_dict = {topic.id: {'topic': topic, 'children': []} for topic in topics.all()}
|
||||
|
||||
for topic in topics:
|
||||
if topic.parent_id is not None:
|
||||
parent_comment = topics_dict.get(topic.parent_id)
|
||||
if parent_comment:
|
||||
parent_comment['children'].append(topics_dict[topic.id])
|
||||
|
||||
return [topic for topic in topics_dict.values() if topic['topic'].parent_id is None]
|
||||
|
||||
|
||||
# All the following post/comment ranking math is explained at https://medium.com/hacking-and-gonzo/how-reddit-ranking-algorithms-work-ef111e33d0d9
|
||||
epoch = datetime(1970, 1, 1)
|
||||
|
||||
|
@ -775,9 +805,13 @@ def current_theme():
|
|||
if current_user.theme is not None and current_user.theme != '':
|
||||
return current_user.theme
|
||||
else:
|
||||
return g.site.default_theme if g.site.default_theme is not None else ''
|
||||
if hasattr(g, 'site'):
|
||||
site = g.site
|
||||
else:
|
||||
site = Site.query.get(1)
|
||||
return site.default_theme if site.default_theme is not None else ''
|
||||
else:
|
||||
return g.site.default_theme if g.site.default_theme is not None else ''
|
||||
return ''
|
||||
|
||||
|
||||
def theme_list():
|
||||
|
@ -836,3 +870,37 @@ def show_ban_message():
|
|||
resp = make_response(redirect(url_for('main.index')))
|
||||
resp.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30))
|
||||
return resp
|
||||
|
||||
|
||||
# search a sorted list using a binary search. Faster than using 'in' with a unsorted list.
|
||||
def in_sorted_list(arr, target):
|
||||
index = bisect.bisect_left(arr, target)
|
||||
return index < len(arr) and arr[index] == target
|
||||
|
||||
|
||||
@cache.memoize(timeout=600)
|
||||
def recently_upvoted_posts(user_id) -> List[int]:
|
||||
post_ids = db.session.execute(text('SELECT post_id FROM "post_vote" WHERE user_id = :user_id AND effect > 0 ORDER BY id DESC LIMIT 1000'),
|
||||
{'user_id': user_id}).scalars()
|
||||
return sorted(post_ids) # sorted so that in_sorted_list can be used
|
||||
|
||||
|
||||
@cache.memoize(timeout=600)
|
||||
def recently_downvoted_posts(user_id) -> List[int]:
|
||||
post_ids = db.session.execute(text('SELECT post_id FROM "post_vote" WHERE user_id = :user_id AND effect < 0 ORDER BY id DESC LIMIT 1000'),
|
||||
{'user_id': user_id}).scalars()
|
||||
return sorted(post_ids)
|
||||
|
||||
|
||||
@cache.memoize(timeout=600)
|
||||
def recently_upvoted_post_replies(user_id) -> List[int]:
|
||||
reply_ids = db.session.execute(text('SELECT post_reply_id FROM "post_reply_vote" WHERE user_id = :user_id AND effect > 0 ORDER BY id DESC LIMIT 1000'),
|
||||
{'user_id': user_id}).scalars()
|
||||
return sorted(reply_ids) # sorted so that in_sorted_list can be used
|
||||
|
||||
|
||||
@cache.memoize(timeout=600)
|
||||
def recently_downvoted_post_replies(user_id) -> List[int]:
|
||||
reply_ids = db.session.execute(text('SELECT post_reply_id FROM "post_reply_vote" WHERE user_id = :user_id AND effect < 0 ORDER BY id DESC LIMIT 1000'),
|
||||
{'user_id': user_id}).scalars()
|
||||
return sorted(reply_ids)
|
||||
|
|
|
@ -26,7 +26,7 @@ Mailing list, Matrix channel, etc still to come.
|
|||
- Redis
|
||||
|
||||
Python developers with no Flask experience can quickly learn Flask by exploring the
|
||||
[Flask Mega Tutorial](https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world)
|
||||
[Flask Mega Tutorial](https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world-2018)
|
||||
which will guide them through the process of building a simple social media app. Django is
|
||||
very similar to Flask so developers familiar with that framework will have an easier
|
||||
time of things.
|
6
docs/project_management/decision_log.md
Normal file
6
docs/project_management/decision_log.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Decision log
|
||||
|
||||
This document helps prevent rearguing decisions after they have been made. It can also help new contributors come up to
|
||||
speed by providing a summary of how we got to the present state.
|
||||
|
||||
[Switch to using HTML while federating content instead of Markdown](https://codeberg.org/rimu/pyfedi/issues/133#issuecomment-1756067)
|
|
@ -29,15 +29,15 @@ The following are the goals for a 1.0 release, good enough for production use. I
|
|||
|
||||
### Moderation
|
||||
|
||||
- community moderation
|
||||
- ✅ community moderation
|
||||
- ✅ blocking - users, communities, domains, instances. bi-directional.
|
||||
- import/export of block lists
|
||||
|
||||
### Onboarding
|
||||
|
||||
- ✅ choose interests to auto-subscribe new accounts
|
||||
- ✅ choose topics to auto-subscribe new accounts
|
||||
|
||||
### Performance and scaling
|
||||
|
||||
- ✅ background task runner
|
||||
|
||||
- send queue for federation
|
11
docs/project_management/who_is_who.md
Normal file
11
docs/project_management/who_is_who.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# People involved in PieFed
|
||||
|
||||
This document gives the community a shared understanding of who is responsible for what and helps to make sure no one
|
||||
is inadvertently left out of a decision.
|
||||
|
||||
### Rimu
|
||||
|
||||
- https://piefed.social/u/rimu, https://mastodon.nzoss.nz/@rimu
|
||||
|
||||
Founder and lead developer. Approver of PRs, payer of bills.
|
||||
|
32
migrations/versions/91a931afd6d9_contact_email.py
Normal file
32
migrations/versions/91a931afd6d9_contact_email.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
"""contact email
|
||||
|
||||
Revision ID: 91a931afd6d9
|
||||
Revises: 08b3f718df5d
|
||||
Create Date: 2024-04-12 16:22:32.137053
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '91a931afd6d9'
|
||||
down_revision = '08b3f718df5d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('site', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('contact_email', sa.String(length=255), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('site', schema=None) as batch_op:
|
||||
batch_op.drop_column('contact_email')
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -11,7 +11,8 @@ from flask import session, g, json, request, current_app
|
|||
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE
|
||||
from app.models import Site
|
||||
from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access, community_membership, \
|
||||
can_create_post, can_upvote, can_downvote, shorten_number, ap_datetime, current_theme, community_link_to_href
|
||||
can_create_post, can_upvote, can_downvote, shorten_number, ap_datetime, current_theme, community_link_to_href, \
|
||||
in_sorted_list
|
||||
|
||||
app = create_app()
|
||||
cli.register(app)
|
||||
|
@ -42,6 +43,7 @@ with app.app_context():
|
|||
app.jinja_env.globals['can_create'] = can_create_post
|
||||
app.jinja_env.globals['can_upvote'] = can_upvote
|
||||
app.jinja_env.globals['can_downvote'] = can_downvote
|
||||
app.jinja_env.globals['in_sorted_list'] = in_sorted_list
|
||||
app.jinja_env.globals['theme'] = current_theme
|
||||
app.jinja_env.globals['file_exists'] = os.path.exists
|
||||
app.jinja_env.filters['community_links'] = community_link_to_href
|
||||
|
@ -53,7 +55,8 @@ with app.app_context():
|
|||
def before_request():
|
||||
session['nonce'] = gibberish()
|
||||
g.locale = str(get_locale())
|
||||
g.site = Site.query.get(1)
|
||||
if request.path != '/inbox' and not request.path.startswith('/static/'): # do not load g.site on shared inbox, to increase chance of duplicate detection working properly
|
||||
g.site = Site.query.get(1)
|
||||
if current_user.is_authenticated:
|
||||
current_user.last_seen = datetime.utcnow()
|
||||
current_user.email_unread_sent = False
|
||||
|
|
Loading…
Add table
Reference in a new issue