using MongoDB.Entities; using PrivaPub.Domain.Statuses; using PrivaPub.Federation.Actors; using PrivaPub.Federation.Objects; using PrivaPub.Models.Federation; using PrivaPub.Models.Post; using PrivaPub.Models.Social; using PostEntity = PrivaPub.Models.Post.Post; namespace PrivaPub.Federation.Inbox { public static class RemoteEdits { public const int MaxRevisions = 20; //newer than what we hold; or, for a first edit, no older and actually different, because GoToSocial's timestamps are //whole seconds and an edit made in the second the post was published carries updated == published public static bool IsEdit(NoteDocument note, PostEntity post) => note.Updated is { } updated && (updated > (post.EditedAt ?? post.CreationDate) || post.EditedAt == default && updated >= post.CreationDate && Changed(note, post)); static bool Changed(NoteDocument note, PostEntity post) => note.ContentHtml != post.ContentHtml || note.SpoilerText != post.SpoilerText || note.Title != post.Title || note.Sensitive != post.HasContentWarning; public static async Task Apply(PostEntity post, NoteDocument note, string activityId, ILocalActorService localActors, IObjectRecords records, IQuoteService quotes, CancellationToken token) { post.Poll = note.Poll ?? post.Poll; post.QuotePolicy = note.QuotePolicy; post.Video = note.Video ?? post.Video; post.Audio = note.Audio ?? post.Audio; post.Event = note.Event ?? post.Event; if (!IsEdit(note, post)) { await DB.Default.SaveAsync(post, token); await records.Revise(note, activityId, token); await Requote(post, note, quotes, token); return false; } post.Revisions.Add(new PostRevision { Title = post.Title, SpoilerText = post.SpoilerText, ContentHtml = post.ContentHtml, HasContentWarning = post.HasContentWarning, EditedAt = post.EditedAt ?? post.CreationDate }); if (post.Revisions.Count > MaxRevisions) post.Revisions.RemoveRange(0, post.Revisions.Count - MaxRevisions); var mentions = new List(); foreach (var mention in note.Mentions) { var local = await localActors.FindByUri(mention.ActorURI, token); mentions.Add(new PostMention { ActorURI = mention.ActorURI, Handle = mention.Handle, IsLocal = local != default, AccountId = local?.Id }); } post.Title = note.Title; post.SpoilerText = note.SpoilerText; post.Excerpt = note.Excerpt; post.Source = note.Source; post.Emojis = note.Emojis.ToList(); post.CoverURL = note.CoverURL; post.Link = note.Link; post.HasContentWarning = note.Sensitive; post.Text = note.ContentHtml; post.ContentHtml = note.ContentHtml; post.Language = note.Language; post.Tags = note.Tags.ToList(); post.Mentions = mentions; post.Media = note.Attachments.ToList(); post.EditedAt = note.Updated ?? DateTime.UtcNow; post.UpdateDate = DateTime.UtcNow; await DB.Default.SaveAsync(post, token); await records.Revise(note, activityId, token); await Requote(post, note, quotes, token); return true; } static async Task Requote(PostEntity post, NoteDocument note, IQuoteService quotes, CancellationToken token) { if (note.QuoteUri != post.QuoteURI || note.QuoteAuthorization != post.QuoteAuthorizationURI || post.QuoteState != QuoteState.Accepted) await quotes.Resolve(post, note, token); } } public static class RemoteDeletes { public static async Task Tombstone(string objectUri, CancellationToken token) => await DB.Default.Update() .Match(d => d.ObjectURI == objectUri) .Modify(d => d.DeletedAt, DateTime.UtcNow) .Option(o => o.IsUpsert = true) .ExecuteAsync(token); public static async Task Remove(PostEntity post, string objectUri, CancellationToken token) { await Tombstone(objectUri, token); await DB.Default.DeleteAsync(post.ID); await DB.Default.DeleteAsync(r => r.PostId == post.ID || r.ObjectURI == objectUri); await DB.Default.DeleteAsync(e => e.PostId == post.ID || e.ReblogOfPostId == post.ID); await DB.Default.DeleteAsync(p => p.ReblogOfPostId == post.ID); if (!string.IsNullOrEmpty(post.AnsweringToPostId) && Domain.Privacy.Counted.Reply(post)) await DB.Default.Update().MatchID(post.AnsweringToPostId).Modify(b => b.Inc(p => p.RepliesCount, -1)).ExecuteAsync(token); if (post.QuoteState == QuoteState.Accepted && !string.IsNullOrEmpty(post.QuotedPostId)) await DB.Default.Update().MatchID(post.QuotedPostId).Modify(b => b.Inc(p => p.QuotesCount, -1)).ExecuteAsync(token); } } }