Lemmy 1.0's multi-communities and PieFed's feeds (`type: Feed`) resolve as accounts: the feed's `following` is kept
once a day with its counts, each community in it is read in turn, and `/api/v1/accounts/:id/following` lists them.
Lemmy's feed carries the instance's key and its text in `description`; both are taken as they are. Following a feed
answers 422 until the owner decides how a feed is followed.
A post moved to another community (PieFed's Move{object: post, origin, target}), relayed by the community it leaves,
goes there with its thread; a move into a community we host is not taken.
Checked in the pasture: a Lemmy 1.0 feed and a PieFed feed list their communities (ours among them); a PieFed post moved
from its web code arrives moved, with its comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
62189bf11d
commit
3b3f1f6b93
10 files changed
+226
-25
No files matched your search
@@ -412,6 +412,48 @@ namespace PrivaPub.Tests.Federation
|
||||
Assert.Equal(0, (await DB.Default.Find<Post>().OneAsync(reply.ID, token)).FavouritesCount);
|
||||
}
|
||||
|
||||
// PieFed moves a post to another community, and the community it leaves relays the Move: the post's thread goes with it
|
||||
[Fact]
|
||||
public async Task A_post_moved_to_another_community_takes_its_thread_along()
|
||||
{
|
||||
var token = TestContext.Current.CancellationToken;
|
||||
var (root, alice) = await _harness.Persona("alice");
|
||||
var community = new RemoteActor(_harness.Peer, "cats", type: "Group");
|
||||
var other = new RemoteActor(_harness.Peer, "dogs", type: "Group");
|
||||
var poster = new RemoteActor(_harness.Peer, "poster");
|
||||
await _harness.Follows.Follow(root, new FollowForm { AvatarId = alice.Id, Target = community.Id }, token);
|
||||
await DB.Default.Update<Following>().Match(f => f.AvatarId == alice.Id).Modify(f => f.State, FollowState.Accepted).ExecuteAsync(token);
|
||||
async Task<Post> Announced(JsonObject note)
|
||||
{
|
||||
note["audience"] = community.Id;
|
||||
_harness.Peer.Serve(new Uri(IdOf(note)).AbsolutePath, note.ToJsonString());
|
||||
await _harness.Deliver(community, "/human-centipede", Activity(community, "Announce", Create(poster, note)));
|
||||
return await DB.Default.Find<Post>().Match(p => p.ObjectURI == IdOf(note)).ExecuteSingleAsync(token);
|
||||
}
|
||||
var page = PublicNote(poster, "<p>misplaced</p>", community.Id);
|
||||
page["type"] = "Page";
|
||||
page["name"] = "misplaced";
|
||||
var post = await Announced(page);
|
||||
var comment = PublicNote(poster, "<p>a comment</p>", community.Id);
|
||||
comment["inReplyTo"] = IdOf(page);
|
||||
var answer = await Announced(comment);
|
||||
var move = new JsonObject
|
||||
{
|
||||
["id"] = NewId(poster, "moves"), ["type"] = "Move", ["actor"] = poster.Id, ["object"] = post.ObjectURI, ["origin"] = community.Id, ["target"] = other.Id
|
||||
};
|
||||
|
||||
// only the community it leaves speaks for it
|
||||
await _harness.Deliver(community, "/human-centipede", Activity(community, "Announce", new JsonObject
|
||||
{
|
||||
["id"] = NewId(poster, "moves"), ["type"] = "Move", ["actor"] = poster.Id, ["object"] = post.ObjectURI, ["origin"] = other.Id, ["target"] = community.Id
|
||||
}));
|
||||
Assert.Equal(("dropped", "unsupported"), (Processed("Announce").Outcome, Processed("Announce").Reason));
|
||||
await _harness.Deliver(community, "/human-centipede", Activity(community, "Announce", move));
|
||||
Assert.Equal(("accepted", "moved"), (Processed("Announce").Outcome, Processed("Announce").Reason));
|
||||
Assert.Equal(other.Id, (await DB.Default.Find<Post>().OneAsync(post.ID, token)).AudienceURI);
|
||||
Assert.Equal(other.Id, (await DB.Default.Find<Post>().OneAsync(answer.ID, token)).AudienceURI);
|
||||
}
|
||||
|
||||
// Lemmy's moderators lock a community's post and ban members, and the community relays both inside its Announce
|
||||
// (G-0006): a locked post takes no reply, a banned persona posts nothing there, until the Undo
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Models.Jobs;
|
||||
using PrivaPub.Tests.Support;
|
||||
using PrivaPub.Tests.Support.Host;
|
||||
|
||||
using System.Net;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace PrivaPub.Tests.Http
|
||||
{
|
||||
// Lemmy 1.0's multi-communities and PieFed's feeds are `Feed` actors whose `following` is their communities
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class MastodonFeedTests : 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();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_feed_shows_as_an_account_whose_following_is_its_communities()
|
||||
{
|
||||
var token = TestContext.Current.CancellationToken;
|
||||
var reader = await _host.Mastodon("feeder");
|
||||
var cats = new RemoteActor(_peer, "cats", type: "Group");
|
||||
var dogs = new RemoteActor(_peer, "dogs", type: "Group");
|
||||
var name = $"feed{Guid.NewGuid():N}"[..12];
|
||||
var feed = $"{_peer.A}/m/{name}";
|
||||
// as Lemmy serves one: the instance's inbox and key, its text in `description`
|
||||
_peer.Serve($"/m/{name}", new JsonObject
|
||||
{
|
||||
["@context"] = new JsonArray("https://join-lemmy.org/context.json", "https://www.w3.org/ns/activitystreams"),
|
||||
["type"] = "Feed", ["id"] = feed, ["inbox"] = $"{_peer.A}/inbox",
|
||||
["publicKey"] = new JsonObject { ["id"] = $"{_peer.A}/#main-key", ["owner"] = $"{_peer.A}/", ["publicKeyPem"] = "-----BEGIN PUBLIC KEY-----\n-----END PUBLIC KEY-----\n" },
|
||||
["following"] = $"{feed}/following", ["preferredUsername"] = name, ["name"] = "Pets", ["description"] = "every pet",
|
||||
["attributedTo"] = $"{_peer.A}/users/owner"
|
||||
}.ToJsonString());
|
||||
_peer.Serve($"/m/{name}/following", new JsonObject
|
||||
{
|
||||
["type"] = "Collection", ["id"] = $"{feed}/following", ["totalItems"] = 2, ["items"] = new JsonArray(cats.Id, dogs.Id)
|
||||
}.ToJsonString());
|
||||
|
||||
var account = await _host.Get<IRemoteActorService>().GetActor(feed, refresh: true, token);
|
||||
Assert.NotNull(account);
|
||||
await _host.Run(j => j.Kind == JobKind.CountAccount && j.Payload == feed, token);
|
||||
|
||||
var shown = (await reader.Client.Get($"/api/v1/accounts/{account.ID}")).Ok().Body;
|
||||
Assert.Equal(("Pets", 2), (shown.Text("display_name"), shown.Number("following_count")));
|
||||
Assert.Contains("every pet", shown.Text("note"));
|
||||
var communities = (await reader.Client.Get($"/api/v1/accounts/{account.ID}/following")).Ok().Array;
|
||||
Assert.Equal(new[] { cats.Id, dogs.Id }, communities.Select(c => c.Text("uri")));
|
||||
Assert.All(communities, c => Assert.True(c!["group"]!.GetValue<bool>()));
|
||||
Assert.Equal(HttpStatusCode.UnprocessableEntity, (await reader.Client.Post($"/api/v1/accounts/{account.ID}/follow")).Status);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user