Arrival carries a verdict that handlers set with one line before an existing return (Arrival.Drop, Reject, Accept, About), so no handler signature changes. InboxProcessor records it as an 'in' event, with the time taken, the wait since the inbox accepted it, the attempt, the audience (from the post's visibility, or else the activity's addressing), the local actor kind, the object's age for updates, deletes and reactions, and the FEP features of a delivered object. It also records types nothing handles (unknown-type), actors that cannot be loaded (deferred) and handler failures. Drop reasons: fetch-failed, unparseable, misattributed, duplicate, deleted, not-addressed, not-followed, not-public, not-visible, not-deleted, unknown-object, unknown-recipient, cross-origin, unsupported. Accepted sub-reasons: stored, poll-vote, edit, refresh, actor-refresh, actor-delete, removed, tombstone-only, auto-accepted, pending, follow-answer, quote-answer, quote-granted, undone, reaction, reported. Rejected: blocked, ignored, quote-refused. A circle's traffic is private and kindless, and a stranger posting into one is just not-addressed, so the event store cannot reveal a circle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
139 lines
4.5 KiB
C#
139 lines
4.5 KiB
C#
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<PostMention>();
|
|
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);
|
|
}
|
|
}
|
|
}
|