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
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('<p>') or request_json['object']['content'].startswith('<blockquote>')):
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 \
'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]:

View file

@ -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

View file

@ -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.')