[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:
|
case UserDeleteJobData userDeleteJob:
|
||||||
await ProcessUserDelete(userDeleteJob, scope, token);
|
await ProcessUserDelete(userDeleteJob, scope, token);
|
||||||
break;
|
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 db = scope.GetRequiredService<DatabaseContext>();
|
||||||
var queue = scope.GetRequiredService<QueueService>();
|
var queue = scope.GetRequiredService<QueueService>();
|
||||||
var logger = scope.GetRequiredService<ILogger<BackgroundTaskQueue>>();
|
var logger = scope.GetRequiredService<ILogger<BackgroundTaskQueue>>();
|
||||||
|
var renderer = scope.GetRequiredService<ActivityPub.UserRenderer>();
|
||||||
|
var deliver = scope.GetRequiredService<ActivityPub.ActivityDeliverService>();
|
||||||
var followupTaskSvc = scope.GetRequiredService<FollowupTaskService>();
|
var followupTaskSvc = scope.GetRequiredService<FollowupTaskService>();
|
||||||
|
|
||||||
logger.LogDebug("Processing delete for user {id}", jobData.UserId);
|
logger.LogDebug("Processing delete for user {id}", jobData.UserId);
|
||||||
|
@ -247,6 +252,13 @@ public class BackgroundTaskQueue(int parallelism)
|
||||||
return;
|
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);
|
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);
|
logger.LogDebug("Removing {count} files for user {id}", fileIds.Count, user.Id);
|
||||||
foreach (var id in fileIds)
|
foreach (var id in fileIds)
|
||||||
|
@ -260,6 +272,8 @@ public class BackgroundTaskQueue(int parallelism)
|
||||||
db.Remove(user);
|
db.Remove(user);
|
||||||
await db.SaveChangesAsync(token);
|
await db.SaveChangesAsync(token);
|
||||||
|
|
||||||
|
if (user.IsRemoteUser)
|
||||||
|
{
|
||||||
await followupTaskSvc.ExecuteTask("UpdateInstanceUserCounter", async provider =>
|
await followupTaskSvc.ExecuteTask("UpdateInstanceUserCounter", async provider =>
|
||||||
{
|
{
|
||||||
var bgDb = provider.GetRequiredService<DatabaseContext>();
|
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),
|
.ExecuteUpdateAsync(p => p.SetProperty(i => i.UsersCount, i => i.UsersCount - 1),
|
||||||
token);
|
token);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.LogDebug("User {id} deleted successfully", jobData.UserId);
|
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")]
|
[JsonDerivedType(typeof(DriveFileDeleteJobData), "driveFileDelete")]
|
||||||
|
@ -279,6 +339,7 @@ public class BackgroundTaskQueue(int parallelism)
|
||||||
[JsonDerivedType(typeof(MuteExpiryJobData), "muteExpiry")]
|
[JsonDerivedType(typeof(MuteExpiryJobData), "muteExpiry")]
|
||||||
[JsonDerivedType(typeof(FilterExpiryJobData), "filterExpiry")]
|
[JsonDerivedType(typeof(FilterExpiryJobData), "filterExpiry")]
|
||||||
[JsonDerivedType(typeof(UserDeleteJobData), "userDelete")]
|
[JsonDerivedType(typeof(UserDeleteJobData), "userDelete")]
|
||||||
|
[JsonDerivedType(typeof(UserPurgeJobData), "userPurge")]
|
||||||
public abstract class BackgroundTaskJobData;
|
public abstract class BackgroundTaskJobData;
|
||||||
|
|
||||||
public class DriveFileDeleteJobData : BackgroundTaskJobData
|
public class DriveFileDeleteJobData : BackgroundTaskJobData
|
||||||
|
@ -306,3 +367,8 @@ public class UserDeleteJobData : BackgroundTaskJobData
|
||||||
{
|
{
|
||||||
[JR] [J("userId")] public required string UserId { get; set; }
|
[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 });
|
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)
|
public void UpdateOauthTokenMetadata(OauthToken token)
|
||||||
{
|
{
|
||||||
UpdateUserLastActive(token.User);
|
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