diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index 8af0d686..60d1a05d 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -300,6 +300,7 @@ def shared_inbox(): if 'id' in request_json: if activity_already_ingested(request_json['id']): # Lemmy has an extremely short POST timeout and tends to retry unnecessarily. Ignore their retries. activity_log.result = 'ignored' + activity_log.exception_message = 'Unnecessary retry attempt' db.session.add(activity_log) db.session.commit() return '' @@ -458,7 +459,6 @@ def process_inbox_request(request_json, activitypublog_id, ip_address): activity_log.exception_message = 'Could not detect type of like' if activity_log.result == 'success': ... - # todo: recalculate 'hotness' of liked post/reply # todo: if vote was on content in local community, federate the vote out to followers else: activity_log.exception_message = 'Cannot upvote this' @@ -723,7 +723,6 @@ def process_inbox_request(request_json, activitypublog_id, ip_address): activity_log.exception_message = 'Could not detect type of like' if activity_log.result == 'success': ... - # todo: recalculate 'hotness' of liked post/reply # todo: if vote was on content in local community, federate the vote out to followers else: activity_log.exception_message = 'Cannot upvote this' @@ -754,7 +753,6 @@ def process_inbox_request(request_json, activitypublog_id, ip_address): elif isinstance(disliked, PostReply): downvote_post_reply(disliked, user) activity_log.result = 'success' - # todo: recalculate 'hotness' of liked post/reply # todo: if vote was on content in the local community, federate the vote out to followers else: activity_log.exception_message = 'Could not detect type of like' diff --git a/app/activitypub/util.py b/app/activitypub/util.py index cf54bbb3..5ac0aae9 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -21,7 +21,7 @@ from io import BytesIO from app.utils import get_request, allowlist_html, html_to_markdown, get_setting, ap_datetime, markdown_to_html, \ is_image_url, domain_from_url, gibberish, ensure_directory_exists, markdown_to_text, head_request, post_ranking, \ - shorten_string + shorten_string, reply_already_exists, reply_is_just_link_to_gif_reaction def public_key(): @@ -924,6 +924,17 @@ def create_post_reply(activity_log: ActivityPubLog, community: Community, in_rep activity_log.result = 'ignored' return None + if reply_already_exists(user_id=user.id, post_id=post.id, parent_id=post_reply.parent_id, body=post_reply.body): + activity_log.exception_message = 'Duplicate reply' + activity_log.result = 'ignored' + return None + + if reply_is_just_link_to_gif_reaction(post_reply.body): + user.reputation -= 1 + activity_log.exception_message = 'gif comment ignored' + activity_log.result = 'ignored' + return None + db.session.add(post_reply) post.reply_count += 1 community.post_reply_count += 1 diff --git a/app/community/routes.py b/app/community/routes.py index 4d242209..e0bd0b4b 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -25,6 +25,7 @@ from datetime import timezone, timedelta @bp.route('/add_local', methods=['GET', 'POST']) @login_required def add_local(): + flash('PieFed is still being tested so hosting communities on piefed.social is not advised except for testing purposes.', 'warning') form = AddLocalCommunity() if g.site.enable_nsfw is False: form.nsfw.render_kw = {'disabled': True} diff --git a/app/main/routes.py b/app/main/routes.py index 628f4743..b9a2134b 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -14,7 +14,7 @@ from flask_babel import _, get_locale from sqlalchemy import select, desc 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 + ap_datetime, ip_address, retrieve_block_list, shorten_string, markdown_to_text from app.models import Community, CommunityMember, Post, Site, User, utcnow, Domain, Topic @@ -35,6 +35,7 @@ def index(): page = request.args.get('page', 1, type=int) if current_user.is_anonymous: + flash(_('Create an account to tailor this feed to your interests.')) posts = Post.query.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False) else: posts = Post.query.join(CommunityMember, Post.community_id == CommunityMember.community_id).filter(CommunityMember.is_banned == False) @@ -54,7 +55,8 @@ def index(): POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK, SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, etag=f"home_{hash(str(g.site.last_active))}", next_url=next_url, prev_url=prev_url, - rss_feed=f"https://{current_app.config['SERVER_NAME']}/feed", rss_feed_name=f"Posts on " + g.site.name) + rss_feed=f"https://{current_app.config['SERVER_NAME']}/feed", rss_feed_name=f"Posts on " + g.site.name, + title=f"{g.site.name} - {g.site.description}", description=shorten_string(markdown_to_text(g.site.sidebar), 150)) @bp.route('/new', methods=['HEAD', 'GET', 'POST']) diff --git a/app/post/routes.py b/app/post/routes.py index 4cacaeb6..30520db5 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -18,7 +18,8 @@ from app.models import Post, PostReply, \ 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, domain_from_url, validate_image, gibberish, ap_datetime, return_304, \ - request_etag_matches, ip_address, user_ip_banned, instance_banned, can_downvote, can_upvote, post_ranking + 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 def show_post(post_id: int): @@ -58,6 +59,16 @@ def show_post(post_id: int): flash(_('You cannot reply to %(name)s', name=post.author.display_name())) return redirect(url_for('activitypub.post_ap', post_id=post_id)) + # avoid duplicate replies + if reply_already_exists(user_id=current_user.id, post_id=post.id, parent_id=None, body=form.body.data): + return redirect(url_for('activitypub.post_ap', post_id=post_id)) + + # disallow low-effort gif reaction posts + if reply_is_just_link_to_gif_reaction(form.body.data): + current_user.reputation -= 1 + flash(_('This type of comment is not accepted, sorry.'), 'error') + return redirect(url_for('activitypub.post_ap', post_id=post_id)) + reply = PostReply(user_id=current_user.id, post_id=post.id, community_id=community.id, body=form.body.data, body_html=markdown_to_html(form.body.data), body_html_safe=True, from_bot=current_user.bot, up_votes=1, nsfw=post.nsfw, nsfl=post.nsfl, @@ -375,6 +386,20 @@ def add_reply(post_id: int, comment_id: int): form = NewReplyForm() if form.validate_on_submit(): + if reply_already_exists(user_id=current_user.id, post_id=post.id, parent_id=in_reply_to.id, body=form.body.data): + if in_reply_to.depth <= constants.THREAD_CUTOFF_DEPTH: + return redirect(url_for('activitypub.post_ap', post_id=post_id, _anchor=f'comment_{in_reply_to.id}')) + else: + return redirect(url_for('post.continue_discussion', post_id=post_id, comment_id=in_reply_to.parent_id)) + + if reply_is_just_link_to_gif_reaction(form.body.data): + current_user.reputation -= 1 + flash(_('This type of comment is not accepted, sorry.'), 'error') + if in_reply_to.depth <= constants.THREAD_CUTOFF_DEPTH: + return redirect(url_for('activitypub.post_ap', post_id=post_id, _anchor=f'comment_{in_reply_to.id}')) + else: + return redirect(url_for('post.continue_discussion', post_id=post_id, comment_id=in_reply_to.parent_id)) + current_user.last_seen = utcnow() current_user.ip_address = ip_address() reply = PostReply(user_id=current_user.id, post_id=post.id, parent_id=in_reply_to.id, depth=in_reply_to.depth + 1, @@ -487,7 +512,7 @@ def add_reply(post_id: int, comment_id: int): send_to_remote_instance(instance.id, post.community.id, announce) if reply.depth <= constants.THREAD_CUTOFF_DEPTH: - return redirect(url_for('activitypub.post_ap', post_id=post_id, _anchor=f'comment_{reply.parent_id}')) + return redirect(url_for('activitypub.post_ap', post_id=post_id, _anchor=f'comment_{reply.id}')) else: return redirect(url_for('post.continue_discussion', post_id=post_id, comment_id=reply.parent_id)) else: diff --git a/app/utils.py b/app/utils.py index 0733ec6a..fa14d1bc 100644 --- a/app/utils.py +++ b/app/utils.py @@ -436,6 +436,30 @@ def can_create(user, content: Union[Community, Post, PostReply]) -> bool: return True +def reply_already_exists(user_id, post_id, parent_id, body) -> bool: + if parent_id is None: + num_matching_replies = db.session.execute(text( + 'SELECT COUNT(id) as c FROM "post_reply" WHERE user_id = :user_id AND post_id = :post_id AND parent_id is null AND body = :body'), + {'user_id': user_id, 'post_id': post_id, 'body': body}).scalar() + else: + num_matching_replies = db.session.execute(text( + 'SELECT COUNT(id) as c FROM "post_reply" WHERE user_id = :user_id AND post_id = :post_id AND parent_id = :parent_id AND body = :body'), + {'user_id': user_id, 'post_id': post_id, 'parent_id': parent_id, 'body': body}).scalar() + return num_matching_replies != 0 + + +def reply_is_just_link_to_gif_reaction(body) -> bool: + tmp_body = body.strip() + if tmp_body.startswith('https://media.tenor.com/') or \ + tmp_body.startswith('https://media1.giphy.com/') or \ + tmp_body.startswith('https://media2.giphy.com/') or \ + tmp_body.startswith('https://media3.giphy.com/') or \ + tmp_body.startswith('https://media4.giphy.com/'): + return True + else: + return False + + def inbox_domain(inbox: str) -> str: inbox = inbox.lower() if 'https://' in inbox or 'http://' in inbox: