From b49bb4cf3f056ab9e699c9f7d2e99772d7087a99 Mon Sep 17 00:00:00 2001 From: freamon Date: Wed, 9 Oct 2024 23:46:57 +0000 Subject: [PATCH 1/2] API: initial support for /search endpoint (enough for instance view) --- app/api/alpha/routes.py | 15 ++++++- app/api/alpha/utils/__init__.py | 1 + app/api/alpha/utils/misc.py | 76 +++++++++++++++++++++++++++++++++ app/api/alpha/views.py | 11 +++++ 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 app/api/alpha/utils/misc.py diff --git a/app/api/alpha/routes.py b/app/api/alpha/routes.py index 02b9f7d0..d664212d 100644 --- a/app/api/alpha/routes.py +++ b/app/api/alpha/routes.py @@ -1,5 +1,6 @@ from app.api.alpha import bp from app.api.alpha.utils import get_site, \ + 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, \ get_community_list, get_community, post_community_follow, post_community_block, \ @@ -21,6 +22,19 @@ def get_alpha_site(): return jsonify({"error": str(ex)}), 400 +# Misc +@bp.route('/api/alpha/search', methods=['GET']) +def get_alpha_search(): + if not current_app.debug: + return jsonify({'error': 'alpha api routes only available in debug mode'}) + try: + auth = request.headers.get('Authorization') + data = request.args.to_dict() or None + return jsonify(get_search(auth, data)) + except Exception as ex: + return jsonify({"error": str(ex)}), 400 + + # Community @bp.route('/api/alpha/community', methods=['GET']) def get_alpha_community(): @@ -228,7 +242,6 @@ def alpha_site(): # Miscellaneous - not yet implemented @bp.route('/api/alpha/modlog', methods=['GET']) -@bp.route('/api/alpha/search', methods=['GET']) @bp.route('/api/alpha/resolve_object', methods=['GET']) @bp.route('/api/alpha/federated_instances', methods=['GET']) def alpha_miscellaneous(): diff --git a/app/api/alpha/utils/__init__.py b/app/api/alpha/utils/__init__.py index 7c22f790..b9d1d1c5 100644 --- a/app/api/alpha/utils/__init__.py +++ b/app/api/alpha/utils/__init__.py @@ -1,4 +1,5 @@ from app.api.alpha.utils.site import get_site +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 from app.api.alpha.utils.community import get_community, get_community_list, post_community_follow, post_community_block diff --git a/app/api/alpha/utils/misc.py b/app/api/alpha/utils/misc.py new file mode 100644 index 00000000..96189b38 --- /dev/null +++ b/app/api/alpha/utils/misc.py @@ -0,0 +1,76 @@ +from app.models import Community, Post, User, utcnow +from app.utils import authorise_api_user +from app.api.alpha.views import search_view, community_view, post_view, user_view + +from datetime import timedelta +from sqlalchemy import desc + + +def get_communities_list(sort, page, limit, listing_type, user_id): + # only support 'api/alpha/search?q&type_=Communities&sort=TopAll&listing_type=Local&page=1&limit=15' for now + # (enough for instance view) + communities = Community.query.filter_by(ap_id=None).order_by(desc(Community.subscriptions_count)) + communities = communities.paginate(page=page, per_page=limit, error_out=False) + + community_list = [] + for community in communities: + community_list.append(community_view(community, variant=2, stub=True)) + return community_list + + +def get_posts_list(sort, page, limit, listing_type, user_id): + # only support 'api/alpha/search?q&type_=Posts&sort=TopAll&listing_type=Local&page=1&limit=15' for now + # (enough for instance view) + posts = Post.query.filter_by(instance_id=1, deleted=False) + + if sort == "Hot": + posts = posts.order_by(desc(Post.ranking)).order_by(desc(Post.posted_at)) + elif sort == "TopDay": + posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=1)).order_by(desc(Post.up_votes - Post.down_votes)) + elif sort == "New": + posts = posts.order_by(desc(Post.posted_at)) + elif sort == "Active": + posts = posts.order_by(desc(Post.last_active)) + + posts = posts.paginate(page=page, per_page=limit, error_out=False) + + post_list = [] + for post in posts: + post_list.append(post_view(post, variant=2, stub=True)) + return post_list + + +def get_users_list(sort, page, limit, listing_type, user_id): + # only support 'api/alpha/search?q&type_=Users&sort=TopAll&listing_type=Local&page=1&limit=15' for now + # (enough for instance view) + users = User.query.filter_by(instance_id=1, deleted=False).order_by(User.id) + users = users.paginate(page=page, per_page=limit, error_out=False) + + user_list = [] + for user in users: + user_list.append(user_view(user, variant=2, stub=True)) + return user_list + + +def get_search(auth, data): + if not data or ('q' not in data and 'type_' not in data): + raise Exception('missing_parameters') + + type = data['type_'] + sort = data['sort'] if 'sort' in data else 'Top' + page = int(data['page']) if 'page' in data else 1 + limit = int(data['limit']) if 'limit' in data else 15 + listing_type = data['listing_type'] if 'listing_type' in data else 'Local' + + user_id = authorise_api_user(auth) if auth else None + + search_json = search_view(type) + if type == 'Communities': + search_json['communities'] = get_communities_list(sort, page, limit, listing_type, user_id) + elif type == 'Posts': + search_json['posts'] = get_posts_list(sort, page, limit, listing_type, user_id) + elif type == 'Users': + search_json['users'] = get_users_list(sort, page, limit, listing_type, user_id) + + return search_json + diff --git a/app/api/alpha/views.py b/app/api/alpha/views.py index f7fa2eb5..0a4f7a0f 100644 --- a/app/api/alpha/views.py +++ b/app/api/alpha/views.py @@ -332,6 +332,17 @@ def reply_view(reply: PostReply | int, variant, user_id=None, my_vote=0): return v4 +def search_view(type): + v1 = { + 'type_': type, + 'comments': [], + 'posts': [], + 'communities': [], + 'users': [] + } + 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() From d09c21a32ee338f5dcdcff9e4fbafd452b03fd9f Mon Sep 17 00:00:00 2001 From: freamon Date: Wed, 9 Oct 2024 23:48:58 +0000 Subject: [PATCH 2/2] API: support /site/block endpoint --- app/api/alpha/routes.py | 15 +++++++-- app/api/alpha/utils/__init__.py | 2 +- app/api/alpha/utils/community.py | 11 +++++- app/api/alpha/utils/post.py | 6 +++- app/api/alpha/utils/reply.py | 5 ++- app/api/alpha/utils/site.py | 29 ++++++++++++++-- app/api/alpha/views.py | 18 +++++++++- app/shared/site.py | 57 ++++++++++++++++++++++++++++++++ app/utils.py | 6 ++++ 9 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 app/shared/site.py 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]