Merge remote-tracking branch 'origin/main'

This commit is contained in:
rimu 2024-05-23 15:26:05 +12:00
commit dee81cca8f
4 changed files with 163 additions and 67 deletions

View file

@ -21,7 +21,7 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti
upvote_post, delete_post_or_comment, community_members, \ upvote_post, delete_post_or_comment, community_members, \
user_removed_from_remote_server, create_post, create_post_reply, update_post_reply_from_activity, \ user_removed_from_remote_server, create_post, create_post_reply, update_post_reply_from_activity, \
update_post_from_activity, undo_vote, undo_downvote, post_to_page, get_redis_connection, find_reported_object, \ update_post_from_activity, undo_vote, undo_downvote, post_to_page, get_redis_connection, find_reported_object, \
process_report, ensure_domains_match, can_edit, can_delete process_report, ensure_domains_match, can_edit, can_delete, remove_data_from_banned_user
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, render_template, \ from app.utils import gibberish, get_setting, is_image_url, allowlist_html, render_template, \
domain_from_url, markdown_to_html, community_membership, ap_datetime, ip_address, can_downvote, \ domain_from_url, markdown_to_html, community_membership, ap_datetime, ip_address, can_downvote, \
can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply, sha256_digest, \ can_upvote, can_create_post, awaken_dormant_instance, shorten_string, can_create_post_reply, sha256_digest, \
@ -439,6 +439,11 @@ def shared_inbox():
return '' return ''
@bp.route('/site_inbox', methods=['GET', 'POST'])
def site_inbox():
return shared_inbox()
@celery.task @celery.task
def process_inbox_request(request_json, activitypublog_id, ip_address): def process_inbox_request(request_json, activitypublog_id, ip_address):
with current_app.app_context(): with current_app.app_context():
@ -800,6 +805,15 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
if existing_membership: if existing_membership:
existing_membership.is_moderator = False existing_membership.is_moderator = False
activity_log.result = 'success' activity_log.result = 'success'
elif request_json['object']['type'] == 'Block' and 'target' in request_json['object']:
activity_log.activity_type = 'Community Ban'
mod_ap_id = request_json['object']['actor']
user_ap_id = request_json['object']['object']
target = request_json['object']['target']
remove_data = request_json['object']['removeData']
if target == request_json['actor'] and remove_data == True:
remove_data_from_banned_user(mod_ap_id, user_ap_id, target)
activity_log.result = 'success'
else: else:
activity_log.exception_message = 'Invalid type for Announce' activity_log.exception_message = 'Invalid type for Announce'
@ -1076,6 +1090,15 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
activity_log.result = 'success' activity_log.result = 'success'
else: else:
activity_log.exception_message = 'Report ignored due to missing user or content' activity_log.exception_message = 'Report ignored due to missing user or content'
elif request_json['type'] == 'Block':
activity_log.activity_type = 'Site Ban'
admin_ap_id = request_json['actor']
user_ap_id = request_json['object']
target = request_json['target']
remove_data = request_json['removeData']
if remove_data == True:
remove_data_from_banned_user(admin_ap_id, user_ap_id, target)
activity_log.result = 'success'
# Flush the caches of any major object that was created. To be sure. # Flush the caches of any major object that was created. To be sure.
if 'user' in vars() and user is not None: if 'user' in vars() and user is not None:

View file

