diff --git a/app/api/alpha/routes.py b/app/api/alpha/routes.py index 840f6d58..bda1291d 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, put_post_save, \ - get_reply_list, post_reply_like, put_reply_save, \ + 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 from app.shared.auth import log_user_in @@ -95,6 +95,18 @@ def put_alpha_post_save(): return jsonify({"error": str(ex)}), 400 +@bp.route('/api/alpha/post/subscribe', methods=['PUT']) +def put_alpha_post_subscribe(): + 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_subscribe(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(): @@ -132,6 +144,18 @@ def put_alpha_comment_save(): return jsonify({"error": str(ex)}), 400 +@bp.route('/api/alpha/comment/subscribe', methods=['PUT']) +def put_alpha_comment_subscribe(): + 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_subscribe(auth, data)) + except Exception as ex: + return jsonify({"error": str(ex)}), 400 + + # User @bp.route('/api/alpha/user', methods=['GET']) def get_alpha_user(): diff --git a/app/api/alpha/utils/__init__.py b/app/api/alpha/utils/__init__.py index 2bf6cf3d..cb2aac29 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, put_post_save -from app.api.alpha.utils.reply import get_reply_list, post_reply_like, put_reply_save +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 diff --git a/app/api/alpha/utils/post.py b/app/api/alpha/utils/post.py index f427ff70..177c3e34 100644 --- a/app/api/alpha/utils/post.py +++ b/app/api/alpha/utils/post.py @@ -2,7 +2,7 @@ from app import cache 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 +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 datetime import timedelta @@ -150,6 +150,25 @@ def put_post_save(auth, data): raise +def put_post_subscribe(auth, data): + try: + required(['post_id', 'subscribe'], data) + integer_expected(['post_id'], data) + boolean_expected(['subscribe'], data) + except: + raise + + post_id = data['post_id'] + subscribe = data['subscribe'] # not actually processed - is just a toggle + + try: + user_id = toggle_post_notification(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 ed8d344d..99eb9eca 100644 --- a/app/api/alpha/utils/reply.py +++ b/app/api/alpha/utils/reply.py @@ -3,7 +3,7 @@ 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 +from app.shared.reply import vote_for_reply, bookmark_the_post_reply, remove_the_bookmark_from_post_reply, toggle_post_reply_notification from sqlalchemy import desc @@ -115,4 +115,20 @@ def put_reply_save(auth, data): raise +def put_reply_subscribe(auth, data): + try: + required(['comment_id', 'subscribe'], data) + integer_expected(['comment_id'], data) + boolean_expected(['subscribe'], data) + except: + raise + reply_id = data['comment_id'] + subscribe = data['subscribe'] # not actually processed - is just a toggle + + try: + user_id = toggle_post_reply_notification(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/shared/post.py b/app/shared/post.py index 9f85ede4..f49de24a 100644 --- a/app/shared/post.py +++ b/app/shared/post.py @@ -1,8 +1,9 @@ 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, PostBookmark, User -from app.utils import gibberish, instance_banned, render_template, authorise_api_user, recently_upvoted_posts, recently_downvoted_posts +from app.constants import * +from app.models import NotificationSubscription, Post, PostBookmark, User +from app.utils import gibberish, instance_banned, render_template, authorise_api_user, recently_upvoted_posts, recently_downvoted_posts, shorten_string from flask import abort, current_app, flash, redirect, request, url_for from flask_babel import _ @@ -158,3 +159,40 @@ def remove_the_bookmark_from_post(post_id: int, src, auth=None): 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_notification in app/post/routes would just need to do 'return toggle_post_notification(post_id, SRC_WEB)' +def toggle_post_notification(post_id: int, src, auth=None): + # Toggle whether the current user is subscribed to notifications about top-level replies to this post or not + 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_notification = NotificationSubscription.query.filter(NotificationSubscription.entity_id == post.id, + NotificationSubscription.user_id == user_id, + NotificationSubscription.type == NOTIF_POST).first() + if existing_notification: + db.session.delete(existing_notification) + db.session.commit() + else: # no subscription yet, so make one + new_notification = NotificationSubscription(name=shorten_string(_('Replies to my post %(post_title)s', + post_title=post.title)), + user_id=user_id, entity_id=post.id, + type=NOTIF_POST) + db.session.add(new_notification) + db.session.commit() + + if src == SRC_API: + return user_id + else: + return render_template('post/_post_notification_toggle.html', post=post) diff --git a/app/shared/reply.py b/app/shared/reply.py index e69c9dcb..d1c11c6a 100644 --- a/app/shared/reply.py +++ b/app/shared/reply.py @@ -1,8 +1,9 @@ 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, PostReplyBookmark, User -from app.utils import gibberish, instance_banned, render_template, authorise_api_user, recently_upvoted_post_replies, recently_downvoted_post_replies +from app.constants import * +from app.models import NotificationSubscription, PostReply, PostReplyBookmark, User +from app.utils import gibberish, instance_banned, render_template, authorise_api_user, recently_upvoted_post_replies, recently_downvoted_post_replies, shorten_string from flask import abort, current_app, flash, redirect, request, url_for from flask_babel import _ @@ -158,3 +159,40 @@ def remove_the_bookmark_from_post_reply(comment_id: int, src, auth=None): return user_id else: return redirect(url_for('activitypub.post_ap', post_id=post_reply.post_id)) + + +# function can be shared between WEB and API (only API calls it for now) +# post_reply_notification in app/post/routes would just need to do 'return toggle_post_reply_notification(post_reply_id, SRC_WEB)' +def toggle_post_reply_notification(post_reply_id: int, src, auth=None): + # Toggle whether the current user is subscribed to notifications about replies to this reply or not + if src == SRC_API and auth is not None: + post_reply = PostReply.query.get(post_reply_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(post_reply_id) + if post_reply.deleted: + abort(404) + user_id = current_user.id + + existing_notification = NotificationSubscription.query.filter(NotificationSubscription.entity_id == post_reply.id, + NotificationSubscription.user_id == user_id, + NotificationSubscription.type == NOTIF_REPLY).first() + if existing_notification: + db.session.delete(existing_notification) + db.session.commit() + else: # no subscription yet, so make one + new_notification = NotificationSubscription(name=shorten_string(_('Replies to my comment on %(post_title)s', + post_title=post_reply.post.title)), user_id=user_id, entity_id=post_reply.id, + type=NOTIF_REPLY) + db.session.add(new_notification) + db.session.commit() + + if src == SRC_API: + return user_id + else: + return render_template('post/_reply_notification_toggle.html', comment={'comment': post_reply})