[backend/api] Add moderation endpoints (ISH-116)
This commit is contained in:
parent
3274259f12
commit
5f5a0c5c0f
3 changed files with 165 additions and 8 deletions
72
Iceshrimp.Backend/Controllers/Web/ModerationController.cs
Normal file
72
Iceshrimp.Backend/Controllers/Web/ModerationController.cs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue