Replies a post's author approves (FEP-5624), checked live with PeerTube
PeerTube puts canReply on a video whose comments wait for approval, and answers each comment with ApproveReply. A persona's reply to such a post now waits (privapub.approval: pending), its Create going to the author alone; the author's ApproveReply, signed by the author and naming the post answered, lets it out to its audience with replyApproval, and RejectReply leaves it ours. A null canReply (PeerTube's open comments) says nothing; an empty one refuses. The PeerTube scenario holds a comment for review and approves it through PeerTube's API (28 checks). 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
5d5f1d6909
commit
a3a66b6db2
17 files changed
+256
-25
No files matched your search
@@ -203,5 +203,82 @@ namespace PrivaPub.Tests.Federation
|
||||
await _harness.Deliver(carol, "/human-centipede", Create(carol, asked));
|
||||
Assert.True(await DB.Default.Find<Post>().Match(p => p.ObjectURI == IdOf(asked)).ExecuteAnyAsync(token));
|
||||
}
|
||||
|
||||
// bob's public post whose replies wait for his approval (FEP-5624's canReply, as PeerTube sets it on a video whose
|
||||
// comments are moderated), which alice follows bob to see
|
||||
async Task<(PrivaPub.Federation.Actors.LocalActor Alice, RemoteActor Bob, Post Post, RemoteActor Fan)> Moderated(JsonNode canReply)
|
||||
{
|
||||
var token = TestContext.Current.CancellationToken;
|
||||
var (_, alice) = await _harness.Persona("alice");
|
||||
var bob = new RemoteActor(_harness.Peer, "bob");
|
||||
var fan = new RemoteActor(_harness.Peer, "fan");
|
||||
await Follows(alice.Id, bob);
|
||||
await _harness.FollowedBy(alice, fan);
|
||||
var note = PublicNote(bob, "<p>a moderated post</p>");
|
||||
note["canReply"] = canReply;
|
||||
await _harness.Deliver(bob, "/human-centipede", Create(bob, note));
|
||||
var post = await DB.Default.Find<Post>().Match(p => p.ObjectURI == IdOf(note)).ExecuteSingleAsync(token);
|
||||
return (alice, bob, post, fan);
|
||||
}
|
||||
|
||||
Task Answer(RemoteActor from, string type, Post reply, string inReplyTo) => _harness.Deliver(from, "/human-centipede", new JsonObject
|
||||
{
|
||||
["id"] = NewId(from, "approvals"), ["type"] = type, ["actor"] = from.Id, ["object"] = reply.ObjectURI, ["inReplyTo"] = inReplyTo
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public async Task A_reply_its_author_approves_goes_to_that_author_alone_then_out_with_the_approval()
|
||||
{
|
||||
var token = TestContext.Current.CancellationToken;
|
||||
var (alice, bob, post, fan) = await Moderated("https://www.w3.org/ns/activitystreams#Public");
|
||||
|
||||
var reply = (await _harness.Statuses.Publish(alice, new StatusDraft { Text = "a comment to approve", InReplyTo = post.ID }, token)).Post;
|
||||
|
||||
Assert.Equal(ApprovalState.Pending, reply.Approval);
|
||||
var asked = Assert.Single(await To(bob), a => a["type"]!.GetValue<string>() == "Create");
|
||||
Assert.Equal(reply.ObjectURI, asked["object"]!["id"]!.GetValue<string>());
|
||||
Assert.DoesNotContain(await To(bob), a => a["type"]!.GetValue<string>() == "ReplyRequest");
|
||||
Assert.DoesNotContain(await ToShared(fan), a => a["type"]!.GetValue<string>() == "Create");
|
||||
|
||||
var approval = NewId(bob, "approve-reply");
|
||||
await _harness.Deliver(bob, "/human-centipede", new JsonObject
|
||||
{
|
||||
["id"] = approval, ["type"] = "ApproveReply", ["actor"] = bob.Id, ["object"] = reply.ObjectURI, ["inReplyTo"] = post.ObjectURI
|
||||
});
|
||||
|
||||
var after = await DB.Default.Find<Post>().OneAsync(reply.ID, token);
|
||||
Assert.Equal((ApprovalState.Accepted, approval), (after.Approval, after.ReplyApprovalURI));
|
||||
var create = Assert.Single(await ToShared(fan), a => a["type"]!.GetValue<string>() == "Create");
|
||||
Assert.Equal(approval, create["object"]!["replyApproval"]!.GetValue<string>());
|
||||
Assert.Null(create["object"]!["replyAuthorization"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_refused_reply_stays_ours_alone_and_an_approval_from_elsewhere_changes_nothing()
|
||||
{
|
||||
var token = TestContext.Current.CancellationToken;
|
||||
var (alice, bob, post, fan) = await Moderated("https://www.w3.org/ns/activitystreams#Public");
|
||||
var reply = (await _harness.Statuses.Publish(alice, new StatusDraft { Text = "a comment to refuse", InReplyTo = post.ID }, token)).Post;
|
||||
|
||||
// an approval by someone else than the post's author, or naming another post, is no approval
|
||||
await Answer(new RemoteActor(_harness.Peer, "mallory"), "ApproveReply", reply, post.ObjectURI);
|
||||
await Answer(bob, "ApproveReply", reply, "https://elsewhere.invalid/post");
|
||||
Assert.Equal(ApprovalState.Pending, (await DB.Default.Find<Post>().OneAsync(reply.ID, token)).Approval);
|
||||
|
||||
await Answer(bob, "RejectReply", reply, post.ObjectURI);
|
||||
Assert.Equal(ApprovalState.Rejected, (await DB.Default.Find<Post>().OneAsync(reply.ID, token)).Approval);
|
||||
Assert.DoesNotContain(await ToShared(fan), a => a["type"]!.GetValue<string>() == "Create");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Only_those_its_canReply_names_or_it_mentions_may_reply()
|
||||
{
|
||||
var (alice, _, post, _) = await Moderated(new JsonArray());
|
||||
|
||||
var refused = await _harness.Statuses.Publish(alice, new StatusDraft { Text = "nobody asked me", InReplyTo = post.ID },
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(422, refused.Status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,26 @@ namespace PrivaPub.Tests.Federation
|
||||
Assert.Equal(["https://privapub.test/peasants/flor_pp98"], note.Cc);
|
||||
}
|
||||
|
||||
// PeerTube's video whose comments wait for approval names who may comment (FEP-5624), apart from GoToSocial's policy
|
||||
[Fact]
|
||||
public void Reads_who_may_reply_pending_approval()
|
||||
{
|
||||
var video = NoteParser.Parse(JsonNode.Parse("""
|
||||
{"id":"https://peertube.test/videos/watch/1","type":"Video","name":"a video","attributedTo":["https://peertube.test/accounts/pt"],
|
||||
"canReply":"https://www.w3.org/ns/activitystreams#Public","commentsEnabled":true}
|
||||
"""));
|
||||
var open = NoteParser.Parse(JsonNode.Parse("""
|
||||
{"id":"https://peertube.test/videos/watch/2","type":"Video","name":"an open video","attributedTo":["https://peertube.test/accounts/pt"],
|
||||
"canReply":null,"commentsPolicy":1}
|
||||
"""));
|
||||
var note = NoteParser.Parse(JsonNode.Parse(MastodonNote));
|
||||
|
||||
Assert.Equal(["https://www.w3.org/ns/activitystreams#Public"], video.ReplyApprovals.Manual);
|
||||
Assert.Null(video.ReplyPolicy);
|
||||
Assert.Null(open.ReplyApprovals);
|
||||
Assert.Null(note.ReplyApprovals);
|
||||
}
|
||||
|
||||
// Pleroma's edited post carries its earlier versions (formerRepresentations, newest first)
|
||||
[Fact]
|
||||
public void Reads_the_earlier_versions_of_a_pleroma_edit()
|
||||
|
||||
@@ -65,6 +65,8 @@ namespace PrivaPub.Tests.Support
|
||||
new FollowHandler(Db, Local, Remote, Delivery),
|
||||
new AcceptHandler(Db, Local, Quotes, Approvals, Participations, Relays, Queue),
|
||||
new RejectHandler(Db, Local, Quotes, Approvals, Participations, Relays),
|
||||
new ReplyApprovalHandler(Approvals),
|
||||
new ReplyRejectionHandler(Approvals),
|
||||
new UndoHandler(Db, Local, Reactions),
|
||||
new LikeHandler(Db, Reactions),
|
||||
new EmojiReactHandler(Db, Reactions),
|
||||
|
||||
Reference in new issue
Block a user