using MongoDB.Entities; using OpenIddict.Abstractions; using PrivaPub.Domain.Media; using PrivaPub.Domain.Timelines; using PrivaPub.Federation.Actors; using PrivaPub.Federation.Outbox; using PrivaPub.Federation.Rendering; using PrivaPub.Models.Federation; using PrivaPub.Models.Social; using PrivaPub.Models.User; using PrivaPub.StaticServices; using System.Text.Json.Nodes; using GroupEntity = PrivaPub.Models.Group.Group; using PostEntity = PrivaPub.Models.Post.Post; namespace PrivaPub.Services { public interface IPersonaRemoval { /// Deletes one persona, and every group it owns: false when there is no such persona, or it is gone. Task Remove(string avatarId, CancellationToken token); } public interface IGroupRemoval { /// Deletes one group: false when there is no such group, or it is gone. Task Remove(string groupId, CancellationToken token); } // What deleting an actor tells: Delete{Actor} to its followers' (shared) inboxes, and to the inboxes of the other accounts // named (those a persona follows, a group's members elsewhere). A suspended persona sends it too: it is the one thing a // suspension lets through (DeliveryService.HeldBack). public static class ActorDeletion { public static async Task Announce(LocalActor actor, IEnumerable others, DbEntities dbEntities, IDeliveryService delivery, CancellationToken token) { var inboxes = (await delivery.FollowerInboxes(actor, token)).ToList(); var named = others.Where(uri => !string.IsNullOrEmpty(uri)).Distinct().ToList(); if (named.Count > 0) inboxes.AddRange((await dbEntities.ForeignAvatars.Match(a => named.Contains(a.ActorURI)).ExecuteAsync(token)) .Select(a => string.IsNullOrEmpty(a.SharedInboxURL) ? a.InboxURL : a.SharedInboxURL)); await delivery.Enqueue(actor, inboxes.Where(i => !string.IsNullOrEmpty(i)).Distinct(), new JsonObject { ["@context"] = ActivityPubRenderer.Context(), ["id"] = actor.ActivityUri("delete-actor"), ["type"] = "Delete", ["actor"] = actor.Uri, ["object"] = actor.Uri, ["to"] = new JsonArray(ActivityPubRenderer.Public), ["cc"] = new JsonArray(actor.Followers) }, token); } } // Deleting a persona (completion program R6: by its own login with its password, by an administrator with theirs, and // for each persona of a deleted login through RootRemoval) deletes everything public it had, as a deleted login's // personas always were: its Mastodon tokens end first, each group it owns goes (GroupRemoval), a Delete of the actor goes // to its followers and to the accounts it follows, its posts are emptied, and its name stays reserved while its documents // answer 410. What it kept for itself (lists, filters, followed hashtags, scheduled posts, what it did with the // announcements, the feeds it read, its featured hashtags, endorsements, notes and dismissed suggestions, its media) // goes with it. Its link to its login stays, so the login keeps no other // trace than that. public class PersonaRemoval : IPersonaRemoval { readonly DbEntities _dbEntities; readonly ILocalActorService _localActors; readonly IDeliveryService _delivery; readonly IMediaService _media; readonly IGroupRemoval _groups; readonly IOpenIddictTokenManager _tokens; readonly IOpenIddictAuthorizationManager _authorizations; public PersonaRemoval(DbEntities dbEntities, ILocalActorService localActors, IDeliveryService delivery, IMediaService media, IGroupRemoval groups, IOpenIddictTokenManager tokens, IOpenIddictAuthorizationManager authorizations) { _dbEntities = dbEntities; _localActors = localActors; _delivery = delivery; _media = media; _groups = groups; _tokens = tokens; _authorizations = authorizations; } public async Task Remove(string avatarId, CancellationToken token) { var avatar = string.IsNullOrEmpty(avatarId) ? default : await _dbEntities.Avatars.MatchID(avatarId).Match(a => !a.DeletionAt.HasValue).ExecuteFirstAsync(token); if (avatar == default) return false; var now = DateTime.UtcNow; // nothing acts as it while it goes await _tokens.RevokeBySubjectAsync(avatar.ID, token); await _authorizations.RevokeBySubjectAsync(avatar.ID, token); Streams.CloseFor(avatar.ID); foreach (var group in await _dbEntities.Groups.Match(g => g.OwnerAvatarId == avatar.ID && !g.DeletionAt.HasValue).ExecuteAsync(token)) await _groups.Remove(group.ID, token); var persona = _localActors.FromAvatar(avatar); var followed = (await _dbEntities.Followings.Match(f => f.AvatarId == avatar.ID && !f.TargetIsLocal).ExecuteAsync(token)) .Select(f => f.TargetActorURI); await ActorDeletion.Announce(persona, followed, _dbEntities, _delivery, token); await DB.Default.Update().MatchID(avatar.ID).Modify(a => a.DeletionAt, now).ExecuteAsync(token); await Empty(avatar.ID, now, token); await DB.Default.DeleteAsync(m => m.AvatarId == avatar.ID); await DB.Default.DeleteAsync(l => l.AvatarId == avatar.ID); await DB.Default.DeleteAsync(f => f.AvatarId == avatar.ID); await DB.Default.DeleteAsync(t => t.AvatarId == avatar.ID); // what it did with the server's announcements, kept for it alone await DB.Default.DeleteAsync(r => r.AvatarId == avatar.ID); await DB.Default.DeleteAsync(r => r.AvatarId == avatar.ID); // (the reader of feeds stops following what nobody reads any more at its next round) await DB.Default.DeleteAsync(s => s.AvatarId == avatar.ID); // what the Mastodon API kept for it (R8): its featured hashtags (the Delete of the actor takes them from other // servers), its endorsements, its private notes and its dismissed suggestions, and what other personas here kept // about it: their endorsements of it and their notes on it await DB.Default.DeleteAsync(t => t.AvatarId == avatar.ID); await DB.Default.DeleteAsync(e => e.AvatarId == avatar.ID || e.TargetAccountId == avatar.ID); await DB.Default.DeleteAsync(n => n.AvatarId == avatar.ID || n.TargetAccountId == avatar.ID); await DB.Default.DeleteAsync(d => d.AvatarId == avatar.ID || d.AccountId == avatar.ID); // nothing of it publishes later, and none of its files stays served: its posts' media, its pictures and what its // scheduled posts held await DB.Default.DeleteAsync(s => s.AvatarId == avatar.ID); await _media.Trash(m => m.OwnerAvatarId == avatar.ID, "persona removed", token); return true; } // a persona's posts keep their ids and lose their content, like a single deleted post; a group writes none of its own static async Task Empty(string avatarId, DateTime now, CancellationToken token) { var postIds = (await DB.Default.Find().Match(p => p.GroupUserId == avatarId && !p.IsFederatedCopy && !p.DeletedAt.HasValue) .Project(p => p.Include(x => x.ID)).ExecuteAsync(token)).Select(p => p.ID).ToList(); if (postIds.Count == 0) return; await DB.Default.Update().Match(p => postIds.Contains(p.ID)) .Modify(p => p.DeletedAt, now) .Modify(p => p.Text, null) .Modify(p => p.ContentHtml, null) .Modify(p => p.Title, null) .Modify(p => p.SpoilerText, null) .Modify(p => p.Media, new List()) .Modify(p => p.Revisions, new List()) .ExecuteAsync(token); await DB.Default.DeleteAsync(e => postIds.Contains(e.PostId) || postIds.Contains(e.ReblogOfPostId)); } } // Deleting a group (completion program R6: by its owner with the login's password, by an administrator with theirs, and // with the persona that owns it): a Delete of the group's actor to its followers and its members elsewhere, its name kept // reserved while its documents answer 410, and the personas here that followed or joined it no longer do. The posts made // in it stay their authors'. public class GroupRemoval : IGroupRemoval { readonly DbEntities _dbEntities; readonly ILocalActorService _localActors; readonly IDeliveryService _delivery; public GroupRemoval(DbEntities dbEntities, ILocalActorService localActors, IDeliveryService delivery) { _dbEntities = dbEntities; _localActors = localActors; _delivery = delivery; } public async Task Remove(string groupId, CancellationToken token) { var group = string.IsNullOrEmpty(groupId) ? default : await _dbEntities.Groups.MatchID(groupId).Match(g => !g.DeletionAt.HasValue).ExecuteFirstAsync(token); if (group == default) return false; var actor = _localActors.FromGroup(group); await ActorDeletion.Announce(actor, group.Members.Where(m => m.IsForeign).Select(m => m.AvatarId), _dbEntities, _delivery, token); await DB.Default.Update().MatchID(group.ID).Modify(g => g.DeletionAt, DateTime.UtcNow).ExecuteAsync(token); await DB.Default.DeleteAsync(f => f.TargetActorURI == actor.Uri); await DB.Default.DeleteAsync(f => f.LocalActorId == group.ID && f.LocalActorKind == LocalActorKind.Group); return true; } } }