[backend/api] Add support for creating, forwarding & resolving reports (ISH-116)
This commit is contained in:
parent
7594c652f5
commit
f3999f80be
6 changed files with 110 additions and 27 deletions
|
@ -20,7 +20,8 @@ public class ModerationController(
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
NoteService noteSvc,
|
NoteService noteSvc,
|
||||||
UserService userSvc,
|
UserService userSvc,
|
||||||
ReportRenderer reportRenderer
|
ReportRenderer reportRenderer,
|
||||||
|
ReportService reportSvc
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpPost("notes/{id}/delete")]
|
[HttpPost("notes/{id}/delete")]
|
||||||
|
@ -101,4 +102,38 @@ public class ModerationController(
|
||||||
|
|
||||||
return await reportRenderer.RenderManyAsync(reports);
|
return await reportRenderer.RenderManyAsync(reports);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("reports/{id}/resolve")]
|
||||||
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
|
public async Task ResolveReport(string id)
|
||||||
|
{
|
||||||
|
var report = await db.Reports.FirstOrDefaultAsync(p => p.Id == id)
|
||||||
|
?? throw GracefulException.NotFound("Report not found");
|
||||||
|
|
||||||
|
report.Resolved = true;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("reports/{id}/forward")]
|
||||||
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
[ProducesErrors(HttpStatusCode.BadRequest, HttpStatusCode.NotFound)]
|
||||||
|
public async Task ForwardReport(string id, [FromBody] NoteReportRequest? request)
|
||||||
|
{
|
||||||
|
var report = await db.Reports
|
||||||
|
.Include(p => p.TargetUser)
|
||||||
|
.Include(p => p.Notes)
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == id)
|
||||||
|
?? throw GracefulException.NotFound("Report not found");
|
||||||
|
|
||||||
|
if (report.TargetUserHost == null)
|
||||||
|
throw GracefulException.BadRequest("Cannot forward report to local instance");
|
||||||
|
if (report.Forwarded)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await reportSvc.ForwardReportAsync(report, request?.Comment);
|
||||||
|
|
||||||
|
report.Forwarded = true;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,8 @@ public class NoteController(
|
||||||
UserRenderer userRenderer,
|
UserRenderer userRenderer,
|
||||||
CacheService cache,
|
CacheService cache,
|
||||||
BiteService biteSvc,
|
BiteService biteSvc,
|
||||||
PollService pollSvc
|
PollService pollSvc,
|
||||||
|
ReportService reportSvc
|
||||||
) : ControllerBase
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
|
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
|
||||||
|
@ -645,4 +646,18 @@ public class NoteController(
|
||||||
|
|
||||||
return await noteRenderer.RenderOne(note, user);
|
return await noteRenderer.RenderOne(note, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id}/report")]
|
||||||
|
[Authenticate]
|
||||||
|
[Authorize]
|
||||||
|
[ProducesResults(HttpStatusCode.OK)]
|
||||||
|
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||||
|
public async Task ReportNote(string id, [FromBody] NoteReportRequest request)
|
||||||
|
{
|
||||||
|
var user = HttpContext.GetUserOrFail();
|
||||||
|
var note = await db.Notes.Include(p => p.User).EnsureVisibleFor(user).FirstOrDefaultAsync(p => p.Id == id)
|
||||||
|
?? throw GracefulException.NotFound("Note not found");
|
||||||
|
|
||||||
|
await reportSvc.CreateReportAsync(user, note.User, [note], request.Comment);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -28,7 +28,8 @@ public class ActivityHandlerService(
|
||||||
FollowupTaskService followupTaskSvc,
|
FollowupTaskService followupTaskSvc,
|
||||||
EmojiService emojiSvc,
|
EmojiService emojiSvc,
|
||||||
EventService eventSvc,
|
EventService eventSvc,
|
||||||
RelayService relaySvc
|
RelayService relaySvc,
|
||||||
|
ReportService reportSvc
|
||||||
) : IScopedService
|
) : IScopedService
|
||||||
{
|
{
|
||||||
public async Task PerformActivityAsync(ASActivity activity, string? inboxUserId, string? authenticatedUserId)
|
public async Task PerformActivityAsync(ASActivity activity, string? inboxUserId, string? authenticatedUserId)
|
||||||
|
@ -556,20 +557,7 @@ public class ActivityHandlerService(
|
||||||
if (noteMatches.Count != 0 && noteMatches.Any(p => p.User != userMatch))
|
if (noteMatches.Count != 0 && noteMatches.Any(p => p.User != userMatch))
|
||||||
throw GracefulException.UnprocessableEntity("Refusing to process ASFlag: note author mismatch");
|
throw GracefulException.UnprocessableEntity("Refusing to process ASFlag: note author mismatch");
|
||||||
|
|
||||||
var report = new Report
|
await reportSvc.CreateReportAsync(resolvedActor, userMatch, noteMatches, flag.Content ?? "");
|
||||||
{
|
|
||||||
Id = IdHelpers.GenerateSnowflakeId(),
|
|
||||||
CreatedAt = DateTime.UtcNow,
|
|
||||||
TargetUser = userMatch,
|
|
||||||
TargetUserHost = userMatch.Host,
|
|
||||||
Reporter = resolvedActor,
|
|
||||||
ReporterHost = resolvedActor.Host,
|
|
||||||
Notes = noteMatches,
|
|
||||||
Comment = flag.Content ?? ""
|
|
||||||
};
|
|
||||||
|
|
||||||
db.Add(report);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UnfollowAsync(ASActor followeeActor, User follower)
|
private async Task UnfollowAsync(ASActor followeeActor, User follower)
|
||||||
|
|
|
@ -240,19 +240,11 @@ public class ActivityRenderer(
|
||||||
To = userRenderer.RenderLite(fallbackTo)
|
To = userRenderer.RenderLite(fallbackTo)
|
||||||
};
|
};
|
||||||
|
|
||||||
public ASFlag RenderFlag(User actor, IEnumerable<Note> notes, string comment) => new()
|
public ASFlag RenderFlag(User actor, User user, IEnumerable<Note> notes, string comment) => new()
|
||||||
{
|
{
|
||||||
Id = GenerateActivityId(),
|
Id = GenerateActivityId(),
|
||||||
Actor = userRenderer.RenderLite(actor),
|
Actor = userRenderer.RenderLite(actor),
|
||||||
Object = notes.Select(noteRenderer.RenderLite).ToArray<ASObject>(),
|
Object = notes.Select(noteRenderer.RenderLite).Prepend<ASObject>(userRenderer.RenderLite(user)).ToArray(),
|
||||||
Content = comment
|
|
||||||
};
|
|
||||||
|
|
||||||
public ASFlag RenderFlag(User actor, User user, string comment) => new()
|
|
||||||
{
|
|
||||||
Id = GenerateActivityId(),
|
|
||||||
Actor = userRenderer.RenderLite(actor),
|
|
||||||
Object = [userRenderer.RenderLite(user)],
|
|
||||||
Content = comment
|
Content = comment
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
47
Iceshrimp.Backend/Core/Services/ReportService.cs
Normal file
47
Iceshrimp.Backend/Core/Services/ReportService.cs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
using Iceshrimp.Backend.Core.Database;
|
||||||
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Core.Services;
|
||||||
|
|
||||||
|
public class ReportService(
|
||||||
|
ActivityPub.ActivityDeliverService deliverSvc,
|
||||||
|
ActivityPub.ActivityRenderer activityRenderer,
|
||||||
|
SystemUserService sysUserSvc,
|
||||||
|
DatabaseContext db
|
||||||
|
) : IScopedService
|
||||||
|
{
|
||||||
|
public async Task ForwardReportAsync(Report report, string? comment)
|
||||||
|
{
|
||||||
|
if (report.TargetUser.IsLocalUser)
|
||||||
|
throw new Exception("Refusing to forward report to local instance");
|
||||||
|
|
||||||
|
var actor = await sysUserSvc.GetInstanceActorAsync();
|
||||||
|
var activity = activityRenderer.RenderFlag(actor, report.TargetUser, report.Notes, comment ?? report.Comment);
|
||||||
|
|
||||||
|
var inbox = report.TargetUser.SharedInbox
|
||||||
|
?? report.TargetUser.Inbox
|
||||||
|
?? throw new Exception("Target user does not have inbox");
|
||||||
|
|
||||||
|
await deliverSvc.DeliverToAsync(activity, actor, inbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateReportAsync(User reporter, User target, IEnumerable<Note> notes, string comment)
|
||||||
|
{
|
||||||
|
var report = new Report
|
||||||
|
{
|
||||||
|
Id = IdHelpers.GenerateSnowflakeId(),
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
TargetUser = target,
|
||||||
|
TargetUserHost = target.Host,
|
||||||
|
Reporter = reporter,
|
||||||
|
ReporterHost = reporter.Host,
|
||||||
|
Notes = notes.ToArray(),
|
||||||
|
Comment = comment
|
||||||
|
};
|
||||||
|
|
||||||
|
db.Add(report);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
6
Iceshrimp.Shared/Schemas/Web/NoteReportRequest.cs
Normal file
6
Iceshrimp.Shared/Schemas/Web/NoteReportRequest.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Iceshrimp.Shared.Schemas.Web;
|
||||||
|
|
||||||
|
public class NoteReportRequest
|
||||||
|
{
|
||||||
|
public required string Comment { get; set; }
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue