using MongoDB.Entities; using PrivaPub.Domain.Privacy; using PrivaPub.Models.Post; using PrivaPub.Models.Social; using PrivaPub.Models.User; using PostEntity = PrivaPub.Models.Post.Post; namespace PrivaPub.Federation.Inbox { // A remote account that deleted itself keeps its posts, hidden (owner decision 2026-10-03), but leaves nothing in our // numbers: its likes, downvotes, reactions and poll votes go, and the counters they had moved come back, as do the // reblog, reply and quote counts its own posts had added. Its notifications go too. ForgottenAt makes it run once. public static class GoneActors { public static async Task Forget(ForeignAvatar actor, CancellationToken token) { if (actor == default || actor.ForgottenAt.HasValue) return; var uri = actor.ActorURI; foreach (var like in await DB.Default.Find().Match(f => f.ActorURI == uri).ExecuteAsync(token)) { await DB.Default.Update().MatchID(like.PostId).Modify(b => b.Inc(p => p.FavouritesCount, -1)).ExecuteAsync(token); await DB.Default.DeleteAsync(like.ID); } foreach (var down in await DB.Default.Find().Match(d => d.ActorURI == uri).ExecuteAsync(token)) { await DB.Default.Update().MatchID(down.PostId).Modify(b => b.Inc(p => p.DownvotesCount, -1)).ExecuteAsync(token); await DB.Default.DeleteAsync(down.ID); } await DB.Default.DeleteAsync(r => r.ActorURI == uri); foreach (var ballot in (await DB.Default.Find().Match(v => v.ActorURI == uri).ExecuteAsync(token)).GroupBy(v => v.PostId)) { var poll = await DB.Default.Find().MatchID(ballot.Key).ExecuteFirstAsync(token); if (poll?.Poll != default) { var update = DB.Default.Update().MatchID(poll.ID); foreach (var vote in ballot.Where(v => v.Choice >= 0 && v.Choice < poll.Poll.Options.Count)) update = update.Modify(b => b.Inc($"{nameof(PostEntity.Poll)}.{nameof(PostPoll.Options)}.{vote.Choice}.{nameof(PollOption.Votes)}", -1)); if (poll.Poll.VotersCount.HasValue) update = update.Modify(b => b.Inc($"{nameof(PostEntity.Poll)}.{nameof(PostPoll.VotersCount)}", -1)); await update.ExecuteAsync(token); } await DB.Default.DeleteAsync(v => v.PostId == ballot.Key && v.ActorURI == uri); } foreach (var post in await DB.Default.Find().Match(p => p.ActorURI == uri && !p.DeletedAt.HasValue).ExecuteAsync(token)) { if (!string.IsNullOrEmpty(post.ReblogOfPostId)) await DB.Default.Update().MatchID(post.ReblogOfPostId).Modify(b => b.Inc(p => p.ReblogsCount, -1)).ExecuteAsync(token); if (!string.IsNullOrEmpty(post.AnsweringToPostId) && Counted.Reply(post)) await DB.Default.Update().MatchID(post.AnsweringToPostId).Modify(b => b.Inc(p => p.RepliesCount, -1)).ExecuteAsync(token); if (!string.IsNullOrEmpty(post.QuotedPostId) && post.QuoteState == QuoteState.Accepted) await DB.Default.Update().MatchID(post.QuotedPostId).Modify(b => b.Inc(p => p.QuotesCount, -1)).ExecuteAsync(token); } await DB.Default.DeleteAsync(n => n.FromActorURI == uri); await DB.Default.Update().MatchID(actor.ID).Modify(a => a.ForgottenAt, DateTime.UtcNow).ExecuteAsync(token); } } }