Posts reach their audience, edits follow them, deletes leave a tombstone

OutboxPublisher works out who a post goes to, Mastodon's way: followers'
shared inboxes for public, unlisted and followers-only; every mentioned
remote account's own inbox; the recipients only for direct; the parent's
author for a reply; a community's followers for a post in it; nobody for
a circle. Creates, updates and deletes all use it.

- Local posts take a visibility (public, unlisted, followers-only) and a
  spoiler text next to the content-warning flag.
- /clientapi/post/update keeps the previous version as a revision,
  re-renders, and sends Update{Note} with `updated` to the same to/cc.
- Deleting is now soft: the content, title, spoiler, media and revisions
  are cleared, timeline entries removed, the parent's reply count goes
  down, Delete goes to the stored audience, and the object answers 410
  with a Tombstone instead of 404.
- Editing a persona's profile sends Update{Person} to its followers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-01 11:34:35 +02:00
1 parent 61f6ca0676
commit a76cc34557
10 files changed
+368 -33

No files matched your search

+19
View File
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using MongoDB.Entities;
using PrivaPub.Domain.Content;
using PrivaPub.Domain.Social;
using PrivaPub.Federation.Actors;
using PrivaPub.Federation.Inbox;
@@ -11,9 +12,11 @@ using PrivaPub.Federation.Inbox.Handlers;
using PrivaPub.Federation.Outbox;
using PrivaPub.Infrastructure.Jobs;
using PrivaPub.Models;
using PrivaPub.Models.Federation;
using PrivaPub.Models.Jobs;
using PrivaPub.Models.User;
using PrivaPub.Resources;
using PrivaPub.Services;
using PrivaPub.StaticServices;
using System.Text.Json;
@@ -45,6 +48,9 @@ namespace PrivaPub.Tests.Support
new UpdateHandler(Db, Local, Remote)
}, NullLogger<InboxProcessor>.Instance);
Follows = new FollowService(Db, Local, Remote, Delivery, new KeyLocalizer<GenericRes>(), NullLogger<FollowService>.Instance);
Content = new ContentRenderer(Local, Remote);
Outbox = new OutboxPublisher(Db, Local, Delivery);
Posts = new PostsService(Db, Local, Remote, Delivery, Content, Outbox, new KeyLocalizer<GenericRes>(), NullLogger<PostsService>.Instance);
}
public Peer Peer { get; }
@@ -56,6 +62,19 @@ namespace PrivaPub.Tests.Support
public InboxReceiver Receiver { get; }
public InboxProcessor Processor { get; }
public FollowService Follows { get; }
public ContentRenderer Content { get; }
public OutboxPublisher Outbox { get; }
public PostsService Posts { get; }
public async Task FollowedBy(LocalActor local, RemoteActor follower) =>
await DB.Default.SaveAsync(new Follower
{
LocalActorId = local.Id,
LocalActorKind = local.Kind,
ActorURI = follower.Id,
InboxURL = follower.Id + "/inbox",
SharedInboxURL = follower.SharedInbox
});
public static async Task<Harness> Start() => new(await Peer.Start());