A post named by its page is found as by its id
Pixelfed names one of our posts by the address of its page (/@name/<post id>, the post's url) in its Like, Announce and their Undo, so its likes were dropped as unknown objects. Before an activity is handled, such a reference to a post of ours, as its object or the object of the activity it undoes, is replaced by the post's id. Found by the pasture's new Pixelfed peer. 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
34c0f696af
commit
26dac40720
4 files changed
+72
-2
No files matched your search
@@ -42,6 +42,31 @@ namespace PrivaPub.Tests.Federation
|
||||
|
||||
PrivaPub.Models.Statistics.InteractionEvent Processed(string activity) => _harness.Ledger.Of("in").Last(e => e.Activity == activity);
|
||||
|
||||
// Pixelfed names our post by its page (/@name/<id>, the post's url) in its Like, Announce and their Undo: the post is
|
||||
// found the same as by its id
|
||||
[Fact]
|
||||
public async Task A_like_naming_a_post_by_its_page_counts_and_its_undo_too()
|
||||
{
|
||||
var token = TestContext.Current.CancellationToken;
|
||||
var (_, alice) = await _harness.Persona("alice");
|
||||
var fan = new RemoteActor(_harness.Peer, "fan");
|
||||
var post = (await _harness.Statuses.Publish(alice, new StatusDraft { Text = "a picture" }, token)).Post;
|
||||
var page = alice.PostHtmlUrl(post.ID);
|
||||
Assert.NotEqual(post.ObjectURI, page);
|
||||
var like = new JsonObject { ["id"] = NewId(fan, "likes"), ["type"] = "Like", ["actor"] = fan.Id, ["object"] = page };
|
||||
|
||||
await _harness.Deliver(fan, "/human-centipede", like);
|
||||
Assert.Equal("accepted", Processed("Like").Outcome);
|
||||
Assert.Equal(1, (await DB.Default.Find<Post>().OneAsync(post.ID, token)).FavouritesCount);
|
||||
await _harness.Deliver(fan, "/human-centipede", Activity(fan, "Undo", like));
|
||||
Assert.Equal(0, (await DB.Default.Find<Post>().OneAsync(post.ID, token)).FavouritesCount);
|
||||
|
||||
// a page that is no post of ours stays as it is
|
||||
var elsewhere = new JsonObject { ["id"] = NewId(fan, "likes"), ["type"] = "Like", ["actor"] = fan.Id, ["object"] = alice.PostHtmlUrl("000000000000000000000000") };
|
||||
await _harness.Deliver(fan, "/human-centipede", elsewhere);
|
||||
Assert.Equal(("dropped", "unknown-object"), (Processed("Like").Outcome, Processed("Like").Reason));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_actor_update_refreshes_the_account_from_its_origin_even_when_its_updated_is_older()
|
||||
{
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace PrivaPub.Tests.Support
|
||||
new BlockHandler(Db, Local)
|
||||
};
|
||||
((AnnounceHandler)Handlers.First(h => h is AnnounceHandler)).Relays = Handlers;
|
||||
Processor = new InboxProcessor(Remote, Handlers, NullLogger<InboxProcessor>.Instance, Ledger);
|
||||
Processor = new InboxProcessor(Remote, Handlers, NullLogger<InboxProcessor>.Instance, Ledger, Local);
|
||||
Follows = new FollowService(Db, Local, Remote, Delivery, new KeyLocalizer<GenericRes>(), NullLogger<FollowService>.Instance);
|
||||
Content = new ContentRenderer(Local, Remote);
|
||||
Media = new MediaService(new StaticOptions<MediaOptions>(new MediaOptions { Root = Path.Combine(Path.GetTempPath(), $"privapub-media-{Guid.NewGuid():N}") }),
|
||||
|
||||
@@ -26,10 +26,12 @@ namespace PrivaPub.Federation.Inbox
|
||||
readonly IReadOnlyDictionary<string, IActivityHandler> _handlers;
|
||||
readonly ILogger<InboxProcessor> _logger;
|
||||
readonly IInteractionLedger _ledger;
|
||||
readonly ILocalActorService _localActors;
|
||||
|
||||
public InboxProcessor(IRemoteActorService remoteActors, IEnumerable<IActivityHandler> handlers, ILogger<InboxProcessor> logger,
|
||||
IInteractionLedger ledger = default)
|
||||
IInteractionLedger ledger = default, ILocalActorService localActors = default)
|
||||
{
|
||||
_localActors = localActors;
|
||||
_remoteActors = remoteActors;
|
||||
_handlers = handlers.ToDictionary(h => h.Type, StringComparer.Ordinal);
|
||||
_logger = logger;
|
||||
@@ -65,6 +67,7 @@ namespace PrivaPub.Federation.Inbox
|
||||
Arrival.Current = arrival;
|
||||
try
|
||||
{
|
||||
await LocalReferences.Canonicalise(activity, _localActors?.BaseAddress, token);
|
||||
await handler.Handle(activity, actor, token);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PostEntity = PrivaPub.Models.Post.Post;
|
||||
|
||||
namespace PrivaPub.Federation.Inbox
|
||||
{
|
||||
// Some servers name one of our posts by the address of its page instead of its id (Pixelfed likes /@name/<post id>, the
|
||||
// post's `url`). Before an activity is handled, such a reference to a post of ours, as the activity's object or as the
|
||||
// object of the activity it undoes, is replaced by the post's id, so that every handler finds the post the same way.
|
||||
public static partial class LocalReferences
|
||||
{
|
||||
[GeneratedRegex("^/@[^/?#]+/([0-9a-f]{24})$")]
|
||||
private static partial Regex PostPage();
|
||||
|
||||
public static async Task Canonicalise(JsonNode activity, string baseAddress, CancellationToken token)
|
||||
{
|
||||
if (activity is not JsonObject outer || string.IsNullOrEmpty(baseAddress))
|
||||
return;
|
||||
await Replace(outer, baseAddress, token);
|
||||
if (outer["object"] is JsonObject inner)
|
||||
await Replace(inner, baseAddress, token);
|
||||
}
|
||||
|
||||
static async Task Replace(JsonObject node, string baseAddress, CancellationToken token)
|
||||
{
|
||||
if (node["object"] is not JsonValue value || !value.TryGetValue<string>(out var uri) || PostId(uri, baseAddress) is not { } id)
|
||||
return;
|
||||
var post = await DB.Default.Find<PostEntity>().Match(p => p.ID == id && !p.IsFederatedCopy).ExecuteFirstAsync(token);
|
||||
if (post?.ObjectURI is { } canonical)
|
||||
node["object"] = canonical;
|
||||
}
|
||||
|
||||
// the id of the post whose page the address is, on our own site
|
||||
public static string PostId(string uri, string baseAddress) =>
|
||||
uri.StartsWith(baseAddress + "/@", StringComparison.OrdinalIgnoreCase) && PostPage().Match(uri[baseAddress.Length..]) is { Success: true } page
|
||||
? page.Groups[1].Value
|
||||
: default;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user