diff --git a/app/api/alpha/routes.py b/app/api/alpha/routes.py index c3556ed1..e1d30dc3 100644 --- a/app/api/alpha/routes.py +++ b/app/api/alpha/routes.py @@ -1,8 +1,10 @@ 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, post_post_delete, post_post_report, post_post_lock, post_post_feature, \ - get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe, post_reply, put_reply, post_reply_delete, post_reply_report, \ + get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe, post_post, \ + put_post, post_post_delete, post_post_report, post_post_lock, post_post_feature, post_post_remove, \ + 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 from app.shared.auth import log_user_in @@ -231,6 +233,18 @@ def post_alpha_post_feature(): return jsonify({"error": str(ex)}), 400 +@bp.route('/api/alpha/post/remove', methods=['POST']) +def post_alpha_post_remove(): + 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_remove(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(): @@ -368,108 +382,105 @@ def post_alpha_user_block(): # Not yet implemented. Copied from lemmy's V3 api, so some aren't needed, and some need changing # Site - not yet implemented -@bp.route('/api/alpha/site', methods=['POST']) -@bp.route('/api/alpha/site', methods=['PUT']) +@bp.route('/api/alpha/site', methods=['POST']) # Create New Site. No plans to implement +@bp.route('/api/alpha/site', methods=['PUT']) # Edit Site. Not available in app def alpha_site(): return jsonify({"error": "not_yet_implemented"}), 400 # Miscellaneous - not yet implemented -@bp.route('/api/alpha/modlog', methods=['GET']) -@bp.route('/api/alpha/resolve_object', methods=['GET']) -@bp.route('/api/alpha/federated_instances', methods=['GET']) +@bp.route('/api/alpha/modlog', methods=['GET']) # Get Modlog. Not usually public +@bp.route('/api/alpha/resolve_object', methods=['GET']) # Stage 1: Needed for search +@bp.route('/api/alpha/federated_instances', methods=['GET']) # No plans to implement - only V3 version needed def alpha_miscellaneous(): return jsonify({"error": "not_yet_implemented"}), 400 # Community - not yet implemented -@bp.route('/api/alpha/community', methods=['POST']) -@bp.route('/api/alpha/community', methods=['PUT']) -@bp.route('/api/alpha/community/hide', methods=['PUT']) -@bp.route('/api/alpha/community/delete', methods=['POST']) -@bp.route('/api/alpha/community/remove', methods=['POST']) -@bp.route('/api/alpha/community/transfer', methods=['POST']) -@bp.route('/api/alpha/community/ban_user', methods=['POST']) -@bp.route('/api/alpha/community/mod', methods=['POST']) +@bp.route('/api/alpha/community', methods=['POST']) # (none +@bp.route('/api/alpha/community', methods=['PUT']) # of +@bp.route('/api/alpha/community/hide', methods=['PUT']) # these +@bp.route('/api/alpha/community/delete', methods=['POST']) # are +@bp.route('/api/alpha/community/remove', methods=['POST']) # available +@bp.route('/api/alpha/community/transfer', methods=['POST']) # in +@bp.route('/api/alpha/community/ban_user', methods=['POST']) # the +@bp.route('/api/alpha/community/mod', methods=['POST']) # app) def alpha_community(): return jsonify({"error": "not_yet_implemented"}), 400 # Post - not yet implemented -@bp.route('/api/alpha/post/remove', methods=['POST']) -@bp.route('/api/alpha/post/feature', methods=['POST']) -@bp.route('/api/alpha/post/report', methods=['POST']) -@bp.route('/api/alpha/post/report/resolve', methods=['PUT']) -@bp.route('/api/alpha/post/report/list', methods=['GET']) -@bp.route('/api/alpha/post/site_metadata', methods=['GET']) +@bp.route('/api/alpha/post/report/resolve', methods=['PUT']) # Stage 2 +@bp.route('/api/alpha/post/report/list', methods=['GET']) # Stage 2 +@bp.route('/api/alpha/post/site_metadata', methods=['GET']) # Not available in app def alpha_post(): return jsonify({"error": "not_yet_implemented"}), 400 # Reply - not yet implemented -@bp.route('/api/alpha/comment', methods=['GET']) -@bp.route('/api/alpha/comment/remove', methods=['POST']) -@bp.route('/api/alpha/comment/mark_as_read', methods=['POST']) -@bp.route('/api/alpha/comment/distinguish', methods=['POST']) -@bp.route('/api/alpha/comment/report/resolve', methods=['PUT']) -@bp.route('/api/alpha/comment/report/list', methods=['GET']) +@bp.route('/api/alpha/comment', methods=['GET']) # Stage 1 if needed for search +@bp.route('/api/alpha/comment/remove', methods=['POST']) # Stage 1 +@bp.route('/api/alpha/comment/mark_as_read', methods=['POST']) # No DB support +@bp.route('/api/alpha/comment/distinguish', methods=['POST']) # Not really used +@bp.route('/api/alpha/comment/report/resolve', methods=['PUT']) # Stage 2 +@bp.route('/api/alpha/comment/report/list', methods=['GET']) # Stage 2 def alpha_reply(): return jsonify({"error": "not_yet_implemented"}), 400 # Chat - not yet implemented -@bp.route('/api/alpha/private_message/list', methods=['GET']) -@bp.route('/api/alpha/private_message', methods=['PUT']) -@bp.route('/api/alpha/private_message', methods=['POST']) -@bp.route('/api/alpha/private_message/delete', methods=['POST']) -@bp.route('/api/alpha/private_message/mark_as_read', methods=['POST']) -@bp.route('/api/alpha/private_message/report', methods=['POST']) -@bp.route('/api/alpha/private_message/report/resolve', methods=['PUT']) -@bp.route('/api/alpha/private_message/report/list', methods=['GET']) +@bp.route('/api/alpha/private_message/list', methods=['GET']) # Stage 1 +@bp.route('/api/alpha/private_message', methods=['PUT']) # Stage 1 +@bp.route('/api/alpha/private_message', methods=['POST']) # Stage 1 +@bp.route('/api/alpha/private_message/delete', methods=['POST']) # Stage 1 +@bp.route('/api/alpha/private_message/mark_as_read', methods=['POST']) # Stage 1 +@bp.route('/api/alpha/private_message/report', methods=['POST']) # Stage 1 +@bp.route('/api/alpha/private_message/report/resolve', methods=['PUT']) # Stage 2 +@bp.route('/api/alpha/private_message/report/list', methods=['GET']) # Stage 2 def alpha_chat(): return jsonify({"error": "not_yet_implemented"}), 400 # User - not yet implemented -@bp.route('/api/alpha/user/register', methods=['POST']) -@bp.route('/api/alpha/user/get_captcha', methods=['GET']) -@bp.route('/api/alpha/user/mention', methods=['GET']) -@bp.route('/api/alpha/user/mention/mark_as_read', methods=['POST']) -@bp.route('/api/alpha/user/replies', methods=['GET']) -@bp.route('/api/alpha/user/ban', methods=['POST']) -@bp.route('/api/alpha/user/banned', methods=['GET']) -@bp.route('/api/alpha/user/delete_account', methods=['POST']) -@bp.route('/api/alpha/user/password_reset', methods=['POST']) -@bp.route('/api/alpha/user/password_change', methods=['POST']) -@bp.route('/api/alpha/user/mark_all_as_read', methods=['POST']) -@bp.route('/api/alpha/user/save_user_settings', methods=['PUT']) -@bp.route('/api/alpha/user/change_password', methods=['PUT']) -@bp.route('/api/alpha/user/repost_count', methods=['GET']) -@bp.route('/api/alpha/user/unread_count', methods=['GET']) -@bp.route('/api/alpha/user/verify_email', methods=['POST']) -@bp.route('/api/alpha/user/leave_admin', methods=['POST']) -@bp.route('/api/alpha/user/totp/generate', methods=['POST']) -@bp.route('/api/alpha/user/totp/update', methods=['POST']) -@bp.route('/api/alpha/user/export_settings', methods=['GET']) -@bp.route('/api/alpha/user/import_settings', methods=['POST']) -@bp.route('/api/alpha/user/list_logins', methods=['GET']) -@bp.route('/api/alpha/user/validate_auth', methods=['GET']) -@bp.route('/api/alpha/user/logout', methods=['POST']) +@bp.route('/api/alpha/user/register', methods=['POST']) # Not available in app +@bp.route('/api/alpha/user/get_captcha', methods=['GET']) # Not available in app +@bp.route('/api/alpha/user/mention', methods=['GET']) # No DB support +@bp.route('/api/alpha/user/mention/mark_as_read', methods=['POST']) # No DB support +@bp.route('/api/alpha/user/replies', methods=['GET']) # Stage 1 +@bp.route('/api/alpha/user/ban', methods=['POST']) # Admin function. No plans to implement +@bp.route('/api/alpha/user/banned', methods=['GET']) # Admin function. No plans to implement +@bp.route('/api/alpha/user/delete_account', methods=['POST']) # Not available in app +@bp.route('/api/alpha/user/password_reset', methods=['POST']) # Not available in app +@bp.route('/api/alpha/user/password_change', methods=['POST']) # Not available in app +@bp.route('/api/alpha/user/mark_all_as_read', methods=['POST']) # Stage 1 +@bp.route('/api/alpha/user/save_user_settings', methods=['PUT']) # Not available in app +@bp.route('/api/alpha/user/change_password', methods=['PUT']) # Not available in app +@bp.route('/api/alpha/user/report_count', methods=['GET']) # Stage 2 +@bp.route('/api/alpha/user/unread_count', methods=['GET']) # Stage 1 +@bp.route('/api/alpha/user/verify_email', methods=['POST']) # Admin function. No plans to implement +@bp.route('/api/alpha/user/leave_admin', methods=['POST']) # Admin function. No plans to implement +@bp.route('/api/alpha/user/totp/generate', methods=['POST']) # Not available in app +@bp.route('/api/alpha/user/totp/update', methods=['POST']) # Not available in app +@bp.route('/api/alpha/user/export_settings', methods=['GET']) # Not available in app +@bp.route('/api/alpha/user/import_settings', methods=['POST']) # Not available in app +@bp.route('/api/alpha/user/list_logins', methods=['GET']) # Not available in app +@bp.route('/api/alpha/user/validate_auth', methods=['GET']) # Not available in app +@bp.route('/api/alpha/user/logout', methods=['POST']) # Stage 1 def alpha_user(): return jsonify({"error": "not_yet_implemented"}), 400 # Admin - not yet implemented @bp.route('/api/alpha/admin/add', methods=['POST']) -@bp.route('/api/alpha/admin/registration_application/count', methods=['GET']) -@bp.route('/api/alpha/admin/registration_application/list', methods=['GET']) -@bp.route('/api/alpha/admin/registration_application/approve', methods=['PUT']) -@bp.route('/api/alpha/admin/purge/person', methods=['POST']) -@bp.route('/api/alpha/admin/purge/community', methods=['POST']) -@bp.route('/api/alpha/admin/purge/post', methods=['POST']) -@bp.route('/api/alpha/admin/purge/comment', methods=['POST']) -@bp.route('/api/alpha/post/like/list', methods=['GET']) -@bp.route('/api/alpha/comment/like/list', methods=['GET']) +@bp.route('/api/alpha/admin/registration_application/count', methods=['GET']) # (no +@bp.route('/api/alpha/admin/registration_application/list', methods=['GET']) # plans +@bp.route('/api/alpha/admin/registration_application/approve', methods=['PUT']) # to +@bp.route('/api/alpha/admin/purge/person', methods=['POST']) # implement +@bp.route('/api/alpha/admin/purge/community', methods=['POST']) # any +@bp.route('/api/alpha/admin/purge/post', methods=['POST']) # endpoints +@bp.route('/api/alpha/admin/purge/comment', methods=['POST']) # for +@bp.route('/api/alpha/post/like/list', methods=['GET']) # admin +@bp.route('/api/alpha/comment/like/list', methods=['GET']) # use) def alpha_admin(): return jsonify({"error": "not_yet_implemented"}), 400 # CustomEmoji - not yet implemented -@bp.route('/api/alpha/custom_emoji', methods=['PUT']) -@bp.route('/api/alpha/custom_emoji', methods=['POST']) -@bp.route('/api/alpha/custom_emoji/delete', methods=['POST']) +@bp.route('/api/alpha/custom_emoji', methods=['PUT']) # (doesn't +@bp.route('/api/alpha/custom_emoji', methods=['POST']) # seem +@bp.route('/api/alpha/custom_emoji/delete', methods=['POST']) # important) def alpha_emoji(): return jsonify({"error": "not_yet_implemented"}), 400 diff --git a/app/api/alpha/utils/__init__.py b/app/api/alpha/utils/__init__.py index 2aa98bbb..944e8164 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, post_post_delete, post_post_report, post_post_lock, post_post_feature +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, post_post_report, post_post_lock, post_post_feature, post_post_remove 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 a46cf38a..f1902c92 100644 --- a/app/api/alpha/utils/post.py +++ b/app/api/alpha/utils/post.py @@ -3,7 +3,8 @@ from app.api.alpha.views import post_view, post_report_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, delete_post, restore_post, report_post, lock_post, sticky_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, report_post, lock_post, sticky_post, mod_remove_post, mod_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 @@ -267,3 +268,23 @@ def post_post_feature(auth, data): post_json = post_view(post=post, variant=4, user_id=user_id) return post_json + + +def post_post_remove(auth, data): + required(['post_id', 'removed'], data) + integer_expected(['post_id'], data) + boolean_expected(['removed'], data) + string_expected(['reason'], data) + + post_id = data['post_id'] + removed = data['removed'] + + if removed == True: + reason = data['reason'] if 'reason' in data else 'Removed by mod' + user_id, post = mod_remove_post(post_id, reason, SRC_API, auth) + else: + reason = data['reason'] if 'reason' in data else 'Restored by mod' + user_id, post = mod_restore_post(post_id, reason, 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 894861f3..67846b28 100644 --- a/app/shared/post.py +++ b/app/shared/post.py @@ -572,9 +572,15 @@ def sticky_post(post_id, featured, src, auth=None): if post.community.is_moderator(user) or post.community.is_instance_admin(user): post.sticky = featured + if featured: + modlog_type = 'featured_post' + else: + modlog_type = 'unfeatured_post' if not community.ap_featured_url: community.ap_featured_url = community.ap_profile_id + '/featured' db.session.commit() + add_to_modlog_activitypub(modlog_type, user, community_id=post.community_id, + link_text=shorten_string(post.title), link=f'post/{post.id}', reason='') if featured: task_selector('sticky_post', user_id=user.id, post_id=post_id) @@ -584,4 +590,68 @@ def sticky_post(post_id, featured, src, auth=None): return user.id, post +# mod deletes +def mod_remove_post(post_id, reason, src, auth): + if src == SRC_API: + user = authorise_api_user(auth, return_type='model') + else: + user = current_user + + post = Post.query.filter_by(id=post_id, user_id=user.id, deleted=False).one() + if not post.community.is_moderator(user) and not post.community.is_instance_admin(user): + raise Exception('Does not have permission') + + 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.')) + + add_to_modlog_activitypub('delete_post', user, community_id=post.community_id, + link_text=shorten_string(post.title), link=f'post/{post.id}', reason=reason) + + task_selector('delete_post', user_id=user.id, post_id=post.id, reason=reason) + + if src == SRC_API: + return user.id, post + else: + return + + +def mod_restore_post(post_id, reason, src, auth): + if src == SRC_API: + user = authorise_api_user(auth, return_type='model') + else: + user = current_user + + post = Post.query.filter_by(id=post_id, user_id=user.id, deleted=True).one() + if not post.community.is_moderator(user) and not post.community.is_instance_admin(user): + raise Exception('Does not have permission') + + 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.')) + + add_to_modlog_activitypub('restore_post', user, community_id=post.community_id, + link_text=shorten_string(post.title), link=f'post/{post.id}', reason=reason) + + task_selector('restore_post', user_id=user.id, post_id=post.id, reason=reason) + + if src == SRC_API: + return user.id, post + else: + return + diff --git a/app/shared/tasks/deletes.py b/app/shared/tasks/deletes.py index d82902b7..6d0aa0f6 100644 --- a/app/shared/tasks/deletes.py +++ b/app/shared/tasks/deletes.py @@ -13,6 +13,7 @@ Delete: 'type': 'actor': 'object': + 'summary': (if deleted by mod / admin) '@context': 'audience': 'to': [] @@ -23,30 +24,30 @@ For Announce, remove @context from inner object, and use same fields except audi @celery.task -def delete_reply(send_async, user_id, reply_id): +def delete_reply(send_async, user_id, reply_id, reason=None): reply = PostReply.query.filter_by(id=reply_id).one() delete_object(user_id, reply) @celery.task -def restore_reply(send_async, user_id, reply_id): +def restore_reply(send_async, user_id, reply_id, reason=None): reply = PostReply.query.filter_by(id=reply_id).one() delete_object(user_id, reply, is_restore=True) @celery.task -def delete_post(send_async, user_id, post_id): +def delete_post(send_async, user_id, post_id, reason=None): post = Post.query.filter_by(id=post_id).one() - delete_object(user_id, post, is_post=True) + delete_object(user_id, post, is_post=True, reason=reason) @celery.task -def restore_post(send_async, user_id, post_id): +def restore_post(send_async, user_id, post_id, reason=None): post = Post.query.filter_by(id=post_id).one() - delete_object(user_id, post, is_post=True, is_restore=True) + delete_object(user_id, post, is_post=True, is_restore=True, reason=reason) -def delete_object(user_id, object, is_post=False, is_restore=False): +def delete_object(user_id, object, is_post=False, is_restore=False, reason=None): user = User.query.filter_by(id=user_id).one() community = object.community @@ -81,6 +82,8 @@ def delete_object(user_id, object, is_post=False, is_restore=False): 'to': to, 'cc': cc } + if reason: + delete['summary'] = reason if is_restore: del delete['@context'] @@ -127,6 +130,9 @@ def delete_object(user_id, object, is_post=False, is_restore=False): post_request(community.ap_inbox_url, payload, user.private_key, user.public_url() + '#main-key') domains_sent_to.append(community.instance.domain) + if reason: + return + if is_post and followers: payload = undo if is_restore else delete for follower in followers: