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
@@ -139,15 +139,15 @@ namespace PrivaPub.Tests.Federation
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_legacy_post_is_quoted_without_asking_and_another_persona_cannot_be_quoted_yet()
|
||||
public async Task A_legacy_post_is_quoted_without_asking_and_a_persona_that_allows_nobody_cannot_be_quoted()
|
||||
{
|
||||
var token = TestContext.Current.CancellationToken;
|
||||
var (aliceRoot, alice) = await _harness.Persona("alice");
|
||||
var (bobRoot, bob) = await _harness.Persona("bob");
|
||||
var kitty = new RemoteActor(_harness.Peer, "kitty");
|
||||
var legacy = await Delivered(kitty, alice.Uri);
|
||||
await _harness.Posts.InsertPost(bobRoot, new InsertPostForm { AvatarId = bob.Id, Text = "mine" }, token);
|
||||
var bobs = await DB.Default.Find<Post>().Match(p => p.GroupUserId == bob.Id).ExecuteFirstAsync(token);
|
||||
bob.Settings.QuotePolicy = PrivaPub.Models.User.QuotePolicies.Nobody;
|
||||
var bobs = (await _harness.Statuses.Publish(bob, new StatusDraft { Text = "mine" }, token)).Post;
|
||||
|
||||
var outcome = await _harness.Statuses.Publish(alice, new StatusDraft { Text = "nice", QuotedStatusId = legacy.ID }, token);
|
||||
Assert.Equal(QuoteState.Accepted, outcome.Post.QuoteState);
|
||||
@@ -155,5 +155,67 @@ namespace PrivaPub.Tests.Federation
|
||||
Assert.False((await _harness.Statuses.Publish(alice, new StatusDraft { Text = "and this", QuotedStatusId = bobs.ID }, token)).Ok);
|
||||
Assert.True((await _harness.Statuses.Publish(bob, new StatusDraft { Text = "me again", QuotedStatusId = bobs.ID }, token)).Ok);
|
||||
}
|
||||
|
||||
JsonObject Request(RemoteActor quoter, string quotedUri, string quotingId) => new()
|
||||
{
|
||||
["id"] = NewId(quoter, "activities"), ["type"] = "QuoteRequest", ["actor"] = quoter.Id, ["object"] = quotedUri,
|
||||
["instrument"] = new JsonObject { ["id"] = quotingId, ["type"] = "Note", ["attributedTo"] = quoter.Id, ["quote"] = quotedUri, ["content"] = "<p>so true</p>" }
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task Anyone_may_quote_a_persona_by_default_and_the_licence_can_be_revoked()
|
||||
{
|
||||
var token = TestContext.Current.CancellationToken;
|
||||
var (_, alice) = await _harness.Persona("alice");
|
||||
var mallory = new RemoteActor(_harness.Peer, "mallory");
|
||||
await _harness.Deliver(mallory, "/human-centipede", new JsonObject { ["id"] = NewId(mallory, "follows"), ["type"] = "Follow", ["actor"] = mallory.Id, ["object"] = alice.Uri });
|
||||
var original = (await _harness.Statuses.Publish(alice, new StatusDraft { Text = "quote me" }, token)).Post;
|
||||
var created = (await _harness.Outgoing(mallory.Id + "/inbox")).Single(a => a["type"]!.GetValue<string>() == "Create")["object"]!;
|
||||
Assert.Equal(Addressing.Public, created["interactionPolicy"]!["canQuote"]!["automaticApproval"]![0]!.GetValue<string>());
|
||||
|
||||
var quotingId = NewId(mallory, "notes");
|
||||
await _harness.Deliver(mallory, "/human-centipede", Request(mallory, original.ObjectURI, quotingId));
|
||||
var accept = Assert.Single(await _harness.Outgoing(mallory.Id + "/inbox"), a => a["type"]!.GetValue<string>() == "Accept" && a["result"] != null);
|
||||
var licence = accept["result"]!.GetValue<string>();
|
||||
Assert.StartsWith(alice.Uri + "/parrot-licences/", licence);
|
||||
|
||||
var quoting = await Delivered(mallory, alice.Uri, n => { n["id"] = quotingId; n["quote"] = original.ObjectURI; n["quoteAuthorization"] = licence; });
|
||||
Assert.Equal(QuoteState.Accepted, quoting.QuoteState);
|
||||
Assert.Equal(1, (await DB.Default.Find<Post>().OneAsync(original.ID, token)).QuotesCount);
|
||||
|
||||
Assert.True(await _harness.Quotes.RevokeLicence(original, quoting, token));
|
||||
Assert.Equal(QuoteState.Revoked, (await DB.Default.Find<Post>().OneAsync(quoting.ID, token)).QuoteState);
|
||||
Assert.Contains(await _harness.Outgoing(mallory.Id + "/inbox"), a => a["type"]!.GetValue<string>() == "Delete" && a["object"]!.GetValue<string>() == licence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_followers_only_persona_turns_a_stranger_down()
|
||||
{
|
||||
var token = TestContext.Current.CancellationToken;
|
||||
var (_, alice) = await _harness.Persona("alice");
|
||||
alice.Settings.QuotePolicy = PrivaPub.Models.User.QuotePolicies.Followers;
|
||||
var original = (await _harness.Statuses.Publish(alice, new StatusDraft { Text = "friends only quoting" }, token)).Post;
|
||||
var stranger = new RemoteActor(_harness.Peer, "stranger");
|
||||
|
||||
await _harness.Deliver(stranger, "/human-centipede", Request(stranger, original.ObjectURI, NewId(stranger, "notes")));
|
||||
|
||||
Assert.Equal("Reject", Assert.Single(await _harness.Outgoing(stranger.Id + "/inbox"))["type"]!.GetValue<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task One_persona_quotes_another_with_a_licence()
|
||||
{
|
||||
var token = TestContext.Current.CancellationToken;
|
||||
var (_, alice) = await _harness.Persona("alice");
|
||||
var (_, bob) = await _harness.Persona("bob");
|
||||
var original = (await _harness.Statuses.Publish(alice, new StatusDraft { Text = "public thought" }, token)).Post;
|
||||
|
||||
var outcome = await _harness.Statuses.Publish(bob, new StatusDraft { Text = "agreed", QuotedStatusId = original.ID }, token);
|
||||
|
||||
Assert.True(outcome.Ok);
|
||||
Assert.Equal(QuoteState.Accepted, outcome.Post.QuoteState);
|
||||
Assert.StartsWith(alice.Uri + "/parrot-licences/", outcome.Post.QuoteAuthorizationURI);
|
||||
Assert.Equal(1, (await DB.Default.Find<Post>().OneAsync(original.ID, token)).QuotesCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user