From 6f240828cfecf923b39816bf0e095be4b030043e Mon Sep 17 00:00:00 2001 From: thepra Date: Wed, 7 Oct 2026 02:14:16 +0200 Subject: [PATCH] A located post reaches no home, its author's included The fan-out put every post in its author's home, located ones too, while the Mastodon API looks past located posts everywhere else. In the author's home a located post showed as public (the mapper has no word for it) and every action on it answered 404: decePub's end-to-end runs, which write one from Milano for the globe, found it there. The fan-out now gives a located post no home, no stream and no notification, and the home timeline passes over the entries left from before. A located post is read through /clientapi/post/nearby and deleted through /clientapi/post/delete. NearbyTests checks it has no TimelineEntry; the suite passes (881). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw --- CLAUDE.md | 4 +++- PrivaPub.Tests/Domain/NearbyTests.cs | 2 ++ PrivaPub/Api/Mastodon/Controllers/TimelinesController.cs | 4 +++- PrivaPub/Domain/Timelines/Fanout.cs | 5 +++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 81e1bca..aa0b71f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -217,7 +217,9 @@ group www-data and reaches the private mongod; `sudo -u www-data` works too. when a refetch answers 404, which is why this matters. **Communities** are FEP-1b12 groups: `GroupDistributor` announces the whole activity (plus the object for new posts, for Mastodon), top-level posts are `Page`s with a `name`, posting follows `Group.PostingPolicy`. - Located posts (`LocalGeo`) are the only local-only posts. + Located posts (`LocalGeo`) are the only local-only posts. The Mastodon API looks past them everywhere, homes + included (the fan-out gives them no `TimelineEntry`, not even their author's): they are read through + `/clientapi/post/nearby` and deleted through `/clientapi/post/delete`. 9. **A DM joins a conversation only by `DmGroup.ParticipantsKey`**, the exact set of its participants; a remote `context` decides nothing. DMs are `Post`s with `Visibility = Direct` and a `ConversationId` (`DmPost` is legacy). `DmGroup.LastPostId` pages `/api/v1/conversations`, and `ConversationState` keeps each persona's read and removed diff --git a/PrivaPub.Tests/Domain/NearbyTests.cs b/PrivaPub.Tests/Domain/NearbyTests.cs index 29f0bb6..cac86f7 100644 --- a/PrivaPub.Tests/Domain/NearbyTests.cs +++ b/PrivaPub.Tests/Domain/NearbyTests.cs @@ -59,6 +59,8 @@ namespace PrivaPub.Tests.Domain .Select(j => JsonSerializer.Deserialize(j.Payload)) .Where(p => p.Body.Contains(post.ID)); Assert.Empty(outgoing); + // nor reaches a home, its author's included: the Mastodon API looks past it, so there it could not be acted on + Assert.False(await DB.Default.Find().Match(e => e.PostId == post.ID).ExecuteAnyAsync(token)); } [Fact] diff --git a/PrivaPub/Api/Mastodon/Controllers/TimelinesController.cs b/PrivaPub/Api/Mastodon/Controllers/TimelinesController.cs index 67d55de..1d53d40 100644 --- a/PrivaPub/Api/Mastodon/Controllers/TimelinesController.cs +++ b/PrivaPub/Api/Mastodon/Controllers/TimelinesController.cs @@ -43,7 +43,9 @@ namespace PrivaPub.Api.Mastodon.Controllers home.Match(e => !apart.Contains(e.AuthorAccountId)); var entries = await Page.From(Params, Limit()).Fetch(home, e => e.PostId, token); var ids = entries.Select(e => e.PostId).ToList(); - var posts = (await _dbEntities.Posts.Match(p => ids.Contains(p.ID)).Match(VisibilityPolicy.IsShown).ExecuteAsync(token)).ToDictionary(p => p.ID); + // (located posts once reached their author's home: the entries left from then are passed over) + var posts = (await _dbEntities.Posts.Match(p => ids.Contains(p.ID) && p.Visibility != PostVisibility.LocalGeo).Match(VisibilityPolicy.IsShown) + .ExecuteAsync(token)).ToDictionary(p => p.ID); var statuses = await _mapper.Statuses(ids.Where(posts.ContainsKey).Select(id => posts[id]).ToList(), MyId, token); Link("/api/v1/timelines/home", entries.LastOrDefault()?.PostId, entries.FirstOrDefault()?.PostId); return Json(statuses); diff --git a/PrivaPub/Domain/Timelines/Fanout.cs b/PrivaPub/Domain/Timelines/Fanout.cs index 0f3a42b..807b5e5 100644 --- a/PrivaPub/Domain/Timelines/Fanout.cs +++ b/PrivaPub/Domain/Timelines/Fanout.cs @@ -27,6 +27,11 @@ namespace PrivaPub.Domain.Timelines public async Task Distribute(Post post, CancellationToken token) { + // a located post is read only through /clientapi/post/nearby: the Mastodon API looks past it everywhere, so in a + // home, its author's included, it would be a status nothing can be done to (and it would read as public) + if (post.Visibility == PostVisibility.LocalGeo) + return; + var recipients = new HashSet(StringComparer.Ordinal); if (!post.IsFederatedCopy && !string.IsNullOrEmpty(post.GroupUserId)) recipients.Add(post.GroupUserId);