From 308f29ba383ab0bc5db86a94bb57c643df29a11f Mon Sep 17 00:00:00 2001 From: freamon Date: Sun, 27 Oct 2024 10:20:38 +0000 Subject: [PATCH] API: support /comment/report --- app/api/alpha/routes.py | 15 ++++++- app/api/alpha/utils/__init__.py | 2 +- app/api/alpha/utils/reply.py | 20 +++++++-- app/api/alpha/views.py | 42 ++++++++++++++++++ app/shared/reply.py | 76 ++++++++++++++++++++++++++++++++- 5 files changed, 148 insertions(+), 7 deletions(-) diff --git a/app/api/alpha/routes.py b/app/api/alpha/routes.py index dd4ee498..f26fe531 100644 --- a/app/api/alpha/routes.py +++ b/app/api/alpha/routes.py @@ -2,7 +2,7 @@ from app.api.alpha import bp 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, post_reply, put_reply, post_reply_delete, \ + get_reply_list, post_reply_like, put_reply_save, put_reply_subscribe, post_reply, put_reply, post_reply_delete, post_reply_report, \ get_community_list, get_community, post_community_follow, post_community_block, \ get_user, post_user_block from app.shared.auth import log_user_in @@ -242,6 +242,18 @@ def post_alpha_comment_delete(): return jsonify({"error": str(ex)}), 400 +@bp.route('/api/alpha/comment/report', methods=['POST']) +def post_alpha_comment_report(): + 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_reply_report(auth, data)) + except Exception as ex: + return jsonify({"error": str(ex)}), 400 + + # User @bp.route('/api/alpha/user', methods=['GET']) def get_alpha_user(): @@ -325,7 +337,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/report', methods=['POST']) @bp.route('/api/alpha/comment/report/resolve', methods=['PUT']) @bp.route('/api/alpha/comment/report/list', methods=['GET']) def alpha_reply(): diff --git a/app/api/alpha/utils/__init__.py b/app/api/alpha/utils/__init__.py index d9688437..824050da 100644 --- a/app/api/alpha/utils/__init__.py +++ b/app/api/alpha/utils/__init__.py @@ -1,7 +1,7 @@ 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, post_reply, put_reply, post_reply_delete +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 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 diff --git a/app/api/alpha/utils/reply.py b/app/api/alpha/utils/reply.py index aa457900..fab300a0 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.api.alpha.utils.validators import required, integer_expected, boolean_expected, string_expected -from app.api.alpha.views import reply_view +from app.api.alpha.views import reply_view, reply_report_view from app.models import PostReply, Post from app.shared.reply import vote_for_reply, bookmark_the_post_reply, remove_the_bookmark_from_post_reply, toggle_post_reply_notification, make_reply, edit_reply, \ - delete_reply, restore_reply + delete_reply, restore_reply, report_reply from app.utils import authorise_api_user, blocked_users, blocked_instances from sqlalchemy import desc @@ -182,4 +182,18 @@ def post_reply_delete(auth, data): reply_json = reply_view(reply=reply, variant=4, user_id=user_id) return reply_json - return {} + +def post_reply_report(auth, data): + required(['comment_id', 'reason'], data) + integer_expected(['comment_id'], data) + string_expected(['reason'], data) + + reply_id = data['comment_id'] + reason = data['reason'] + input = {'reason': reason, 'description': '', 'report_remote': True} + + user_id, report = report_reply(reply_id, input, SRC_API, auth) + + reply_json = reply_report_view(report=report, reply_id=reply_id, user_id=user_id) + return reply_json + diff --git a/app/api/alpha/views.py b/app/api/alpha/views.py index ee1cdd7a..57de04fa 100644 --- a/app/api/alpha/views.py +++ b/app/api/alpha/views.py @@ -338,6 +338,48 @@ def reply_view(reply: PostReply | int, variant, user_id=None, my_vote=0): return v4 +def reply_report_view(report, reply_id, user_id): + # views/comment_report_view.dart - /comment/report api endpoint + reply_json = reply_view(reply=reply_id, variant=2, user_id=user_id) + post_json = post_view(post=reply_json['comment']['post_id'], variant=1, stub=True) + community_json = community_view(community=post_json['community_id'], variant=1, stub=True) + + banned = db.session.execute(text('SELECT user_id FROM "community_ban" WHERE user_id = :user_id and community_id = :community_id'), {'user_id': report.reporter_id, 'community_id': community_json['id']}).scalar() + moderator = db.session.execute(text('SELECT is_moderator FROM "community_member" WHERE user_id = :user_id and community_id = :community_id'), {'user_id': report.reporter_id, 'community_id': community_json['id']}).scalar() + admin = db.session.execute(text('SELECT user_id FROM "user_role" WHERE user_id = :user_id and role_id = 4'), {'user_id': report.reporter_id}).scalar() + + creator_banned_from_community = True if banned else False + creator_is_moderator = True if moderator else False + creator_is_admin = True if admin else False + + v1 = { + 'comment_report_view': { + 'comment_report': { + 'id': report.id, + 'creator_id': report.reporter_id, + 'comment_id': report.suspect_post_reply_id, + 'original_comment_text': reply_json['comment']['body'], + 'reason': report.reasons, + 'resolved': report.status == 3, + 'published': report.created_at.isoformat() + 'Z' + }, + 'comment': reply_json['comment'], + 'post': post_json, + 'community': community_json, + 'creator': user_view(user=user_id, variant=1, stub=True), + 'comment_creator': user_view(user=report.suspect_user_id, variant=1, stub=True), + 'counts': reply_json['counts'], + 'creator_banned_from_community': creator_banned_from_community, + 'creator_is_moderator': creator_is_moderator, + 'creator_is_admin': creator_is_admin, + 'creator_blocked': False, + 'subscribed': reply_json['subscribed'], + 'saved': reply_json['saved'] + } + } + return v1 + + def search_view(type): v1 = { 'type_': type, diff --git a/app/shared/reply.py b/app/shared/reply.py index 417f07e3..04f8b324 100644 --- a/app/shared/reply.py +++ b/app/shared/reply.py @@ -2,7 +2,7 @@ from app import cache, db from app.activitypub.signature import default_context, post_request_in_background, post_request from app.community.util import send_to_remote_instance from app.constants import * -from app.models import NotificationSubscription, Post, PostReply, PostReplyBookmark, User, utcnow +from app.models import Instance, Notification, NotificationSubscription, Post, PostReply, PostReplyBookmark, Report, Site, User, utcnow from app.utils import gibberish, instance_banned, render_template, authorise_api_user, recently_upvoted_post_replies, recently_downvoted_post_replies, shorten_string, \ piefed_markdown_to_lemmy_markdown, markdown_to_html, ap_datetime @@ -642,3 +642,77 @@ def restore_reply(reply_id, src, auth): return user.id, reply else: return + + +def report_reply(reply_id, input, src, auth=None): + if src == SRC_API: + reply = PostReply.query.filter_by(id=reply_id).one() + user = authorise_api_user(auth, return_type='model') + reason = input['reason'] + description = input['description'] + report_remote = input['report_remote'] + else: + reply = PostReply.query.get_or_404(reply_id) + user = current_user + reason = input.reasons_to_string(input.reasons.data) + description = input.description.data + report_remote = input.report_remote.data + + if reply.reports == -1: # When a mod decides to ignore future reports, reply.reports is set to -1 + if src == SRC_API: + raise Exception('already_reported') + else: + flash(_('Comment has already been reported, thank you!')) + return + + report = Report(reasons=reason, description=description, type=2, reporter_id=user.id, suspect_post_id=reply.post.id, suspect_community_id=reply.community.id, + suspect_user_id=reply.author.id, suspect_post_reply_id=reply.id, in_community_id=reply.community.id, source_instance_id=1) + db.session.add(report) + + # Notify moderators + already_notified = set() + for mod in reply.community.moderators(): + moderator = User.query.get(mod.user_id) + if moderator and moderator.is_local(): + notification = Notification(user_id=mod.user_id, title=_('A comment has been reported'), + url=f"https://{current_app.config['SERVER_NAME']}/comment/{reply.id}", + author_id=user.id) + db.session.add(notification) + already_notified.add(mod.user_id) + reply.reports += 1 + # todo: only notify admins for certain types of report + for admin in Site.admins(): + if admin.id not in already_notified: + notify = Notification(title='Suspicious content', url='/admin/reports', user_id=admin.id, author_id=user.id) + db.session.add(notify) + admin.unread_notifications += 1 + db.session.commit() + + # federate report to originating instance + if not reply.community.is_local() and report_remote: + summary = reason + if description: + summary += ' - ' + description + report_json = { + 'actor': user.public_url(), + 'audience': reply.community.public_url(), + 'content': None, + 'id': f"https://{current_app.config['SERVER_NAME']}/activities/flag/{gibberish(15)}", + 'object': reply.ap_id, + 'summary': summary, + 'to': [ + reply.community.public_url() + ], + 'type': 'Flag' + } + instance = Instance.query.get(reply.community.instance_id) + if reply.community.ap_inbox_url and not user.has_blocked_instance(instance.id) and not instance_banned(instance.domain): + success = post_request(reply.community.ap_inbox_url, report_json, user.private_key, user.public_url() + '#main-key') + if success is False or isinstance(success, str): + if src == SRC_WEB: + flash('Failed to send report to remote server', 'error') + + if src == SRC_API: + return user.id, report + else: + return