Public threads name their replies and context
Owner decision of 2026-10-06: a persona's public or unlisted post outside any group names its replies
(…/scribbles/{id}/replies) and its conversation's context (FEP-7888), the root's …/context that a reply inherits from
its parent, ours or another server's. Both list only the public and unlisted posts PrivaPub holds; followers-only,
circle, direct and local-only posts name neither and their collections answer 404. Checked live: opening alice's
thread on Mastodon finds carol's reply, which nobody there follows (scenarios/threads.sh).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
93e13e5563
commit
bf37223849
8 files changed
+152
-1
No files matched your search
@@ -235,6 +235,42 @@ namespace PrivaPub.Federation.Controllers
|
||||
return Activity(note);
|
||||
}
|
||||
|
||||
// the public and unlisted replies PrivaPub holds to a public post (owner decision 2026-10-06), oldest first
|
||||
[HttpGet, Route("{actor}/scribbles/{postId}/replies")]
|
||||
public async Task<IActionResult> Replies(string actor, string postId, CancellationToken token)
|
||||
{
|
||||
var (local, post) = await PublicPost(actor, postId, token);
|
||||
if (post == default || post.IsLocalOnly || !string.IsNullOrEmpty(post.GroupId))
|
||||
return NotFound();
|
||||
var replies = await _dbEntities.Posts.Match(p => p.AnsweringToPostId == post.ID && !p.IsLocalOnly && p.ReblogOfPostId == null)
|
||||
.Match(VisibilityPolicy.IsPublic).Sort(p => p.CreationDate, Order.Ascending).Limit(MaxThread).ExecuteAsync(token);
|
||||
return Activity(ActivityPubRenderer.OrderedCollection(local.PostUri(post.ID) + "/replies", replies.Count,
|
||||
replies.Select(r => (JsonNode)r.ObjectURI)));
|
||||
}
|
||||
|
||||
// a public conversation this post starts (FEP-7888): it and every public or unlisted reply under it PrivaPub holds
|
||||
[HttpGet, Route("{actor}/scribbles/{postId}/context")]
|
||||
public async Task<IActionResult> Context(string actor, string postId, CancellationToken token)
|
||||
{
|
||||
var (local, root) = await PublicPost(actor, postId, token);
|
||||
if (root == default || root.IsLocalOnly || !string.IsNullOrEmpty(root.GroupId) || root.ContextURI != local.PostUri(root.ID) + "/context")
|
||||
return NotFound();
|
||||
var thread = new List<PostEntity> { root };
|
||||
var level = new List<string> { root.ID };
|
||||
for (var depth = 0; depth < PrivaPub.Federation.Inbox.RemotePosts.MaxDepth && level.Count > 0 && thread.Count < MaxThread; depth++)
|
||||
{
|
||||
var next = await _dbEntities.Posts.Match(p => level.Contains(p.AnsweringToPostId) && !p.IsLocalOnly && p.ReblogOfPostId == null)
|
||||
.Match(VisibilityPolicy.IsPublic).Sort(p => p.CreationDate, Order.Ascending).Limit(MaxThread - thread.Count).ExecuteAsync(token);
|
||||
thread.AddRange(next);
|
||||
level = next.Select(p => p.ID).ToList();
|
||||
}
|
||||
var collection = ActivityPubRenderer.OrderedCollection(root.ContextURI, thread.Count, thread.Select(p => (JsonNode)p.ObjectURI));
|
||||
collection["attributedTo"] = local.Uri;
|
||||
return Activity(collection);
|
||||
}
|
||||
|
||||
const int MaxThread = 500;
|
||||
|
||||
[HttpGet, Route("{actor}/parrot-licences/{licenceId}")]
|
||||
public async Task<IActionResult> ParrotLicence(string actor, string licenceId, CancellationToken token)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user