From eb7af155ee15a5a629dd935c3260b15725fd495b Mon Sep 17 00:00:00 2001 From: freamon Date: Sat, 18 Jan 2025 17:56:09 +0000 Subject: [PATCH] API: post reports (creation) --- app/api/alpha/routes.py | 14 +++++++- app/api/alpha/utils/__init__.py | 2 +- app/api/alpha/utils/post.py | 21 ++++++++++-- app/api/alpha/views.py | 41 ++++++++++++++++++++++ app/shared/post.py | 60 ++++++++++++++++++++++++++++++++- app/shared/tasks/__init__.py | 5 +-- app/shared/tasks/flags.py | 8 ++++- 7 files changed, 143 insertions(+), 8 deletions(-) diff --git a/app/api/alpha/routes.py b/app/api/alpha/routes.py index 20c5be37..b6dcc8a2 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, post_site_block, \ get_search, \ - get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe, post_post, put_post, post_post_delete, \ + get_post_list, get_post, post_post_like, put_post_save, put_post_subscribe, post_post, put_post, post_post_delete, post_post_report, \ 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 @@ -195,6 +195,18 @@ def post_alpha_post_delete(): return jsonify({"error": str(ex)}), 400 +@bp.route('/api/alpha/post/report', methods=['POST']) +def post_alpha_post_report(): + if not enable_api(): + return jsonify({'error': 'alpha api is not enabled'}) + try: + auth = request.headers.get('Authorization') + data = request.get_json(force=True) or {} + return jsonify(post_post_report(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(): diff --git a/app/api/alpha/utils/__init__.py b/app/api/alpha/utils/__init__.py index 79428210..7f9d1d21 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, 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, post_post, put_post, post_post_delete +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 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/post.py b/app/api/alpha/utils/post.py index 3da7dc21..32deb0ce 100644 --- a/app/api/alpha/utils/post.py +++ b/app/api/alpha/utils/post.py @@ -1,9 +1,9 @@ from app import cache -from app.api.alpha.views import post_view +from app.api.alpha.views import post_view, post_report_view from app.api.alpha.utils.validators import required, integer_expected, boolean_expected, string_expected from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_VIDEO 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, make_post, edit_post, delete_post, restore_post +from app.shared.post import vote_for_post, bookmark_the_post, remove_the_bookmark_from_post, toggle_post_notification, make_post, edit_post, delete_post, restore_post, report_post from app.utils import authorise_api_user, blocked_users, blocked_communities, blocked_instances, community_ids_from_instances, is_image_url, is_video_url from datetime import timedelta @@ -222,3 +222,20 @@ def post_post_delete(auth, data): post_json = post_view(post=post, variant=4, user_id=user_id) return post_json + + + +def post_post_report(auth, data): + required(['post_id', 'reason'], data) + integer_expected(['post_id'], data) + string_expected(['reason'], data) + + post_id = data['post_id'] + reason = data['reason'] + input = {'reason': reason, 'description': '', 'report_remote': True} + + user_id, report = report_post(post_id, input, SRC_API, auth) + + post_json = post_report_view(report=report, post_id=post_id, user_id=user_id) + return post_json + diff --git a/app/api/alpha/views.py b/app/api/alpha/views.py index 3270ec94..ce2d301d 100644 --- a/app/api/alpha/views.py +++ b/app/api/alpha/views.py @@ -373,6 +373,47 @@ def reply_report_view(report, reply_id, user_id): return v1 +def post_report_view(report, post_id, user_id): + # views/post_report_view.dart - /post/report api endpoint + post_json = post_view(post=post_id, variant=2, user_id=user_id) + community_json = community_view(community=post_json['post']['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 = { + 'post_report_view': { + 'post_report': { + 'id': report.id, + 'creator_id': report.reporter_id, + 'post_id': report.suspect_post_id, + 'original_post_name': post_json['post']['title'], + 'original_post_body': '', + 'reason': report.reasons, + 'resolved': report.status == 3, + 'published': report.created_at.isoformat() + 'Z' + }, + 'post': post_json['post'], + 'community': community_json, + 'creator': user_view(user=user_id, variant=1, stub=True), + 'post_creator': user_view(user=report.suspect_user_id, variant=1, stub=True), + 'counts': post_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': post_json['subscribed'], + 'saved': post_json['saved'] + } + } + return v1 + + def search_view(type): v1 = { 'type_': type, diff --git a/app/shared/post.py b/app/shared/post.py index 08e5109e..f330a4c5 100644 --- a/app/shared/post.py +++ b/app/shared/post.py @@ -2,7 +2,7 @@ from app import db, cache from app.activitypub.util import make_image_sizes from app.constants import * from app.community.util import tags_from_string_old, end_poll_date -from app.models import File, Language, NotificationSubscription, Poll, PollChoice, Post, PostBookmark, PostVote, utcnow +from app.models import File, Language, Notification, NotificationSubscription, Poll, PollChoice, Post, PostBookmark, PostVote, Report, Site, User, utcnow from app.shared.tasks import task_selector from app.utils import render_template, authorise_api_user, shorten_string, gibberish, ensure_directory_exists, \ piefed_markdown_to_lemmy_markdown, markdown_to_html, remove_tracking_from_link, domain_from_url, \ @@ -473,3 +473,61 @@ def restore_post(post_id, src, auth): return user_id, post else: return + + +def report_post(post_id, input, src, auth=None): + if src == SRC_API: + post = Post.query.filter_by(id=post_id).one() + user_id = authorise_api_user(auth) + reason = input['reason'] + description = input['description'] + report_remote = input['report_remote'] + else: + post = Post.query.get_or_404(post_id) + user_id = current_user.id + reason = input.reasons_to_string(input.reasons.data) + description = input.description.data + report_remote = input.report_remote.data + + if post.reports == -1: # When a mod decides to ignore future reports, post.reports is set to -1 + if src == SRC_API: + raise Exception('already_reported') + else: + flash(_('Post has already been reported, thank you!')) + return + + report = Report(reasons=reason, description=description, type=1, reporter_id=user_id, suspect_post_id=post.id, suspect_community_id=post.community_id, + suspect_user_id=post.user_id, in_community_id=post.community_id, source_instance_id=1) + db.session.add(report) + + # Notify moderators + already_notified = set() + for mod in post.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/{post.id}", + author_id=user_id) + db.session.add(notification) + already_notified.add(mod.user_id) + post.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 post.community.is_local() and report_remote: + summary = reason + if description: + summary += ' - ' + description + + task_selector('report_post', user_id=user_id, post_id=post_id, summary=summary) + + if src == SRC_API: + return user_id, report + else: + return diff --git a/app/shared/tasks/__init__.py b/app/shared/tasks/__init__.py index 7fc2d3c9..9d5bba5a 100644 --- a/app/shared/tasks/__init__.py +++ b/app/shared/tasks/__init__.py @@ -2,7 +2,7 @@ from app.shared.tasks.follows import join_community, leave_community from app.shared.tasks.likes import vote_for_post, vote_for_reply from app.shared.tasks.notes import make_reply, edit_reply from app.shared.tasks.deletes import delete_reply, restore_reply, delete_post, restore_post -from app.shared.tasks.flags import report_reply +from app.shared.tasks.flags import report_reply, report_post from app.shared.tasks.pages import make_post, edit_post from flask import current_app @@ -22,7 +22,8 @@ def task_selector(task_key, send_async=True, **kwargs): 'make_post': make_post, 'edit_post': edit_post, 'delete_post': delete_post, - 'restore_post': restore_post + 'restore_post': restore_post, + 'report_post': report_post } if current_app.debug: diff --git a/app/shared/tasks/flags.py b/app/shared/tasks/flags.py index f461794f..9981ae99 100644 --- a/app/shared/tasks/flags.py +++ b/app/shared/tasks/flags.py @@ -1,6 +1,6 @@ from app import celery from app.activitypub.signature import default_context, post_request -from app.models import CommunityBan, PostReply, User +from app.models import CommunityBan, Post, PostReply, User from app.utils import gibberish, instance_banned from flask import current_app @@ -27,6 +27,12 @@ def report_reply(send_async, user_id, reply_id, summary): report_object(user_id, reply, summary) +@celery.task +def report_post(send_async, user_id, post_id, summary): + post = Post.query.filter_by(id=post_id).one() + report_object(user_id, post, summary) + + def report_object(user_id, object, summary): user = User.query.filter_by(id=user_id).one() community = object.community