From 0f79b8075f9d3b4c6103bf20c1e8429b1b20ec2e Mon Sep 17 00:00:00 2001 From: freamon Date: Thu, 23 Jan 2025 23:21:24 +0000 Subject: [PATCH] API: inbox private messages --- app/api/alpha/routes.py | 49 +++++++++++++++++++------- app/api/alpha/utils/__init__.py | 3 +- app/api/alpha/utils/private_message.py | 38 ++++++++++++++++++++ app/api/alpha/utils/user.py | 20 ++++++++++- app/api/alpha/views.py | 26 +++++++++++++- 5 files changed, 120 insertions(+), 16 deletions(-) create mode 100644 app/api/alpha/utils/private_message.py diff --git a/app/api/alpha/routes.py b/app/api/alpha/routes.py index bc87a4eb..197557f4 100644 --- a/app/api/alpha/routes.py +++ b/app/api/alpha/routes.py @@ -6,7 +6,8 @@ from app.api.alpha.utils import get_site, post_site_block, \ get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe, post_reply, put_reply, post_reply_mark_as_read, \ post_reply_delete, post_reply_report, post_reply_remove, \ get_community_list, get_community, post_community_follow, post_community_block, \ - get_user, post_user_block, get_user_unread_count, get_user_replies + get_user, post_user_block, get_user_unread_count, get_user_replies, post_user_mark_all_as_read, \ + get_private_message_list from app.shared.auth import log_user_in from flask import current_app, jsonify, request @@ -366,6 +367,19 @@ def post_alpha_comment_mark_as_read(): return jsonify({"error": str(ex)}), 400 +# Private Message +@bp.route('/api/alpha/private_message/list', methods=['GET']) +def get_alpha_private_message_list(): + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) + try: + auth = request.headers.get('Authorization') + data = request.args.to_dict() or None + return jsonify(get_private_message_list(auth, data)) + except Exception as ex: + return jsonify({"error": str(ex)}), 400 + + # User @bp.route('/api/alpha/user', methods=['GET']) def get_alpha_user(): @@ -426,6 +440,17 @@ def post_alpha_user_block(): return jsonify({"error": str(ex)}), 400 +@bp.route('/api/alpha/user/mark_all_as_read', methods=['POST']) +def post_alpha_user_mark_all_as_read(): + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) + try: + auth = request.headers.get('Authorization') + return jsonify(post_user_mark_all_as_read(auth)) + 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 @@ -436,7 +461,7 @@ def alpha_site(): # Miscellaneous - not yet implemented @bp.route('/api/alpha/modlog', methods=['GET']) # Get Modlog. Not usually public -@bp.route('/api/alpha/resolve_object', methods=['GET']) # Stage 1: Needed for search +@bp.route('/api/alpha/resolve_object', methods=['GET']) # Stage 2 @bp.route('/api/alpha/federated_instances', methods=['GET']) # No plans to implement - only V3 version needed def alpha_miscellaneous(): return jsonify({"error": "not_yet_implemented"}), 400 @@ -469,12 +494,11 @@ def alpha_reply(): return jsonify({"error": "not_yet_implemented"}), 400 # Chat - not yet implemented -@bp.route('/api/alpha/private_message/list', methods=['GET']) # Stage 1 -@bp.route('/api/alpha/private_message', methods=['PUT']) # Stage 1 -@bp.route('/api/alpha/private_message', methods=['POST']) # Stage 1 -@bp.route('/api/alpha/private_message/delete', methods=['POST']) # Stage 1 -@bp.route('/api/alpha/private_message/mark_as_read', methods=['POST']) # Stage 1 -@bp.route('/api/alpha/private_message/report', methods=['POST']) # Stage 1 +@bp.route('/api/alpha/private_message', methods=['PUT']) # Not available in app +@bp.route('/api/alpha/private_message', methods=['POST']) # Not available in app +@bp.route('/api/alpha/private_message/delete', methods=['POST']) # Not available in app +@bp.route('/api/alpha/private_message/mark_as_read', methods=['POST']) # Not available in app +@bp.route('/api/alpha/private_message/report', methods=['POST']) # Not available in app @bp.route('/api/alpha/private_message/report/resolve', methods=['PUT']) # Stage 2 @bp.route('/api/alpha/private_message/report/list', methods=['GET']) # Stage 2 def alpha_chat(): @@ -484,15 +508,14 @@ def alpha_chat(): @bp.route('/api/alpha/user/register', methods=['POST']) # Not available in app @bp.route('/api/alpha/user/get_captcha', methods=['GET']) # Not available in app @bp.route('/api/alpha/user/mention', methods=['GET']) # No DB support -@bp.route('/api/alpha/user/mention/mark_as_read', methods=['POST']) # No DB support +@bp.route('/api/alpha/user/mention/mark_as_read', methods=['POST']) # No DB support / Not available in app (using mark_all instead) @bp.route('/api/alpha/user/ban', methods=['POST']) # Admin function. No plans to implement @bp.route('/api/alpha/user/banned', methods=['GET']) # Admin function. No plans to implement @bp.route('/api/alpha/user/delete_account', methods=['POST']) # Not available in app @bp.route('/api/alpha/user/password_reset', methods=['POST']) # Not available in app @bp.route('/api/alpha/user/password_change', methods=['POST']) # Not available in app -@bp.route('/api/alpha/user/mark_all_as_read', methods=['POST']) # Stage 1 -@bp.route('/api/alpha/user/save_user_settings', methods=['PUT']) # Not available in app -@bp.route('/api/alpha/user/change_password', methods=['PUT']) # Not available in app +@bp.route('/api/alpha/user/save_user_settings', methods=['PUT']) # Stage 2 +@bp.route('/api/alpha/user/change_password', methods=['PUT']) # Stage 2 @bp.route('/api/alpha/user/report_count', methods=['GET']) # Stage 2 @bp.route('/api/alpha/user/verify_email', methods=['POST']) # Admin function. No plans to implement @bp.route('/api/alpha/user/leave_admin', methods=['POST']) # Admin function. No plans to implement @@ -502,7 +525,7 @@ def alpha_chat(): @bp.route('/api/alpha/user/import_settings', methods=['POST']) # Not available in app @bp.route('/api/alpha/user/list_logins', methods=['GET']) # Not available in app @bp.route('/api/alpha/user/validate_auth', methods=['GET']) # Not available in app -@bp.route('/api/alpha/user/logout', methods=['POST']) # Stage 1 +@bp.route('/api/alpha/user/logout', methods=['POST']) # Stage 2 def alpha_user(): return jsonify({"error": "not_yet_implemented"}), 400 diff --git a/app/api/alpha/utils/__init__.py b/app/api/alpha/utils/__init__.py index a2dd5aee..167b936a 100644 --- a/app/api/alpha/utils/__init__.py +++ b/app/api/alpha/utils/__init__.py @@ -3,6 +3,7 @@ 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, post_post, put_post, post_post_delete, post_post_report, post_post_lock, post_post_feature, post_post_remove from app.api.alpha.utils.reply import get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe, post_reply, put_reply, post_reply_delete, post_reply_report, post_reply_remove, post_reply_mark_as_read from app.api.alpha.utils.community import get_community, get_community_list, post_community_follow, post_community_block -from app.api.alpha.utils.user import get_user, post_user_block, get_user_unread_count, get_user_replies +from app.api.alpha.utils.user import get_user, post_user_block, get_user_unread_count, get_user_replies, post_user_mark_all_as_read +from app.api.alpha.utils.private_message import get_private_message_list diff --git a/app/api/alpha/utils/private_message.py b/app/api/alpha/utils/private_message.py new file mode 100644 index 00000000..d3599141 --- /dev/null +++ b/app/api/alpha/utils/private_message.py @@ -0,0 +1,38 @@ +from app import db +from app.api.alpha.views import private_message_view +from app.models import ChatMessage, Conversation +from app.utils import authorise_api_user + +from flask import current_app + +from sqlalchemy import text + +import re + +def get_private_message_list(auth, data): + page = int(data['page']) if data and 'page' in data else 1 + limit = int(data['limit']) if data and 'limit' in data else 10 + unread_only = data['unread_only'] if data and 'unread_only' in data else True + + user_id = authorise_api_user(auth) + + read = not unread_only + + unread_urls = db.session.execute(text("select url from notification where user_id = :user_id and read = false and url ilike '%#message_" + for url in unread_urls: + match = re.search(pattern, url) + if match: + unread_ids.append(match.group(1)) + + private_messages = ChatMessage.query.filter(ChatMessage.recipient_id == user_id, ChatMessage.id.in_(unread_ids)).join(Conversation, Conversation.id == ChatMessage.conversation_id).filter_by(read=read) + pm_list = [] + for private_message in private_messages: + ap_id = 'https://' + current_app.config['SERVER_NAME'] + '/chat/' + str(private_message.conversation_id) + '#message_' + pm_list.append(private_message_view(private_message, user_id, ap_id)) + + pm_json = { + "private_messages": pm_list + } + return pm_json diff --git a/app/api/alpha/utils/user.py b/app/api/alpha/utils/user.py index fc429f42..60776b74 100644 --- a/app/api/alpha/utils/user.py +++ b/app/api/alpha/utils/user.py @@ -4,7 +4,7 @@ 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.models import PostReply, User +from app.models import Conversation, ChatMessage, Notification, PostReply, User from app.shared.user import block_another_user, unblock_another_user from sqlalchemy import text, desc @@ -96,6 +96,8 @@ def get_user_unread_count(auth): unread_messages = db.session.execute(text("SELECT * from chat_message AS cm INNER JOIN conversation c ON cm.conversation_id =c.id WHERE c.read = false AND cm.recipient_id = :user_id"), {'user_id': user_id}).scalar() if not unread_messages: unread_messages = 0 + if unread_notifications == 0: + unread_messages = 0 unread_count = { "replies": unread_notifications - unread_messages, @@ -132,3 +134,19 @@ def get_user_replies(auth, data): return list_json +def post_user_mark_all_as_read(auth): + user_id = authorise_api_user(auth) + + notifications = Notification.query.filter_by(user_id=user_id, read=False) + for notification in notifications: + notification.read = True + + conversations = Conversation.query.filter_by(read=False).join(ChatMessage, ChatMessage.conversation_id == Conversation.id).filter_by(recipient_id=user_id) + for conversation in conversations: + conversation.read = True + + db.session.commit() + + return {'replies': []} + + diff --git a/app/api/alpha/views.py b/app/api/alpha/views.py index 7ddb73c6..31bf81e9 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, Instance, Post, PostReply, PostVote, User +from app.models import ChatMessage, Community, CommunityMember, Instance, Post, PostReply, PostVote, User from app.utils import blocked_communities from sqlalchemy import text @@ -471,6 +471,30 @@ def instance_view(instance: Instance | int, variant): return v1 +def private_message_view(cm: ChatMessage, user_id, ap_id): + creator = user_view(cm.sender_id, variant=1) + recipient = user_view(cm.recipient_id, variant=1) + is_local = creator['instance_id'] == 1 + + v1 = { + 'private_message': { + 'id': cm.id, + 'creator_id': cm.sender_id, + 'recipient_id': user_id, + 'content': cm.body, + 'deleted': False, + 'read': cm.read, + 'published': cm.created_at.isoformat() + 'Z', + 'ap_id': ap_id, + 'local': is_local + }, + 'creator': creator, + 'recipient': recipient + } + + 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()