@ -1041,23 +1041,22 @@ def find_instance_id(server):
db.session.commit() db.session.commit()
# Spawn background task to fill in more details # Spawn background task to fill in more details
refresh_instance_profile(new_instance.id) new_instance_profile(new_instance.id)
return new_instance.id return new_instance.id
def refresh_instance_profile(instance_id: int): def new_instance_profile(instance_id: int):
if instance_id: if instance_id:
if current_app.debug: if current_app.debug:
refresh_instance_profile_task(instance_id) new_instance_profile_task(instance_id)
else: else:
refresh_instance_profile_task.apply_async(args=(instance_id,), countdown=randint(1, 10)) new_instance_profile_task.apply_async(args=(instance_id,), countdown=randint(1, 10))
@celery.task @celery.task
def refresh_instance_profile_task(instance_id: int): def new_instance_profile_task(instance_id: int):
instance = Instance.query.get(instance_id) instance = Instance.query.get(instance_id)
if instance.inbox is None or instance.updated_at < utcnow() - timedelta(days=7):
try: try:
instance_data = get_request(f"https://{instance.domain}", headers={'Accept': 'application/activity+json'}) instance_data = get_request(f"https://{instance.domain}", headers={'Accept': 'application/activity+json'})
except: except:
@ -1069,23 +1068,9 @@ def refresh_instance_profile_task(instance_id: int):
except requests.exceptions.JSONDecodeError as ex: except requests.exceptions.JSONDecodeError as ex:
instance_json = {} instance_json = {}
if 'type' in instance_json and instance_json['type'] == 'Application': if 'type' in instance_json and instance_json['type'] == 'Application':
# 'name' is unreliable as the admin can change it to anything. todo: find better way
if instance_json['name'].lower() == 'kbin':
software = 'kbin'
elif instance_json['name'].lower() == 'mbin':
software = 'mbin'
elif instance_json['name'].lower() == 'piefed':
software = 'piefed'
elif instance_json['name'].lower() == 'system account':
software = 'friendica'
else:
software = 'lemmy'
instance.inbox = instance_json['inbox'] instance.inbox = instance_json['inbox']
instance.outbox = instance_json['outbox'] instance.outbox = instance_json['outbox']
instance.software = software else: # it's pretty much always /inbox so just assume that it is for whatever this instance is running
if instance.inbox.endswith('/site_inbox'): # Lemmy provides a /site_inbox but it always returns 400 when trying to POST to it. wtf.
instance.inbox = instance.inbox.replace('/site_inbox', '/inbox')
else: # it's pretty much always /inbox so just assume that it is for whatever this instance is running (mostly likely Mastodon)
instance.inbox = f"https://{instance.domain}/inbox" instance.inbox = f"https://{instance.domain}/inbox"
instance.updated_at = utcnow() instance.updated_at = utcnow()
db.session.commit() db.session.commit()
@ -1122,12 +1107,39 @@ def refresh_instance_profile_task(instance_id: int):
InstanceRole.instance_id == instance.id, InstanceRole.instance_id == instance.id,
InstanceRole.role == 'admin').delete() InstanceRole.role == 'admin').delete()
db.session.commit() db.session.commit()
elif instance_data.status_code == 406: # Mastodon does this elif instance_data.status_code == 406: # Mastodon and PeerTube do this
instance.software = 'mastodon'
instance.inbox = f"https://{instance.domain}/inbox" instance.inbox = f"https://{instance.domain}/inbox"
instance.updated_at = utcnow() instance.updated_at = utcnow()
db.session.commit() db.session.commit()
HEADERS = {'User-Agent': 'PieFed/1.0', 'Accept': 'application/activity+json'}
try:
nodeinfo = requests.get(f"https://{instance.domain}/.well-known/nodeinfo", headers=HEADERS,
timeout=5, allow_redirects=True)
if nodeinfo.status_code == 200:
nodeinfo_json = nodeinfo.json()
for links in nodeinfo_json['links']:
if 'rel' in links and (
links['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0' or
links['rel'] == 'https://nodeinfo.diaspora.software/ns/schema/2.0'):
try:
time.sleep(0.1)
node = requests.get(links['href'], headers=HEADERS, timeout=5,
allow_redirects=True)
if node.status_code == 200:
node_json = node.json()
if 'software' in node_json:
instance.software = node_json['software']['name'].lower()
instance.version = node_json['software']['version']
db.session.commit()
except:
# todo: update new field in Instance to indicate bad nodeinfo response
return
except:
# todo: update new field in Instance to indicate bad nodeinfo response
return
# alter the effect of upvotes based on their instance. Default to 1.0 # alter the effect of upvotes based on their instance. Default to 1.0
@cache.memoize(timeout=50) @cache.memoize(timeout=50)
@ -1329,6 +1341,57 @@ def delete_post_or_comment_task(user_ap_id, community_ap_id, to_be_deleted_ap_id
db.session.commit() db.session.commit()
def remove_data_from_banned_user(deletor_ap_id, user_ap_id, target):
if current_app.debug:
remove_data_from_banned_user_task(deletor_ap_id, user_ap_id, target)
else:
remove_data_from_banned_user_task.delay(deletor_ap_id, user_ap_id, target)
@celery.task
def remove_data_from_banned_user_task(deletor_ap_id, user_ap_id, target):
deletor = find_actor_or_create(deletor_ap_id, create_if_not_found=False)
user = find_actor_or_create(user_ap_id, create_if_not_found=False)
community = Community.query.filter_by(ap_profile_id=target).first()
if not deletor and not user:
return
# site bans by admins
if deletor.instance.user_is_admin(deletor.id) and target == f"https://{deletor.instance.domain}/" and deletor.instance_id == user.instance_id:
post_replies = PostReply.query.filter_by(user_id=user.id)
posts = Post.query.filter_by(user_id=user.id)
# community bans by mods
elif community and community.is_moderator(deletor):
post_replies = PostReply.query.filter_by(user_id=user.id, community_id=community.id)
posts = Post.query.filter_by(user_id=user.id, community_id=community.id)
else:
return
for pr in post_replies:
pr.post.reply_count -= 1
if pr.has_replies():
pr.body = 'Banned'
pr.body_html = lemmy_markdown_to_html(pr.body)
else:
pr.delete_dependencies()
db.session.delete(pr)
db.session.commit()
for p in posts:
if p.cross_posts:
old_cross_posts = Post.query.filter(Post.id.in_(p.cross_posts)).all()
for ocp in old_cross_posts:
if ocp.cross_posts is not None:
ocp.cross_posts.remove(p.id)
p.delete_dependencies()
db.session.delete(p)
p.community.post_count -= 1
db.session.commit()
def create_post_reply(activity_log: ActivityPubLog, community: Community, in_reply_to, request_json: dict, user: User, announce_id=None) -> Union[Post, None]: def create_post_reply(activity_log: ActivityPubLog, community: Community, in_reply_to, request_json: dict, user: User, announce_id=None) -> Union[Post, None]:
if community.local_only: if community.local_only:
activity_log.exception_message = 'Community is local only, reply discarded' activity_log.exception_message = 'Community is local only, reply discarded'

View file

@ -1,6 +1,7 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from threading import Thread from threading import Thread
from time import sleep from time import sleep
from random import randint
from typing import List from typing import List
import requests import requests
from PIL import Image, ImageOps from PIL import Image, ImageOps
@ -44,7 +45,7 @@ def search_for_community(address: str):
webfinger_data = get_request(f"https://{server}/.well-known/webfinger", webfinger_data = get_request(f"https://{server}/.well-known/webfinger",
params={'resource': f"acct:{address[1:]}"}) params={'resource': f"acct:{address[1:]}"})
except requests.exceptions.ReadTimeout: except requests.exceptions.ReadTimeout:
time.sleep(randint(3, 10)) sleep(randint(3, 10))
try: try:
webfinger_data = get_request(f"https://{server}/.well-known/webfinger", webfinger_data = get_request(f"https://{server}/.well-known/webfinger",
params={'resource': f"acct:{address[1:]}"}) params={'resource': f"acct:{address[1:]}"})

View file

@ -429,6 +429,15 @@ def activitypub_application():
'updated': ap_datetime(g.site.updated), 'updated': ap_datetime(g.site.updated),
'inbox': f"https://{current_app.config['SERVER_NAME']}/site_inbox", 'inbox': f"https://{current_app.config['SERVER_NAME']}/site_inbox",
'outbox': f"https://{current_app.config['SERVER_NAME']}/site_outbox", 'outbox': f"https://{current_app.config['SERVER_NAME']}/site_outbox",
'icon': {
'type': 'Image',
'url': f"https://{current_app.config['SERVER_NAME']}/static/images/logo2.png"
},
'publicKey': {
'id': f"https://{current_app.config['SERVER_NAME']}/#main-key",
'owner': f"https://{current_app.config['SERVER_NAME']}/",
'publicKeyPem': g.site.public_key
}
} }
resp = jsonify(application_data) resp = jsonify(application_data)
resp.content_type = 'application/activity+json' resp.content_type = 'application/activity+json'