diff --git a/app/api/alpha/routes.py b/app/api/alpha/routes.py index d664212d..dec62f7b 100644 --- a/app/api/alpha/routes.py +++ b/app/api/alpha/routes.py @@ -1,5 +1,5 @@ from app.api.alpha import bp -from app.api.alpha.utils import get_site, \ +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, \ get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe, \ @@ -22,6 +22,18 @@ def get_alpha_site(): return jsonify({"error": str(ex)}), 400 +@bp.route('/api/alpha/site/block', methods=['POST']) +def get_alpha_site_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_site_block(auth, data)) + except Exception as ex: + return jsonify({"error": str(ex)}), 400 + + # Misc @bp.route('/api/alpha/search', methods=['GET']) def get_alpha_search(): @@ -236,7 +248,6 @@ def post_alpha_user_block(): # Site - not yet implemented @bp.route('/api/alpha/site', methods=['POST']) @bp.route('/api/alpha/site', methods=['PUT']) -@bp.route('/api/alpha/site/block', methods=['POST']) def alpha_site(): return jsonify({"error": "not_yet_implemented"}), 400 diff --git a/app/api/alpha/utils/__init__.py b/app/api/alpha/utils/__init__.py index b9d1d1c5..c00f038a 100644 --- a/app/api/alpha/utils/__init__.py +++ b/app/api/alpha/utils/__init__.py @@ -1,4 +1,4 @@ -from app.api.alpha.utils.site import get_site +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 from app.api.alpha.utils.reply import get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe diff --git a/app/api/alpha/utils/community.py b/app/api/alpha/utils/community.py index 6a5337b5..02c6dfaa 100644 --- a/app/api/alpha/utils/community.py +++ b/app/api/alpha/utils/community.py @@ -4,7 +4,7 @@ from app.api.alpha.utils.validators import required, integer_expected, boolean_e from app.utils import authorise_api_user from app.models import Community, CommunityMember from app.shared.community import join_community, leave_community, block_community, unblock_community -from app.utils import communities_banned_from +from app.utils import communities_banned_from, blocked_instances @cache.memoize(timeout=3) @@ -17,6 +17,15 @@ def cached_community_list(type, user_id): else: communities = Community.query.filter_by(banned=False) + print(len(communities.all())) + + if user_id is not None: + blocked_instance_ids = blocked_instances(user_id) + if blocked_instance_ids: + communities = communities.filter(Community.instance_id.not_in(blocked_instance_ids)) + + print(len(communities.all())) + return communities.all() diff --git a/app/api/alpha/utils/post.py b/app/api/alpha/utils/post.py index dd8d221f..563dd2bc 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, blocked_users, blocked_communities +from app.utils import authorise_api_user, blocked_users, blocked_communities, blocked_instances, community_ids_from_instances from datetime import timedelta from sqlalchemy import desc @@ -35,6 +35,10 @@ def cached_post_list(type, sort, user_id, community_id, community_name, person_i blocked_community_ids = blocked_communities(user_id) if blocked_community_ids: posts = posts.filter(Post.community_id.not_in(blocked_community_ids)) + blocked_instance_ids = blocked_instances(user_id) + if blocked_instance_ids: + posts = posts.filter(Post.instance_id.not_in(blocked_instance_ids)) # users from blocked instance + posts = posts.filter(Post.community_id.not_in(community_ids_from_instances(blocked_instance_ids))) # communities from blocked instance if sort == "Hot": posts = posts.order_by(desc(Post.ranking)).order_by(desc(Post.posted_at)) diff --git a/app/api/alpha/utils/reply.py b/app/api/alpha/utils/reply.py index 90f1f856..06085e86 100644 --- a/app/api/alpha/utils/reply.py +++ b/app/api/alpha/utils/reply.py @@ -3,7 +3,7 @@ from app.api.alpha.utils.validators import required, integer_expected, boolean_e 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 app.utils import authorise_api_user, blocked_users, blocked_instances from sqlalchemy import desc @@ -19,6 +19,9 @@ def cached_reply_list(post_id, person_id, sort, max_depth, user_id): blocked_person_ids = blocked_users(user_id) if blocked_person_ids: replies = replies.filter(PostReply.user_id.not_in(blocked_person_ids)) + blocked_instance_ids = blocked_instances(user_id) + if blocked_instance_ids: + replies = replies.filter(PostReply.instance_id.not_in(blocked_instance_ids)) if sort == "Hot": replies = replies.order_by(desc(PostReply.ranking)).order_by(desc(PostReply.posted_at)) diff --git a/app/api/alpha/utils/site.py b/app/api/alpha/utils/site.py index 40a512cd..6e3d1fbe 100644 --- a/app/api/alpha/utils/site.py +++ b/app/api/alpha/utils/site.py @@ -1,7 +1,9 @@ from app import db -from app.api.alpha.views import user_view, community_view +from app.api.alpha.views import user_view, community_view, instance_view +from app.api.alpha.utils.validators import required, integer_expected, boolean_expected from app.utils import authorise_api_user -from app.models import Language +from app.models import InstanceBlock, Language +from app.shared.site import block_remote_instance, unblock_remote_instance from flask import current_app, g @@ -68,7 +70,7 @@ def get_site(auth): #"moderates": [], #"follows": [], "community_blocks": [], - "instance_blocks": [], # TODO + "instance_blocks": [], "person_blocks": [], "discussion_languages": [] # TODO } @@ -87,6 +89,9 @@ def get_site(auth): blocked_ids = db.session.execute(text('SELECT community_id FROM "community_block" WHERE user_id = :user_id'), {"user_id": user.id}).scalars() for blocked_id in blocked_ids: my_user['community_blocks'].append({'person': user_view(user, variant=1, stub=True), 'community': community_view(blocked_id, variant=1, stub=True)}) + blocked_ids = db.session.execute(text('SELECT instance_id FROM "instance_block" WHERE user_id = :user_id'), {"user_id": user.id}).scalars() + for blocked_id in blocked_ids: + my_user['instance_blocks'].append({'person': user_view(user, variant=1, stub=True), 'instance': instance_view(blocked_id, variant=1)}) data = { "version": "1.0.0", "site": site @@ -96,3 +101,21 @@ def get_site(auth): return data + +SRC_API = 3 + +def post_site_block(auth, data): + required(['instance_id', 'block'], data) + integer_expected(['instance_id'], data) + boolean_expected(['block'], data) + + instance_id = data['instance_id'] + block = data['block'] + + user_id = block_remote_instance(instance_id, SRC_API, auth) if block else unblock_remote_instance(instance_id, SRC_API, auth) + blocked = InstanceBlock.query.filter_by(user_id=user_id, instance_id=instance_id).first() + block = True if blocked else False + data = { + "blocked": block + } + return data diff --git a/app/api/alpha/views.py b/app/api/alpha/views.py index 0a4f7a0f..aefef3d8 100644 --- a/app/api/alpha/views.py +++ b/app/api/alpha/views.py @@ -2,7 +2,7 @@ from __future__ import annotations from app import cache, db from app.constants import * -from app.models import Community, CommunityMember, Post, PostReply, PostVote, User +from app.models import Community, CommunityMember, Instance, Post, PostReply, PostVote, User from app.utils import blocked_communities from sqlalchemy import text @@ -343,6 +343,22 @@ def search_view(type): return v1 +def instance_view(instance: Instance | int, variant): + if isinstance(instance, int): + instance = Instance.query.get(instance) + if not instance: + raise Exception('instance_not_found') + + if variant == 1: + include = ['id', 'domain', 'software', 'version'] + v1 = {column.name: getattr(instance, column.name) for column in instance.__table__.columns if column.name in include} + if not v1['version']: + v1.update({'version': '0.0.1'}) + v1.update({'published': instance.created_at.isoformat() + 'Z', 'updated': instance.updated_at.isoformat() + 'Z'}) + + return v1 + + @cache.memoize(timeout=86400) def cached_modlist_for_community(community_id): moderator_ids = db.session.execute(text('SELECT user_id FROM "community_member" WHERE community_id = :community_id and is_moderator = True'), {'community_id': community_id}).scalars() diff --git a/app/shared/site.py b/app/shared/site.py new file mode 100644 index 00000000..13d1b132 --- /dev/null +++ b/app/shared/site.py @@ -0,0 +1,57 @@ +from app import cache, db +from app.models import InstanceBlock +from app.utils import authorise_api_user, blocked_instances + +from flask import flash +from flask_babel import _ +from flask_login import current_user + +# would be in app/constants.py +SRC_WEB = 1 +SRC_PUB = 2 +SRC_API = 3 + + +def block_remote_instance(instance_id, src, auth=None): + if src == SRC_API: + user_id = authorise_api_user(auth) + else: + user_id = current_user.id + + if instance_id == 1: + if src == SRC_API: + raise Exception('cannot_block_local_instance') + else: + flash(_('You cannot block the local instance.'), 'error') + return + + existing = InstanceBlock.query.filter_by(user_id=user_id, instance_id=instance_id).first() + if not existing: + db.session.add(InstanceBlock(user_id=user_id, instance_id=instance_id)) + db.session.commit() + + cache.delete_memoized(blocked_instances, user_id) + + if src == SRC_API: + return user_id + else: + return # let calling function handle confirmation flash message and redirect + + +def unblock_remote_instance(instance_id, src, auth=None): + if src == SRC_API: + user_id = authorise_api_user(auth) + else: + user_id = current_user.id + + existing = InstanceBlock.query.filter_by(user_id=user_id, instance_id=instance_id).first() + if existing: + db.session.delete(existing) + db.session.commit() + + cache.delete_memoized(blocked_instances, user_id) + + if src == SRC_API: + return user_id + else: + return # let calling function handle confirmation flash message and redirect diff --git a/app/utils.py b/app/utils.py index d865b126..bf431def 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1301,3 +1301,9 @@ def authorise_api_user(auth, return_type='id'): raise Exception('incorrect_login') except jwt.InvalidTokenError: raise Exception('invalid_token') + + +@cache.memoize(timeout=86400) +def community_ids_from_instances(instance_ids) -> List[int]: + communities = Community.query.join(Instance, Instance.id == Community.instance_id).filter(Instance.id.in_(instance_ids)) + return [community.id for community in communities]