diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 99af82e1..c82f702d 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -1565,108 +1565,45 @@ def create_post_reply(activity_log: ActivityPubLog, community: Community, in_rep # set depth to +1 of the parent depth if parent_comment_id: parent_comment = PostReply.query.get(parent_comment_id) - depth = parent_comment.depth + 1 else: - depth = 0 - post_reply = PostReply(user_id=user.id, community_id=community.id, - post_id=post_id, parent_id=parent_comment_id, - root_id=root_id, - nsfw=community.nsfw, - nsfl=community.nsfl, - from_bot=user.bot, - up_votes=1, - depth=depth, - score=instance_weight(user.ap_domain), - ap_id=request_json['object']['id'], - ap_create_id=request_json['id'], - ap_announce_id=announce_id, - instance_id=user.instance_id) + parent_comment = None + if post_id is None: + activity_log.exception_message = 'Could not find parent post' + return None + post = Post.query.get(post_id) + + body = body_html = '' if 'content' in request_json['object']: # Kbin, Mastodon, etc provide their posts as html if not (request_json['object']['content'].startswith('

') or request_json['object']['content'].startswith('

')): request_json['object']['content'] = '

' + request_json['object']['content'] + '

' - post_reply.body_html = allowlist_html(request_json['object']['content']) + body_html = allowlist_html(request_json['object']['content']) if 'source' in request_json['object'] and isinstance(request_json['object']['source'], dict) and \ 'mediaType' in request_json['object']['source'] and request_json['object']['source']['mediaType'] == 'text/markdown': - post_reply.body = request_json['object']['source']['content'] - post_reply.body_html = markdown_to_html(post_reply.body) # prefer Markdown if provided, overwrite version obtained from HTML + body = request_json['object']['source']['content'] + body_html = markdown_to_html(body) # prefer Markdown if provided, overwrite version obtained from HTML else: - post_reply.body = html_to_text(post_reply.body_html) + body = html_to_text(body_html) + # Language - Lemmy uses 'language' while Mastodon uses 'contentMap' + language_id = None if 'language' in request_json['object'] and isinstance(request_json['object']['language'], dict): language = find_language_or_create(request_json['object']['language']['identifier'], request_json['object']['language']['name']) - post_reply.language_id = language.id + language_id = language.id elif 'contentMap' in request_json['object'] and isinstance(request_json['object']['contentMap'], dict): language = find_language(next(iter(request_json['object']['contentMap']))) # Combination of next and iter gets the first key in a dict - post_reply.language_id = language.id if language else None + language_id = language.id if language else None - if post_id is not None: - # Discard post_reply if it contains certain phrases. Good for stopping spam floods. - if post_reply.body: - for blocked_phrase in blocked_phrases(): - if blocked_phrase in post_reply.body: - return None - post = Post.query.get(post_id) - - if post.comments_enabled: - anchor = None - if not parent_comment_id: - notification_target = post - else: - notification_target = PostReply.query.get(parent_comment_id) - - if notification_target.author.has_blocked_user(post_reply.user_id): - activity_log.exception_message = 'Replier blocked, reply discarded' - 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 - - if reply_is_stupid(post_reply.body): - activity_log.exception_message = 'Stupid reply' - activity_log.result = 'ignored' - return None - - db.session.add(post_reply) - if not user.bot: - post.reply_count += 1 - community.post_reply_count += 1 - community.last_active = post.last_active = utcnow() - activity_log.result = 'success' - post_reply.ranking = confidence(post_reply.up_votes, post_reply.down_votes) - user.post_reply_count += 1 - db.session.commit() - - # send notification to the post/comment being replied to - if parent_comment_id: - notify_about_post_reply(parent_comment, post_reply) - else: - notify_about_post_reply(None, post_reply) - - if user.reputation > 100: - post_reply.up_votes += 1 - post_reply.score += 1 - post_reply.ranking = confidence(post_reply.up_votes, post_reply.down_votes) - db.session.commit() - else: - activity_log.exception_message = 'Comments disabled, reply discarded' - activity_log.result = 'ignored' - return None - return post_reply - else: - activity_log.exception_message = 'Could not find parent post' - return None - else: - activity_log.exception_message = 'Parent not found' + post_reply = None + try: + post_reply = PostReply.new(user, post, parent_comment, notify_author=True, body=body, body_html=body_html, + language_id=language_id, request_json=request_json, announce_id=announce_id) + activity_log.result = 'success' + except Exception as ex: + activity_log.exception_message = str(ex) + activity_log.result = 'ignored' + db.session.commit() + return post_reply def create_post(activity_log: ActivityPubLog, community: Community, request_json: dict, user: User, announce_id=None) -> Union[Post, None]: diff --git a/app/models.py b/app/models.py index e140a3f4..43fe8378 100644 --- a/app/models.py +++ b/app/models.py @@ -1170,6 +1170,10 @@ class Post(db.Model): 'name': f'#{tag.name}'}) return return_value + def post_reply_count_recalculate(self): + self.post_reply_count = db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE post_id = :post_id AND deleted is false'), + {'post_id': self.id}).scalar() + # 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) @@ -1308,6 +1312,94 @@ class PostReply(db.Model): community = db.relationship('Community', lazy='joined', overlaps='replies', foreign_keys=[community_id]) language = db.relationship('Language', foreign_keys=[language_id]) + @classmethod + def new(cls, user: User, post: Post, in_reply_to, body, body_html, notify_author, language_id, request_json: dict = None, announce_id=None): + + from app.utils import shorten_string, blocked_phrases, recently_upvoted_post_replies, reply_already_exists, reply_is_just_link_to_gif_reaction, reply_is_stupid + from app.activitypub.util import notify_about_post_reply + + if not post.comments_enabled: + raise Exception('Comments are disabled on this post') + + if in_reply_to is not None: + parent_id = in_reply_to.id + depth = in_reply_to.depth + 1 + else: + parent_id = None + depth = 0 + + reply = PostReply(user_id=user.id, post_id=post.id, parent_id=parent_id, + depth=depth, + community_id=post.community.id, body=body, + body_html=body_html, body_html_safe=True, + from_bot=user.bot, nsfw=post.nsfw, nsfl=post.nsfl, + notify_author=notify_author, instance_id=user.instance_id, + language_id=language_id, + ap_id=request_json['object']['id'] if request_json else None, + ap_create_id=request_json['id'] if request_json else None, + ap_announce_id=announce_id) + if reply.body: + for blocked_phrase in blocked_phrases(): + if blocked_phrase in reply.body: + raise Exception('Blocked phrase in comment') + if in_reply_to is None or in_reply_to.parent_id is None: + notification_target = post + else: + notification_target = PostReply.query.get(in_reply_to.parent_id) + + if notification_target.author.has_blocked_user(reply.user_id): + raise Exception('Replier blocked') + + if reply_already_exists(user_id=user.id, post_id=post.id, parent_id=reply.parent_id, body=reply.body): + raise Exception('Duplicate reply') + + if reply_is_just_link_to_gif_reaction(reply.body): + user.reputation -= 1 + raise Exception('Gif comment ignored') + + if reply_is_stupid(reply.body): + raise Exception('Stupid reply') + + db.session.add(reply) + db.session.commit() + + # Notify subscribers + notify_about_post_reply(in_reply_to, reply) + + # Subscribe to own comment + if notify_author: + new_notification = NotificationSubscription(name=shorten_string(_('Replies to my comment on %(post_title)s', + post_title=post.title), 50), + user_id=user.id, entity_id=reply.id, + type=NOTIF_REPLY) + db.session.add(new_notification) + + # upvote own reply + reply.score = 1 + reply.up_votes = 1 + reply.ranking = PostReply.confidence(1, 0) + vote = PostReplyVote(user_id=user.id, post_reply_id=reply.id, author_id=user.id, effect=1) + db.session.add(vote) + if user.is_local(): + cache.delete_memoized(recently_upvoted_post_replies, user.id) + + reply.ap_id = reply.profile_id() + if user.reputation > 100: + reply.up_votes += 1 + reply.score += 1 + reply.ranking += 1 + elif user.reputation < -100: + reply.score -= 1 + reply.ranking -= 1 + if not user.bot: + post.reply_count += 1 + post.community.post_reply_count += 1 + post.community.last_active = post.last_active = utcnow() + user.post_reply_count += 1 + db.session.commit() + + return reply + def language_code(self): if self.language_id: return self.language.code @@ -1387,7 +1479,8 @@ class PostReply(db.Model): return existing_notification is not None # used for ranking comments - def _confidence(self, ups, downs): + @classmethod + def _confidence(cls, ups, downs): n = ups + downs if n == 0: @@ -1402,7 +1495,8 @@ class PostReply(db.Model): return (left - right) / under - def confidence(self, ups, downs) -> float: + @classmethod + def confidence(cls, ups, downs) -> float: if ups is None or ups < 0: ups = 0 if downs is None or downs < 0: @@ -1410,7 +1504,7 @@ class PostReply(db.Model): if ups + downs == 0: return 0.0 else: - return self._confidence(ups, downs) + return cls._confidence(ups, downs) def vote(self, user: User, vote_direction: str): existing_vote = PostReplyVote.query.filter_by(user_id=user.id, post_reply_id=self.id).first() @@ -1460,7 +1554,7 @@ class PostReply(db.Model): self.author.reputation += effect db.session.add(vote) user.last_seen = utcnow() - self.ranking = self.confidence(self.up_votes, self.down_votes) + self.ranking = PostReply.confidence(self.up_votes, self.down_votes) user.recalculate_attitude() db.session.commit() return undo diff --git a/app/post/routes.py b/app/post/routes.py index e100ec37..702cb077 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -15,7 +15,7 @@ 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 CreateLinkForm, CreateImageForm, CreateDiscussionForm, CreateVideoForm, CreatePollForm, EditImageForm -from app.post.util import post_replies, get_comment_branch, get_post_reply_count, tags_to_string, url_needs_archive, \ +from app.post.util import post_replies, get_comment_branch, tags_to_string, url_needs_archive, \ generate_archive_link, body_has_no_archive_link from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR, POST_TYPE_LINK, \ POST_TYPE_IMAGE, \ @@ -69,82 +69,16 @@ def show_post(post_id: int): form.language_id.choices = languages_for_form() if current_user.is_authenticated and current_user.verified and form.validate_on_submit(): - if not post.comments_enabled: - flash('Comments have been disabled.', 'warning') + try: + reply = PostReply.new(current_user, post, in_reply_to=None, body=piefed_markdown_to_lemmy_markdown(form.body.data), + body_html=markdown_to_html(form.body.data), notify_author=form.notify_author.data, + language_id=form.language_id.data) + except Exception as ex: + flash(_('Your reply was not accepted because %(reason)s', reason=str(ex)), 'error') return redirect(url_for('activitypub.post_ap', post_id=post_id)) - if current_user.banned: - flash('You have been banned.', 'error') - logout_user() - resp = make_response(redirect(url_for('main.index'))) - resp.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30)) - return resp - - if post.author.has_blocked_user(current_user.id): - 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)) - - # respond to comments that are just variants of 'this' - if reply_is_stupid(form.body.data): - existing_vote = PostVote.query.filter_by(user_id=current_user.id, post_id=post.id).first() - if existing_vote is None: - flash(_('We have upvoted the post for you.'), 'warning') - post_vote(post.id, 'upvote') - else: - flash(_('You have already upvoted the post, you do not need to say "this" also.'), '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=piefed_markdown_to_lemmy_markdown(form.body.data), - body_html=markdown_to_html(form.body.data), body_html_safe=True, - from_bot=current_user.bot, nsfw=post.nsfw, nsfl=post.nsfl, - notify_author=form.notify_author.data, language_id=form.language_id.data, instance_id=1) - - post.last_active = community.last_active = utcnow() - post.reply_count += 1 - community.post_reply_count += 1 - current_user.post_reply_count += 1 current_user.language_id = form.language_id.data - - db.session.add(reply) - db.session.commit() - - notify_about_post_reply(None, reply) - - # Subscribe to own comment - if form.notify_author.data: - new_notification = NotificationSubscription(name=shorten_string(_('Replies to my comment on %(post_title)s', - post_title=post.title), 50), - user_id=current_user.id, entity_id=reply.id, - type=NOTIF_REPLY) - db.session.add(new_notification) - db.session.commit() - - # upvote own reply - reply.score = 1 - reply.up_votes = 1 - reply.ranking = confidence(1, 0) - vote = PostReplyVote(user_id=current_user.id, post_reply_id=reply.id, author_id=current_user.id, effect=1) - db.session.add(vote) - cache.delete_memoized(recently_upvoted_post_replies, current_user.id) - reply.ap_id = reply.profile_id() - if current_user.reputation > 100: - reply.up_votes += 1 - reply.score += 1 - reply.ranking += 1 - elif current_user.reputation < -100: - reply.score -= 1 - reply.ranking -= 1 db.session.commit() form.body.data = '' flash('Your comment has been added.') @@ -595,49 +529,13 @@ def add_reply(post_id: int, comment_id: int): current_user.last_seen = utcnow() current_user.ip_address = ip_address() current_user.language_id = form.language_id.data - reply = PostReply(user_id=current_user.id, post_id=post.id, parent_id=in_reply_to.id, depth=in_reply_to.depth + 1, - community_id=post.community.id, body=piefed_markdown_to_lemmy_markdown(form.body.data), - body_html=markdown_to_html(form.body.data), body_html_safe=True, - from_bot=current_user.bot, nsfw=post.nsfw, nsfl=post.nsfl, - notify_author=form.notify_author.data, instance_id=1, language_id=form.language_id.data) - if reply.body: - for blocked_phrase in blocked_phrases(): - if blocked_phrase in reply.body: - abort(401) - db.session.add(reply) - db.session.commit() - # Notify subscribers - notify_about_post_reply(in_reply_to, reply) + reply = PostReply.new(current_user, post, in_reply_to, + body=piefed_markdown_to_lemmy_markdown(form.body.data), + body_html=markdown_to_html(form.body.data), + notify_author=form.notify_author.data, + language_id=form.language_id.data) - # Subscribe to own comment - if form.notify_author.data: - new_notification = NotificationSubscription(name=shorten_string(_('Replies to my comment on %(post_title)s', - post_title=post.title), 50), - user_id=current_user.id, entity_id=reply.id, - type=NOTIF_REPLY) - db.session.add(new_notification) - - # upvote own reply - reply.score = 1 - reply.up_votes = 1 - reply.ranking = confidence(1, 0) - vote = PostReplyVote(user_id=current_user.id, post_reply_id=reply.id, author_id=current_user.id, effect=1) - db.session.add(vote) - cache.delete_memoized(recently_upvoted_post_replies, current_user.id) - - reply.ap_id = reply.profile_id() - if current_user.reputation > 100: - reply.up_votes += 1 - reply.score += 1 - reply.ranking += 1 - elif current_user.reputation < -100: - reply.score -= 1 - reply.ranking -= 1 - post.reply_count = get_post_reply_count(post.id) - post.last_active = post.community.last_active = utcnow() - current_user.post_reply_count += 1 - db.session.commit() form.body.data = '' flash('Your comment has been added.')