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
@@ -27,7 +27,9 @@ namespace PrivaPub.Domain.Statuses
|
||||
// GoToSocial's interaction policies (canReply, canLike, canAnnounce): who may answer, like or boost a post at once, who
|
||||
// must ask its author first, and who may not. Asked, the author answers Accept with an authorization (`result`), which
|
||||
// the interaction then carries for everyone else (replyAuthorization, likeAuthorization, announceAuthorization), or
|
||||
// Reject. Our own posts state no such policy: anyone may.
|
||||
// Reject. And FEP-5624's canReply (PeerTube's): who may reply, the reply itself going to the author alone, who answers
|
||||
// ApproveReply, which the reply then carries (replyApproval), or RejectReply. Our own posts state no such policy:
|
||||
// anyone may.
|
||||
public interface IInteractionApprovals
|
||||
{
|
||||
Task<QuotePermission> Judge(PostEntity target, LocalActor actor, InteractionKind kind, CancellationToken token);
|
||||
@@ -68,9 +70,20 @@ namespace PrivaPub.Domain.Statuses
|
||||
_ => post.AnnouncePolicy
|
||||
};
|
||||
|
||||
// FEP-5624's canReply, where GoToSocial's policy says nothing of replies
|
||||
static bool ApprovesReplies(PostEntity target, InteractionKind kind) =>
|
||||
kind == InteractionKind.Reply && target.ReplyPolicy == default && target.ReplyApprovals != default;
|
||||
|
||||
public async Task<QuotePermission> Judge(PostEntity target, LocalActor actor, InteractionKind kind, CancellationToken token)
|
||||
{
|
||||
if (!target.IsFederatedCopy || Rule(target, kind) is not { } rule)
|
||||
if (!target.IsFederatedCopy)
|
||||
return QuotePermission.Granted;
|
||||
// those it names, or mentions, may reply, and every reply waits for its author's approval
|
||||
if (ApprovesReplies(target, kind))
|
||||
return await Includes(target.ReplyApprovals.Manual, target, actor, token) || target.Mentions.Any(m => m.ActorURI == actor.Uri)
|
||||
? QuotePermission.AskFirst
|
||||
: QuotePermission.Denied;
|
||||
if (Rule(target, kind) is not { } rule)
|
||||
return QuotePermission.Granted;
|
||||
if (await Includes(rule.Automatic, target, actor, token))
|
||||
return QuotePermission.Granted;
|
||||
@@ -102,6 +115,13 @@ namespace PrivaPub.Domain.Statuses
|
||||
var author = await _dbEntities.ForeignAvatars.Match(f => f.ActorURI == target.ActorURI).ExecuteFirstAsync(token);
|
||||
if (string.IsNullOrEmpty(author?.InboxURL))
|
||||
return;
|
||||
// FEP-5624: the reply itself is the question, to the author alone
|
||||
if (ApprovesReplies(target, kind))
|
||||
{
|
||||
await _delivery.Enqueue(actor, new[] { author.InboxURL }, ActivityPubRenderer.Create(actor, (JsonObject)interaction.DeepClone(), $"create-{localId}"),
|
||||
token);
|
||||
return;
|
||||
}
|
||||
var instrument = (JsonObject)interaction.DeepClone();
|
||||
instrument.Remove("@context");
|
||||
await _delivery.Enqueue(actor, new[] { author.InboxURL }, new JsonObject
|
||||
@@ -177,14 +197,28 @@ namespace PrivaPub.Domain.Statuses
|
||||
await DB.Default.Update<PostEntity>().MatchID(target.ID).Modify(b => b.Inc(p => p.ReblogsCount, -1)).ExecuteAsync(token);
|
||||
return true;
|
||||
}
|
||||
var authorization = Id(answer["result"]);
|
||||
var interactionUri = kind == InteractionKind.Reply ? post.ObjectURI : post.ActivityURI;
|
||||
if (authorization == default || !await Verified(authorization, interactionUri, target, token))
|
||||
return true;
|
||||
post.Approval = ApprovalState.Accepted;
|
||||
post.ApprovalURI = authorization;
|
||||
await DB.Default.Update<PostEntity>().MatchID(post.ID).Modify(p => p.Approval, ApprovalState.Accepted).Modify(p => p.ApprovalURI, authorization)
|
||||
.ExecuteAsync(token);
|
||||
// FEP-5624's ApproveReply is its own stamp, from the author (the delivery's signature says so) and naming the post
|
||||
// the reply answers
|
||||
if (Value(answer, "type") == "ApproveReply")
|
||||
{
|
||||
if (kind != InteractionKind.Reply || Id(answer) is not { } approval || Id(answer["inReplyTo"]) is { } answers && answers != target.ObjectURI)
|
||||
return true;
|
||||
post.Approval = ApprovalState.Accepted;
|
||||
post.ReplyApprovalURI = approval;
|
||||
await DB.Default.Update<PostEntity>().MatchID(post.ID).Modify(p => p.Approval, ApprovalState.Accepted).Modify(p => p.ReplyApprovalURI, approval)
|
||||
.ExecuteAsync(token);
|
||||
}
|
||||
else
|
||||
{
|
||||
var authorization = Id(answer["result"]);
|
||||
var interactionUri = kind == InteractionKind.Reply ? post.ObjectURI : post.ActivityURI;
|
||||
if (authorization == default || !await Verified(authorization, interactionUri, target, token))
|
||||
return true;
|
||||
post.Approval = ApprovalState.Accepted;
|
||||
post.ApprovalURI = authorization;
|
||||
await DB.Default.Update<PostEntity>().MatchID(post.ID).Modify(p => p.Approval, ApprovalState.Accepted).Modify(p => p.ApprovalURI, authorization)
|
||||
.ExecuteAsync(token);
|
||||
}
|
||||
// now it goes where it was meant to, carrying the authorization
|
||||
if (kind == InteractionKind.Reply)
|
||||
{
|
||||
@@ -202,7 +236,7 @@ namespace PrivaPub.Domain.Statuses
|
||||
["to"] = new JsonArray(post.To.Select(t => (JsonNode)t).ToArray()),
|
||||
["cc"] = new JsonArray(post.Cc.Select(c => (JsonNode)c).ToArray()),
|
||||
["object"] = target.ObjectURI,
|
||||
["announceAuthorization"] = authorization
|
||||
["announceAuthorization"] = post.ApprovalURI
|
||||
};
|
||||
await _delivery.EnqueueToFollowers(author, announce, token, string.IsNullOrEmpty(actor.InboxURL) ? default : new[] { actor.InboxURL });
|
||||
return true;
|
||||
|
||||
Reference in new issue
Block a user