Home timelines, mention notifications, and threads that fetch their parents

Fanout writes a TimelineEntry for every persona a post should reach: the
author, local followers of a local author, local followers of a remote one,
the members of a direct conversation or a circle. Mastodon's home rules
apply when it is written: a reply shows only to followers of both sides
(or to the one replied to), a reblog only where reblogs are wanted. A
mention of a local persona becomes a Mention notification.

Inbound posts are now also kept when a persona follows their author.

RemotePosts holds what CreateHandler and backfill share: building a Post
from a note, and fetching a public parent the first time a reply to it
arrives, so its author is known (a reply to an unknown or non-public
parent stays out of home timelines). Each fetched ancestor queues a
FetchAncestors job for the next one, up to ten deep.

/clientapi/timeline/home and /clientapi/notifications (with
/notifications/read) page by max_id for the persona's own root only.

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:37:41 +02:00
1 parent a76cc34557
commit e99b76dbd7
12 files changed
+609 -52

No files matched your search

+7
View File
@@ -15,6 +15,7 @@ using System.Text.Json.Nodes;
using PostEntity = PrivaPub.Models.Post.Post;
using PrivaPub.Domain.Content;
using PrivaPub.Domain.Timelines;
using PrivaPub.Federation.Actors;
using PrivaPub.Federation.Rendering;
using PrivaPub.Federation.Outbox;
@@ -41,6 +42,7 @@ namespace PrivaPub.Services
readonly IDeliveryService _delivery;
readonly IContentRenderer _content;
readonly IOutboxPublisher _outbox;
readonly IFanout _fanout;
readonly IStringLocalizer<GenericRes> _localizer;
readonly ILogger<PostsService> _logger;
@@ -50,6 +52,7 @@ namespace PrivaPub.Services
IDeliveryService delivery,
IContentRenderer content,
IOutboxPublisher outbox,
IFanout fanout,
IStringLocalizer<GenericRes> localizer,
ILogger<PostsService> logger)
{
@@ -59,6 +62,7 @@ namespace PrivaPub.Services
_delivery = delivery;
_content = content;
_outbox = outbox;
_fanout = fanout;
_localizer = localizer;
_logger = logger;
}
@@ -113,6 +117,7 @@ namespace PrivaPub.Services
if (isLocalOnly)
{
await DB.Default.SaveAsync(post, token);
await _fanout.Distribute(post, token);
result.Data = ToView(post);
return result;
}
@@ -126,6 +131,7 @@ namespace PrivaPub.Services
if (parent != default)
await DB.Default.Update<PostEntity>().MatchID(parent.ID).Modify(b => b.Inc(p => p.RepliesCount, 1)).ExecuteAsync(token);
await _fanout.Distribute(post, token);
await _outbox.Publish(author, post, create, token);
if (group is { IsFederated: true } && post.Visibility is PostVisibility.Public or PostVisibility.Unlisted)
await _delivery.EnqueueToFollowers(group, ActivityPubRenderer.Announce(group, post.ObjectURI, $"announce-{post.ID}"), token);
@@ -367,6 +373,7 @@ namespace PrivaPub.Services
dm.Mentions = remote.Select(r => new PostMention { ActorURI = r.Uri, Handle = "@" + r.Handle, IsLocal = r.Uri.StartsWith(author.BaseAddress + "/") }).ToList();
await DB.Default.SaveAsync(dm, token);
await DB.Default.Update<DmGroup>().MatchID(dmGroup.ID).Modify(g => g.UpdatedAt, DateTime.UtcNow).ExecuteAsync(token);
await _fanout.Distribute(dm, token);
if (inboxes.Count > 0)
await _delivery.Enqueue(author, inboxes, directCreate, token);