federation - receive remote posts, including edits and deletions

This commit is contained in:
rimu 2023-11-21 23:05:07 +13:00
parent 58ebdadc44
commit 6ec660005c
4 changed files with 134 additions and 7 deletions

View file

@ -1,4 +1,6 @@
from app import db
from datetime import datetime
from app import db, constants
from app.activitypub import bp
from flask import request, Response, current_app, abort, jsonify, json
@ -221,7 +223,97 @@ def shared_inbox():
if 'type' in request_json:
activity_log.activity_type = request_json['type']
if not instance_blocked(request_json['id']):
# Announce is new content and votes
# Create is new content and votes, kbin style
if request_json['type'] == 'Create':
activity_log.activity_type = request_json['type'] == 'Create (kbin)'
user_ap_id = request_json['object']['attributedTo']
community_ap_id = request_json['to'][0]
community = find_actor_or_create(community_ap_id)
user = find_actor_or_create(user_ap_id)
if user and community:
object_type = request_json['object']['type']
new_content_types = ['Page', 'Article', 'Link', 'Note']
if object_type in new_content_types: # create a new post
in_reply_to = request_json['object']['inReplyTo'] if 'inReplyTo' in \
request_json[
'object'] else None
if not in_reply_to:
post = Post(user_id=user.id, community_id=community.id,
title=request_json['object']['name'],
comments_enabled=request_json['object'][
'commentsEnabled'],
sticky=request_json['object']['stickied'] if 'stickied' in
request_json[
'object'] else False,
nsfw=request_json['object']['sensitive'],
nsfl=request_json['object']['nsfl'] if 'nsfl' in request_json[
'object'] else False,
ap_id=request_json['object']['id'],
ap_create_id=request_json['id'],
ap_announce_id=None,
type=constants.POST_TYPE_ARTICLE
)
if 'source' in request_json['object'] and \
request_json['object']['source'][
'mediaType'] == 'text/markdown':
post.body = request_json['object']['source']['content']
post.body_html = markdown_to_html(post.body)
elif 'content' in request_json['object']:
post.body_html = allowlist_html(request_json['object']['content'])
post.body = html_to_markdown(post.body_html)
if 'attachment' in request_json['object'] and \
len(request_json['object']['attachment']) > 0 and \
'type' in request_json['object']['attachment'][0]:
if request_json['object']['attachment'][0]['type'] == 'Link':
post.url = request_json['object']['attachment'][0]['href']
if is_image_url(post.url):
post.type = POST_TYPE_IMAGE
else:
post.type = POST_TYPE_LINK
domain = domain_from_url(post.url)
if not domain.banned:
post.domain_id = domain.id
else:
post = None
activity_log.exception_message = domain.name + ' is blocked by admin'
activity_log.result = 'failure'
if 'image' in request_json['object']:
image = File(source_url=request_json['object']['image']['url'])
db.session.add(image)
post.image = image
if post is not None:
db.session.add(post)
community.post_count += 1
db.session.commit()
else:
post_id, parent_comment_id, root_id = find_reply_parent(in_reply_to)
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,
ap_id=request_json['object']['id'],
ap_create_id=request_json['id'],
ap_announce_id=None)
if 'source' in request_json['object'] 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)
elif 'content' in request_json['object']:
post_reply.body_html = allowlist_html(
request_json['object']['content'])
post_reply.body = html_to_markdown(post_reply.body_html)
if post_reply is not None:
db.session.add(post_reply)
community.post_reply_count += 1
db.session.commit()
else:
activity_log.exception_message = 'Unacceptable type (kbin): ' + object_type
# Announce is new content and votes, lemmy style
if request_json['type'] == 'Announce':
if request_json['object']['type'] == 'Create':
activity_log.activity_type = request_json['object']['type']
@ -246,6 +338,7 @@ def shared_inbox():
ap_id=request_json['object']['object']['id'],
ap_create_id=request_json['object']['id'],
ap_announce_id=request_json['id'],
type=constants.POST_TYPE_ARTICLE
)
if 'source' in request_json['object']['object'] and \
request_json['object']['object']['source']['mediaType'] == 'text/markdown':
@ -431,7 +524,32 @@ def shared_inbox():
...
elif request_json['object']['type'] == 'Dislike': # Undoing a downvote
...
elif request_json['type'] == 'Update':
if request_json['object']['type'] == 'Page': # Editing a post
post = Post.query.filter_by(ap_id=request_json['object']['id']).first()
if post:
if 'source' in request_json['object'] and \
request_json['object']['source']['mediaType'] == 'text/markdown':
post.body = request_json['object']['source']['content']
post.body_html = markdown_to_html(post.body)
elif 'content' in request_json['object']:
post.body_html = allowlist_html(request_json['object']['content'])
post.body = html_to_markdown(post.body_html)
post.edited_at = datetime.utcnow()
db.session.commit()
activity_log.result = 'success'
elif request_json['type'] == 'Delete':
post = Post.query.filter_by(ap_id=request_json['object']['id']).first()
if post:
post.delete_dependencies()
db.session.delete(post)
else:
reply = PostReply.query.filter_by(ap_id=request_json['object']['id']).first()
if reply:
reply.body_html = '<p><em>deleted</em></p>'
reply.body = 'deleted'
db.session.commit()
activity_log.result = 'success'
else:
activity_log.exception_message = 'Instance banned'
else:

