[backend/api] Add moderation endpoints (ISH-116)

This commit is contained in:
Laura Hausmann 2024-10-09 19:11:54 +02:00
parent 3274259f12
commit 5f5a0c5c0f
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
3 changed files with 165 additions and 8 deletions

View file

@ -0,0 +1,72 @@
using System.Net;
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.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Iceshrimp.Backend.Controllers.Web;
[Authenticate]
[Authorize("role:moderator")]
[ApiController]
[Route("/api/iceshrimp/moderation")]
public class ModerationController(DatabaseContext db, NoteService noteSvc, UserService userSvc) : 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");
await noteSvc.DeleteNoteAsync(note);
}
[HttpPost("users/{id}/suspend")]
[ProducesResults(HttpStatusCode.OK)]
[ProducesErrors(HttpStatusCode.NotFound)]
public async Task SuspendUser(string id)
{
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id) ??
throw GracefulException.NotFound("User not found");
await userSvc.SuspendUserAsync(user);
}
[HttpPost("users/{id}/unsuspend")]
[ProducesResults(HttpStatusCode.OK)]
[ProducesErrors(HttpStatusCode.NotFound)]
public async Task UnsuspendUser(string id)
{
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id) ??
throw GracefulException.NotFound("User not found");
await userSvc.UnsuspendUserAsync(user);
}
[HttpPost("users/{id}/delete")]
[ProducesResults(HttpStatusCode.OK)]
[ProducesErrors(HttpStatusCode.NotFound)]
public async Task DeleteUser(string id)
{
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id) ??
throw GracefulException.NotFound("User not found");
await userSvc.DeleteUserAsync(user);
}
[HttpPost("users/{id}/purge")]
[ProducesResults(HttpStatusCode.OK)]
[ProducesErrors(HttpStatusCode.NotFound)]
public async Task PurgeUser(string id)
{
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id) ??
throw GracefulException.NotFound("User not found");
await userSvc.PurgeUserAsync(user);
}
}

View file

