using MongoDB.Entities; using PrivaPub.Models.User; using PrivaPub.StaticServices; namespace PrivaPub.Services { public interface IRootRemoval { Task Remove(string rootId, CancellationToken token); } // Deleting a root deletes everything public it had (owner decision 2026-10-04): each of its personas goes as a persona // is deleted (PersonaRemoval: each group it owns, a Delete of the actor to everyone who follows it and everyone it // follows, its posts emptied, its name kept reserved and its documents answering 410). The root's sessions end first, so // nothing acts as it while it goes, and it keeps only a deleted-{id} name. public class RootRemoval : IRootRemoval { readonly DbEntities _dbEntities; readonly IRootSessions _sessions; readonly IPersonaRemoval _personas; public RootRemoval(DbEntities dbEntities, IRootSessions sessions, IPersonaRemoval personas) { _dbEntities = dbEntities; _sessions = sessions; _personas = personas; } public async Task Remove(string rootId, CancellationToken token) { var root = await _dbEntities.RootUsers.MatchID(rootId).Match(u => u.DeletedAt == null).ExecuteFirstAsync(token); if (root == default) return false; await _sessions.Revoke(root.ID, token); var now = DateTime.UtcNow; var avatarIds = (await _dbEntities.RootToAvatars.Match(ra => ra.RootId == root.ID).ExecuteAsync(token)).Select(ra => ra.AvatarId).ToList(); foreach (var avatarId in avatarIds) await _personas.Remove(avatarId, token); await DB.Default.Update().MatchID(root.ID) .Modify(u => u.UserName, $"deleted-{root.ID}")//unique, so a second deletion never collides with the first .Modify(u => u.Email, null) .Modify(u => u.HashedPassword, null) .Modify(u => u.Policies, new List()) .Modify(u => u.IsBanned, false) .Modify(u => u.IsEmailValidated, false) .Modify(u => u.DeletedAt, now) .ExecuteAsync(token); await DB.Default.DeleteAsync(r => r.RootUserId == root.ID); await Domain.Moderation.Restrictions.Reload(CancellationToken.None); return true; } } }