diff --git a/app/activitypub/routes.py b/app/activitypub/routes.py index e48019ca..a976f170 100644 --- a/app/activitypub/routes.py +++ b/app/activitypub/routes.py @@ -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, \ 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, \ - 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, \ 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, \ @@ -439,6 +439,11 @@ def shared_inbox(): return '' +@bp.route('/site_inbox', methods=['GET', 'POST']) +def site_inbox(): + return shared_inbox() + + @celery.task def process_inbox_request(request_json, activitypublog_id, ip_address): with current_app.app_context(): @@ -800,6 +805,15 @@ def process_inbox_request(request_json, activitypublog_id, ip_address): if existing_membership: existing_membership.is_moderator = False 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: 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' else: 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. if 'user' in vars() and user is not None: diff --git a/app/activitypub/util.py b/app/activitypub/util.py index 710fe99b..ff8d7550 100644 --- a/app/activitypub/util.py +++ b/app/activitypub/util.py @@ -1041,92 +1041,104 @@ def find_instance_id(server): db.session.commit() # Spawn background task to fill in more details - refresh_instance_profile(new_instance.id) + new_instance_profile(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 current_app.debug: - refresh_instance_profile_task(instance_id) + new_instance_profile_task(instance_id) 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 -def refresh_instance_profile_task(instance_id: int): +def new_instance_profile_task(instance_id: int): instance = Instance.query.get(instance_id) - if instance.inbox is None or instance.updated_at < utcnow() - timedelta(days=7): + try: + instance_data = get_request(f"https://{instance.domain}", headers={'Accept': 'application/activity+json'}) + except: + return + if instance_data.status_code == 200: try: - instance_data = get_request(f"https://{instance.domain}", headers={'Accept': 'application/activity+json'}) + instance_json = instance_data.json() + instance_data.close() + except requests.exceptions.JSONDecodeError as ex: + instance_json = {} + if 'type' in instance_json and instance_json['type'] == 'Application': + instance.inbox = instance_json['inbox'] + instance.outbox = instance_json['outbox'] + else: # it's pretty much always /inbox so just assume that it is for whatever this instance is running + instance.inbox = f"https://{instance.domain}/inbox" + instance.updated_at = utcnow() + db.session.commit() + + # retrieve list of Admins from /api/v3/site, update InstanceRole + try: + response = get_request(f'https://{instance.domain}/api/v3/site') except: - return - if instance_data.status_code == 200: - try: - instance_json = instance_data.json() - instance_data.close() - except requests.exceptions.JSONDecodeError as ex: - instance_json = {} - 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.outbox = instance_json['outbox'] - instance.software = software - 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.updated_at = utcnow() - db.session.commit() + response = None - # retrieve list of Admins from /api/v3/site, update InstanceRole + if response and response.status_code == 200: try: - response = get_request(f'https://{instance.domain}/api/v3/site') + instance_data = response.json() except: - response = None + instance_data = None + finally: + response.close() - if response and response.status_code == 200: - try: - instance_data = response.json() - except: - instance_data = None - finally: - response.close() - - if instance_data: - if 'admins' in instance_data: - admin_profile_ids = [] - for admin in instance_data['admins']: - admin_profile_ids.append(admin['person']['actor_id'].lower()) - user = find_actor_or_create(admin['person']['actor_id']) - if user and not instance.user_is_admin(user.id): - new_instance_role = InstanceRole(instance_id=instance.id, user_id=user.id, role='admin') - db.session.add(new_instance_role) - db.session.commit() - # remove any InstanceRoles that are no longer part of instance-data['admins'] - for instance_admin in InstanceRole.query.filter_by(instance_id=instance.id): - if instance_admin.user.profile_id() not in admin_profile_ids: - db.session.query(InstanceRole).filter( + if instance_data: + if 'admins' in instance_data: + admin_profile_ids = [] + for admin in instance_data['admins']: + admin_profile_ids.append(admin['person']['actor_id'].lower()) + user = find_actor_or_create(admin['person']['actor_id']) + if user and not instance.user_is_admin(user.id): + new_instance_role = InstanceRole(instance_id=instance.id, user_id=user.id, role='admin') + db.session.add(new_instance_role) + db.session.commit() + # remove any InstanceRoles that are no longer part of instance-data['admins'] + for instance_admin in InstanceRole.query.filter_by(instance_id=instance.id): + if instance_admin.user.profile_id() not in admin_profile_ids: + db.session.query(InstanceRole).filter( InstanceRole.user_id == instance_admin.user.id, InstanceRole.instance_id == instance.id, InstanceRole.role == 'admin').delete() + db.session.commit() + elif instance_data.status_code == 406: # Mastodon and PeerTube do this + instance.inbox = f"https://{instance.domain}/inbox" + instance.updated_at = utcnow() + 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() - elif instance_data.status_code == 406: # Mastodon does this - instance.software = 'mastodon' - instance.inbox = f"https://{instance.domain}/inbox" - instance.updated_at = utcnow() - 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 @@ -1329,6 +1341,57 @@ def delete_post_or_comment_task(user_ap_id, community_ap_id, to_be_deleted_ap_id 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]: if community.local_only: activity_log.exception_message = 'Community is local only, reply discarded' diff --git a/app/community/util.py b/app/community/util.py index 38b5dcfe..16c73c9c 100644 --- a/app/community/util.py +++ b/app/community/util.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta from threading import Thread from time import sleep +from random import randint from typing import List import requests 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", params={'resource': f"acct:{address[1:]}"}) except requests.exceptions.ReadTimeout: - time.sleep(randint(3, 10)) + sleep(randint(3, 10)) try: webfinger_data = get_request(f"https://{server}/.well-known/webfinger", params={'resource': f"acct:{address[1:]}"}) diff --git a/app/main/routes.py b/app/main/routes.py index 4e805eb8..b3ce8932 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -429,6 +429,15 @@ def activitypub_application(): 'updated': ap_datetime(g.site.updated), 'inbox': f"https://{current_app.config['SERVER_NAME']}/site_inbox", '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.content_type = 'application/activity+json'