using MongoDB.Entities; using PrivaPub.Federation.Actors; using PrivaPub.Domain.Statuses; using PrivaPub.Federation.Objects; using PrivaPub.Federation.Outbox; using PrivaPub.Models.Post; using PrivaPub.Models.User; using PrivaPub.StaticServices; using System.Text.Json.Nodes; using static PrivaPub.Federation.Objects.ActivityJson; using PostEntity = PrivaPub.Models.Post.Post; namespace PrivaPub.Federation.Inbox.Handlers { public class UpdateHandler : IActivityHandler { const int MaxRevisions = 20; readonly DbEntities _dbEntities; readonly ILocalActorService _localActors; readonly IRemoteActorService _remoteActors; readonly IGroupDistributor _groups; readonly IObjectRecords _records; readonly IQuoteService _quotes; public UpdateHandler(DbEntities dbEntities, ILocalActorService localActors, IRemoteActorService remoteActors, IGroupDistributor groups, IObjectRecords records, IQuoteService quotes) { _quotes = quotes; _records = records; _groups = groups; _dbEntities = dbEntities; _localActors = localActors; _remoteActors = remoteActors; } public string Type => "Update"; public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token) { var inner = activity["object"]; if (Id(inner) == actor.ActorURI) { await _remoteActors.GetActor(actor.ActorURI, refresh: true, token); Arrival.Accept("actor-refresh"); Arrival.About(actor.AvatarType.ToString()); return; } if (inner is not JsonObject || !Origin.Same(Id(inner), actor.ActorURI)) { Arrival.Drop("cross-origin"); return; } var note = NoteParser.Parse(inner); if (note == default || note.AttributedTo != actor.ActorURI) { Arrival.Drop(note == default ? "unparseable" : "misattributed"); return; } var post = await _dbEntities.Posts.Match(p => p.ObjectURI == note.Id && p.ActorURI == actor.ActorURI).ExecuteFirstAsync(token); if (post == default || post.DeletedAt.HasValue) { Arrival.Drop("unknown-object"); return; } Arrival.About(note.Type, post.Visibility, post.CreationDate); 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)) { Arrival.Accept("refresh"); await DB.Default.SaveAsync(post, token); await _records.Revise(note, Id(activity), token); await Requote(post, note, token); return; } Arrival.Accept("edit"); 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, Id(activity), token); await Requote(post, note, token); if (!string.IsNullOrEmpty(post.GroupId) && await _localActors.FindById(Models.Federation.LocalActorKind.Group, post.GroupId, token) is { IsCircle: false } community) await _groups.Announce(community, activity.AsObject(), post.ObjectURI, isNewPost: false, token); } static bool IsEdit(NoteDocument note, PostEntity post) => note.Updated is { } updated && updated > (post.EditedAt ?? post.CreationDate); async Task Requote(PostEntity post, NoteDocument note, CancellationToken token) { if (note.QuoteUri != post.QuoteURI || note.QuoteAuthorization != post.QuoteAuthorizationURI || post.QuoteState != QuoteState.Accepted) await _quotes.Resolve(post, note, token); } } }