@ -43,6 +43,9 @@ public class BackgroundTaskQueue(int parallelism)
case UserDeleteJobData userDeleteJob:
await ProcessUserDelete(userDeleteJob, scope, token);
break;
case UserPurgeJobData userPurgeJob:
await ProcessUserPurge(userPurgeJob, scope, token);
break;
}
}
@ -236,6 +239,8 @@ public class BackgroundTaskQueue(int parallelism)
var db = scope.GetRequiredService<DatabaseContext>();
var queue = scope.GetRequiredService<QueueService>();
var logger = scope.GetRequiredService<ILogger<BackgroundTaskQueue>>();
var renderer = scope.GetRequiredService<ActivityPub.UserRenderer>();
var deliver = scope.GetRequiredService<ActivityPub.ActivityDeliverService>();
var followupTaskSvc = scope.GetRequiredService<FollowupTaskService>();
logger.LogDebug("Processing delete for user {id}", jobData.UserId);
@ -247,6 +252,13 @@ public class BackgroundTaskQueue(int parallelism)
return;
}
if (user.IsLocalUser)
{
var actor = renderer.RenderLite(user);
var activity = ActivityPub.ActivityRenderer.RenderDelete(actor, actor);
await deliver.DeliverToFollowersAsync(activity, user, []);
}
var fileIds = await db.DriveFiles.Where(p => p.User == user).Select(p => p.Id).ToListAsync(token);
logger.LogDebug("Removing {count} files for user {id}", fileIds.Count, user.Id);
foreach (var id in fileIds)
@ -260,6 +272,8 @@ public class BackgroundTaskQueue(int parallelism)
db.Remove(user);
await db.SaveChangesAsync(token);
if (user.IsRemoteUser)
{
await followupTaskSvc.ExecuteTask("UpdateInstanceUserCounter", async provider =>
{
var bgDb = provider.GetRequiredService<DatabaseContext>();
@ -269,9 +283,55 @@ public class BackgroundTaskQueue(int parallelism)
.ExecuteUpdateAsync(p => p.SetProperty(i => i.UsersCount, i => i.UsersCount - 1),
token);
});
}
logger.LogDebug("User {id} deleted successfully", jobData.UserId);
}
private static async Task ProcessUserPurge(
UserPurgeJobData jobData,
IServiceProvider scope,
CancellationToken token
)
{
var db = scope.GetRequiredService<DatabaseContext>();
var queue = scope.GetRequiredService<QueueService>();
var logger = scope.GetRequiredService<ILogger<BackgroundTaskQueue>>();
var noteSvc = scope.GetRequiredService<NoteService>();
logger.LogDebug("Processing purge for user {id}", jobData.UserId);
var user = await db.Users.FirstOrDefaultAsync(p => p.Id == jobData.UserId, token);
if (user == null)
{
logger.LogDebug("Failed to purge user {id}: id not found in database", jobData.UserId);
return;
}
var fileIdQ = db.DriveFiles.Where(p => p.User == user).Select(p => p.Id);
var fileIdCnt = fileIdQ.CountAsync(token);
var fileIds = fileIdQ.AsChunkedAsyncEnumerable(50, p => p);
logger.LogDebug("Removing {count} files for user {id}", fileIdCnt, user.Id);
await foreach (var id in fileIds)
{
await queue.BackgroundTaskQueue.EnqueueAsync(new DriveFileDeleteJobData
{
DriveFileId = id, Expire = false
});
}
var noteQ = db.Notes.Where(p => p.User == user).Select(p => p.Id);
var noteCnt = noteQ.CountAsync(token);
var noteIds = noteQ.AsChunkedAsyncEnumerable(50, p => p);
logger.LogDebug("Removing {count} notes for user {id}", noteCnt, user.Id);
await foreach (var id in noteIds)
{
var note = await db.Notes.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id, cancellationToken: token);
if (note != null) await noteSvc.DeleteNoteAsync(note);
}
logger.LogDebug("User {id} purged successfully", jobData.UserId);
}
}
[JsonDerivedType(typeof(DriveFileDeleteJobData), "driveFileDelete")]
@ -279,6 +339,7 @@ public class BackgroundTaskQueue(int parallelism)
[JsonDerivedType(typeof(MuteExpiryJobData), "muteExpiry")]
[JsonDerivedType(typeof(FilterExpiryJobData), "filterExpiry")]
[JsonDerivedType(typeof(UserDeleteJobData), "userDelete")]
[JsonDerivedType(typeof(UserPurgeJobData), "userPurge")]
public abstract class BackgroundTaskJobData;
public class DriveFileDeleteJobData : BackgroundTaskJobData
@ -306,3 +367,8 @@ public class UserDeleteJobData : BackgroundTaskJobData
{
[JR] [J("userId")] public required string UserId { get; set; }
}
public class UserPurgeJobData : BackgroundTaskJobData
{
[JR] [J("userId")] public required string UserId { get; set; }
}

View file

@ -517,6 +517,11 @@ public class UserService(
await queueSvc.BackgroundTaskQueue.EnqueueAsync(new UserDeleteJobData { UserId = user.Id });
}
public async Task PurgeUserAsync(User user)
{
await queueSvc.BackgroundTaskQueue.EnqueueAsync(new UserPurgeJobData { UserId = user.Id });
}
public void UpdateOauthTokenMetadata(OauthToken token)
{
UpdateUserLastActive(token.User);
@ -1345,4 +1350,18 @@ public class UserService(
}
}
}
public async Task SuspendUserAsync(User user)
{
if (user.IsSuspended) return;
user.IsSuspended = true;
await db.SaveChangesAsync();
}
public async Task UnsuspendUserAsync(User user)
{
if (!user.IsSuspended) return;
user.IsSuspended = false;
await db.SaveChangesAsync();
}
}