A community's vote on our reply counts, followed or not
Lemmy sends a vote to the community alone, which relays it to the post's server. A persona's reply in a thread of a community nobody here follows lost its Lemmy upvotes: the relay was dropped as "not followed". Such a relay is now taken when the vote (or its undo) is on one of our posts in a thread rooted in that community; from any other unfollowed group it is still dropped. For that, a post keeps the community its `audience` names (FEP-1b12) however it arrived, fetched for a thread as well as announced. Found by the town's village (p191: 1 like counted of 2). 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
8686275b69
commit
7e37f56db6
4 files changed
+62
-2
No files matched your search
+3
-1
@@ -104,7 +104,9 @@ A group is either a **community** or a **circle**.
|
||||
of a member's server, and that copy names the member (or the members on that server); anyone else gets 404. A
|
||||
Mastodon member's replies reach only the people they mention.
|
||||
- Announces from **remote** groups (Lemmy communities) are followed through to the activity: the object is fetched
|
||||
from its own origin, never taken from the announce.
|
||||
from its own origin, never taken from the announce. They are taken from a group someone here follows, and also when
|
||||
they relay a vote on one of our posts in a thread of that group (Lemmy sends a vote to the community alone). A post
|
||||
keeps the group its `audience` names, however it arrived.
|
||||
- A remote community's **moderation** reaches the posts it holds. A removal (`Announce{Delete}`) is believed once the
|
||||
post's origin answers it gone. A lock (`Announce{Lock}`, or `commentsEnabled: false` on the post) refuses replies,
|
||||
ours included, until `Undo{Lock}`. A ban of a persona (`Announce{Block}` with the community as `target`) shows as
|
||||
|
||||
@@ -209,6 +209,36 @@ namespace PrivaPub.Tests.Federation
|
||||
Assert.False(await DB.Default.Find<Favourite>().Match(f => f.PostId == post.ID).ExecuteAnyAsync(token));
|
||||
}
|
||||
|
||||
// Lemmy sends a vote to the community alone: a vote on our reply in a thread of a community nobody here follows still
|
||||
// reaches us through it, and only through that community (found by the town's village)
|
||||
[Fact]
|
||||
public async Task A_vote_on_our_reply_in_an_unfollowed_communitys_thread_counts_through_that_community_only()
|
||||
{
|
||||
var token = TestContext.Current.CancellationToken;
|
||||
var (_, 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");
|
||||
var voter = new RemoteActor(_harness.Peer, "voter");
|
||||
var page = PublicNote(poster, "<p>a thread</p>", community.Id);
|
||||
page["type"] = "Page";
|
||||
page["name"] = "a thread";
|
||||
page["audience"] = community.Id;
|
||||
_harness.Peer.Serve(new Uri(IdOf(page)).AbsolutePath, page.ToJsonString());
|
||||
var root = await _harness.RemotePosts.StoreContext(IdOf(page), 0, token);
|
||||
Assert.Equal(community.Id, root.AudienceURI);
|
||||
var reply = (await _harness.Statuses.Publish(alice, new StatusDraft { Text = "my answer", InReplyTo = root.ID }, token)).Post;
|
||||
var like = new JsonObject { ["id"] = NewId(voter, "likes"), ["type"] = "Like", ["actor"] = voter.Id, ["object"] = reply.ObjectURI };
|
||||
|
||||
await _harness.Deliver(other, "/human-centipede", Activity(other, "Announce", like));
|
||||
Assert.Equal(("dropped", "not-followed"), (Processed("Announce").Outcome, Processed("Announce").Reason));
|
||||
await _harness.Deliver(community, "/human-centipede", Activity(community, "Announce", like));
|
||||
Assert.Equal("accepted", Processed("Announce").Outcome);
|
||||
Assert.Equal(1, (await DB.Default.Find<Post>().OneAsync(reply.ID, token)).FavouritesCount);
|
||||
await _harness.Deliver(community, "/human-centipede", Activity(community, "Announce", Activity(voter, "Undo", like)));
|
||||
Assert.Equal(0, (await DB.Default.Find<Post>().OneAsync(reply.ID, token)).FavouritesCount);
|
||||
}
|
||||
|
||||
// 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]
|
||||
|
||||
@@ -143,7 +143,8 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
{
|
||||
Arrival.About(Value(inner, "type"));
|
||||
if (group.AvatarType != AvatarType.Group
|
||||
|| !await _dbEntities.Followings.Match(f => f.TargetActorURI == group.ActorURI && f.State == FollowState.Accepted).ExecuteAnyAsync(token))
|
||||
|| !await _dbEntities.Followings.Match(f => f.TargetActorURI == group.ActorURI && f.State == FollowState.Accepted).ExecuteAnyAsync(token)
|
||||
&& !await OurPostInItsThread(inner, group, token))
|
||||
{
|
||||
Arrival.Drop("not-followed");
|
||||
return;
|
||||
@@ -230,6 +231,28 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
}
|
||||
}
|
||||
|
||||
// A vote (or its undoing) on one of our posts in a thread the community holds: the community relays it to the post's
|
||||
// server whether or not anyone here follows the community (Lemmy sends a vote to the community alone)
|
||||
async Task<bool> OurPostInItsThread(JsonNode inner, ForeignAvatar group, CancellationToken token)
|
||||
{
|
||||
var target = Value(inner, "type") switch
|
||||
{
|
||||
"Like" or "Dislike" => Id(inner["object"]),
|
||||
"Undo" when (inner["object"] as JsonObject)?["type"]?.GetValue<string>() is "Like" or "Dislike" => Id(inner["object"]?["object"]),
|
||||
_ => default
|
||||
};
|
||||
var post = target == default
|
||||
? default
|
||||
: await _dbEntities.Posts.Match(p => p.ObjectURI == target && !p.IsFederatedCopy && !p.DeletedAt.HasValue).ExecuteFirstAsync(token);
|
||||
for (var depth = 0; post != default && depth < RemotePosts.MaxDepth; depth++)
|
||||
{
|
||||
if (post.AudienceURI == group.ActorURI)
|
||||
return true;
|
||||
post = string.IsNullOrEmpty(post.AnsweringToPostId) ? default : await _dbEntities.Posts.MatchID(post.AnsweringToPostId).ExecuteFirstAsync(token);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A community's moderators lock one of its posts, or unlock it: no more replies, ours included. The community vouches
|
||||
// for what is done to its own posts.
|
||||
async Task Lock(string objectUri, ForeignAvatar group, bool locked, CancellationToken token)
|
||||
|
||||
@@ -94,6 +94,11 @@ namespace PrivaPub.Federation.Inbox
|
||||
Cc = cc.ToList(),
|
||||
Url = note.Url,
|
||||
ContextURI = note.Context,
|
||||
// the community it lives in (FEP-1b12), as the post itself names it: a post fetched for a thread knows its
|
||||
// community as well as one its community announced
|
||||
AudienceURI = note.Audience != default && note.Audience != author.ActorURI && !note.Audience.EndsWith("/followers", StringComparison.Ordinal)
|
||||
? note.Audience
|
||||
: default,
|
||||
QuoteURI = note.QuoteUri,
|
||||
ObjectType = note.Type,
|
||||
Title = note.Title,
|
||||
|
||||
Reference in new issue
Block a user