diff --git a/app/api/alpha/routes.py b/app/api/alpha/routes.py index 7d1cfd09..840f6d58 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, \ - get_post_list, get_post, post_post_like, \ - get_reply_list, post_reply_like, \ + get_post_list, get_post, post_post_like, put_post_save, \ + get_reply_list, post_reply_like, put_reply_save, \ get_community_list, get_community, \ get_user from app.shared.auth import log_user_in @@ -83,6 +83,18 @@ def post_alpha_post_like(): return jsonify({"error": str(ex)}), 400 +@bp.route('/api/alpha/post/save', methods=['PUT']) +def put_alpha_post_save(): + 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(put_post_save(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(): @@ -108,6 +120,18 @@ def post_alpha_comment_like(): return jsonify({"error": str(ex)}), 400 +@bp.route('/api/alpha/comment/save', methods=['PUT']) +def put_alpha_comment_save(): + 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(put_reply_save(auth, data)) + except Exception as ex: + return jsonify({"error": str(ex)}), 400 + + # User @bp.route('/api/alpha/user', methods=['GET']) def get_alpha_user(): @@ -171,7 +195,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/like', 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']) @@ -188,7 +211,6 @@ def alpha_post(): @bp.route('/api/alpha/comment/remove', methods=['POST']) @bp.route('/api/alpha/comment/mark_as_read', methods=['POST']) @bp.route('/api/alpha/comment/distinguish', methods=['POST']) -@bp.route('/api/alpha/comment/save', methods=['PUT']) @bp.route('/api/alpha/comment/report', methods=['POST']) @bp.route('/api/alpha/comment/report/resolve', methods=['PUT']) @bp.route('/api/alpha/comment/report/list', methods=['GET']) diff --git a/app/api/alpha/utils/__init__.py b/app/api/alpha/utils/__init__.py index c988562d..2bf6cf3d 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 -from app.api.alpha.utils.post import get_post_list, get_post, post_post_like -from app.api.alpha.utils.reply import get_reply_list, post_reply_like +from app.api.alpha.utils.post import get_post_list, get_post, post_post_like, put_post_save +from app.api.alpha.utils.reply import get_reply_list, post_reply_like, put_reply_save from app.api.alpha.utils.community import get_community, get_community_list from app.api.alpha.utils.user import get_user diff --git a/app/api/alpha/utils/post.py b/app/api/alpha/utils/post.py index 85b87c34..f427ff70 100644 --- a/app/api/alpha/utils/post.py +++ b/app/api/alpha/utils/post.py @@ -1,8 +1,8 @@ from app import cache from app.api.alpha.views import post_view -from app.api.alpha.utils.validators import required, integer_expected +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 +from app.shared.post import vote_for_post, bookmark_the_post, remove_the_bookmark_from_post from app.utils import authorise_api_user from datetime import timedelta @@ -128,6 +128,28 @@ def post_post_like(auth, data): raise +def put_post_save(auth, data): + try: + required(['post_id', 'save'], data) + integer_expected(['post_id'], data) + boolean_expected(['save'], data) + except: + raise + + post_id = data['post_id'] + save = data['save'] + + try: + if save is True: + user_id = bookmark_the_post(post_id, SRC_API, auth) + else: + user_id = remove_the_bookmark_from_post(post_id, SRC_API, auth) + post_json = post_view(post=post_id, variant=4, user_id=user_id) + return post_json + except: + raise + + diff --git a/app/api/alpha/utils/reply.py b/app/api/alpha/utils/reply.py index a61dde5f..ed8d344d 100644 --- a/app/api/alpha/utils/reply.py +++ b/app/api/alpha/utils/reply.py @@ -1,9 +1,9 @@ from app import cache from app.utils import authorise_api_user -from app.api.alpha.utils.validators import required, integer_expected +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 +from app.shared.reply import vote_for_reply, bookmark_the_post_reply, remove_the_bookmark_from_post_reply from sqlalchemy import desc @@ -93,4 +93,26 @@ def post_reply_like(auth, data): raise +def put_reply_save(auth, data): + try: + required(['comment_id', 'save'], data) + integer_expected(['comment_id'], data) + boolean_expected(['save'], data) + except: + raise + + reply_id = data['comment_id'] + save = data['save'] + + try: + if save is True: + user_id = bookmark_the_post_reply(reply_id, SRC_API, auth) + else: + user_id = remove_the_bookmark_from_post_reply(reply_id, SRC_API, auth) + reply_json = reply_view(reply=reply_id, variant=4, user_id=user_id) + return reply_json + except: + raise + + diff --git a/app/api/alpha/views.py b/app/api/alpha/views.py index 78051b28..13f645aa 100644 --- a/app/api/alpha/views.py +++ b/app/api/alpha/views.py @@ -50,8 +50,10 @@ 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() + saved = True if bookmarked else False v2 = {'post': post_view(post=post, variant=1, stub=stub), 'counts': counts, 'banned_from_community': False, 'subscribed': 'NotSubscribed', - 'saved': False, 'read': False, 'hidden': False, 'creator_blocked': False, 'unread_comments': post.reply_count, 'my_vote': my_vote} + 'saved': saved, 'read': False, 'hidden': False, 'creator_blocked': False, 'unread_comments': post.reply_count, 'my_vote': my_vote} try: creator = user_view(user=post.user_id, variant=1, stub=True) @@ -95,7 +97,7 @@ def post_view(post: Post | int, variant, stub=False, user_id=None, my_vote=0): return v3 - # Variant 4 - models/post/post_response.dart - /post/like api endpoint + # Variant 4 - models/post/post_response.dart - api endpoint for /post/like and post/save if variant == 4: v4 = {'post_view': post_view(post=post, variant=2, user_id=user_id)} @@ -255,8 +257,10 @@ def reply_view(reply: PostReply | int, variant, user_id=None, my_vote=0): # counts - models/comment/comment_aggregates.dart counts = {'comment_id': reply.id, 'score': reply.score, 'upvotes': reply.up_votes, 'downvotes': reply.down_votes, 'published': reply.posted_at.isoformat() + 'Z', 'child_count': 1 if calculate_if_has_children(reply) else 0} + bookmarked = db.session.execute(text('SELECT user_id FROM "post_reply_bookmark" WHERE post_reply_id = :post_reply_id and user_id = :user_id'), {'post_reply_id': reply.id, 'user_id': user_id}).scalar() + saved = True if bookmarked else False v2 = {'comment': reply_view(reply=reply, variant=1), 'counts': counts, 'banned_from_community': False, 'subscribed': 'NotSubscribed', - 'saved': False, 'creator_blocked': False, 'my_vote': my_vote} + 'saved': saved, 'creator_blocked': False, 'my_vote': my_vote} try: creator = user_view(user=reply.user_id, variant=1, stub=True) community = community_view(community=reply.community_id, variant=1, stub=True) diff --git a/app/shared/post.py b/app/shared/post.py index 7c8d0f2a..9f85ede4 100644 --- a/app/shared/post.py +++ b/app/shared/post.py @@ -1,10 +1,11 @@ -from app import cache +from app import cache, db from app.activitypub.signature import default_context, post_request_in_background from app.community.util import send_to_remote_instance -from app.models import Post, User +from app.models import Post, PostBookmark, User from app.utils import gibberish, instance_banned, render_template, authorise_api_user, recently_upvoted_posts, recently_downvoted_posts -from flask import current_app, request +from flask import abort, current_app, flash, redirect, request, url_for +from flask_babel import _ from flask_login import current_user @@ -92,3 +93,68 @@ def vote_for_post(post_id: int, vote_direction, src, auth=None): template = 'post/_post_voting_buttons.html' if request.args.get('style', '') == '' else 'post/_post_voting_buttons_masonry.html' return render_template(template, post=post, community=post.community, recently_upvoted=recently_upvoted, recently_downvoted=recently_downvoted) + + +# function can be shared between WEB and API (only API calls it for now) +# post_bookmark in app/post/routes would just need to do 'return bookmark_the_post(post_id, SRC_WEB)' +def bookmark_the_post(post_id: int, src, auth=None): + if src == SRC_API and auth is not None: + post = Post.query.get(post_id) + if not post or post.deleted: + raise Exception('post_not_found') + try: + user_id = authorise_api_user(auth) + except: + raise + else: + post = Post.query.get_or_404(post_id) + if post.deleted: + abort(404) + user_id = current_user.id + + existing_bookmark = PostBookmark.query.filter(PostBookmark.post_id == post_id, PostBookmark.user_id == user_id).first() + if not existing_bookmark: + db.session.add(PostBookmark(post_id=post_id, user_id=user_id)) + db.session.commit() + if src == SRC_WEB: + flash(_('Bookmark added.')) + else: + if src == SRC_WEB: + flash(_('This post has already been bookmarked.')) + + if src == SRC_API: + return user_id + else: + return redirect(url_for('activitypub.post_ap', post_id=post.id)) + + +# function can be shared between WEB and API (only API calls it for now) +# post_remove_bookmark in app/post/routes would just need to do 'return remove_the_bookmark_from_post(post_id, SRC_WEB)' +def remove_the_bookmark_from_post(post_id: int, src, auth=None): + if src == SRC_API and auth is not None: + post = Post.query.get(post_id) + if not post or post.deleted: + raise Exception('post_not_found') + try: + user_id = authorise_api_user(auth) + except: + raise + else: + post = Post.query.get_or_404(post_id) + if post.deleted: + abort(404) + user_id = current_user.id + + existing_bookmark = PostBookmark.query.filter(PostBookmark.post_id == post_id, PostBookmark.user_id == user_id).first() + if existing_bookmark: + db.session.delete(existing_bookmark) + db.session.commit() + if src == SRC_WEB: + flash(_('Bookmark has been removed.')) + + if src == SRC_API: + return user_id + else: + return redirect(url_for('activitypub.post_ap', post_id=post.id)) + + diff --git a/app/shared/reply.py b/app/shared/reply.py index 38db1cfc..e69c9dcb 100644 --- a/app/shared/reply.py +++ b/app/shared/reply.py @@ -1,10 +1,11 @@ -from app import cache +from app import cache, db from app.activitypub.signature import default_context, post_request_in_background from app.community.util import send_to_remote_instance -from app.models import PostReply, User +from app.models import PostReply, PostReplyBookmark, User from app.utils import gibberish, instance_banned, render_template, authorise_api_user, recently_upvoted_post_replies, recently_downvoted_post_replies -from flask import current_app, request +from flask import abort, current_app, flash, redirect, request, url_for +from flask_babel import _ from flask_login import current_user @@ -94,4 +95,66 @@ def vote_for_reply(reply_id: int, vote_direction, src, auth=None): community=reply.community) +# function can be shared between WEB and API (only API calls it for now) +# post_reply_bookmark in app/post/routes would just need to do 'return bookmark_the_post_reply(comment_id, SRC_WEB)' +def bookmark_the_post_reply(comment_id: int, src, auth=None): + if src == SRC_API and auth is not None: + post_reply = PostReply.query.get(comment_id) + if not post_reply or post_reply.deleted: + raise Exception('comment_not_found') + try: + user_id = authorise_api_user(auth) + except: + raise + else: + post_reply = PostReply.query.get_or_404(comment_id) + if post_reply.deleted: + abort(404) + user_id = current_user.id + existing_bookmark = PostReplyBookmark.query.filter(PostReplyBookmark.post_reply_id == comment_id, + PostReplyBookmark.user_id == user_id).first() + if not existing_bookmark: + db.session.add(PostReplyBookmark(post_reply_id=comment_id, user_id=user_id)) + db.session.commit() + if src == SRC_WEB: + flash(_('Bookmark added.')) + else: + if src == SRC_WEB: + flash(_('This comment has already been bookmarked')) + + if src == SRC_API: + return user_id + else: + return redirect(url_for('activitypub.post_ap', post_id=post_reply.post_id, _anchor=f'comment_{comment_id}')) + + +# function can be shared between WEB and API (only API calls it for now) +# post_reply_remove_bookmark in app/post/routes would just need to do 'return remove_the_bookmark_from_post_reply(comment_id, SRC_WEB)' +def remove_the_bookmark_from_post_reply(comment_id: int, src, auth=None): + if src == SRC_API and auth is not None: + post_reply = PostReply.query.get(comment_id) + if not post_reply or post_reply.deleted: + raise Exception('comment_not_found') + try: + user_id = authorise_api_user(auth) + except: + raise + else: + post_reply = PostReply.query.get_or_404(comment_id) + if post_reply.deleted: + abort(404) + user_id = current_user.id + + existing_bookmark = PostReplyBookmark.query.filter(PostReplyBookmark.post_reply_id == comment_id, + PostReplyBookmark.user_id == user_id).first() + if existing_bookmark: + db.session.delete(existing_bookmark) + db.session.commit() + if src == SRC_WEB: + flash(_('Bookmark has been removed.')) + + if src == SRC_API: + return user_id + else: + return redirect(url_for('activitypub.post_ap', post_id=post_reply.post_id))