From 3219a55d986159be0f92fdaf46871aa2e6a4172e Mon Sep 17 00:00:00 2001 From: freamon Date: Tue, 28 May 2024 20:18:12 +0100 Subject: [PATCH 1/3] Same default for initial fetch and refresh if 'postingRestrictedToMods' is missing from community json --- app/activitypub/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 782705cd..eb46b0a4 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -560,7 +560,7 @@ def refresh_community_profile_task(community_id): community.description_html = markdown_to_html(community.description) community.rules = activity_json['rules'] if 'rules' in activity_json else '' community.rules_html = lemmy_markdown_to_html(activity_json['rules'] if 'rules' in activity_json else '') - community.restricted_to_mods = activity_json['postingRestrictedToMods'] if 'postingRestrictedToMods' in activity_json else True + community.restricted_to_mods = activity_json['postingRestrictedToMods'] if 'postingRestrictedToMods' in activity_json else False community.new_mods_wanted = activity_json['newModsWanted'] if 'newModsWanted' in activity_json else False community.private_mods = activity_json['privateMods'] if 'privateMods' in activity_json else False community.ap_moderators_url = mods_url From 788cad653b8ec7a2b46916de4b2dfd5aabdbad8a Mon Sep 17 00:00:00 2001 From: freamon Date: Tue, 28 May 2024 20:44:06 +0100 Subject: [PATCH 2/3] Clobber PeerTube communitites to restrict posting to mods irrespective of what JSON says --- app/activitypub/util.py | 3 +++ app/community/util.py | 1 + 2 files changed, 4 insertions(+) diff --git a/app/activitypub/util.py b/app/activitypub/util.py index eb46b0a4..2f7810ee 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -611,6 +611,9 @@ def refresh_community_profile_task(community_id): new_language = find_language_or_create(ap_language['identifier'], ap_language['name']) if new_language not in community.languages: community.languages.append(new_language) + instance = Instance.query.get(community.instance_id) + if instance and instance.software == 'peertube': + community.restricted_to_mods = True db.session.commit() if community.icon_id and icon_changed: make_image_sizes(community.icon_id, 60, 250, 'communities') diff --git a/app/community/util.py b/app/community/util.py index 3cd712df..81146573 100644 --- a/app/community/util.py +++ b/app/community/util.py @@ -94,6 +94,7 @@ def retrieve_peertube_mods_and_backfill(community_id: int, mods: list): else: new_membership = CommunityMember(community_id=community.id, user_id=user.id, is_moderator=True) db.session.add(new_membership) + community.restricted_to_mods = True db.session.commit() if community.ap_public_url: From ce1d01de09d6010a08c97a77a6ba61b0e71b15c2 Mon Sep 17 00:00:00 2001 From: freamon Date: Tue, 28 May 2024 22:28:03 +0100 Subject: [PATCH 3/3] Retrieve remote post from Search (no pre-supplied community required) --- app/activitypub/util.py | 103 ++++++++++++++++++ app/search/routes.py | 19 ++++ .../community/retrieve_remote_post.html | 6 +- app/templates/search/start.html | 2 +- 4 files changed, 128 insertions(+), 2 deletions(-) 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 @@
{{ _('Retrieve Remote Post') }}
-

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) }}
diff --git a/app/templates/search/start.html b/app/templates/search/start.html index f1a8ea48..1699b26a 100644 --- a/app/templates/search/start.html +++ b/app/templates/search/start.html @@ -55,7 +55,7 @@
{{ _('Trying to add a communitiy or post from another instance?') }}

{{ _('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.') }}

+

{{ _('Retrieve remote post') }}