diff --git a/app/api/alpha/routes.py b/app/api/alpha/routes.py index bda1291d..116bd780 100644 --- a/app/api/alpha/routes.py +++ b/app/api/alpha/routes.py @@ -3,7 +3,7 @@ from app.api.alpha.utils import get_site, \ get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe, \ get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe, \ get_community_list, get_community, \ - get_user + get_user, post_user_block from app.shared.auth import log_user_in from flask import current_app, jsonify, request @@ -170,7 +170,7 @@ def get_alpha_user(): @bp.route('/api/alpha/user/login', methods=['POST']) -def post_alpha_login(): +def post_alpha_user_login(): if not current_app.debug: return jsonify({'error': 'alpha api routes only available in debug mode'}) try: @@ -181,6 +181,18 @@ def post_alpha_login(): return jsonify({"error": str(ex)}), 400 +@bp.route('/api/alpha/user/block', methods=['POST']) +def post_alpha_user_block(): + if not current_app.debug: + return jsonify({'error': 'alpha api routes only available in debug mode'}) + try: + auth = request.headers.get('Authorization') + data = request.get_json(force=True) or {} + return jsonify(post_user_block(auth, data)) + except Exception as ex: + return jsonify({"error": str(ex)}), 400 + + # Not yet implemented. Copied from lemmy's V3 api, so some aren't needed, and some need changing # Site - not yet implemented @@ -219,7 +231,6 @@ def alpha_community(): @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/save', methods=['PUT']) @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']) @@ -261,7 +272,6 @@ def alpha_chat(): @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/block', methods=['POST']) @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']) diff --git a/app/api/alpha/utils/__init__.py b/app/api/alpha/utils/__init__.py index cb2aac29..401e4785 100644 --- a/app/api/alpha/utils/__init__.py +++ b/app/api/alpha/utils/__init__.py @@ -2,6 +2,6 @@ from app.api.alpha.utils.site import get_site from app.api.alpha.utils.post import get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe from app.api.alpha.utils.reply import get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe from app.api.alpha.utils.community import get_community, get_community_list -from app.api.alpha.utils.user import get_user +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 177c3e34..2bdf4dc4 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 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 -from app.utils import authorise_api_user +from app.utils import authorise_api_user, blocked_users from datetime import timedelta from sqlalchemy import desc @@ -28,6 +28,11 @@ def cached_post_list(type, sort, user_id, community_id, community_name, person_i else: posts = Post.query.filter_by(deleted=False) + if user_id is not None: + blocked_person_ids = blocked_users(user_id) + if blocked_person_ids: + posts = posts.filter(Post.user_id.not_in(blocked_person_ids)) + if sort == "Hot": posts = posts.order_by(desc(Post.ranking)).order_by(desc(Post.posted_at)) elif sort == "TopDay": @@ -49,8 +54,8 @@ def get_post_list(auth, data, user_id=None): if auth: try: user_id = authorise_api_user(auth) - except Exception as e: - raise e + except: + raise # user_id: the logged in user # person_id: the author of the posts being requested diff --git a/app/api/alpha/utils/reply.py b/app/api/alpha/utils/reply.py index 99eb9eca..e459eb06 100644 --- a/app/api/alpha/utils/reply.py +++ b/app/api/alpha/utils/reply.py @@ -1,20 +1,25 @@ from app import cache -from app.utils import authorise_api_user from app.api.alpha.utils.validators import required, integer_expected, boolean_expected from app.api.alpha.views import reply_view from app.models import PostReply from app.shared.reply import vote_for_reply, bookmark_the_post_reply, remove_the_bookmark_from_post_reply, toggle_post_reply_notification +from app.utils import authorise_api_user, blocked_users from sqlalchemy import desc - +# person_id param: the author of the reply; user_id param: the current logged-in user @cache.memoize(timeout=3) -def cached_reply_list(post_id, person_id, sort, max_depth): +def cached_reply_list(post_id, person_id, sort, max_depth, user_id): if post_id: replies = PostReply.query.filter(PostReply.deleted == False, PostReply.post_id == post_id, PostReply.depth <= max_depth) if person_id: replies = PostReply.query.filter_by(deleted=False, user_id=person_id) + if user_id is not None: + blocked_person_ids = blocked_users(user_id) + if blocked_person_ids: + replies = replies.filter(PostReply.user_id.not_in(blocked_person_ids)) + if sort == "Hot": replies = replies.order_by(desc(PostReply.ranking)).order_by(desc(PostReply.posted_at)) elif sort == "Top": @@ -36,13 +41,12 @@ def get_reply_list(auth, data, user_id=None): if data and not post_id and not person_id: raise Exception('missing_parameters') else: - replies = cached_reply_list(post_id, person_id, sort, max_depth) - - if auth: - try: - user_id = authorise_api_user(auth) - except Exception as e: - raise e + if auth: + try: + user_id = authorise_api_user(auth) + except: + raise + replies = cached_reply_list(post_id, person_id, sort, max_depth, user_id) # user_id: the logged in user # person_id: the author of the posts being requested diff --git a/app/api/alpha/utils/site.py b/app/api/alpha/utils/site.py index 869a579b..93864464 100644 --- a/app/api/alpha/utils/site.py +++ b/app/api/alpha/utils/site.py @@ -1,4 +1,5 @@ from app import db +from app.api.alpha.views import user_view from app.utils import authorise_api_user from app.models import Language @@ -71,7 +72,7 @@ def get_site(auth): #"follows": [], "community_blocks": [], # TODO "instance_blocks": [], # TODO - "person_blocks": [], # TODO + "person_blocks": [], "discussion_languages": [] # TODO } """ @@ -83,7 +84,9 @@ def get_site(auth): for cm in cms: my_user['follows'].append({'community': Community.api_json(variant=1, id=cm.community_id, stub=True), 'follower': User.api_json(variant=1, id=user_id, stub=True)}) """ - + blocked_ids = db.session.execute(text('SELECT blocked_id FROM "user_block" WHERE blocker_id = :blocker_id'), {"blocker_id": user.id}).scalars() + for blocked_id in blocked_ids: + my_user['person_blocks'].append({'person': user_view(user, variant=1, stub=True), 'target': user_view(blocked_id, variant=1, stub=True)}) data = { "version": "1.0.0", "site": site diff --git a/app/api/alpha/utils/user.py b/app/api/alpha/utils/user.py index 651a3808..84459b24 100644 --- a/app/api/alpha/utils/user.py +++ b/app/api/alpha/utils/user.py @@ -2,6 +2,8 @@ from app.api.alpha.views import user_view from app.utils import authorise_api_user from app.api.alpha.utils.post import get_post_list from app.api.alpha.utils.reply import get_reply_list +from app.api.alpha.utils.validators import required, integer_expected, boolean_expected +from app.shared.user import block_another_user, unblock_another_user def get_user(auth, data): @@ -39,4 +41,26 @@ def get_user(auth, data): raise +# would be in app/constants.py +SRC_API = 3 +def post_user_block(auth, data): + try: + required(['person_id', 'block'], data) + integer_expected(['post_id'], data) + boolean_expected(['block'], data) + except: + raise + + person_id = data['person_id'] + block = data['block'] + + try: + if block == True: + user_id = block_another_user(person_id, SRC_API, auth) + else: + user_id = unblock_another_user(person_id, SRC_API, auth) + user_json = user_view(user=person_id, variant=4, user_id=user_id) + return user_json + except: + raise diff --git a/app/api/alpha/views.py b/app/api/alpha/views.py index 1a2c8513..ae3c552e 100644 --- a/app/api/alpha/views.py +++ b/app/api/alpha/views.py @@ -50,8 +50,11 @@ def post_view(post: Post | int, variant, stub=False, user_id=None, my_vote=0): # counts - models/post/post_aggregates.dart counts = {'post_id': post.id, 'comments': post.reply_count, 'score': post.score, 'upvotes': post.up_votes, 'downvotes': post.down_votes, 'published': post.posted_at.isoformat() + 'Z', 'newest_comment_time': post.last_active.isoformat() + 'Z'} - bookmarked = db.session.execute(text('SELECT user_id FROM "post_bookmark" WHERE post_id = :post_id and user_id = :user_id'), {'post_id': post.id, 'user_id': user_id}).scalar() - post_sub = db.session.execute(text('SELECT user_id FROM "notification_subscription" WHERE type = :type and entity_id = :entity_id and user_id = :user_id'), {'type': NOTIF_POST, 'entity_id': post.id, 'user_id': user_id}).scalar() + if user_id: + bookmarked = db.session.execute(text('SELECT user_id FROM "post_bookmark" WHERE post_id = :post_id and user_id = :user_id'), {'post_id': post.id, 'user_id': user_id}).scalar() + post_sub = db.session.execute(text('SELECT user_id FROM "notification_subscription" WHERE type = :type and entity_id = :entity_id and user_id = :user_id'), {'type': NOTIF_POST, 'entity_id': post.id, 'user_id': user_id}).scalar() + else: + bookmarked = post_sub = False if not stub: banned = db.session.execute(text('SELECT user_id FROM "community_ban" WHERE user_id = :user_id and community_id = :community_id'), {'user_id': post.user_id, 'community_id': post.community_id}).scalar() moderator = db.session.execute(text('SELECT is_moderator FROM "community_member" WHERE user_id = :user_id and community_id = :community_id'), {'user_id': post.user_id, 'community_id': post.community_id}).scalar() @@ -73,7 +76,7 @@ def post_view(post: Post | int, variant, stub=False, user_id=None, my_vote=0): creator_is_moderator = True if moderator else False creator_is_admin = True if admin else False v2 = {'post': post_view(post=post, variant=1, stub=stub), 'counts': counts, 'banned_from_community': False, 'subscribed': 'NotSubscribed', - 'saved': saved, 'read': False, 'hidden': False, 'creator_blocked': False, 'unread_comments': post.reply_count, 'my_vote': my_vote, 'activity_alert': activity_alert, + 'saved': saved, 'read': False, 'hidden': False, 'unread_comments': post.reply_count, 'my_vote': my_vote, 'activity_alert': activity_alert, 'creator_banned_from_community': creator_banned_from_community, 'creator_is_moderator': creator_is_moderator, 'creator_is_admin': creator_is_admin} try: @@ -127,7 +130,8 @@ def cached_user_view_variant_1(user: User, stub=False): return v1 -def user_view(user: User | int, variant, stub=False): +# 'user' param can be anyone (including the logged in user), 'user_id' param belongs to the user making the request +def user_view(user: User | int, variant, stub=False, user_id=None): if isinstance(user, int): user = User.query.get(user) if not user: @@ -144,13 +148,22 @@ def user_view(user: User | int, variant, stub=False): return v2 # Variant 3 - models/user/get_person_details.dart - /user?person_id api endpoint - modlist = cached_modlist_for_user(user) + if variant == 3: + modlist = cached_modlist_for_user(user) - v3 = {'person_view': user_view(user=user, variant=2), - 'moderates': modlist, - 'posts': [], - 'comments': []} - return v3 + v3 = {'person_view': user_view(user=user, variant=2), + 'moderates': modlist, + 'posts': [], + 'comments': []} + return v3 + + # Variant 4 - models/user/block_person_response.dart - /user/block api endpoint + if variant == 4: + block = db.session.execute(text('SELECT blocker_id FROM "user_block" WHERE blocker_id = :blocker_id and blocked_id = :blocked_id'), {'blocker_id': user_id, 'blocked_id': user.id}).scalar() + blocked = True if block else False + v4 = {'person_view': user_view(user=user, variant=2), + 'blocked': blocked} + return v4 @cache.memoize(timeout=600) diff --git a/app/shared/user.py b/app/shared/user.py new file mode 100644 index 00000000..b5916b11 --- /dev/null +++ b/app/shared/user.py @@ -0,0 +1,92 @@ +from app import db, cache +from app.constants import ROLE_STAFF, ROLE_ADMIN +from app.models import UserBlock +from app.utils import authorise_api_user, blocked_users + +from flask import flash +from flask_babel import _ +from flask_login import current_user + +from sqlalchemy import text + +# would be in app/constants.py +SRC_WEB = 1 +SRC_PUB = 2 +SRC_API = 3 + +# only called from API for now, but can be called from web using [un]block_another_user(user.id, SRC_WEB) + +# user_id: the local, logged-in user +# person_id: the person they want to block + +def block_another_user(person_id, src, auth=None): + if src == SRC_API: + try: + user_id = authorise_api_user(auth) + except: + raise + else: + user_id = current_user.id + + if user_id == person_id: + if src == SRC_API: + raise Exception('cannot_block_self') + else: + flash(_('You cannot block yourself.'), 'error') + return + + role = db.session.execute(text('SELECT role_id FROM "user_role" WHERE user_id = :person_id'), {'person_id': person_id}).scalar() + if role == ROLE_ADMIN or role == ROLE_STAFF: + if src == SRC_API: + raise Exception('cannot_block_admin_or_staff') + else: + flash(_('You cannot block admin or staff.'), 'error') + return + + existing_block = UserBlock.query.filter_by(blocker_id=user_id, blocked_id=person_id).first() + if not existing_block: + block = UserBlock(blocker_id=user_id, blocked_id=person_id) + db.session.add(block) + db.session.execute(text('DELETE FROM "notification_subscription" WHERE entity_id = :current_user AND user_id = :user_id'), + {'current_user': user_id, 'user_id': person_id}) + db.session.commit() + + cache.delete_memoized(blocked_users, user_id) + + # Nothing to fed? (Lemmy doesn't federate anything to the blocked person) + + if src == SRC_API: + return user_id + else: + return # let calling function handle confirmation flash message and redirect + + +def unblock_another_user(person_id, src, auth=None): + if src == SRC_API: + try: + user_id = authorise_api_user(auth) + except: + raise + else: + user_id = current_user.id + + if user_id == person_id: + if src == SRC_API: + raise Exception('cannot_unblock_self') + else: + flash(_('You cannot unblock yourself.'), 'error') + return + + existing_block = UserBlock.query.filter_by(blocker_id=user_id, blocked_id=person_id).first() + if existing_block: + db.session.delete(existing_block) + db.session.commit() + + cache.delete_memoized(blocked_users, user_id) + + # Nothing to fed? (Lemmy doesn't federate anything to the unblocked person) + + if src == SRC_API: + return user_id + else: + return # let calling function handle confirmation flash message and redirect