GoToSocial's interaction policies are honoured
A remote post's canReply, canLike and canAnnounce (with the older always and approvalRequired) are kept beside canQuote and judged for each persona: let in at once when the rule names the public, the persona, the author's followers while it follows the author, or the accounts the author follows while the author follows it; asked first when only the manual list names it; refused (422) otherwise. Asked first, a ReplyRequest, LikeRequest or AnnounceRequest with the interaction as its instrument goes to the author alone, and the interaction waits (privapub.approval: pending). The author's Accept brings an authorization, verified on the author's origin as naming the interaction and the post; the reply then goes out with replyAuthorization, the boost with announceAuthorization, the like with likeAuthorization. A Reject leaves the reply ours alone and takes a like or a boost back. As a third party, a reply a policy does not let in at once is kept only with an authorization that verifies. Clients see the rules as GoToSocial's interaction_policy. Checked live against GoToSocial 0.22.1: the scenario's nine new checks pass (64 in all), a reply and a like approved through its interaction requests and a boost refused. 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
c65a44ba88
commit
5860b71223
21 files changed
+687
-26
No files matched your search
@@ -17,12 +17,14 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
readonly DbEntities _dbEntities;
|
||||
readonly ILocalActorService _localActors;
|
||||
readonly IQuoteService _quotes;
|
||||
readonly IInteractionApprovals _approvals;
|
||||
|
||||
public AcceptHandler(DbEntities dbEntities, ILocalActorService localActors, IQuoteService quotes)
|
||||
public AcceptHandler(DbEntities dbEntities, ILocalActorService localActors, IQuoteService quotes, IInteractionApprovals approvals = default)
|
||||
{
|
||||
_dbEntities = dbEntities;
|
||||
_localActors = localActors;
|
||||
_quotes = quotes;
|
||||
_approvals = approvals;
|
||||
}
|
||||
|
||||
public virtual string Type => "Accept";
|
||||
@@ -34,6 +36,11 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
Arrival.Accept("quote-answer");
|
||||
return;
|
||||
}
|
||||
if (_approvals != default && await _approvals.Answered(activity, actor, accepted: Type == "Accept", token))
|
||||
{
|
||||
Arrival.Accept("interaction-answer");
|
||||
return;
|
||||
}
|
||||
var following = await FindFollowing(activity["object"], actor, _dbEntities, _localActors, token);
|
||||
if (following == default)
|
||||
{
|
||||
@@ -72,7 +79,8 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
|
||||
public class RejectHandler : AcceptHandler
|
||||
{
|
||||
public RejectHandler(DbEntities dbEntities, ILocalActorService localActors, IQuoteService quotes) : base(dbEntities, localActors, quotes)
|
||||
public RejectHandler(DbEntities dbEntities, ILocalActorService localActors, IQuoteService quotes, IInteractionApprovals approvals = default)
|
||||
: base(dbEntities, localActors, quotes, approvals)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -40,10 +40,13 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
readonly IPollService _polls;
|
||||
readonly ILinkPreviews _previews;
|
||||
readonly IQuoteService _quotes;
|
||||
readonly IInteractionApprovals _approvals;
|
||||
|
||||
public CreateHandler(DbEntities dbEntities, ILocalActorService localActors, IRemoteActorService remoteActors, IDeliveryService delivery,
|
||||
IDomainBlocks domainBlocks, IFanout fanout, IRemotePosts remotePosts, IGroupDistributor groups, IObjectRecords records, IPollService polls, ILinkPreviews previews, IQuoteService quotes)
|
||||
IDomainBlocks domainBlocks, IFanout fanout, IRemotePosts remotePosts, IGroupDistributor groups, IObjectRecords records, IPollService polls, ILinkPreviews previews, IQuoteService quotes,
|
||||
IInteractionApprovals approvals = default)
|
||||
{
|
||||
_approvals = approvals;
|
||||
_quotes = quotes;
|
||||
_previews = previews;
|
||||
_records = records;
|
||||
@@ -141,6 +144,12 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
var parent = string.IsNullOrEmpty(note.InReplyTo)
|
||||
? default
|
||||
: await _dbEntities.Posts.Match(p => p.ObjectURI == note.InReplyTo && !p.DeletedAt.HasValue).ExecuteFirstAsync(token);
|
||||
// a reply its parent's author has not let in (GoToSocial's canReply), which a third party must not show
|
||||
if (_approvals != default && parent != default && !await _approvals.MayReply(note, author, parent, token))
|
||||
{
|
||||
Arrival.Drop("reply-not-authorized");
|
||||
return;
|
||||
}
|
||||
var circle = localTargets.FirstOrDefault(t => t is { Kind: LocalActorKind.Group, IsCircle: true });
|
||||
if (circle != default)
|
||||
{
|
||||
|
||||
@@ -30,6 +30,9 @@ namespace PrivaPub.Federation.Inbox
|
||||
{
|
||||
post.Poll = note.Poll ?? post.Poll;
|
||||
post.QuotePolicy = note.QuotePolicy;
|
||||
post.ReplyPolicy = note.ReplyPolicy;
|
||||
post.LikePolicy = note.LikePolicy;
|
||||
post.AnnouncePolicy = note.AnnouncePolicy;
|
||||
post.Video = note.Video ?? post.Video;
|
||||
post.Audio = note.Audio ?? post.Audio;
|
||||
post.Event = note.Event ?? post.Event;
|
||||
|
||||
@@ -107,6 +107,9 @@ namespace PrivaPub.Federation.Inbox
|
||||
Source = note.Source,
|
||||
Poll = note.Poll,
|
||||
QuotePolicy = note.QuotePolicy,
|
||||
ReplyPolicy = note.ReplyPolicy,
|
||||
LikePolicy = note.LikePolicy,
|
||||
AnnouncePolicy = note.AnnouncePolicy,
|
||||
Emojis = note.Emojis.ToList(),
|
||||
CoverURL = note.CoverURL,
|
||||
Link = note.Link,
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace PrivaPub.Federation.Objects
|
||||
public string Context { get; init; }
|
||||
public string Audience { get; init; }
|
||||
public string QuoteUri { get; init; }
|
||||
public string ReplyAuthorization { get; init; }//the parent author's ReplyAuthorization (approvedBy before GoToSocial 0.21)
|
||||
public string QuoteAuthorization { get; init; }
|
||||
public bool QuotesByConsent { get; init; }//FEP-044f `quote`, as opposed to the legacy keys that ask nobody
|
||||
public bool QuoteDeleted { get; init; }
|
||||
@@ -38,6 +39,9 @@ namespace PrivaPub.Federation.Objects
|
||||
public PostSource Source { get; init; }
|
||||
public PostPoll Poll { get; init; }
|
||||
public InteractionRule QuotePolicy { get; init; }
|
||||
public InteractionRule ReplyPolicy { get; init; }
|
||||
public InteractionRule LikePolicy { get; init; }
|
||||
public InteractionRule AnnouncePolicy { get; init; }
|
||||
public IReadOnlyList<CustomEmoji> Emojis { get; init; } = Array.Empty<CustomEmoji>();
|
||||
public string CoverURL { get; init; }
|
||||
public PostLink Link { get; init; }
|
||||
@@ -86,6 +90,7 @@ namespace PrivaPub.Federation.Objects
|
||||
Context = Id(note["context"]) ?? Value(note, "conversation"),
|
||||
Audience = Id(note["audience"]),
|
||||
QuoteUri = Id(note["quote"]) ?? Value(note, "quoteUrl") ?? Value(note, "quoteUri") ?? Value(note, "_misskey_quote") ?? QuoteLink(note),
|
||||
ReplyAuthorization = Id(note["replyAuthorization"]) ?? Id(note["approvedBy"]),
|
||||
QuoteAuthorization = Id(note["quoteAuthorization"]),
|
||||
QuotesByConsent = note.ContainsKey("quote"),
|
||||
QuoteDeleted = note["quote"] is JsonObject quoted && Value(quoted, "type") == "Tombstone",
|
||||
@@ -107,6 +112,9 @@ namespace PrivaPub.Federation.Objects
|
||||
Source = ObjectShapes.Source(note),
|
||||
Poll = ObjectShapes.Poll(note),
|
||||
QuotePolicy = ObjectShapes.QuotePolicy(note),
|
||||
ReplyPolicy = ObjectShapes.Policy(note, "canReply"),
|
||||
LikePolicy = ObjectShapes.Policy(note, "canLike"),
|
||||
AnnouncePolicy = ObjectShapes.Policy(note, "canAnnounce"),
|
||||
Emojis = ObjectShapes.Emojis(note["tag"]),
|
||||
CoverURL = ObjectShapes.Cover(note),
|
||||
Link = ObjectShapes.Link(note),
|
||||
|
||||
@@ -98,9 +98,13 @@ namespace PrivaPub.Federation.Objects
|
||||
.Take(MaxEmojis)
|
||||
.ToList();
|
||||
|
||||
public static InteractionRule QuotePolicy(JsonObject note)
|
||||
public static InteractionRule QuotePolicy(JsonObject note) => Policy(note, "canQuote");
|
||||
|
||||
// one rule of a post's interactionPolicy (GoToSocial, FEP-044f): who may, automatically or once asked; the old names
|
||||
// (always, approvalRequired) too. A rule left out means anyone, automatically
|
||||
public static InteractionRule Policy(JsonObject note, string rule)
|
||||
{
|
||||
if (note["interactionPolicy"] is not JsonObject policy || policy["canQuote"] is not JsonObject canQuote)
|
||||
if (note["interactionPolicy"] is not JsonObject policy || policy[rule] is not JsonObject allowed)
|
||||
return default;
|
||||
static List<string> Who(JsonNode node) => node switch
|
||||
{
|
||||
@@ -110,8 +114,8 @@ namespace PrivaPub.Federation.Objects
|
||||
};
|
||||
return new InteractionRule
|
||||
{
|
||||
Automatic = Who(canQuote["automaticApproval"] ?? canQuote["always"]),
|
||||
Manual = Who(canQuote["manualApproval"] ?? canQuote["approvalRequired"])
|
||||
Automatic = Who(allowed["automaticApproval"] ?? allowed["always"]),
|
||||
Manual = Who(allowed["manualApproval"] ?? allowed["approvalRequired"])
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,15 @@ namespace PrivaPub.Federation.Rendering
|
||||
["canQuote"] = new JsonObject { ["@id"] = "gts:canQuote", ["@type"] = "@id" },
|
||||
["automaticApproval"] = new JsonObject { ["@id"] = "gts:automaticApproval", ["@type"] = "@id" },
|
||||
["manualApproval"] = new JsonObject { ["@id"] = "gts:manualApproval", ["@type"] = "@id" },
|
||||
["LikeRequest"] = "gts:LikeRequest",
|
||||
["ReplyRequest"] = "gts:ReplyRequest",
|
||||
["AnnounceRequest"] = "gts:AnnounceRequest",
|
||||
["LikeAuthorization"] = "gts:LikeApproval",
|
||||
["ReplyAuthorization"] = "gts:ReplyAuthorization",
|
||||
["AnnounceAuthorization"] = "gts:AnnounceAuthorization",
|
||||
["likeAuthorization"] = new JsonObject { ["@id"] = "gts:likeAuthorization", ["@type"] = "@id" },
|
||||
["replyAuthorization"] = new JsonObject { ["@id"] = "gts:replyAuthorization", ["@type"] = "@id" },
|
||||
["announceAuthorization"] = new JsonObject { ["@id"] = "gts:announceAuthorization", ["@type"] = "@id" },
|
||||
["litepub"] = "http://litepub.social/ns#",
|
||||
["EmojiReact"] = "litepub:EmojiReact",
|
||||
["focalPoint"] = new JsonObject { ["@container"] = "@list", ["@id"] = "toot:focalPoint" },
|
||||
@@ -329,6 +338,9 @@ namespace PrivaPub.Federation.Rendering
|
||||
}
|
||||
if (!string.IsNullOrEmpty(inReplyTo))
|
||||
note["inReplyTo"] = inReplyTo;
|
||||
// the parent's author let this reply in (GoToSocial's interaction policies)
|
||||
if (!string.IsNullOrEmpty(inReplyTo) && !string.IsNullOrEmpty(post.ApprovalURI))
|
||||
note["replyAuthorization"] = post.ApprovalURI;
|
||||
if (post.EditedAt.HasValue)
|
||||
note["updated"] = Timestamp(post.EditedAt.Value);
|
||||
return note;
|
||||
|
||||
Reference in new issue
Block a user