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
@@ -199,6 +199,33 @@ namespace PrivaPub.Federation.Controllers
|
||||
return Activity(note);
|
||||
}
|
||||
|
||||
[HttpGet, Route("{actor}/parrot-licences/{licenceId}")]
|
||||
public async Task<IActionResult> ParrotLicence(string actor, string licenceId, CancellationToken token)
|
||||
{
|
||||
var author = await _localActors.FindByUserName(actor, token);
|
||||
var licence = author == default ? default : await DB.Default.Find<QuoteLicence>().Match(l => l.ID == licenceId && l.AuthorAvatarId == author.Id).ExecuteFirstAsync(token);
|
||||
var quoted = licence == default ? default : await _dbEntities.Posts.MatchID(licence.PostId).ExecuteFirstAsync(token);
|
||||
if (quoted == default)
|
||||
return NotFound();
|
||||
var uri = Domain.Statuses.QuoteService.LicenceUri(author, licence.ID);
|
||||
if (licence.RevokedAt.HasValue || quoted.DeletedAt.HasValue)
|
||||
return new ContentResult
|
||||
{
|
||||
Content = new JsonObject { ["@context"] = ActivityPubRenderer.ActivityStreams, ["id"] = uri, ["type"] = "Tombstone", ["formerType"] = "QuoteAuthorization" }.ToJsonString(),
|
||||
ContentType = ActivityContentType,
|
||||
StatusCode = StatusCodes.Status410Gone
|
||||
};
|
||||
return Activity(new JsonObject
|
||||
{
|
||||
["@context"] = ActivityPubRenderer.Context(),
|
||||
["id"] = uri,
|
||||
["type"] = "QuoteAuthorization",
|
||||
["attributedTo"] = author.Uri,
|
||||
["interactingObject"] = licence.QuotingObjectURI,
|
||||
["interactionTarget"] = quoted.ObjectURI
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet, Route("{actor}/grunts/{activityId}")]
|
||||
public async Task<IActionResult> Grunt(string actor, string activityId, CancellationToken token)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using PrivaPub.Domain.Statuses;
|
||||
using PrivaPub.Models.User;
|
||||
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace PrivaPub.Federation.Inbox.Handlers
|
||||
{
|
||||
public class QuoteRequestHandler : IActivityHandler
|
||||
{
|
||||
readonly IQuoteService _quotes;
|
||||
|
||||
public QuoteRequestHandler(IQuoteService quotes)
|
||||
{
|
||||
_quotes = quotes;
|
||||
}
|
||||
|
||||
public string Type => "QuoteRequest";
|
||||
|
||||
public Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token) => _quotes.ReceiveRequest(activity, actor, token);
|
||||
}
|
||||
}
|
||||
@@ -43,11 +43,18 @@ namespace PrivaPub.Federation.Rendering
|
||||
["quoteAuthorization"] = new JsonObject { ["@id"] = "https://w3id.org/fep/044f#quoteAuthorization", ["@type"] = "@id" },
|
||||
["QuoteRequest"] = "https://w3id.org/fep/044f#QuoteRequest",
|
||||
["QuoteAuthorization"] = "https://w3id.org/fep/044f#QuoteAuthorization",
|
||||
["interactingObject"] = new JsonObject { ["@id"] = "gts:interactingObject", ["@type"] = "@id" },
|
||||
["interactionTarget"] = new JsonObject { ["@id"] = "gts:interactionTarget", ["@type"] = "@id" },
|
||||
["misskey"] = "https://misskey-hub.net/ns#",
|
||||
["_misskey_quote"] = "misskey:_misskey_quote",
|
||||
["fedibird"] = "http://fedibird.com/ns#",
|
||||
["quoteUri"] = "fedibird:quoteUri",
|
||||
["quoteUrl"] = "as:quoteUrl",
|
||||
["gts"] = "https://gotosocial.org/ns#",
|
||||
["interactionPolicy"] = new JsonObject { ["@id"] = "gts:interactionPolicy", ["@type"] = "@id" },
|
||||
["canQuote"] = new JsonObject { ["@id"] = "gts:canQuote", ["@type"] = "@id" },
|
||||
["automaticApproval"] = new JsonObject { ["@id"] = "gts:automaticApproval", ["@type"] = "@id" },
|
||||
["manualApproval"] = new JsonObject { ["@id"] = "gts:manualApproval", ["@type"] = "@id" },
|
||||
["litepub"] = "http://litepub.social/ns#",
|
||||
["EmojiReact"] = "litepub:EmojiReact",
|
||||
["focalPoint"] = new JsonObject { ["@container"] = "@list", ["@id"] = "toot:focalPoint" },
|
||||
@@ -188,6 +195,9 @@ namespace PrivaPub.Federation.Rendering
|
||||
};
|
||||
}
|
||||
|
||||
public static string QuotableBy(PostEntity post) =>
|
||||
post.Visibility is PostVisibility.Public or PostVisibility.Unlisted ? post.LocalQuotePolicy ?? Models.User.QuotePolicies.Nobody : Models.User.QuotePolicies.Nobody;
|
||||
|
||||
static void AddQuote(JsonObject note, PostEntity post, ref string content)
|
||||
{
|
||||
if (string.IsNullOrEmpty(post.QuoteURI))
|
||||
@@ -262,6 +272,19 @@ namespace PrivaPub.Federation.Rendering
|
||||
.ToArray())
|
||||
};
|
||||
AddQuote(note, post, ref content);
|
||||
if (!post.IsFederatedCopy)
|
||||
note["interactionPolicy"] = new JsonObject
|
||||
{
|
||||
["canQuote"] = new JsonObject
|
||||
{
|
||||
["automaticApproval"] = new JsonArray((JsonNode)(QuotableBy(post) switch
|
||||
{
|
||||
Models.User.QuotePolicies.Public => Objects.Addressing.Public,
|
||||
Models.User.QuotePolicies.Followers => author.Followers,
|
||||
_ => author.Uri
|
||||
}))
|
||||
}
|
||||
};
|
||||
note["content"] = content;
|
||||
if (!string.IsNullOrEmpty(post.Language))
|
||||
note["contentMap"] = new JsonObject { [post.Language] = content };
|
||||
|
||||
Reference in new issue
Block a user