Pixelfed places: a remote post's own place, shown and never federated again

A Note's `location` that is a Place with coordinates (Pixelfed sends
them as strings) is kept in the new Post.Place and shown as
Status.privapub.place {name, latitude, longitude, country}; an edit
replaces it. An Event's location stays its own `places`, and nothing
renders a place back out. Closes the INTEROP P2 Pixelfed location gap
and the ROADMAP long-tail item.

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-04 10:07:06 +02:00
1 parent d9fb5c582f
commit 31d1d21d1e
13 files changed
+214 -14

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
using MongoDB.Entities;
using PrivaPub.Models.Post;
using PrivaPub.Tests.Support;
using PrivaPub.Tests.Support.Host;
using System.Text.Json.Nodes;
namespace PrivaPub.Tests.Http
{
// A Pixelfed post's own place: stored from `location`, shown as `privapub.place`, changed by an edit.
[Trait("Category", "Integration")]
public sealed class PostPlaceTests : IAsyncLifetime
{
PrivaPubHost _host;
Peer _peer;
public async ValueTask InitializeAsync()
{
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
_host = await PrivaPubHost.Shared();
_peer = await Peer.Start();
}
public async ValueTask DisposeAsync()
{
if (_peer != default)
await _peer.DisposeAsync();
}
static CancellationToken Token => TestContext.Current.CancellationToken;
static JsonObject Location(string name, string latitude, string longitude) => new()
{
["type"] = "Place", ["name"] = name, ["latitude"] = latitude, ["longitude"] = longitude, ["country"] = "Italy"
};
[Fact]
public async Task A_pixelfed_place_is_shown_and_follows_the_authors_edits()
{
var alice = await _host.Mastodon("alice");
var ann = new RemoteActor(_peer, "ann");
var post = await _host.PublicPostFrom(ann, alice, "<p>the duomo</p>", note => note["location"] = Location("Milan", "45.4642", "9.19"));
var place = (await alice.Client.Get($"/api/v1/statuses/{post.ID}")).Ok().Body["privapub"]!["place"];
Assert.Equal(("Milan", "Italy"), (place.Text("name"), place.Text("country")));
Assert.Equal(45.4642, place["latitude"]!.GetValue<double>());
Assert.Equal(9.19, place["longitude"]!.GetValue<double>());
await Edit(ann, alice, post, note => note["location"] = Location("Turin", "45.0703", "7.6869"));
Assert.Equal("Turin", (await alice.Client.Get($"/api/v1/statuses/{post.ID}")).Ok().Body["privapub"]!["place"].Text("name"));
await Edit(ann, alice, post, _ => { });
Assert.Null((await alice.Client.Get($"/api/v1/statuses/{post.ID}")).Ok().Body["privapub"]!["place"]);
Assert.Null((await DB.Default.Find<Post>().MatchID(post.ID).ExecuteFirstAsync(Token)).Place);
}
[Fact]
public async Task A_post_without_a_place_has_none()
{
var alice = await _host.Mastodon("alice");
var post = await _host.PublicPostFrom(new RemoteActor(_peer, "plain"), alice);
Assert.Null((await alice.Client.Get($"/api/v1/statuses/{post.ID}")).Ok().Body["privapub"]!["place"]);
}
[Fact]
public async Task A_local_post_never_carries_a_location()
{
var alice = await _host.Mastodon("alice");
var status = await alice.Status("made here");
using var client = _host.Client();
client.DefaultRequestHeaders.Accept.ParseAdd("application/activity+json");
var note = JsonNode.Parse(await client.GetStringAsync(status.Text("uri"), Token))!;
Assert.Null(note["location"]);
}
async Task Edit(RemoteActor author, Mastodon mentioned, Post post, Action<JsonObject> shape)
{
var note = new JsonObject
{
["id"] = post.ObjectURI,
["type"] = "Note",
["attributedTo"] = author.Id,
["to"] = new JsonArray(MastodonHelpers.Public),
["cc"] = new JsonArray(mentioned.Uri),
["content"] = "<p>the duomo, again</p>",
["published"] = post.CreationDate.ToString("O"),
["updated"] = DateTime.UtcNow.ToString("O"),
["tag"] = new JsonArray(new JsonObject { ["type"] = "Mention", ["href"] = mentioned.Uri, ["name"] = "@" + mentioned.UserName })
};
shape(note);
await _host.Deliver(author, mentioned.Mouth, new JsonObject
{
["id"] = $"{post.ObjectURI}/updates/{Guid.NewGuid():N}",
["type"] = "Update",
["actor"] = author.Id,
["to"] = new JsonArray(MastodonHelpers.Public),
["object"] = note
});
}
}
}