View file

@ -3,7 +3,7 @@ from datetime import date, datetime, timedelta
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort
from flask_login import login_user, logout_user, current_user, login_required
from flask_babel import _
from sqlalchemy import or_
from sqlalchemy import or_, desc
from app import db, constants
from app.activitypub.signature import RsaKeys, HttpSignature
@ -86,9 +86,9 @@ def show_community(community: Community):
mod_list = User.query.filter(User.id.in_(mod_user_ids)).all()
if current_user.is_anonymous or current_user.ignore_bots:
posts = community.posts.filter(Post.from_bot == False).all()
posts = community.posts.filter(Post.from_bot == False).order_by(desc(Post.last_active)).all()
else:
posts = community.posts
posts = community.posts.order_by(desc(Post.last_active))
description = shorten_string(community.description, 150) if community.description else None
og_image = community.image.source_url if community.image_id else None

View file

@ -353,6 +353,12 @@ class Post(db.Model):
def get_by_ap_id(cls, ap_id):
return cls.query.filter_by(ap_id=ap_id).first()
def delete_dependencies(self):
db.session.execute(text('DELETE FROM post_reply_vote WHERE post_reply_id IN (SELECT id FROM post_reply WHERE post_id = :post_id)'),
{'post_id': self.id})
db.session.execute(text('DELETE FROM post_reply WHERE post_id = :post_id'), {'post_id': self.id})
db.session.execute(text('DELETE FROM post_vote WHERE post_id = :post_id'), {'post_id': self.id})
class PostReply(db.Model):
query_class = FullTextSearchQuery

View file

@ -18,7 +18,9 @@
<img src="/static/images/external_link_black.svg" class="external_link_icon" alt="External link" />
</a></small></p>
{% endif %}
<p><small>submitted {{ moment(post.posted_at).fromNow() }} by {{ render_username(post.author) }}</small></p>
<p><small>submitted {{ moment(post.posted_at).fromNow() }} by {{ render_username(post.author) }}
{% if post.edited_at %} edited {{ moment(post.edited_at).fromNow() }}{% endif %}
</small></p>
</div>
<div class="col-4">
{% if post.url %}
@ -50,6 +52,7 @@
{% endif %}
<p class="small">submitted {{ moment(post.posted_at).fromNow() }} by
{{ render_username(post.author) }}
{% if post.edited_at %} edited {{ moment(post.edited_at).fromNow() }}{% endif %}
</p>
</div>
{% endif %}