using MongoDB.Entities; using PrivaPub.Domain.Statuses; using PrivaPub.Federation.Actors; using PrivaPub.Federation.Objects; using PrivaPub.Federation.Outbox; using PrivaPub.Federation.Rendering; using PrivaPub.Models.Federation; using PrivaPub.Models.Group; using PrivaPub.Models.Post; using PrivaPub.Models.Social; using PrivaPub.Models.User; using PrivaPub.StaticServices; using System.Text.Json.Nodes; using static PrivaPub.Federation.Inbox.ForeignMembers; using static PrivaPub.Federation.Objects.ActivityJson; using PostEntity = PrivaPub.Models.Post.Post; namespace PrivaPub.Federation.Inbox.Handlers { public class DeleteHandler : IActivityHandler { readonly DbEntities _dbEntities; readonly ILocalActorService _localActors; readonly IRemoteActorService _remoteActors; readonly IDeliveryService _delivery; readonly IGroupDistributor _groups; readonly IQuoteService _quotes; public DeleteHandler(DbEntities dbEntities, ILocalActorService localActors, IRemoteActorService remoteActors, IDeliveryService delivery, IGroupDistributor groups, IQuoteService quotes) { _quotes = quotes; _groups = groups; _dbEntities = dbEntities; _localActors = localActors; _remoteActors = remoteActors; _delivery = delivery; } public string Type => "Delete"; const int MaxObjects = 50; public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token) { // Funkwhale deletes several uploads at once, naming them in one list as the object's id var objectUris = activity["object"] is JsonObject { } inner && inner["id"] is JsonArray ids ? ids.Select(Id).Where(id => id != default).Distinct(StringComparer.Ordinal).Take(MaxObjects).ToList() : new List { Id(activity["object"]) }; foreach (var objectUri in objectUris) await Handle(activity, actor, objectUri, token); } async Task Handle(JsonNode activity, ForeignAvatar actor, string objectUri, CancellationToken token) { if (!Origin.Same(objectUri, actor.ActorURI)) { Arrival.Drop("cross-origin"); return; } if (objectUri == actor.ActorURI) { Arrival.Accept("actor-delete"); Arrival.About(actor.AvatarType.ToString(), created: actor.Published); actor.DeletionAt = DateTime.UtcNow; actor.AccountState = AvatarAccountState.Deleted; await DB.Default.SaveAsync(actor, token); var followers = await _dbEntities.Followers.Match(f => f.ActorURI == actor.ActorURI).ExecuteAsync(token); foreach (var follower in followers) { await DB.Default.DeleteAsync(follower.ID); if (follower.LocalActorKind == LocalActorKind.Group) await RemoveForeignMember(follower.LocalActorId, actor.ActorURI, token); } await DB.Default.DeleteAsync(f => f.TargetActorURI == actor.ActorURI); await DB.Default.DeleteAsync(e => e.AuthorAccountId == actor.ID); await DB.Default.Update().Match(p => p.ActorURI == actor.ActorURI).Modify(p => p.AuthorGone, true).ExecuteAsync(token); await GoneActors.Forget(actor, token); return; } await RemoteDeletes.Tombstone(objectUri, token); var post = await _dbEntities.Posts.Match(p => p.ObjectURI == objectUri && p.ActorURI == actor.ActorURI).ExecuteFirstAsync(token); // deleted by another account of its author's server (Mobilizon's organiser deletes the group's event): once // that server says it is gone if (post == default && await _dbEntities.Posts.Match(p => p.ObjectURI == objectUri).ExecuteFirstAsync(token) is { } held && held.ActorURI != actor.ActorURI && Origin.Same(held.ActorURI, actor.ActorURI) && await _remoteActors.IsGone(objectUri, token)) post = held; if (post == default) { Arrival.Accept("tombstone-only"); await _quotes.Revoke(objectUri, token); return; } Arrival.Accept("removed"); Arrival.About(post.ObjectType, post.Visibility, post.CreationDate); await ReplyRelay.Pass(post, _dbEntities, _localActors, _delivery, token); await RemoteDeletes.Remove(post, objectUri, token); if (!string.IsNullOrEmpty(post.GroupId) && await _localActors.FindById(LocalActorKind.Group, post.GroupId, token) is { IsCircle: false } community) await _groups.Announce(community, activity.AsObject(), post.ObjectURI, isNewPost: false, token); } } }