[backend/api] Add report endpoints (ISH-116)
This commit is contained in:
parent
afaea61fa0
commit
7594c652f5
6 changed files with 133 additions and 13 deletions
|
@ -1,9 +1,12 @@
|
|||
using System.Net;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Schemas;
|
||||
using Iceshrimp.Backend.Controllers.Web.Renderers;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
using Iceshrimp.Shared.Schemas.Web;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
|
@ -13,15 +16,20 @@ namespace Iceshrimp.Backend.Controllers.Web;
|
|||
[Authorize("role:moderator")]
|
||||
[ApiController]
|
||||
[Route("/api/iceshrimp/moderation")]
|
||||
public class ModerationController(DatabaseContext db, NoteService noteSvc, UserService userSvc) : ControllerBase
|
||||
public class ModerationController(
|
||||
DatabaseContext db,
|
||||
NoteService noteSvc,
|
||||
UserService userSvc,
|
||||
ReportRenderer reportRenderer
|
||||
) : ControllerBase
|
||||
{
|
||||
[HttpPost("notes/{id}/delete")]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task DeleteNote(string id)
|
||||
{
|
||||
var note = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id) ??
|
||||
throw GracefulException.NotFound("Note not found");
|
||||
var note = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id)
|
||||
?? throw GracefulException.NotFound("Note not found");
|
||||
|
||||
await noteSvc.DeleteNoteAsync(note);
|
||||
}
|
||||
|
@ -31,8 +39,8 @@ public class ModerationController(DatabaseContext db, NoteService noteSvc, UserS
|
|||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task SuspendUser(string id)
|
||||
{
|
||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser) ??
|
||||
throw GracefulException.NotFound("User not found");
|
||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser)
|
||||
?? throw GracefulException.NotFound("User not found");
|
||||
|
||||
if (user == HttpContext.GetUserOrFail())
|
||||
throw GracefulException.BadRequest("You cannot suspend yourself.");
|
||||
|
@ -45,8 +53,8 @@ public class ModerationController(DatabaseContext db, NoteService noteSvc, UserS
|
|||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task UnsuspendUser(string id)
|
||||
{
|
||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser) ??
|
||||
throw GracefulException.NotFound("User not found");
|
||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser)
|
||||
?? throw GracefulException.NotFound("User not found");
|
||||
|
||||
if (user == HttpContext.GetUserOrFail())
|
||||
throw GracefulException.BadRequest("You cannot unsuspend yourself.");
|
||||
|
@ -59,8 +67,8 @@ public class ModerationController(DatabaseContext db, NoteService noteSvc, UserS
|
|||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task DeleteUser(string id)
|
||||
{
|
||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser) ??
|
||||
throw GracefulException.NotFound("User not found");
|
||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser)
|
||||
?? throw GracefulException.NotFound("User not found");
|
||||
|
||||
if (user == HttpContext.GetUserOrFail())
|
||||
throw GracefulException.BadRequest("You cannot delete yourself.");
|
||||
|
@ -73,9 +81,24 @@ public class ModerationController(DatabaseContext db, NoteService noteSvc, UserS
|
|||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task PurgeUser(string id)
|
||||
{
|
||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser) ??
|
||||
throw GracefulException.NotFound("User not found");
|
||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id && !p.IsSystemUser)
|
||||
?? throw GracefulException.NotFound("User not found");
|
||||
|
||||
await userSvc.PurgeUserAsync(user);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("reports")]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.BadRequest)]
|
||||
[LinkPagination(20, 40)]
|
||||
public async Task<IEnumerable<ReportResponse>> GetReports(PaginationQuery pq, bool resolved = false)
|
||||
{
|
||||
var reports = await db.Reports
|
||||
.IncludeCommonProperties()
|
||||
.Where(p => p.Resolved == resolved)
|
||||
.Paginate(pq, ControllerContext)
|
||||
.ToListAsync();
|
||||
|
||||
return await reportRenderer.RenderManyAsync(reports);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
using Iceshrimp.Shared.Schemas.Web;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Web.Renderers;
|
||||
|
||||
public class ReportRenderer(UserRenderer userRenderer, NoteRenderer noteRenderer) : IScopedService
|
||||
{
|
||||
private static ReportResponse Render(Report report, ReportRendererDto data)
|
||||
{
|
||||
var noteIds = report.Notes.Select(i => i.Id).ToArray();
|
||||
return new ReportResponse
|
||||
{
|
||||
Id = report.Id,
|
||||
CreatedAt = report.CreatedAt,
|
||||
Comment = report.Comment,
|
||||
Forwarded = report.Forwarded,
|
||||
Resolved = report.Resolved,
|
||||
Assignee = data.Users.FirstOrDefault(p => p.Id == report.AssigneeId),
|
||||
TargetUser = data.Users.First(p => p.Id == report.TargetUserId),
|
||||
Reporter = data.Users.First(p => p.Id == report.ReporterId),
|
||||
Notes = data.Notes.Where(p => noteIds.Contains(p.Id)).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ReportResponse> RenderOneAsync(Report report)
|
||||
{
|
||||
return Render(report, await BuildDtoAsync(report));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ReportResponse>> RenderManyAsync(IEnumerable<Report> reports)
|
||||
{
|
||||
var arr = reports.ToArray();
|
||||
var data = await BuildDtoAsync(arr);
|
||||
return arr.Select(p => Render(p, data));
|
||||
}
|
||||
|
||||
private async Task<UserResponse[]> GetUsersAsync(IEnumerable<User> users)
|
||||
=> await userRenderer.RenderManyAsync(users).ToArrayAsync();
|
||||
|
||||
private async Task<NoteResponse[]> GetNotesAsync(IEnumerable<Note> notes)
|
||||
=> await noteRenderer.RenderManyAsync(notes, null).ToArrayAsync();
|
||||
|
||||
private async Task<ReportRendererDto> BuildDtoAsync(params Report[] reports)
|
||||
{
|
||||
var notes = await GetNotesAsync(reports.SelectMany(p => p.Notes));
|
||||
var users = notes.Select(p => p.User).DistinctBy(p => p.Id).ToList();
|
||||
|
||||
var missingUsers = reports.Select(p => p.TargetUser)
|
||||
.Concat(reports.Select(p => p.Assignee))
|
||||
.Concat(reports.Select(p => p.Reporter))
|
||||
.NotNull()
|
||||
.DistinctBy(p => p.Id)
|
||||
.ExceptBy(users.Select(p => p.Id), p => p.Id);
|
||||
|
||||
users.AddRange(await GetUsersAsync(missingUsers));
|
||||
|
||||
return new ReportRendererDto { Users = users.ToArray(), Notes = notes };
|
||||
}
|
||||
|
||||
private class ReportRendererDto
|
||||
{
|
||||
public required UserResponse[] Users;
|
||||
public required NoteResponse[] Notes;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Iceshrimp.EntityFrameworkCore.Extensions;
|
||||
using Iceshrimp.Shared.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
|
@ -13,7 +14,7 @@ namespace Iceshrimp.Backend.Core.Database.Tables;
|
|||
[Index(nameof(TargetUserId))]
|
||||
[Index(nameof(CreatedAt))]
|
||||
[Index(nameof(ReporterHost))]
|
||||
public class Report
|
||||
public class Report : IIdentifiable
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
|
|
|
@ -655,5 +655,14 @@ public static class QueryableExtensions
|
|||
.Include(p => p.Bite);
|
||||
}
|
||||
|
||||
public static IQueryable<Report> IncludeCommonProperties(this IQueryable<Report> query)
|
||||
{
|
||||
return query.Include(p => p.Reporter.UserProfile)
|
||||
.Include(p => p.TargetUser.UserProfile)
|
||||
.Include(p => p.Assignee.UserProfile)
|
||||
.Include(p => p.Notes)
|
||||
.ThenInclude(p => p.User.UserProfile);
|
||||
}
|
||||
|
||||
#pragma warning restore CS8602 // Dereference of a possibly null reference.
|
||||
}
|
|
@ -61,6 +61,11 @@ public static class TaskExtensions
|
|||
return (await task).ToList();
|
||||
}
|
||||
|
||||
public static async Task<T[]> ToArrayAsync<T>(this Task<IEnumerable<T>> task)
|
||||
{
|
||||
return (await task).ToArray();
|
||||
}
|
||||
|
||||
public static async Task ContinueWithResult(this Task task, Action continuation)
|
||||
{
|
||||
await task;
|
||||
|
|
16
Iceshrimp.Shared/Schemas/Web/ReportResponse.cs
Normal file
16
Iceshrimp.Shared/Schemas/Web/ReportResponse.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
using Iceshrimp.Shared.Helpers;
|
||||
|
||||
namespace Iceshrimp.Shared.Schemas.Web;
|
||||
|
||||
public class ReportResponse : IIdentifiable
|
||||
{
|
||||
public required string Id { get; set; }
|
||||
public required DateTime CreatedAt { get; set; }
|
||||
public required UserResponse TargetUser { get; set; }
|
||||
public required UserResponse Reporter { get; set; }
|
||||
public required UserResponse? Assignee { get; set; }
|
||||
public required NoteResponse[] Notes { get; set; }
|
||||
public required bool Resolved { get; set; }
|
||||
public required bool Forwarded { get; set; }
|
||||
public required string Comment { get; set; }
|
||||
}
|
Loading…
Add table
Reference in a new issue