A thread owner's Add of a reply to its context is the reply passed on

FEP-171b, received (Hubzilla, Streams, Forte): an Add whose target is a collection of the actor's own server, and whose
object is someone's Create, Update or Delete of their own object, is queued as that activity forwarded by the owner, so
it is believed on its FEP-8b32 proof or as its origin has it, and shares its job with the plain forward Hubzilla also
sends. Checked live against Hubzilla (unchanged, 20 checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-06 10:03:44 +02:00
1 parent b39eadffeb
commit d06f684f0d
7 files changed
+171 -7

No files matched your search

@@ -0,0 +1,45 @@
using PrivaPub.Federation.Objects;
using PrivaPub.Infrastructure.Jobs;
using PrivaPub.Models.Jobs;
using PrivaPub.Models.User;
using System.Text.Json;
using System.Text.Json.Nodes;
using static PrivaPub.Federation.Objects.ActivityJson;
namespace PrivaPub.Federation.Inbox
{
// FEP-171b conversation containers (Hubzilla, Streams, Forte): a thread's owner adds each activity in it to the
// thread's context, a collection of its own, and tells the thread's audience with Add{activity}. The activity it adds
// is another's, so it is taken as forwarded by the owner: a Create, Update or Delete of the actor's own object,
// believed on its FEP-8b32 proof or as its origin has it (Forwarded). Hubzilla also passes the activity on by itself;
// the two are one job.
public static class ConversationContainers
{
public static async Task<bool> Unwrap(JsonNode add, ForeignAvatar owner, IJobQueue queue, CancellationToken token)
{
if (add["object"] is not JsonObject inner || Value(inner, "type") is not ("Create" or "Update" or "Delete") || Id(inner) is not { } innerId)
return false;
var target = add["target"];
var context = Id(target);
if (context == default || !Origin.Same(context, owner.ActorURI)
|| target is JsonObject collection && Id(collection["attributedTo"]) is { } attributedTo && attributedTo != owner.ActorURI)
return false;
var actorUri = Id(inner["actor"]);
if (actorUri == default || !Forwarded.Takeable(Value(inner, "type"), inner, actorUri))
{
Arrival.Drop("container-unusable");
return true;
}
var payload = new InboxPayload(actorUri, inner.ToJsonString(), "shared", ReceivedAt: DateTime.UtcNow, ForwardedBy: owner.ActorURI);
var queued = await queue.Enqueue(JobKind.ProcessInbox, JsonSerializer.Serialize(payload), new Uri(actorUri).Host.ToLowerInvariant(),
"inbox|forwarded|" + innerId, token);
if (queued)
Arrival.Accept("unwrapped");
else
Arrival.Drop("duplicate");
return true;
}
}
}
@@ -14,13 +14,16 @@ namespace PrivaPub.Federation.Inbox.Handlers
readonly IFeaturedPosts _featured;
readonly IWalls _walls;
readonly IRemoteActorService _remoteActors;
readonly Infrastructure.Jobs.IJobQueue _queue;
readonly bool _add;
public FeaturedHandler(IFeaturedPosts featured, bool add, IWalls walls = default, IRemoteActorService remoteActors = default)
public FeaturedHandler(IFeaturedPosts featured, bool add, IWalls walls = default, IRemoteActorService remoteActors = default,
Infrastructure.Jobs.IJobQueue queue = default)
{
_featured = featured;
_walls = walls;
_remoteActors = remoteActors;
_queue = queue;
_add = add;
}
@@ -28,6 +31,8 @@ namespace PrivaPub.Federation.Inbox.Handlers
public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token)
{
if (_add && _queue != default && await ConversationContainers.Unwrap(activity, actor, _queue, token))
return;
var target = Objects.ActivityJson.Id(activity["target"]);
// a collection of its own we do not know of: its document read again, at most hourly, in case it was kept before
// we read that collection (a wall)
@@ -97,9 +97,11 @@ namespace PrivaPub.Middleware
.AddSingleton<Federation.Actors.IFeaturedPosts, Federation.Actors.FeaturedPosts>()
.AddSingleton<Federation.Actors.IWalls, Federation.Actors.Walls>()
.AddSingleton<IActivityHandler>(services => new FeaturedHandler(services.GetRequiredService<Federation.Actors.IFeaturedPosts>(), add: true,
services.GetRequiredService<Federation.Actors.IWalls>(), services.GetRequiredService<Federation.Actors.IRemoteActorService>()))
services.GetRequiredService<Federation.Actors.IWalls>(), services.GetRequiredService<Federation.Actors.IRemoteActorService>(),
services.GetRequiredService<IJobQueue>()))
.AddSingleton<IActivityHandler>(services => new FeaturedHandler(services.GetRequiredService<Federation.Actors.IFeaturedPosts>(), add: false,
services.GetRequiredService<Federation.Actors.IWalls>(), services.GetRequiredService<Federation.Actors.IRemoteActorService>()))
services.GetRequiredService<Federation.Actors.IWalls>(), services.GetRequiredService<Federation.Actors.IRemoteActorService>(),
services.GetRequiredService<IJobQueue>()))
.AddSingleton<IActivityHandler, MoveHandler>()
.AddSingleton<IActivityHandler, CreateHandler>()
.AddSingleton<IActivityHandler, DeleteHandler>()