post_reply refactor

This commit is contained in:
rimu 2024-09-28 13:05:00 +12:00
parent c893d32aaa
commit 91def29480
3 changed files with 135 additions and 206 deletions

View file

@ -1565,108 +1565,45 @@ def create_post_reply(activity_log: ActivityPubLog, community: Community, in_rep
# set depth to +1 of the parent depth # set depth to +1 of the parent depth
if parent_comment_id: if parent_comment_id:
parent_comment = PostReply.query.get(parent_comment_id) parent_comment = PostReply.query.get(parent_comment_id)
depth = parent_comment.depth + 1
else: else:
depth = 0 parent_comment = None
post_reply = PostReply(user_id=user.id, community_id=community.id, if post_id is None:
post_id=post_id, parent_id=parent_comment_id, activity_log.exception_message = 'Could not find parent post'
root_id=root_id, return None
nsfw=community.nsfw, post = Post.query.get(post_id)
nsfl=community.nsfl,
from_bot=user.bot, body = body_html = ''
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)
if 'content' in request_json['object']: # Kbin, Mastodon, etc provide their posts as html if 'content' in request_json['object']: # Kbin, Mastodon, etc provide their posts as html
if not (request_json['object']['content'].startswith('<p>') or request_json['object']['content'].startswith('<blockquote>')): if not (request_json['object']['content'].startswith('<p>') or request_json['object']['content'].startswith('<blockquote>')):
request_json['object']['content'] = '<p>' + request_json['object']['content'] + '</p>' request_json['object']['content'] = '<p>' + request_json['object']['content'] + '</p>'
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 \ 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': 'mediaType' in request_json['object']['source'] and request_json['object']['source']['mediaType'] == 'text/markdown':
post_reply.body = request_json['object']['source']['content'] 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_html = markdown_to_html(body) # prefer Markdown if provided, overwrite version obtained from HTML
else: 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 - Lemmy uses 'language' while Mastodon uses 'contentMap'
language_id = None
if 'language' in request_json['object'] and isinstance(request_json['object']['language'], dict): if 'language' in request_json['object'] and isinstance(request_json['object']['language'], dict):
language = find_language_or_create(request_json['object']['language']['identifier'], language = find_language_or_create(request_json['object']['language']['identifier'],
request_json['object']['language']['name']) 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): 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 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: post_reply = None
# Discard post_reply if it contains certain phrases. Good for stopping spam floods. try:
if post_reply.body: post_reply = PostReply.new(user, post, parent_comment, notify_author=True, body=body, body_html=body_html,
for blocked_phrase in blocked_phrases(): language_id=language_id, request_json=request_json, announce_id=announce_id)
if blocked_phrase in post_reply.body: activity_log.result = 'success'
return None except Exception as ex:
post = Post.query.get(post_id) activity_log.exception_message = str(ex)
activity_log.result = 'ignored'
if post.comments_enabled: db.session.commit()
anchor = None return post_reply
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'
def create_post(activity_log: ActivityPubLog, community: Community, request_json: dict, user: User, announce_id=None) -> Union[Post, None]: def create_post(activity_log: ActivityPubLog, community: Community, request_json: dict, user: User, announce_id=None) -> Union[Post, None]:

View file

@ -1170,6 +1170,10 @@ class Post(db.Model):
'name': f'#{tag.name}'}) 'name': f'#{tag.name}'})
return return_value 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 # 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) 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]) community = db.relationship('Community', lazy='joined', overlaps='replies', foreign_keys=[community_id])
language = db.relationship('Language', foreign_keys=[language_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): def language_code(self):
if self.language_id: if self.language_id:
return self.language.code return self.language.code
@ -1387,7 +1479,8 @@ class PostReply(db.Model):
return existing_notification is not None return existing_notification is not None
# used for ranking comments # used for ranking comments
def _confidence(self, ups, downs): @classmethod
def _confidence(cls, ups, downs):
n = ups + downs n = ups + downs
if n == 0: if n == 0:
@ -1402,7 +1495,8 @@ class PostReply(db.Model):
return (left - right) / under return (left - right) / under
def confidence(self, ups, downs) -> float: @classmethod
def confidence(cls, ups, downs) -> float:
if ups is None or ups < 0: if ups is None or ups < 0:
ups = 0 ups = 0
if downs is None or downs < 0: if downs is None or downs < 0:
@ -1410,7 +1504,7 @@ class PostReply(db.Model):
if ups + downs == 0: if ups + downs == 0:
return 0.0 return 0.0
else: else:
return self._confidence(ups, downs) return cls._confidence(ups, downs)
def vote(self, user: User, vote_direction: str): def vote(self, user: User, vote_direction: str):
existing_vote = PostReplyVote.query.filter_by(user_id=user.id, post_reply_id=self.id).first() 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 self.author.reputation += effect
db.session.add(vote) db.session.add(vote)
user.last_seen = utcnow() 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() user.recalculate_attitude()
db.session.commit() db.session.commit()
return undo return undo

View file

@ -15,7 +15,7 @@ from app.community.util import save_post, send_to_remote_instance
from app.inoculation import inoculation from app.inoculation import inoculation
from app.post.forms import NewReplyForm, ReportPostForm, MeaCulpaForm from app.post.forms import NewReplyForm, ReportPostForm, MeaCulpaForm
from app.community.forms import CreateLinkForm, CreateImageForm, CreateDiscussionForm, CreateVideoForm, CreatePollForm, EditImageForm 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 generate_archive_link, body_has_no_archive_link
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR, POST_TYPE_LINK, \ from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR, POST_TYPE_LINK, \
POST_TYPE_IMAGE, \ POST_TYPE_IMAGE, \
@ -69,82 +69,16 @@ def show_post(post_id: int):
form.language_id.choices = languages_for_form() form.language_id.choices = languages_for_form()
if current_user.is_authenticated and current_user.verified and form.validate_on_submit(): if current_user.is_authenticated and current_user.verified and form.validate_on_submit():
if not post.comments_enabled: try:
flash('Comments have been disabled.', 'warning') 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)) 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 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() 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() db.session.commit()
form.body.data = '' form.body.data = ''
flash('Your comment has been added.') 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.last_seen = utcnow()
current_user.ip_address = ip_address() current_user.ip_address = ip_address()
current_user.language_id = form.language_id.data 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 reply = PostReply.new(current_user, post, in_reply_to,
notify_about_post_reply(in_reply_to, reply) 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 = '' form.body.data = ''
flash('Your comment has been added.') flash('Your comment has been added.')