diff --git a/app/api/alpha/routes.py b/app/api/alpha/routes.py index ef03ebe8..c3556ed1 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, post_post_lock, \ + 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_community_list, get_community, post_community_follow, post_community_block, \ get_user, post_user_block @@ -218,6 +218,19 @@ def post_alpha_post_lock(): except Exception as ex: return jsonify({"error": str(ex)}), 400 + +@bp.route('/api/alpha/post/feature', methods=['POST']) +def post_alpha_post_feature(): + 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_feature(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(): diff --git a/app/api/alpha/utils/__init__.py b/app/api/alpha/utils/__init__.py index 37d627e0..2aa98bbb 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 +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.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 d3beabb8..a46cf38a 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, lock_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 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 @@ -252,3 +252,18 @@ def post_post_lock(auth, data): post_json = post_view(post=post, variant=4, user_id=user_id) return post_json + + +def post_post_feature(auth, data): + required(['post_id', 'featured', 'feature_type'], data) + integer_expected(['post_id'], data) + boolean_expected(['featured'], data) + string_expected(['feature_type'], data) + + post_id = data['post_id'] + featured = data['featured'] + + user_id, post = sticky_post(post_id, featured, 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 3f9333fb..894861f3 100644 --- a/app/shared/post.py +++ b/app/shared/post.py @@ -561,5 +561,27 @@ def lock_post(post_id, locked, src, auth=None): return user.id, post +def sticky_post(post_id, featured, 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() + community = post.community + + if post.community.is_moderator(user) or post.community.is_instance_admin(user): + post.sticky = featured + if not community.ap_featured_url: + community.ap_featured_url = community.ap_profile_id + '/featured' + db.session.commit() + + if featured: + task_selector('sticky_post', user_id=user.id, post_id=post_id) + else: + task_selector('unsticky_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 9e22628d..3684a7b2 100644 --- a/app/shared/tasks/__init__.py +++ b/app/shared/tasks/__init__.py @@ -5,6 +5,8 @@ from app.shared.tasks.deletes import delete_reply, restore_reply, delete_post, r 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 app.shared.tasks.adds import sticky_post +from app.shared.tasks.removes import unsticky_post from flask import current_app @@ -26,7 +28,9 @@ def task_selector(task_key, send_async=True, **kwargs): 'restore_post': restore_post, 'report_post': report_post, 'lock_post': lock_post, - 'unlock_post': unlock_post + 'unlock_post': unlock_post, + 'sticky_post': sticky_post, + 'unsticky_post': unsticky_post } if current_app.debug: diff --git a/app/shared/tasks/adds.py b/app/shared/tasks/adds.py new file mode 100644 index 00000000..3a259cc0 --- /dev/null +++ b/app/shared/tasks/adds.py @@ -0,0 +1,82 @@ +from app import celery +from app.activitypub.signature import default_context, post_request +from app.models import Community, Post, User +from app.utils import gibberish, instance_banned + +from flask import current_app + + +""" JSON format +Add: +{ + 'id': + 'type': + 'actor': + 'object': + 'target': (featured_url or moderators_url) + '@context': + 'audience': + 'to': [] + 'cc': [] +} +For Announce, remove @context from inner object, and use same fields except audience +""" + + +@celery.task +def sticky_post(send_async, user_id, post_id): + post = Post.query.filter_by(id=post_id).one() + add_object(user_id, post) + + +@celery.task +def add_mod(send_async, user_id, mod_id, community_id): + mod = User.query.filter_by(id=mod_id).one() + add_object(user_id, mod, community_id) + + +def add_object(user_id, object, community_id=None): + user = User.query.filter_by(id=user_id).one() + if not community_id: + community = object.community + else: + community = Community.query.filter_by(id=community_id).one() + + if community.local_only or not community.instance.online(): + return + + add_id = f"https://{current_app.config['SERVER_NAME']}/activities/add/{gibberish(15)}" + to = ["https://www.w3.org/ns/activitystreams#Public"] + cc = [community.public_url()] + add = { + 'id': add_id, + 'type': 'Add', + 'actor': user.public_url(), + 'object': object.public_url(), + 'target': community.ap_moderators_url if community_id else community.ap_featured_url, + '@context': default_context(), + 'audience': community.public_url(), + 'to': to, + 'cc': cc + } + + if community.is_local(): + del add['@context'] + + 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': add, + '@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: + post_request(community.ap_inbox_url, add, user.private_key, user.public_url() + '#main-key') diff --git a/app/shared/tasks/removes.py b/app/shared/tasks/removes.py new file mode 100644 index 00000000..99f09fec --- /dev/null +++ b/app/shared/tasks/removes.py @@ -0,0 +1,82 @@ +from app import celery +from app.activitypub.signature import default_context, post_request +from app.models import Community, Post, User +from app.utils import gibberish, instance_banned + +from flask import current_app + + +""" JSON format +Remove: +{ + 'id': + 'type': + 'actor': + 'object': + 'target': (featured_url or moderators_url) + '@context': + 'audience': + 'to': [] + 'cc': [] +} +For Announce, remove @context from inner object, and use same fields except audience +""" + + +@celery.task +def unsticky_post(send_async, user_id, post_id): + post = Post.query.filter_by(id=post_id).one() + remove_object(user_id, post) + + +@celery.task +def remove_mod(send_async, user_id, mod_id, community_id): + mod = User.query.filter_by(id=mod_id).one() + remove_object(user_id, mod, community_id) + + +def remove_object(user_id, object, community_id=None): + user = User.query.filter_by(id=user_id).one() + if not community_id: + community = object.community + else: + community = Community.query.filter_by(id=community_id).one() + + if community.local_only or not community.instance.online(): + return + + remove_id = f"https://{current_app.config['SERVER_NAME']}/activities/remove/{gibberish(15)}" + to = ["https://www.w3.org/ns/activitystreams#Public"] + cc = [community.public_url()] + remove = { + 'id': remove_id, + 'type': 'Remove', + 'actor': user.public_url(), + 'object': object.public_url(), + 'target': community.ap_moderators_url if community_id else community.ap_featured_url, + '@context': default_context(), + 'audience': community.public_url(), + 'to': to, + 'cc': cc + } + + if community.is_local(): + del remove['@context'] + + 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': remove, + '@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: + post_request(community.ap_inbox_url, remove, user.private_key, user.public_url() + '#main-key')