diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 2f7810ee..9e0fa270 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -2430,3 +2430,106 @@ def resolve_remote_post(uri: str, community_id: int, announce_actor=None) -> Uni return post return None + + +def resolve_remote_post_from_search(uri: str) -> Union[Post, None]: + post = Post.query.filter_by(ap_id=uri).first() + if post: + return post + + site = Site.query.get(1) + + parsed_url = urlparse(uri) + uri_domain = parsed_url.netloc + actor_domain = None + actor = None + post_request = get_request(uri, headers={'Accept': 'application/activity+json'}) + if post_request.status_code == 200: + post_data = post_request.json() + post_request.close() + # check again that it doesn't already exist (can happen with different but equivalent URLs) + post = Post.query.filter_by(ap_id=post_data['id']).first() + if post: + return post + + # find the author of the post. Make sure their domain matches the site hosting it to migitage impersonation attempts + if 'attributedTo' in post_data: + if isinstance(post_data['attributedTo'], str): + actor = post_data['attributedTo'] + parsed_url = urlparse(post_data['attributedTo']) + actor_domain = parsed_url.netloc + elif isinstance(post_data['attributedTo'], list): + for a in post_data['attributedTo']: + if a['type'] == 'Person': + actor = a['id'] + parsed_url = urlparse(a['id']) + actor_domain = parsed_url.netloc + break + if uri_domain != actor_domain: + return None + + # find the community the post was submitted to + community = None + if not community and post_data['type'] == 'Page': # lemmy + if 'audience' in post_data: + community_id = post_data['audience'] + community = Community.query.filter_by(ap_profile_id=community_id).first() + + if not community and post_data['type'] == 'Video': # peertube + if 'attributedTo' in post_data and isinstance(post_data['attributedTo'], list): + for a in post_data['attributedTo']: + if a['type'] == 'Group': + community_id = a['id'] + community = Community.query.filter_by(ap_profile_id=community_id).first() + if community: + break + + if not community: # mastodon, etc + if 'inReplyTo' not in post_data or post_data['inReplyTo'] != None: + return None + + if not community and 'to' in post_data and isinstance(post_data['to'], str): + community_id = post_data['to'].lower() + if not community_id == 'https://www.w3.org/ns/activitystreams#Public' and not community_id.endswith('/followers'): + community = Community.query.filter_by(ap_profile_id=community_id).first() + if not community and 'cc' in post_data and isinstance(post_data['cc'], str): + community_id = post_data['cc'].lower() + if not community_id == 'https://www.w3.org/ns/activitystreams#Public' and not community_id.endswith('/followers'): + community = Community.query.filter_by(ap_profile_id=community_id).first() + if not community and 'to' in post_data and isinstance(post_data['to'], list): + for t in post_data['to']: + community_id = t.lower() + if not community_id == 'https://www.w3.org/ns/activitystreams#Public' and not community_id.endswith('/followers'): + community = Community.query.filter_by(ap_profile_id=community_id).first() + if community: + break + if not community and 'cc' in post_data and isinstance(post_data['to'], list): + for c in post_data['cc']: + community_id = c.lower() + if not community_id == 'https://www.w3.org/ns/activitystreams#Public' and not community_id.endswith('/followers'): + community = Community.query.filter_by(ap_profile_id=community_id).first() + if community: + break + + if not community: + return None + + activity_log = ActivityPubLog(direction='in', activity_id=post_data['id'], activity_type='Resolve Post', result='failure') + if site.log_activitypub_json: + activity_log.activity_json = json.dumps(post_data) + db.session.add(activity_log) + user = find_actor_or_create(actor) + if user and community and post_data: + request_json = { + 'id': f"https://{uri_domain}/activities/create/gibberish(15)", + 'object': post_data + } + post = create_post(activity_log, community, request_json, user) + if post: + if 'published' in post_data: + post.posted_at=post_data['published'] + post.last_active=post_data['published'] + db.session.commit() + return post + + return None diff --git a/app/search/routes.py b/app/search/routes.py index 399595a6..c1b526ae 100644 --- a/app/search/routes.py +++ b/app/search/routes.py @@ -7,6 +7,8 @@ from app.models import Post, Language, Community from app.search import bp from app.utils import moderating_communities, joined_communities, render_template, blocked_domains, blocked_instances, \ communities_banned_from, recently_upvoted_posts, recently_downvoted_posts, blocked_users +from app.community.forms import RetrieveRemotePost +from app.activitypub.util import resolve_remote_post_from_search @bp.route('/search', methods=['GET', 'POST']) @@ -87,3 +89,20 @@ def run_search(): moderating_communities=moderating_communities(current_user.get_id()), joined_communities=joined_communities(current_user.get_id()), site=g.site) + + +@bp.route('/retrieve_remote_post', methods=['GET', 'POST']) +@login_required +def retrieve_remote_post(): + if current_user.banned: + return show_ban_message() + form = RetrieveRemotePost() + new_post = None + if form.validate_on_submit(): + address = form.address.data.strip() + new_post = resolve_remote_post_from_search(address) + if new_post is None: + flash(_('Post not found.'), 'warning') + + return render_template('community/retrieve_remote_post.html', + title=_('Retrieve Remote Post'), form=form, new_post=new_post) diff --git a/app/templates/community/retrieve_remote_post.html b/app/templates/community/retrieve_remote_post.html index a57556a1..0e963f91 100644 --- a/app/templates/community/retrieve_remote_post.html +++ b/app/templates/community/retrieve_remote_post.html @@ -11,7 +11,11 @@
Enter the full URL of the post submitted to {{ community.ap_id }}
+ {% if community %} +Enter the full URL of the post submitted to {{ community.ap_id }}
+ {% else %} +Enter the full URL of the post
+ {% endif %}Note: URL needs to match the one from the post author's instance (which may be different than the community's instance)
{{ render_form(form) }}{{ _('In many types of federated platforms you can put a URL of a post or community into the search in order to add it to your local instance. In PieFed the search is just for searching.') }}
{{ _('Add remote community') }}
-{{ _('To add a post from a remote instance, find the community in PieFed then look for the "Retrieve a post from the original server" link.') }}
+