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);
|
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]
|
[Fact]
|
||||||
public async Task An_add_to_a_collection_on_another_server_is_no_threads()
|
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);
|
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]
|
[Fact]
|
||||||
public async Task A_join_of_a_local_post_is_answered_with_ignore()
|
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");
|
Arrival.Drop("container-unusable");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
var payload = new InboxPayload(actorUri, inner.ToJsonString(), "shared", ReceivedAt: DateTime.UtcNow, ForwardedBy: owner.ActorURI);
|
var payload = JsonSerializer.Serialize(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(),
|
var host = new Uri(actorUri).Host.ToLowerInvariant();
|
||||||
"inbox|forwarded|" + innerId, token);
|
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)
|
if (queued)
|
||||||
Arrival.Accept("unwrapped");
|
Arrival.Accept("unwrapped");
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
|||||||
var follow = activity;
|
var follow = activity;
|
||||||
var follower = actor;
|
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)
|
if (target is not { IsFederated: true } || target.Kind == LocalActorKind.Application)
|
||||||
{
|
{
|
||||||
Arrival.Drop("unknown-recipient");
|
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 followers = await _dbEntities.Followers.Match(f => f.ActorURI == actor.ActorURI).ExecuteAsync(token);
|
||||||
var targetUri = inner is JsonObject ? Id(inner["object"]) : default;
|
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;
|
var undone = false;
|
||||||
foreach (var follower in followers)
|
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 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)
|
if (earlierPayload == default)
|
||||||
return false;
|
return false;
|
||||||
@@ -235,7 +235,7 @@ namespace PrivaPub.Federation.Inbox
|
|||||||
}
|
}
|
||||||
|
|
||||||
// what an activity carries, short: its type, actor and object
|
// 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(
|
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();
|
$"{Value(activity, "type")}\n{Id(activity["actor"])}\n{activity["object"]?.ToJsonString()}")))[..16].ToLowerInvariant();
|
||||||
|
|
||||||
@@ -252,7 +252,8 @@ namespace PrivaPub.Federation.Inbox
|
|||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case "Follow":
|
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)
|
if (target is not { IsFederated: true } || target.Kind == LocalActorKind.Application)
|
||||||
return new(StatusCodes.Status404NotFound, "no such local actor", Reason: "unknown-recipient");
|
return new(StatusCodes.Status404NotFound, "no such local actor", Reason: "unknown-recipient");
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in new issue
Block a user