From 94c07d3d0617fa904d7512b55b291a495abafb42 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sat, 15 Mar 2025 02:44:43 +0100 Subject: [PATCH] [backend/masto-client] Add endpoints for filing reports (ISH-749) --- .../Controllers/Mastodon/ReportController.cs | 63 +++++++++++++++++++ .../Mastodon/Schemas/Entities/ReportEntity.cs | 17 +++++ .../Mastodon/Schemas/ReportSchemas.cs | 24 +++++++ .../Core/Services/ReportService.cs | 4 +- 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 Iceshrimp.Backend/Controllers/Mastodon/ReportController.cs create mode 100644 Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/ReportEntity.cs create mode 100644 Iceshrimp.Backend/Controllers/Mastodon/Schemas/ReportSchemas.cs diff --git a/Iceshrimp.Backend/Controllers/Mastodon/ReportController.cs b/Iceshrimp.Backend/Controllers/Mastodon/ReportController.cs new file mode 100644 index 00000000..d7afd923 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Mastodon/ReportController.cs @@ -0,0 +1,63 @@ +using System.Net; +using System.Net.Mime; +using Iceshrimp.Backend.Controllers.Mastodon.Attributes; +using Iceshrimp.Backend.Controllers.Mastodon.Renderers; +using Iceshrimp.Backend.Controllers.Mastodon.Schemas; +using Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; +using Iceshrimp.Backend.Controllers.Shared.Attributes; +using Iceshrimp.Backend.Core.Database; +using Iceshrimp.Backend.Core.Extensions; +using Iceshrimp.Backend.Core.Middleware; +using Iceshrimp.Backend.Core.Services; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; +using Microsoft.EntityFrameworkCore; + +namespace Iceshrimp.Backend.Controllers.Mastodon; + +[Route("/api/v1/reports")] +[Authenticate] +[MastodonApiController] +[EnableCors("mastodon")] +[EnableRateLimiting("sliding")] +[Produces(MediaTypeNames.Application.Json)] +public class ReportController(ReportService reportSvc, DatabaseContext db, UserRenderer userRenderer) : ControllerBase +{ + [Authorize("write:reports")] + [HttpPost] + [ProducesResults(HttpStatusCode.OK)] + [ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)] + public async Task FileReport([FromHybrid] ReportSchemas.FileReportRequest request) + { + var user = HttpContext.GetUserOrFail(); + if (request.Comment.Length > 2048) + throw GracefulException.BadRequest("Comment length must not exceed 2048 characters"); + if (request.AccountId == user.Id) + throw GracefulException.BadRequest("You cannot report yourself"); + + var target = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == request.AccountId) + ?? throw GracefulException.NotFound("Target user not found"); + + var notes = await db.Notes.Where(p => request.StatusIds.Contains(p.Id)).ToListAsync(); + if (notes.Any(p => p.UserId != target.Id)) + throw GracefulException.BadRequest("Note author does not match target user"); + + var report = await reportSvc.CreateReportAsync(user, target, notes, request.Comment); + var targetAccount = await userRenderer.RenderAsync(report.TargetUser, user); + + return new ReportEntity + { + Id = report.Id, + Category = "other", + Comment = report.Comment, + Forwarded = report.Forwarded, + ActionTaken = report.Resolved, + CreatedAt = report.CreatedAt.ToStringIso8601Like(), + TargetAccount = targetAccount, + RuleIds = null, + StatusIds = request.StatusIds, + ActionTakenAt = null + }; + } +} diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/ReportEntity.cs b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/ReportEntity.cs new file mode 100644 index 00000000..e657cbb9 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/ReportEntity.cs @@ -0,0 +1,17 @@ +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; + +namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas.Entities; + +public class ReportEntity +{ + [J("id")] public required string Id { get; set; } + [J("action_taken")] public required bool ActionTaken { get; set; } + [J("action_taken_at")] public string? ActionTakenAt { get; set; } + [J("category")] public required string Category { get; set; } + [J("comment")] public required string Comment { get; set; } + [J("forwarded")] public required bool Forwarded { get; set; } + [J("created_at")] public required string CreatedAt { get; set; } + [J("status_ids")] public string[]? StatusIds { get; set; } + [J("rule_ids")] public string[]? RuleIds { get; set; } + [J("target_account")] public required AccountEntity TargetAccount { get; set; } +} diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Schemas/ReportSchemas.cs b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/ReportSchemas.cs new file mode 100644 index 00000000..8ac239d3 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/Mastodon/Schemas/ReportSchemas.cs @@ -0,0 +1,24 @@ +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; +using JR = System.Text.Json.Serialization.JsonRequiredAttribute; +using B = Microsoft.AspNetCore.Mvc.BindPropertyAttribute; + +namespace Iceshrimp.Backend.Controllers.Mastodon.Schemas; + +public abstract class ReportSchemas +{ + public class FileReportRequest + { + [B(Name = "account_id")] + [J("account_id")] + [JR] + public string AccountId { get; set; } = null!; + + [B(Name = "status_ids")] + [J("status_ids")] + public string[] StatusIds { get; set; } = []; + + [B(Name = "comment")] + [J("comment")] + public string Comment { get; set; } = ""; + } +} diff --git a/Iceshrimp.Backend/Core/Services/ReportService.cs b/Iceshrimp.Backend/Core/Services/ReportService.cs index 5894b4f6..39ceebce 100644 --- a/Iceshrimp.Backend/Core/Services/ReportService.cs +++ b/Iceshrimp.Backend/Core/Services/ReportService.cs @@ -27,7 +27,7 @@ public class ReportService( await deliverSvc.DeliverToAsync(activity, actor, inbox); } - public async Task CreateReportAsync(User reporter, User target, IEnumerable notes, string comment) + public async Task CreateReportAsync(User reporter, User target, IEnumerable notes, string comment) { var report = new Report { @@ -43,5 +43,7 @@ public class ReportService( db.Add(report); await db.SaveChangesAsync(); + await db.ReloadEntityRecursivelyAsync(report); + return report; } }