A follow of a persona's profile page; an Update under its Create's id
Forte follows a persona by its profile page (/@name, WebFinger's alias) rather than its actor's id: Follow and
Undo{Follow} now find the persona by either (it was a 404). Forte also sends an edit only added to the thread's context
(FEP-171b), under the same activity id as the post's Create: the unwrapping now tells two activities that share an id
apart by what they carry, as the inbox does, so the Update is not taken for a copy.
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
d06f684f0d
commit
99b5e2dbf3
6 files changed
+67
-8
No files matched your search
@@ -90,6 +90,39 @@ namespace PrivaPub.Tests.Federation
|
||||
Assert.Equal((await DB.Default.Find<PostEntity>().Match(p => p.ObjectURI == rootId).ExecuteFirstAsync(Token)).ID, stored.AnsweringToPostId);
|
||||
}
|
||||
|
||||
// Forte gives a post's Update the id of its Create, and sends it only added to the thread's context
|
||||
[Fact]
|
||||
public async Task An_update_added_under_its_creates_id_is_taken_as_the_edit()
|
||||
{
|
||||
var (_, alice) = await _harness.Persona("alice");
|
||||
var owner = new RemoteActor(_harness.Peer, "owner", ed25519: true);
|
||||
await DB.Default.SaveAsync(new Following { AvatarId = alice.Id, TargetActorURI = owner.Id, TargetInboxURL = owner.Id + "/inbox", State = FollowState.Accepted }, Token);
|
||||
var context = $"{Origin(owner)}/conversation/{Guid.NewGuid():N}";
|
||||
var create = owner.Prove(Create(owner, "as first written", context));
|
||||
JsonObject Added(JsonObject inner) => owner.Prove(new JsonObject
|
||||
{
|
||||
["id"] = $"{Origin(owner)}/activity/{Guid.NewGuid():N}", ["type"] = "Add", ["actor"] = owner.Id, ["object"] = inner,
|
||||
["target"] = new JsonObject { ["id"] = context, ["type"] = "Collection", ["attributedTo"] = owner.Id }
|
||||
});
|
||||
await _harness.Deliver(owner, "/human-centipede", Added(create));
|
||||
await RunQueued(create["id"]!.GetValue<string>());
|
||||
var update = create.DeepClone().AsObject();
|
||||
update.Remove("proof");
|
||||
update["type"] = "Update";
|
||||
update["object"]!["content"] = "<p>as edited</p>";
|
||||
update["object"]!["updated"] = DateTime.UtcNow.AddSeconds(1).ToString("O");
|
||||
owner.Prove(update);
|
||||
|
||||
await _harness.Deliver(owner, "/human-centipede", Added(update));
|
||||
var job = await DB.Default.Find<Job>().Match(j => j.DedupeKey.StartsWith("inbox|forwarded|" + update["id"]!.GetValue<string>() + "|") && j.State == JobState.Pending)
|
||||
.ExecuteFirstAsync(Token);
|
||||
Assert.NotNull(job);
|
||||
Assert.Equal(JobResult.Done, (await _harness.Processor.Handle(job, Token)).Result);
|
||||
|
||||
var noteId = create["object"]!["id"]!.GetValue<string>();
|
||||
Assert.Contains("as edited", (await DB.Default.Find<PostEntity>().Match(p => p.ObjectURI == noteId).ExecuteFirstAsync(Token)).ContentHtml);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_add_to_a_collection_on_another_server_is_no_threads()
|
||||
{
|
||||
|
||||
@@ -191,6 +191,27 @@ namespace PrivaPub.Tests.Federation
|
||||
Assert.Equal(300, result.RetryAfterSeconds);
|
||||
}
|
||||
|
||||
// Forte follows a persona by its profile page, as WebFinger names it, rather than by its actor's id; so does its Undo
|
||||
[Fact]
|
||||
public async Task A_follow_of_a_personas_profile_page_follows_the_persona()
|
||||
{
|
||||
var token = TestContext.Current.CancellationToken;
|
||||
var (_, alice) = await _harness.Persona("alice");
|
||||
var forte = new RemoteActor(_harness.Peer, "forte");
|
||||
var follow = new JsonObject { ["id"] = NewId(forte, "follow"), ["type"] = "Follow", ["actor"] = forte.Id, ["object"] = alice.HtmlUrl };
|
||||
|
||||
Assert.Equal(202, (await _harness.Deliver(forte, "/human-centipede", follow)).StatusCode);
|
||||
Assert.True(await DB.Default.Find<PrivaPub.Models.Federation.Follower>().Match(f => f.LocalActorId == alice.Id && f.ActorURI == forte.Id && f.IsAccepted).ExecuteAnyAsync(token));
|
||||
Assert.Contains(await _harness.Outgoing(forte.Id + "/inbox"), a => a["type"]!.GetValue<string>() == "Accept");
|
||||
|
||||
await _harness.Deliver(forte, "/human-centipede", new JsonObject
|
||||
{
|
||||
["id"] = NewId(forte, "undo"), ["type"] = "Undo", ["actor"] = forte.Id,
|
||||
["object"] = new JsonObject { ["type"] = "Follow", ["actor"] = forte.Id, ["object"] = alice.HtmlUrl }
|
||||
});
|
||||
Assert.False(await DB.Default.Find<PrivaPub.Models.Federation.Follower>().Match(f => f.LocalActorId == alice.Id && f.ActorURI == forte.Id).ExecuteAnyAsync(token));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_join_of_a_local_post_is_answered_with_ignore()
|
||||
{
|
||||
|
||||
@@ -32,9 +32,13 @@ namespace PrivaPub.Federation.Inbox
|
||||
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);
|
||||
var payload = JsonSerializer.Serialize(new InboxPayload(actorUri, inner.ToJsonString(), "shared", ReceivedAt: DateTime.UtcNow, ForwardedBy: owner.ActorURI));
|
||||
var host = new Uri(actorUri).Host.ToLowerInvariant();
|
||||
var dedupe = "inbox|forwarded|" + innerId;
|
||||
var queued = await queue.Enqueue(JobKind.ProcessInbox, payload, host, dedupe, token);
|
||||
// one id for two activities (Forte's Update of a post has its Create's id): what each carries tells them apart
|
||||
if (!queued && InboxReceiver.CarriesOther(await queue.Payload(dedupe, token), inner))
|
||||
queued = await queue.Enqueue(JobKind.ProcessInbox, payload, host, $"{dedupe}|{InboxReceiver.Digest(inner)}", token);
|
||||
if (queued)
|
||||
Arrival.Accept("unwrapped");
|
||||
else
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
var follow = activity;
|
||||
var follower = actor;
|
||||
|
||||
var target = await _localActors.FindByUri(Id(follow["object"]), token);
|
||||
var target = await _localActors.FindByAddress(Id(follow["object"]), token);
|
||||
if (target is not { IsFederated: true } || target.Kind == LocalActorKind.Application)
|
||||
{
|
||||
Arrival.Drop("unknown-recipient");
|
||||
|
||||
@@ -83,7 +83,7 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
{
|
||||
var followers = await _dbEntities.Followers.Match(f => f.ActorURI == actor.ActorURI).ExecuteAsync(token);
|
||||
var targetUri = inner is JsonObject ? Id(inner["object"]) : default;
|
||||
var target = targetUri == default ? default : await _localActors.FindByUri(targetUri, token);
|
||||
var target = targetUri == default ? default : await _localActors.FindByAddress(targetUri, token);
|
||||
var undone = false;
|
||||
foreach (var follower in followers)
|
||||
{
|
||||
|
||||
@@ -225,7 +225,7 @@ namespace PrivaPub.Federation.Inbox
|
||||
|
||||
static string HostOf(string uri) => Uri.TryCreate(uri, UriKind.Absolute, out var parsed) ? parsed.Host.ToLowerInvariant() : default;
|
||||
|
||||
static bool CarriesOther(string earlierPayload, JsonNode activity)
|
||||
public static bool CarriesOther(string earlierPayload, JsonNode activity)
|
||||
{
|
||||
if (earlierPayload == default)
|
||||
return false;
|
||||
@@ -235,7 +235,7 @@ namespace PrivaPub.Federation.Inbox
|
||||
}
|
||||
|
||||
// what an activity carries, short: its type, actor and object
|
||||
static string Digest(JsonNode activity) =>
|
||||
public static string Digest(JsonNode activity) =>
|
||||
Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(
|
||||
$"{Value(activity, "type")}\n{Id(activity["actor"])}\n{activity["object"]?.ToJsonString()}")))[..16].ToLowerInvariant();
|
||||
|
||||
@@ -252,7 +252,8 @@ namespace PrivaPub.Federation.Inbox
|
||||
switch (type)
|
||||
{
|
||||
case "Follow":
|
||||
var target = await _localActors.FindByUri(Id(inner), token);
|
||||
// (by its actor's id, or the profile page Forte names instead)
|
||||
var target = await _localActors.FindByAddress(Id(inner), token);
|
||||
if (target is not { IsFederated: true } || target.Kind == LocalActorKind.Application)
|
||||
return new(StatusCodes.Status404NotFound, "no such local actor", Reason: "unknown-recipient");
|
||||
break;
|
||||
|
||||
Reference in new issue
Block a user