From e8d91769318380713c03d45b2acd363480e7e397 Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 19 Jan 2025 21:28:42 +0000 Subject: [PATCH] API: post locks --- app/api/alpha/routes.py | 14 ++++- app/api/alpha/utils/__init__.py | 2 +- app/api/alpha/utils/post.py | 15 ++++- app/shared/post.py | 34 +++++++++++- app/shared/tasks/__init__.py | 5 +- app/shared/tasks/locks.py | 97 +++++++++++++++++++++++++++++++++ 6 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 app/shared/tasks/locks.py diff --git a/app/api/alpha/routes.py b/app/api/alpha/routes.py index b6dcc8a2..ef03ebe8 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, post_post_delete, post_post_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, \ 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 @@ -207,6 +207,17 @@ def post_alpha_post_report(): return jsonify({"error": str(ex)}), 400 +@bp.route('/api/alpha/post/lock', methods=['POST']) +def post_alpha_post_lock(): + 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_lock(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(): @@ -370,7 +381,6 @@ def alpha_community(): # Post - not yet implemented @bp.route('/api/alpha/post/remove', methods=['POST']) -@bp.route('/api/alpha/post/lock', 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']) diff --git a/app/api/alpha/utils/__init__.py b/app/api/alpha/utils/__init__.py index 7f9d1d21..37d627e0 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 +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 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 32deb0ce..d3beabb8 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, 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 +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 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 @@ -239,3 +239,16 @@ def post_post_report(auth, data): post_json = post_report_view(report=report, post_id=post_id, user_id=user_id) return post_json + +def post_post_lock(auth, data): + required(['post_id', 'locked'], data) + integer_expected(['post_id'], data) + boolean_expected(['locked'], data) + + post_id = data['post_id'] + locked = data['locked'] + + user_id, post = lock_post(post_id, locked, 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 f330a4c5..3f9333fb 100644 --- a/app/shared/post.py +++ b/app/shared/post.py @@ -7,7 +7,7 @@ from app.shared.tasks import task_selector from app.utils import render_template, authorise_api_user, shorten_string, gibberish, ensure_directory_exists, \ piefed_markdown_to_lemmy_markdown, markdown_to_html, remove_tracking_from_link, domain_from_url, \ opengraph_parse, url_to_thumbnail_file, can_create_post, is_video_hosting_site, recently_upvoted_posts, \ - is_image_url, is_video_hosting_site + is_image_url, is_video_hosting_site, add_to_modlog_activitypub from flask import abort, flash, redirect, request, url_for, current_app, g from flask_babel import _ @@ -531,3 +531,35 @@ def report_post(post_id, input, src, auth=None): return user_id, report else: return + + +def lock_post(post_id, locked, src, auth=None): + if src == SRC_API: + user = authorise_api_user(auth, return_type='model') + else: + user = current_user + + post = Post.query.filter_by(id=post_id).one() + if locked: + comments_enabled = False + modlog_type = 'lock_post' + else: + comments_enabled = True + modlog_type = 'unlock_post' + + if post.community.is_moderator(user) or post.community.is_instance_admin(user): + post.comments_enabled = comments_enabled + 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 locked: + task_selector('lock_post', user_id=user.id, post_id=post_id) + else: + task_selector('unlock_post', user_id=user.id, post_id=post_id) + + return user.id, post + + + + diff --git a/app/shared/tasks/__init__.py b/app/shared/tasks/__init__.py index 9d5bba5a..9e22628d 100644 --- a/app/shared/tasks/__init__.py +++ b/app/shared/tasks/__init__.py @@ -4,6 +4,7 @@ from app.shared.tasks.notes import make_reply, edit_reply from app.shared.tasks.deletes import delete_reply, restore_reply, delete_post, restore_post from app.shared.tasks.flags import report_reply, report_post from app.shared.tasks.pages import make_post, edit_post +from app.shared.tasks.locks import lock_post, unlock_post from flask import current_app @@ -23,7 +24,9 @@ def task_selector(task_key, send_async=True, **kwargs): 'edit_post': edit_post, 'delete_post': delete_post, 'restore_post': restore_post, - 'report_post': report_post + 'report_post': report_post, + 'lock_post': lock_post, + 'unlock_post': unlock_post } if current_app.debug: diff --git a/app/shared/tasks/locks.py b/app/shared/tasks/locks.py new file mode 100644 index 00000000..61f027cf --- /dev/null +++ b/app/shared/tasks/locks.py @@ -0,0 +1,97 @@ +from app import celery +from app.activitypub.signature import default_context, post_request +from app.models import Post, User +from app.utils import gibberish, instance_banned + +from flask import current_app + + +""" JSON format +Lock: +{ + 'id': + 'type': + 'actor': + 'object': + '@context': + 'audience': + 'to': [] + 'cc': [] +} +For Announce, remove @context from inner object, and use same fields except audience +""" + + +@celery.task +def lock_post(send_async, user_id, post_id): + post = Post.query.filter_by(id=post_id).one() + lock_object(user_id, post) + + +@celery.task +def unlock_post(send_async, user_id, post_id): + post = Post.query.filter_by(id=post_id).one() + lock_object(user_id, post, is_undo=True) + + +def lock_object(user_id, object, is_undo=False): + user = User.query.filter_by(id=user_id).one() + community = object.community + + if community.local_only or not community.instance.online(): + return + + lock_id = f"https://{current_app.config['SERVER_NAME']}/activities/lock/{gibberish(15)}" + to = ["https://www.w3.org/ns/activitystreams#Public"] + cc = [community.public_url()] + lock = { + 'id': lock_id, + 'type': 'Lock', + 'actor': user.public_url(), + 'object': object.public_url(), + '@context': default_context(), + 'audience': community.public_url(), + 'to': to, + 'cc': cc + } + + if is_undo: + del lock['@context'] + undo_id = f"https://{current_app.config['SERVER_NAME']}/activities/undo/{gibberish(15)}" + undo = { + 'id': undo_id, + 'type': 'Undo', + 'actor': user.public_url(), + 'object': lock, + '@context': default_context(), + 'audience': community.public_url(), + 'to': to, + 'cc': cc + } + + if community.is_local(): + if is_undo: + del undo['@context'] + object=undo + else: + del lock['@context'] + object=lock + + announce_id = f"https://{current_app.config['SERVER_NAME']}/activities/announce/{gibberish(15)}" + actor = community.public_url() + cc = [community.ap_followers_url] + announce = { + 'id': announce_id, + 'type': 'Announce', + 'actor': actor, + 'object': object, + '@context': default_context(), + 'to': to, + 'cc': cc + } + 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') + else: + payload = undo if is_undo else lock + post_request(community.ap_inbox_url, payload, user.private_key, user.public_url() + '#main-key')