P6 done: personas can be quoted, under the owner's default of anyone, automatically
- Our public and unlisted posts state interactionPolicy.canQuote. The policy comes from the post, then the persona
(`source[quote_policy]`: public, followers or nobody; default public as the owner chose), and is always nobody for
followers-only posts and DMs.
- QuoteRequests are answered with Accept{result} naming a parrot-licence at /peasants/{name}/parrot-licences/{id}
(the route name the owner chose), or with Reject. Followers-only checks that the requester really follows.
- A licence is a QuoteAuthorization naming both posts; revoking it (POST /api/v1/statuses/:id/quotes/:quoting_id/revoke)
marks it 410, sends Delete{licence} to the quoter and the persona's followers, and revokes our own copy of the quote.
- A quote that arrives with one of our licences is accepted only if that licence is ours, unrevoked and names exactly
that quoting post. One persona quoting another gets a licence too.
- Mastodon API: quote_approval for our posts (automatic, followers, current_user), `quote_approval_policy` when posting,
PUT /api/v1/statuses/:id/interaction_policy, `source.quote_policy`.
Checked live: GoToSocial still accepts our posts with the policy stated, and leaves likes, replies and boosts open.
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
f9658a8e7a
commit
e6729c77e7
19 files changed
+372
-17
No files matched your search
@@ -93,6 +93,7 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
Privacy = actor.Settings.DefaultVisibility ?? "public",
|
||||
Sensitive = actor.Settings.DefaultSensitive,
|
||||
Language = actor.Settings.DefaultLanguage,
|
||||
QuotePolicy = actor.Settings.QuotePolicy ?? QuotePolicies.Public,
|
||||
Note = actor.Summary ?? string.Empty,
|
||||
Fields = actor.Fields.Select(f => new Field { Name = f.Key, Value = f.Value }).ToList(),
|
||||
FollowRequestsCount = (int)await DB.Default.CountAsync<Follower>(f => f.LocalActorId == actor.Id && !f.IsAccepted, token)
|
||||
@@ -238,6 +239,10 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
return status;
|
||||
}
|
||||
|
||||
var localAuthors = all.Where(p => !p.IsFederatedCopy && p.GroupUserId != default).Select(p => p.GroupUserId).Distinct().ToList();
|
||||
var followsAuthor = viewerId == default || localAuthors.Count == 0
|
||||
? new HashSet<string>()
|
||||
: await FollowedLocalAuthors(viewerId, localAuthors, token);
|
||||
var quotedIds = nested ? new List<string>() : all.Where(p => p.QuoteState == QuoteState.Accepted && p.QuotedPostId != default).Select(p => p.QuotedPostId).Distinct().ToList();
|
||||
var quotedPosts = new List<PostEntity>();
|
||||
foreach (var quotedPost in quotedIds.Count == 0 ? new List<PostEntity>() : await _dbEntities.Posts.Match(p => quotedIds.Contains(p.ID) && !p.DeletedAt.HasValue).ExecuteAsync(token))
|
||||
@@ -250,7 +255,7 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
void Quote(Status status, PostEntity post)
|
||||
{
|
||||
status.QuotesCount = post.QuotesCount;
|
||||
status.QuoteApproval = Approval(post, viewerId);
|
||||
status.QuoteApproval = post.IsFederatedCopy ? Approval(post, viewerId) : LocalApproval(post, viewerId, followsAuthor.Contains(post.GroupUserId));
|
||||
if (post.QuoteState == QuoteState.None)
|
||||
return;
|
||||
var state = post.QuoteState.ToString().ToLowerInvariant();
|
||||
@@ -300,6 +305,27 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
return document.Body!.InnerHtml;
|
||||
}
|
||||
|
||||
async Task<HashSet<string>> FollowedLocalAuthors(string viewerId, List<string> authorIds, CancellationToken token)
|
||||
{
|
||||
var uris = new Dictionary<string, string>();
|
||||
foreach (var avatar in await _dbEntities.Avatars.Match(a => authorIds.Contains(a.ID)).ExecuteAsync(token))
|
||||
uris[_localActors.FromAvatar(avatar).Uri] = avatar.ID;
|
||||
var targets = uris.Keys.ToList();
|
||||
return (await _dbEntities.Followings.Match(f => f.AvatarId == viewerId && targets.Contains(f.TargetActorURI) && f.State == FollowState.Accepted).ExecuteAsync(token))
|
||||
.Select(f => uris[f.TargetActorURI]).ToHashSet();
|
||||
}
|
||||
|
||||
static QuoteApprovalEntity LocalApproval(PostEntity post, string viewerId, bool viewerFollows)
|
||||
{
|
||||
var policy = ActivityPubRenderer.QuotableBy(post);
|
||||
var everyone = policy switch { QuotePolicies.Public => new List<string> { "public" }, QuotePolicies.Followers => new List<string> { "followers" }, _ => new List<string>() };
|
||||
var current = viewerId == default ? "unknown"
|
||||
: viewerId == post.GroupUserId && post.Visibility is PostVisibility.Public or PostVisibility.Unlisted ? "automatic"
|
||||
: policy == QuotePolicies.Public || policy == QuotePolicies.Followers && viewerFollows ? "automatic"
|
||||
: "denied";
|
||||
return new QuoteApprovalEntity { Automatic = everyone, CurrentUser = current };
|
||||
}
|
||||
|
||||
static QuoteApprovalEntity Approval(PostEntity post, string viewerId)
|
||||
{
|
||||
if (!post.IsFederatedCopy || post.Visibility is not (PostVisibility.Public or PostVisibility.Unlisted))
|
||||
|
||||
Reference in new issue
Block a user