Allow users to retrieve posts from remote communities

This commit is contained in:
freamon 2024-05-27 23:51:14 +01:00
parent 8da2b68ab1
commit 2bfe5831b9
5 changed files with 113 additions and 6 deletions

View file

@ -2326,10 +2326,10 @@ def can_delete(user_ap_id, post):
return can_edit(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() post = Post.query.filter_by(ap_id=uri).first()
if post: if post:
return post.id return post
community = Community.query.get(community_id) community = Community.query.get(community_id)
site = Site.query.get(1) 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) # 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() post = Post.query.filter_by(ap_id=post_data['id']).first()
if post: if post:
return post.id return post
if 'attributedTo' in post_data: if 'attributedTo' in post_data:
if isinstance(post_data['attributedTo'], str): if isinstance(post_data['attributedTo'], str):
actor = post_data['attributedTo'] 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: if uri_domain != actor_domain:
return None 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') activity_log = ActivityPubLog(direction='in', activity_id=post_data['id'], activity_type='Resolve Post', result='failure')
if site.log_activitypub_json: if site.log_activitypub_json:
activity_log.activity_json = json.dumps(post_data) 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) post = create_post(activity_log, community, request_json, user)
if post: 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 return None

View file

@ -262,3 +262,8 @@ class ReportCommunityForm(FlaskForm):
class DeleteCommunityForm(FlaskForm): class DeleteCommunityForm(FlaskForm):
submit = SubmitField(_l('Delete community')) 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'))

View file

@ -10,12 +10,12 @@ from sqlalchemy import or_, desc, text
from app import db, constants, cache from app import db, constants, cache
from app.activitypub.signature import RsaKeys, post_request, default_context 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.chat.util import send_message
from app.community.forms import SearchRemoteCommunity, CreateDiscussionForm, CreateImageForm, CreateLinkForm, \ from app.community.forms import SearchRemoteCommunity, CreateDiscussionForm, CreateImageForm, CreateLinkForm, \
ReportCommunityForm, \ ReportCommunityForm, \
DeleteCommunityForm, AddCommunityForm, EditCommunityForm, AddModeratorForm, BanUserCommunityForm, \ 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, \ 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, \ 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 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())) joined_communities=joined_communities(current_user.get_id()))
@bp.route('/retrieve_remote_post/<int:community_id>', 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/<actor>', methods=['GET']) - defined in activitypub/routes.py, which calls this function for user requests. A bit weird. # @bp.route('/c/<actor>', methods=['GET']) - defined in activitypub/routes.py, which calls this function for user requests. A bit weird.
def show_community(community: Community): def show_community(community: Community):

View file

@ -151,6 +151,9 @@
<p> <p>
<a href="{{ community.profile_id() }}">View community on original server</a> <a href="{{ community.profile_id() }}">View community on original server</a>
</p> </p>
<p>
<a href="/community/retrieve_remote_post/{{ community.id }}">Retrieve a post from the original server</a>
</p>
{% endif %} {% endif %}
{% if community.local_only %} {% if community.local_only %}
<p>{{ _('Only people on %(instance_name)s can post or reply in this community.', instance_name=current_app.config['SERVER_NAME']) }}</p> <p>{{ _('Only people on %(instance_name)s can post or reply in this community.', instance_name=current_app.config['SERVER_NAME']) }}</p>

View file

@ -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 %}
<div class="row">
<div class="col mx-auto">
<div class="card mt-5">
<div class="card-body p-6">
<div class="card-title">{{ _('Retrieve Remote Post') }}</div>
<p>Enter the full URL of the post submitted to {{ community.ap_id }}</p>
<p>Note: URL needs to match the one from the post author's instance (which may be different than the community's instance)</p>
{{ render_form(form) }}
</div>
</div>
</div>
</div>
{% if new_post %}
<div class="row">
<div class="col mx-auto">
<div class="card mt-5">
<div class="card-body p-6">
<div class="card-title">{{ _('Found a post:') }}</div>
<div class="card-body">
<p>
<a href="/post/{{ new_post.id }}">{{ new_post.title }}</a>
</p>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}