From 2bfe5831b9054ab2bfd3db6220f8993f8031d3fd Mon Sep 17 00:00:00 2001 From: freamon Date: Mon, 27 May 2024 23:51:14 +0100 Subject: [PATCH] Allow users to retrieve posts from remote communities --- app/activitypub/util.py | 52 +++++++++++++++++-- app/community/forms.py | 5 ++ app/community/routes.py | 22 +++++++- app/templates/community/community.html | 3 ++ .../community/retrieve_remote_post.html | 37 +++++++++++++ 5 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 app/templates/community/retrieve_remote_post.html diff --git a/app/activitypub/util.py b/app/activitypub/util.py index f8b5f36e..8bc6048b 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -2326,10 +2326,10 @@ def can_delete(user_ap_id, post): return can_edit(user_ap_id, post) -def resolve_remote_post(uri: str, community_id: int, announce_actor=None) -> Union[str, None]: +def resolve_remote_post(uri: str, community_id: int, announce_actor=None) -> Union[Post, None]: post = Post.query.filter_by(ap_id=uri).first() if post: - return post.id + return post community = Community.query.get(community_id) site = Site.query.get(1) @@ -2350,7 +2350,7 @@ def resolve_remote_post(uri: str, community_id: int, announce_actor=None) -> Uni # check again that it doesn't already exist (can happen with different but equivilent URLs) post = Post.query.filter_by(ap_id=post_data['id']).first() if post: - return post.id + return post if 'attributedTo' in post_data: if isinstance(post_data['attributedTo'], str): actor = post_data['attributedTo'] @@ -2366,6 +2366,46 @@ def resolve_remote_post(uri: str, community_id: int, announce_actor=None) -> Uni if uri_domain != actor_domain: return None + if not announce_actor: + # make sure that the post actually belongs in the community a user says it does + remote_community = None + if post_data['type'] == 'Page': # lemmy + remote_community = post_data['audience'] if 'audience' in post_data else None + if remote_community and remote_community.lower() != community.ap_profile_id: + return None + elif 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': + remote_community = a['id'] + break + if remote_community and remote_community.lower() != community.ap_profile_id: + return None + else: # mastodon, etc + if 'inReplyTo' not in post_data or post_data['inReplyTo'] != None: + return None + community_found = False + if not community_found and 'to' in post_data and isinstance(post_data['to'], str): + remote_community = post_data['to'] + if remote_community.lower() == community.ap_profile_id: + community_found = True + if not community_found and 'cc' in post_data and isinstance(post_data['cc'], str): + remote_community = post_data['cc'] + if remote_community.lower() == community.ap_profile_id: + community_found = True + if not community_found and 'to' in post_data and isinstance(post_data['to'], list): + for t in post_data['to']: + if t.lower() == community.ap_profile_id: + community_found = True + break + if not community_found and 'cc' in post_data and isinstance(post_data['cc'], list): + for c in post_data['cc']: + if c.lower() == community.ap_profile_id: + community_found = True + break + if not community_found: + 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) @@ -2378,6 +2418,10 @@ def resolve_remote_post(uri: str, community_id: int, announce_actor=None) -> Uni } post = create_post(activity_log, community, request_json, user) if post: - return post.id + 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/community/forms.py b/app/community/forms.py index 8db749db..f1bba599 100644 --- a/app/community/forms.py +++ b/app/community/forms.py @@ -262,3 +262,8 @@ class ReportCommunityForm(FlaskForm): class DeleteCommunityForm(FlaskForm): submit = SubmitField(_l('Delete community')) + + +class RetrieveRemotePost(FlaskForm): + address = StringField(_l('Full URL'), render_kw={'placeholder': 'e.g. https://lemmy.world/post/123', 'autofocus': True}, validators=[DataRequired()]) + submit = SubmitField(_l('Retrieve')) diff --git a/app/community/routes.py b/app/community/routes.py index b1c76f3a..2b57b942 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -10,12 +10,12 @@ from sqlalchemy import or_, desc, text from app import db, constants, cache from app.activitypub.signature import RsaKeys, post_request, default_context -from app.activitypub.util import notify_about_post, make_image_sizes +from app.activitypub.util import notify_about_post, make_image_sizes, resolve_remote_post from app.chat.util import send_message from app.community.forms import SearchRemoteCommunity, CreateDiscussionForm, CreateImageForm, CreateLinkForm, \ ReportCommunityForm, \ DeleteCommunityForm, AddCommunityForm, EditCommunityForm, AddModeratorForm, BanUserCommunityForm, \ - EscalateReportForm, ResolveReportForm, CreateVideoForm, CreatePollForm + EscalateReportForm, ResolveReportForm, CreateVideoForm, CreatePollForm, RetrieveRemotePost from app.community.util import search_for_community, actor_to_community, \ opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file, send_to_remote_instance, \ delete_post_from_community, delete_post_reply_from_community, community_in_list @@ -129,6 +129,24 @@ def add_remote(): joined_communities=joined_communities(current_user.get_id())) +@bp.route('/retrieve_remote_post/', methods=['GET', 'POST']) +@login_required +def retrieve_remote_post(community_id: int): + if current_user.banned: + return show_ban_message() + form = RetrieveRemotePost() + new_post = None + community = Community.query.get_or_404(community_id) + if form.validate_on_submit(): + address = form.address.data.strip() + new_post = resolve_remote_post(address, community_id) + 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, community=community) + + # @bp.route('/c/', methods=['GET']) - defined in activitypub/routes.py, which calls this function for user requests. A bit weird. def show_community(community: Community): diff --git a/app/templates/community/community.html b/app/templates/community/community.html index 51d877d1..d872a67a 100644 --- a/app/templates/community/community.html +++ b/app/templates/community/community.html @@ -151,6 +151,9 @@

View community on original server

+

+ Retrieve a post from the original server +

{% endif %} {% if community.local_only %}

{{ _('Only people on %(instance_name)s can post or reply in this community.', instance_name=current_app.config['SERVER_NAME']) }}

diff --git a/app/templates/community/retrieve_remote_post.html b/app/templates/community/retrieve_remote_post.html new file mode 100644 index 00000000..a57556a1 --- /dev/null +++ b/app/templates/community/retrieve_remote_post.html @@ -0,0 +1,37 @@ +{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %} + {% extends 'themes/' + theme() + '/base.html' %} +{% else %} + {% extends "base.html" %} +{% endif %} %} +{% from 'bootstrap/form.html' import render_form %} + +{% block app_content %} +
+
+
+
+
{{ _('Retrieve Remote Post') }}
+

Enter the full URL of the post submitted to {{ community.ap_id }}

+

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) }} +
+
+
+
+ {% if new_post %} +
+
+
+
+
{{ _('Found a post:') }}
+ +
+
+
+
+ {% endif %} +{% endblock %}