From ac6f66e892d9c78f4123181bd6f4a85da5d6326e Mon Sep 17 00:00:00 2001 From: freamon Date: Sat, 18 Jan 2025 14:52:09 +0000 Subject: [PATCH] API: post delete and restore --- app/api/alpha/routes.py | 15 ++++++++-- app/api/alpha/utils/__init__.py | 2 +- app/api/alpha/utils/post.py | 19 +++++++++++- app/shared/post.py | 52 +++++++++++++++++++++++++++++++++ app/shared/reply.py | 12 +++----- app/shared/tasks/__init__.py | 6 ++-- app/shared/tasks/deletes.py | 46 ++++++++++++++++++++++++++--- 7 files changed, 134 insertions(+), 18 deletions(-) diff --git a/app/api/alpha/routes.py b/app/api/alpha/routes.py index 5de30055..20c5be37 100644 --- a/app/api/alpha/routes.py +++ b/app/api/alpha/routes.py @@ -1,7 +1,7 @@ from app.api.alpha import bp from app.api.alpha.utils import get_site, post_site_block, \ get_search, \ - get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe, post_post, put_post, \ + get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe, post_post, put_post, post_post_delete, \ get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe, post_reply, put_reply, post_reply_delete, post_reply_report, \ get_community_list, get_community, post_community_follow, post_community_block, \ get_user, post_user_block @@ -183,6 +183,18 @@ def put_alpha_post(): return jsonify({"error": str(ex)}), 400 +@bp.route('/api/alpha/post/delete', methods=['POST']) +def post_alpha_post_delete(): + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) + try: + auth = request.headers.get('Authorization') + data = request.get_json(force=True) or {} + return jsonify(post_post_delete(auth, data)) + except Exception as ex: + return jsonify({"error": str(ex)}), 400 + + # Reply @bp.route('/api/alpha/comment/list', methods=['GET']) def get_alpha_comment_list(): @@ -345,7 +357,6 @@ def alpha_community(): return jsonify({"error": "not_yet_implemented"}), 400 # Post - not yet implemented -@bp.route('/api/alpha/post/delete', methods=['POST']) @bp.route('/api/alpha/post/remove', methods=['POST']) @bp.route('/api/alpha/post/lock', methods=['POST']) @bp.route('/api/alpha/post/feature', methods=['POST']) diff --git a/app/api/alpha/utils/__init__.py b/app/api/alpha/utils/__init__.py index a770a08c..79428210 100644 --- a/app/api/alpha/utils/__init__.py +++ b/app/api/alpha/utils/__init__.py @@ -1,6 +1,6 @@ from app.api.alpha.utils.site import get_site, post_site_block from app.api.alpha.utils.misc import get_search -from app.api.alpha.utils.post import get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe, post_post, put_post +from app.api.alpha.utils.post import get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe, post_post, put_post, post_post_delete from app.api.alpha.utils.reply import get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe, post_reply, put_reply, post_reply_delete, post_reply_report from app.api.alpha.utils.community import get_community, get_community_list, post_community_follow, post_community_block from app.api.alpha.utils.user import get_user, post_user_block diff --git a/app/api/alpha/utils/post.py b/app/api/alpha/utils/post.py index c5ee29a0..3da7dc21 100644 --- a/app/api/alpha/utils/post.py +++ b/app/api/alpha/utils/post.py @@ -3,7 +3,7 @@ from app.api.alpha.views import post_view from app.api.alpha.utils.validators import required, integer_expected, boolean_expected, string_expected from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_VIDEO from app.models import Post, Community, CommunityMember, utcnow -from app.shared.post import vote_for_post, bookmark_the_post, remove_the_bookmark_from_post, toggle_post_notification, make_post, edit_post +from app.shared.post import vote_for_post, bookmark_the_post, remove_the_bookmark_from_post, toggle_post_notification, make_post, edit_post, delete_post, restore_post from app.utils import authorise_api_user, blocked_users, blocked_communities, blocked_instances, community_ids_from_instances, is_image_url, is_video_url from datetime import timedelta @@ -205,3 +205,20 @@ def put_post(auth, data): post_json = post_view(post=post, variant=4, user_id=user_id) return post_json + + +def post_post_delete(auth, data): + required(['post_id', 'deleted'], data) + integer_expected(['post_id'], data) + boolean_expected(['deleted'], data) + + post_id = data['post_id'] + deleted = data['deleted'] + + if deleted == True: + user_id, post = delete_post(post_id, SRC_API, auth) + else: + user_id, post = restore_post(post_id, SRC_API, auth) + + post_json = post_view(post=post, variant=4, user_id=user_id) + return post_json diff --git a/app/shared/post.py b/app/shared/post.py index 5116b9d8..08e5109e 100644 --- a/app/shared/post.py +++ b/app/shared/post.py @@ -421,3 +421,55 @@ def edit_post(input, post, type, src, user=None, auth=None, uploaded_file=None, elif from_scratch: return post + +# just for deletes by owner (mod deletes are classed as 'remove') +def delete_post(post_id, src, auth): + if src == SRC_API: + user_id = authorise_api_user(auth) + else: + user_id = current_user.id + + post = Post.query.filter_by(id=post_id, user_id=user_id, deleted=False).one() + if post.url: + post.calculate_cross_posts(delete_only=True) + + post.deleted = True + post.deleted_by = user_id + post.author.post_count -= 1 + post.community.post_count -= 1 + db.session.commit() + if src == SRC_WEB: + flash(_('Post deleted.')) + + task_selector('delete_post', user_id=user_id, post_id=post.id) + + if src == SRC_API: + return user_id, post + else: + return + + +def restore_post(post_id, src, auth): + if src == SRC_API: + user_id = authorise_api_user(auth) + else: + user_id = current_user.id + + post = Post.query.filter_by(id=post_id, user_id=user_id, deleted=True).one() + if post.url: + post.calculate_cross_posts() + + post.deleted = False + post.deleted_by = None + post.author.post_count -= 1 + post.community.post_count -= 1 + db.session.commit() + if src == SRC_WEB: + flash(_('Post restored.')) + + task_selector('restore_post', user_id=user_id, post_id=post.id) + + if src == SRC_API: + return user_id, post + else: + return diff --git a/app/shared/reply.py b/app/shared/reply.py index d75153ec..8e6c0560 100644 --- a/app/shared/reply.py +++ b/app/shared/reply.py @@ -233,12 +233,11 @@ def edit_reply(input, reply, post, src, auth=None): # just for deletes by owner (mod deletes are classed as 'remove') def delete_reply(reply_id, src, auth): if src == SRC_API: - reply = PostReply.query.filter_by(id=reply_id, deleted=False).one() - user_id = authorise_api_user(auth, id_match=reply.user_id) + user_id = authorise_api_user(auth) else: - reply = PostReply.query.get_or_404(reply_id) user_id = current_user.id + reply = PostReply.query.filter_by(id=reply_id, user_id=user_id, deleted=False).one() reply.deleted = True reply.deleted_by = user_id @@ -259,14 +258,11 @@ def delete_reply(reply_id, src, auth): def restore_reply(reply_id, src, auth): if src == SRC_API: - reply = PostReply.query.filter_by(id=reply_id, deleted=True).one() - user_id = authorise_api_user(auth, id_match=reply.user_id) - if reply.user_id != reply.deleted_by: - raise Exception('incorrect_login') + user_id = authorise_api_user(auth) else: - reply = PostReply.query.get_or_404(reply_id) user_id = current_user.id + reply = PostReply.query.filter_by(id=reply_id, user_id=user_id, deleted=True).one() reply.deleted = False reply.deleted_by = None diff --git a/app/shared/tasks/__init__.py b/app/shared/tasks/__init__.py index 8605a5b0..7fc2d3c9 100644 --- a/app/shared/tasks/__init__.py +++ b/app/shared/tasks/__init__.py @@ -1,7 +1,7 @@ from app.shared.tasks.follows import join_community, leave_community from app.shared.tasks.likes import vote_for_post, vote_for_reply from app.shared.tasks.notes import make_reply, edit_reply -from app.shared.tasks.deletes import delete_reply, restore_reply +from app.shared.tasks.deletes import delete_reply, restore_reply, delete_post, restore_post from app.shared.tasks.flags import report_reply from app.shared.tasks.pages import make_post, edit_post @@ -20,7 +20,9 @@ def task_selector(task_key, send_async=True, **kwargs): 'restore_reply': restore_reply, 'report_reply': report_reply, 'make_post': make_post, - 'edit_post': edit_post + 'edit_post': edit_post, + 'delete_post': delete_post, + 'restore_post': restore_post } if current_app.debug: diff --git a/app/shared/tasks/deletes.py b/app/shared/tasks/deletes.py index 6ff31c79..d82902b7 100644 --- a/app/shared/tasks/deletes.py +++ b/app/shared/tasks/deletes.py @@ -1,6 +1,6 @@ from app import celery from app.activitypub.signature import default_context, post_request -from app.models import CommunityBan, PostReply, User +from app.models import CommunityBan, Instance, Post, PostReply, User, UserFollower from app.utils import gibberish, instance_banned from flask import current_app @@ -34,10 +34,31 @@ def restore_reply(send_async, user_id, reply_id): delete_object(user_id, reply, is_restore=True) -def delete_object(user_id, object, is_restore=False): +@celery.task +def delete_post(send_async, user_id, post_id): + post = Post.query.filter_by(id=post_id).one() + delete_object(user_id, post, is_post=True) + + +@celery.task +def restore_post(send_async, user_id, post_id): + post = Post.query.filter_by(id=post_id).one() + delete_object(user_id, post, is_post=True, is_restore=True) + + +def delete_object(user_id, object, is_post=False, is_restore=False): user = User.query.filter_by(id=user_id).one() community = object.community - if community.local_only or not community.instance.online(): + + # local_only communities can also be used to send activity to User Followers (only applies to posts, not comments) + # return now though, if there aren't any + if not is_post and community.local_only: + return + followers = UserFollower.query.filter_by(local_user_id=user.id).all() + if not followers and community.local_only: + return + + if not community.instance.online(): return banned = CommunityBan.query.filter_by(user_id=user_id, community_id=community.id).first() @@ -75,6 +96,8 @@ def delete_object(user_id, object, is_restore=False): 'cc': cc } + domains_sent_to = [] + if community.is_local(): if is_restore: del undo['@context'] @@ -97,9 +120,24 @@ def delete_object(user_id, object, is_restore=False): } for instance in community.following_instances(): if instance.inbox and instance.online() and not user.has_blocked_instance(instance.id) and not instance_banned(instance.domain): - post_request(instance.inbox, announce, community.private_key, community.public_url() + '#main-key') + post_request(instance.inbox, announce, community.private_key, community.public_url() + '#main-key') + domains_sent_to.append(instance.domain) else: payload = undo if is_restore else delete post_request(community.ap_inbox_url, payload, user.private_key, user.public_url() + '#main-key') + domains_sent_to.append(community.instance.domain) + + if is_post and followers: + payload = undo if is_restore else delete + for follower in followers: + user_details = User.query.get(follower.remote_user_id) + if user_details: + payload['cc'].append(user_details.public_url()) + instances = Instance.query.join(User, User.instance_id == Instance.id).join(UserFollower, UserFollower.remote_user_id == User.id) + instances = instances.filter(UserFollower.local_user_id == user.id).filter(Instance.gone_forever == False) + for instance in instances: + if instance.domain not in domains_sent_to: + post_request(instance.inbox, payload, user.private_key, user.public_url() + '#main-key') +