mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-02-03 00:31:25 -08:00
Merge remote-tracking branch 'origin/main'
# Conflicts: # app/activitypub/routes.py # app/activitypub/util.py
This commit is contained in:
commit
bf11490011
4 changed files with 222 additions and 59 deletions
|
@ -18,8 +18,8 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti
|
|||
lemmy_site_data, instance_weight, is_activitypub_request, downvote_post_reply, downvote_post, upvote_post_reply, \
|
||||
upvote_post, activity_already_ingested, delete_post_or_comment, community_members, \
|
||||
user_removed_from_remote_server, create_post, create_post_reply, update_post_reply_from_activity, \
|
||||
update_post_from_activity, undo_vote, undo_downvote
|
||||
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, render_template, \
|
||||
update_post_from_activity, undo_vote, undo_downvote, post_to_page
|
||||
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \
|
||||
domain_from_url, markdown_to_html, community_membership, ap_datetime, markdown_to_text, ip_address, can_downvote, \
|
||||
can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply, sha256_digest, \
|
||||
community_moderators
|
||||
|
@ -432,20 +432,43 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
activity_log.result = 'success'
|
||||
else:
|
||||
try:
|
||||
community_ap_id = request_json['to'][0]
|
||||
if community_ap_id == 'https://www.w3.org/ns/activitystreams#Public': # kbin does this when posting a reply
|
||||
if 'to' in request_json['object'] and request_json['object']['to']:
|
||||
community_ap_id = request_json['object']['to'][0]
|
||||
if community_ap_id == 'https://www.w3.org/ns/activitystreams#Public' and 'cc' in \
|
||||
request_json['object'] and request_json['object']['cc']:
|
||||
community_ap_id = request_json['object']['cc'][0]
|
||||
elif 'cc' in request_json['object'] and request_json['object']['cc']:
|
||||
community_ap_id = request_json['object']['cc'][0]
|
||||
if community_ap_id.endswith('/followers'): # mastodon
|
||||
if 'inReplyTo' in request_json['object']:
|
||||
post_being_replied_to = Post.query.filter_by(ap_id=request_json['object']['inReplyTo']).first()
|
||||
if post_being_replied_to:
|
||||
community_ap_id = post_being_replied_to.community.ap_profile_id
|
||||
community_ap_id = ''
|
||||
locations = ['audience', 'cc', 'to']
|
||||
if 'object' in request_json:
|
||||
rjs = [ request_json, request_json['object'] ]
|
||||
else:
|
||||
rjs = [ request_json ]
|
||||
local_community_prefix = f"https://{current_app.config['SERVER_NAME']}/c/"
|
||||
followers_suffix = '/followers'
|
||||
for rj in rjs:
|
||||
for loc in locations:
|
||||
if loc in rj:
|
||||
id = rj[loc]
|
||||
if isinstance(id, str):
|
||||
if id.startswith(local_community_prefix) and not id.endswith(followers_suffix):
|
||||
community_ap_id = id
|
||||
if isinstance(id, list):
|
||||
for c in id:
|
||||
if c.startswith(local_community_prefix) and not c.endswith(followers_suffix):
|
||||
community_ap_id = c
|
||||
break
|
||||
if community_ap_id:
|
||||
break
|
||||
if community_ap_id:
|
||||
break
|
||||
if not community_ap_id and 'object' in request_json and 'inReplyTo' in request_json['object']:
|
||||
post_being_replied_to = Post.query.filter_by(ap_id=request_json['object']['inReplyTo']).first()
|
||||
if post_being_replied_to:
|
||||
community_ap_id = post_being_replied_to.community.ap_profile_id
|
||||
else:
|
||||
comment_being_replied_to = PostReply.query.filter_by(ap_id=request_json['object']['inReplyTo']).first()
|
||||
if comment_being_replied_to:
|
||||
community_ap_id = comment_being_replied_to.community.ap_profile_id
|
||||
if not community_ap_id:
|
||||
activity_log.result = 'failure'
|
||||
activity_log.exception_message = 'Unable to extract community'
|
||||
db.session.commit()
|
||||
return
|
||||
except:
|
||||
activity_log.activity_type = 'exception'
|
||||
db.session.commit()
|
||||
|
@ -1029,6 +1052,27 @@ def community_outbox(actor):
|
|||
return jsonify(community_data)
|
||||
|
||||
|
||||
@bp.route('/c/<actor>/featured', methods=['GET'])
|
||||
def community_featured(actor):
|
||||
actor = actor.strip()
|
||||
community = Community.query.filter_by(name=actor, banned=False, ap_id=None).first()
|
||||
if community is not None:
|
||||
posts = Post.query.filter_by(community_id=community.id, sticky=True).all()
|
||||
|
||||
community_data = {
|
||||
"@context": default_context(),
|
||||
"type": "OrderedCollection",
|
||||
"id": f"https://{current_app.config['SERVER_NAME']}/c/{actor}/featured",
|
||||
"totalItems": len(posts),
|
||||
"orderedItems": []
|
||||
}
|
||||
|
||||
for post in posts:
|
||||
community_data['orderedItems'].append(post_to_page(post, community))
|
||||
|
||||
return jsonify(community_data)
|
||||
|
||||
|
||||
@bp.route('/c/<actor>/moderators', methods=['GET'])
|
||||
def community_moderators_route(actor):
|
||||
actor = actor.strip()
|
||||
|
@ -1069,7 +1113,7 @@ def community_followers(actor):
|
|||
if community is not None:
|
||||
result = {
|
||||
"@context": default_context(),
|
||||
"id": f'https://{current_app.config["SERVER_NAME"]}/c/actor/followers',
|
||||
"id": f'https://{current_app.config["SERVER_NAME"]}/c/{actor}/followers',
|
||||
"type": "Collection",
|
||||
"totalItems": community_members(community.id),
|
||||
"items": []
|
||||
|
|
|
@ -25,7 +25,7 @@ import pytesseract
|
|||
from app.utils import get_request, allowlist_html, get_setting, ap_datetime, markdown_to_html, \
|
||||
is_image_url, domain_from_url, gibberish, ensure_directory_exists, markdown_to_text, head_request, post_ranking, \
|
||||
shorten_string, reply_already_exists, reply_is_just_link_to_gif_reaction, confidence, remove_tracking_from_link, \
|
||||
blocked_phrases
|
||||
blocked_phrases, microblog_content_to_title
|
||||
|
||||
|
||||
def public_key():
|
||||
|
@ -178,6 +178,43 @@ def post_to_activity(post: Post, community: Community):
|
|||
return activity_data
|
||||
|
||||
|
||||
def post_to_page(post: Post, community: Community):
|
||||
activity_data = {
|
||||
"type": "Page",
|
||||
"id": post.ap_id,
|
||||
"attributedTo": post.author.ap_public_url,
|
||||
"to": [
|
||||
f"https://{current_app.config['SERVER_NAME']}/c/{community.name}",
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"name": post.title,
|
||||
"cc": [],
|
||||
"content": post.body_html if post.body_html else '',
|
||||
"mediaType": "text/html",
|
||||
"source": {
|
||||
"content": post.body if post.body else '',
|
||||
"mediaType": "text/markdown"
|
||||
},
|
||||
"attachment": [],
|
||||
"commentsEnabled": post.comments_enabled,
|
||||
"sensitive": post.nsfw or post.nsfl,
|
||||
"published": ap_datetime(post.created_at),
|
||||
"stickied": post.sticky,
|
||||
"audience": f"https://{current_app.config['SERVER_NAME']}/c/{community.name}"
|
||||
}
|
||||
if post.edited_at is not None:
|
||||
activity_data["updated"] = ap_datetime(post.edited_at)
|
||||
if post.language is not None:
|
||||
activity_data["language"] = {"identifier": post.language}
|
||||
if post.type == POST_TYPE_LINK and post.url is not None:
|
||||
activity_data["attachment"] = [{"href": post.url, "type": "Link"}]
|
||||
if post.image_id is not None:
|
||||
activity_data["image"] = {"url": post.image.view_url(), "type": "Image"}
|
||||
if post.image.alt_text:
|
||||
activity_data["image"]['altText'] = post.image.alt_text
|
||||
return activity_data
|
||||
|
||||
|
||||
def banned_user_agents():
|
||||
return [] # todo: finish this function
|
||||
|
||||
|
@ -1258,11 +1295,17 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
|
|||
activity_log.exception_message = 'Community is local only, post discarded'
|
||||
activity_log.result = 'ignored'
|
||||
return None
|
||||
if 'name' not in request_json['object']: # Microblog posts sometimes get Announced by lemmy. They don't have a title, so we can't use them.
|
||||
return None
|
||||
nsfl_in_title = '[NSFL]' in request_json['object']['name'].upper() or '(NSFL)' in request_json['object']['name'].upper()
|
||||
if 'name' not in request_json['object']: # Microblog posts
|
||||
if 'content' in request_json['object'] and request_json['object']['content'] is not None:
|
||||
name = "[Microblog]"
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
name = request_json['object']['name']
|
||||
|
||||
nsfl_in_title = '[NSFL]' in name.upper() or '(NSFL)' in name.upper()
|
||||
post = Post(user_id=user.id, community_id=community.id,
|
||||
title=html.unescape(request_json['object']['name']),
|
||||
title=html.unescape(name),
|
||||
comments_enabled=request_json['object']['commentsEnabled'] if 'commentsEnabled' in request_json['object'] else True,
|
||||
sticky=request_json['object']['stickied'] if 'stickied' in request_json['object'] else False,
|
||||
nsfw=request_json['object']['sensitive'] if 'sensitive' in request_json['object'] else False,
|
||||
|
@ -1283,7 +1326,12 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
|
|||
post.body_html = markdown_to_html(post.body)
|
||||
elif 'content' in request_json['object'] and request_json['object']['content'] is not None: # Kbin
|
||||
post.body_html = allowlist_html(request_json['object']['content'])
|
||||
post.body = ''
|
||||
post.body = html_to_markdown(post.body_html)
|
||||
if name == "[Microblog]":
|
||||
name += ' ' + microblog_content_to_title(post.body_html)
|
||||
if '[NSFL]' in name.upper() or '(NSFL)' in name.upper():
|
||||
post.nsfl = True
|
||||
post.title = name
|
||||
# Discard post if it contains certain phrases. Good for stopping spam floods.
|
||||
blocked_phrases_list = blocked_phrases()
|
||||
for blocked_phrase in blocked_phrases_list:
|
||||
|
@ -1296,7 +1344,10 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
|
|||
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']
|
||||
post.url = request_json['object']['attachment'][0]['href'] # Lemmy
|
||||
if request_json['object']['attachment'][0]['type'] == 'Document':
|
||||
post.url = request_json['object']['attachment'][0]['url'] # Mastodon
|
||||
if post.url:
|
||||
if is_image_url(post.url):
|
||||
post.type = POST_TYPE_IMAGE
|
||||
if 'image' in request_json['object'] and 'url' in request_json['object']['image']:
|
||||
|
|
|
@ -539,14 +539,13 @@ def add_reply(post_id: int, comment_id: int):
|
|||
reply_json = {
|
||||
'type': 'Note',
|
||||
'id': reply.profile_id(),
|
||||
'attributedTo': current_user.profile_id(),
|
||||
'attributedTo': current_user.public_url(),
|
||||
'to': [
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
in_reply_to.author.profile_id()
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc': [
|
||||
post.community.profile_id(),
|
||||
current_user.followers_url()
|
||||
post.community.public_url(),
|
||||
in_reply_to.author.public_url()
|
||||
],
|
||||
'content': reply.body_html,
|
||||
'inReplyTo': in_reply_to.profile_id(),
|
||||
|
@ -558,7 +557,7 @@ def add_reply(post_id: int, comment_id: int):
|
|||
},
|
||||
'published': ap_datetime(utcnow()),
|
||||
'distinguished': False,
|
||||
'audience': post.community.profile_id(),
|
||||
'audience': post.community.public_url(),
|
||||
'contentMap': {
|
||||
'en': reply.body_html
|
||||
}
|
||||
|
@ -566,15 +565,14 @@ def add_reply(post_id: int, comment_id: int):
|
|||
create_json = {
|
||||
'@context': default_context(),
|
||||
'type': 'Create',
|
||||
'actor': current_user.profile_id(),
|
||||
'audience': post.community.profile_id(),
|
||||
'actor': current_user.public_url(),
|
||||
'audience': post.community.public_url(),
|
||||
'to': [
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
in_reply_to.author.profile_id()
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc': [
|
||||
post.community.profile_id(),
|
||||
current_user.followers_url()
|
||||
post.community.public_url(),
|
||||
in_reply_to.author.public_url()
|
||||
],
|
||||
'object': reply_json,
|
||||
'id': f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}"
|
||||
|
@ -582,8 +580,15 @@ def add_reply(post_id: int, comment_id: int):
|
|||
if in_reply_to.notify_author and in_reply_to.author.ap_id is not None:
|
||||
reply_json['tag'] = [
|
||||
{
|
||||
'href': in_reply_to.author.ap_profile_id,
|
||||
'name': '@' + in_reply_to.author.ap_id,
|
||||
'href': in_reply_to.author.public_url(),
|
||||
'name': in_reply_to.author.mention_tag(),
|
||||
'type': 'Mention'
|
||||
}
|
||||
]
|
||||
create_json['tag'] = [
|
||||
{
|
||||
'href': in_reply_to.author.public_url(),
|
||||
'name': in_reply_to.author.mention_tag(),
|
||||
'type': 'Mention'
|
||||
}
|
||||
]
|
||||
|
@ -599,7 +604,7 @@ def add_reply(post_id: int, comment_id: int):
|
|||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"actor": post.community.ap_profile_id,
|
||||
"actor": post.community.public_url(),
|
||||
"cc": [
|
||||
post.community.ap_followers_url
|
||||
],
|
||||
|
@ -611,6 +616,17 @@ def add_reply(post_id: int, comment_id: int):
|
|||
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
|
||||
send_to_remote_instance(instance.id, post.community.id, announce)
|
||||
|
||||
# send copy of Note to comment author (who won't otherwise get it if no-one else on their instance is subscribed to the community)
|
||||
if not in_reply_to.author.is_local() and in_reply_to.author.ap_domain != reply.community.ap_domain:
|
||||
if not post.community.is_local() or (post.community.is_local and not post.community.has_followers_from_domain(in_reply_to.author.ap_domain)):
|
||||
success = post_request(in_reply_to.author.ap_inbox_url, create_json, current_user.private_key,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
if not success:
|
||||
# sending to shared inbox is good enough for Mastodon, but Lemmy will reject it the local community has no followers
|
||||
personal_inbox = in_reply_to.author.public_url() + '/inbox'
|
||||
post_request(personal_inbox, create_json, current_user.private_key,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
|
||||
if reply.depth <= constants.THREAD_CUTOFF_DEPTH:
|
||||
return redirect(url_for('activitypub.post_ap', post_id=post_id, _anchor=f'comment_{reply.id}'))
|
||||
else:
|
||||
|
@ -1047,14 +1063,13 @@ def post_reply_edit(post_id: int, comment_id: int):
|
|||
reply_json = {
|
||||
'type': 'Note',
|
||||
'id': post_reply.profile_id(),
|
||||
'attributedTo': current_user.profile_id(),
|
||||
'attributedTo': current_user.public_url(),
|
||||
'to': [
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
in_reply_to.author.profile_id()
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc': [
|
||||
post.community.profile_id(),
|
||||
current_user.followers_url()
|
||||
post.community.public_url(),
|
||||
in_reply_to.author.public_url()
|
||||
],
|
||||
'content': post_reply.body_html,
|
||||
'inReplyTo': in_reply_to.profile_id(),
|
||||
|
@ -1067,37 +1082,54 @@ def post_reply_edit(post_id: int, comment_id: int):
|
|||
'published': ap_datetime(post_reply.posted_at),
|
||||
'updated': ap_datetime(post_reply.edited_at),
|
||||
'distinguished': False,
|
||||
'audience': post.community.profile_id(),
|
||||
'audience': post.community.public_url(),
|
||||
'contentMap': {
|
||||
'en': post_reply.body_html
|
||||
}
|
||||
}
|
||||
update_json = {
|
||||
'id': f"https://{current_app.config['SERVER_NAME']}/activities/update/{gibberish(15)}",
|
||||
'@context': default_context(),
|
||||
'type': 'Update',
|
||||
'actor': current_user.profile_id(),
|
||||
'audience': post.community.profile_id(),
|
||||
'to': [post.community.profile_id(), 'https://www.w3.org/ns/activitystreams#Public'],
|
||||
'published': ap_datetime(utcnow()),
|
||||
'actor': current_user.public_url(),
|
||||
'audience': post.community.public_url(),
|
||||
'to': [
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc': [
|
||||
current_user.followers_url()
|
||||
post.community.public_url(),
|
||||
in_reply_to.author.public_url()
|
||||
],
|
||||
'object': reply_json,
|
||||
'id': f"https://{current_app.config['SERVER_NAME']}/activities/update/{gibberish(15)}"
|
||||
}
|
||||
|
||||
if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
|
||||
if in_reply_to.notify_author and in_reply_to.author.ap_id is not None:
|
||||
reply_json['tag'] = [
|
||||
{
|
||||
'href': in_reply_to.author.public_url(),
|
||||
'name': in_reply_to.author.mention_tag(),
|
||||
'type': 'Mention'
|
||||
}
|
||||
]
|
||||
update_json['tag'] = [
|
||||
{
|
||||
'href': in_reply_to.author.public_url(),
|
||||
'name': in_reply_to.author.mention_tag(),
|
||||
'type': 'Mention'
|
||||
}
|
||||
]
|
||||
if not post.community.is_local(): # this is a remote community, send it to the instance that hosts it
|
||||
success = post_request(post.community.ap_inbox_url, update_json, current_user.private_key,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
if not success:
|
||||
flash('Failed to send edit to remote server', 'error')
|
||||
else: # local community - send it to followers on remote instances
|
||||
flash('Failed to send send edit to remote server', 'error')
|
||||
else: # local community - send it to followers on remote instances
|
||||
announce = {
|
||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}",
|
||||
"type": 'Announce',
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"actor": post.community.ap_profile_id,
|
||||
"actor": post.community.public_url(),
|
||||
"cc": [
|
||||
post.community.ap_followers_url
|
||||
],
|
||||
|
@ -1106,9 +1138,20 @@ def post_reply_edit(post_id: int, comment_id: int):
|
|||
}
|
||||
|
||||
for instance in post.community.following_instances():
|
||||
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(
|
||||
instance.domain):
|
||||
if instance.inbox and not current_user.has_blocked_instance(instance.id) and not instance_banned(instance.domain):
|
||||
send_to_remote_instance(instance.id, post.community.id, announce)
|
||||
|
||||
# send copy of Note to post author (who won't otherwise get it if no-one else on their instance is subscribed to the community)
|
||||
if not in_reply_to.author.is_local() and in_reply_to.author.ap_domain != post_reply.community.ap_domain:
|
||||
if not post.community.is_local() or (post.community.is_local and not post.community.has_followers_from_domain(in_reply_to.author.ap_domain)):
|
||||
success = post_request(in_reply_to.author.ap_inbox_url, update_json, current_user.private_key,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
if not success:
|
||||
# sending to shared inbox is good enough for Mastodon, but Lemmy will reject it the local community has no followers
|
||||
personal_inbox = in_reply_to.author.public_url() + '/inbox'
|
||||
post_request(personal_inbox, update_json, current_user.private_key,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
|
||||
return redirect(url_for('activitypub.post_ap', post_id=post.id))
|
||||
else:
|
||||
form.body.data = post_reply.body
|
||||
|
|
25
app/utils.py
25
app/utils.py
|
@ -227,6 +227,31 @@ def markdown_to_text(markdown_text) -> str:
|
|||
return markdown_text.replace("# ", '')
|
||||
|
||||
|
||||
def microblog_content_to_title(html: str) -> str:
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
|
||||
title_found = False
|
||||
for tag in soup.find_all():
|
||||
if tag.name == 'p':
|
||||
if not title_found:
|
||||
title_found = True
|
||||
continue
|
||||
else:
|
||||
tag = tag.extract()
|
||||
|
||||
if title_found:
|
||||
result = soup.text
|
||||
if len(result) > 150:
|
||||
for i in range(149, -1, -1):
|
||||
if result[i] == ' ':
|
||||
break;
|
||||
result = result[:i] + ' ...' if i > 0 else ''
|
||||
else:
|
||||
result = ''
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def domain_from_url(url: str, create=True) -> Domain:
|
||||
parsed_url = urlparse(url.lower().replace('www.', ''))
|
||||
if parsed_url and parsed_url.hostname:
|
||||
|
|
Loading…
Add table
Reference in a new issue