Edit history from formerRepresentations
Pleroma and Akkoma send a post's earlier versions with it. A post PrivaPub first meets after its edits now has its history, and an edit that carries them brings the versions missed in between; each is read as the post itself is. The Akkoma scenario checks a post fetched after two edits by an account no persona follows. 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
02928caac1
commit
679463eb3f
12 files changed
+280
-16
No files matched your search
@@ -518,7 +518,7 @@ tools/pasture/run.sh down # removes e
|
||||
shipped in the release (CAStore, certifi), so the entrypoint appends Caddy's CA there. `pleroma_ctl` passes its
|
||||
arguments on unquoted: no value may contain a space. Its Linkify never takes `@user@host.test` for a mention, so the
|
||||
scenario addresses alice with Pleroma's `to[]`; its API never reports a remote blocker as `blocked_by`, so the block is
|
||||
read from its `user_relationships`. 44 checks.
|
||||
read from its `user_relationships`. `akkoma_token <user>` signs one of its users in. 45 checks.
|
||||
- **Lemmy (1.0.0-beta.2):** the backend alone on the shared Postgres, its admin made by `setup` in the generated
|
||||
`config.hjson`. It trusts Caddy's CA through `SSL_CERT_FILE` and reaches the network through
|
||||
`DANGER_FEDERATION_ALLOW_LOCAL_IP=1`; 0.19 cannot join (its rustls trusts only its bundled roots). Its API is
|
||||
|
||||
@@ -248,6 +248,8 @@ tags.
|
||||
content, one per choice, counted once per voter. Counts are refreshed with an `Update{Question}` at most every three
|
||||
minutes. Our own votes on other servers' polls are sent the same way to the poll's author only, without `published`.
|
||||
An incoming `Update` without a newer `updated` only refreshes counts and details; it is never recorded as an edit.
|
||||
A post's earlier versions in `formerRepresentations` (Pleroma, Akkoma) are its edit history, read as the post itself is:
|
||||
a post first met after its edits has them, and an edit that carries them brings the versions missed in between.
|
||||
|
||||
**Quotes** (FEP-044f, with the older keys):
|
||||
- **Received.** A quote is read from `quote`, `quoteUrl`, `quoteUri`, `_misskey_quote` or a FEP-e232 `Link` tag.
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Federation.Objects;
|
||||
using PrivaPub.Models.Post;
|
||||
using PrivaPub.Models.Social;
|
||||
using PrivaPub.Tests.Support;
|
||||
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace PrivaPub.Tests.Federation
|
||||
{
|
||||
// Pleroma's and Akkoma's edited posts carry their earlier versions (formerRepresentations): a post first met after its
|
||||
// edits has its history, and an edit tells the ones missed in between
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class FormerRepresentationsTests : IAsyncLifetime
|
||||
{
|
||||
Harness _harness;
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
|
||||
_harness = await Harness.Start();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_harness != default)
|
||||
await _harness.DisposeAsync();
|
||||
}
|
||||
|
||||
static string Origin(RemoteActor actor) => new Uri(actor.Id).GetLeftPart(UriPartial.Authority);
|
||||
static readonly DateTime Start = DateTime.UtcNow.AddHours(-1);
|
||||
|
||||
static JsonObject Version(string text, int minutes) => new()
|
||||
{
|
||||
["type"] = "Note", ["content"] = $"<p>{text}</p>", ["published"] = Start.ToString("O"),
|
||||
["updated"] = minutes == 0 ? default : Start.AddMinutes(minutes).ToString("O")
|
||||
};
|
||||
|
||||
async Task<(RemoteActor Carol, JsonObject Note)> Followed(string text, int minutes, params JsonObject[] former)
|
||||
{
|
||||
var token = TestContext.Current.CancellationToken;
|
||||
var (_, alice) = await _harness.Persona("alice");
|
||||
var carol = new RemoteActor(_harness.Peer, "carol");
|
||||
await DB.Default.SaveAsync(new Following { AvatarId = alice.Id, TargetActorURI = carol.Id, State = FollowState.Accepted }, token);
|
||||
var note = Version(text, minutes);
|
||||
note["id"] = $"{Origin(carol)}/objects/{Guid.NewGuid():N}";
|
||||
note["attributedTo"] = carol.Id;
|
||||
note["to"] = new JsonArray(Addressing.Public);
|
||||
note["cc"] = new JsonArray($"{carol.Id}/followers");
|
||||
if (former.Length > 0)
|
||||
note["formerRepresentations"] = new JsonObject { ["type"] = "OrderedCollection", ["totalItems"] = former.Length, ["orderedItems"] = new JsonArray(former) };
|
||||
return (carol, note);
|
||||
}
|
||||
|
||||
Task Deliver(RemoteActor carol, string type, JsonObject note) => _harness.Deliver(carol, "/human-centipede", new JsonObject
|
||||
{
|
||||
["id"] = $"{Origin(carol)}/activities/{Guid.NewGuid():N}", ["type"] = type, ["actor"] = carol.Id,
|
||||
["to"] = new JsonArray(Addressing.Public), ["object"] = note.DeepClone()
|
||||
});
|
||||
|
||||
Task<Post> Stored(JsonObject note) =>
|
||||
DB.Default.Find<Post>().Match(p => p.ObjectURI == note["id"]!.GetValue<string>()).ExecuteSingleAsync(TestContext.Current.CancellationToken);
|
||||
|
||||
[Fact]
|
||||
public async Task A_post_first_met_after_its_edits_has_its_history()
|
||||
{
|
||||
var (carol, note) = await Followed("third", 40, Version("second", 20), Version("first", 0));
|
||||
|
||||
await Deliver(carol, "Create", note);
|
||||
|
||||
var post = await Stored(note);
|
||||
Assert.Equal(["<p>first</p>", "<p>second</p>"], post.Revisions.Select(r => r.ContentHtml));
|
||||
Assert.Equal("<p>third</p>", post.ContentHtml);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task An_edit_tells_the_versions_missed_in_between()
|
||||
{
|
||||
var (carol, note) = await Followed("first", 0);
|
||||
await Deliver(carol, "Create", note);
|
||||
|
||||
var edited = (JsonObject)note.DeepClone();
|
||||
edited["content"] = "<p>third</p>";
|
||||
edited["updated"] = Start.AddMinutes(40).ToString("O");
|
||||
edited["formerRepresentations"] = new JsonObject
|
||||
{
|
||||
["type"] = "OrderedCollection", ["totalItems"] = 2, ["orderedItems"] = new JsonArray(Version("second", 20), Version("first", 0))
|
||||
};
|
||||
await Deliver(carol, "Update", edited);
|
||||
|
||||
var post = await Stored(note);
|
||||
Assert.Equal(["<p>first</p>", "<p>second</p>"], post.Revisions.Select(r => r.ContentHtml));
|
||||
Assert.Equal("<p>third</p>", post.ContentHtml);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,36 @@ namespace PrivaPub.Tests.Federation
|
||||
Assert.Equal(["https://privapub.test/peasants/flor_pp98"], note.Cc);
|
||||
}
|
||||
|
||||
// Pleroma's edited post carries its earlier versions (formerRepresentations, newest first)
|
||||
[Fact]
|
||||
public void Reads_the_earlier_versions_of_a_pleroma_edit()
|
||||
{
|
||||
var note = NoteParser.Parse(JsonNode.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "pleroma", "edited.json"))));
|
||||
|
||||
var before = Assert.Single(note.FormerRepresentations);
|
||||
Assert.StartsWith("tomorrow library garden bridge", before.ContentHtml);
|
||||
Assert.Equal(new DateTime(2026, 10, 5, 0, 49, 58, DateTimeKind.Utc).AddTicks(8_600_710), before.EditedAt);
|
||||
Assert.Null(before.SpoilerText);
|
||||
Assert.EndsWith("(edited)", note.ContentHtml);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Earlier_versions_come_oldest_first_each_read_as_the_post_is()
|
||||
{
|
||||
var note = NoteParser.Parse(JsonNode.Parse("""
|
||||
{"id":"https://pl.example/objects/1","type":"Note","attributedTo":"https://pl.example/users/carol","content":"<p>third</p>",
|
||||
"published":"2026-10-06T08:00:00Z","updated":"2026-10-06T09:00:00Z",
|
||||
"formerRepresentations":{"type":"OrderedCollection","totalItems":2,"orderedItems":[
|
||||
{"type":"Note","content":"<p>second</p>","summary":"careful","published":"2026-10-06T08:00:00Z","updated":"2026-10-06T08:30:00Z"},
|
||||
{"type":"Note","content":"<p>first<script>x</script></p>","summary":"","published":"2026-10-06T08:00:00Z"}]}}
|
||||
"""));
|
||||
|
||||
Assert.Equal(["<p>first</p>", "<p>second</p>"], note.FormerRepresentations.Select(r => r.ContentHtml));
|
||||
Assert.Equal([new DateTime(2026, 10, 6, 8, 0, 0, DateTimeKind.Utc), new DateTime(2026, 10, 6, 8, 30, 0, DateTimeKind.Utc)],
|
||||
note.FormerRepresentations.Select(r => r.EditedAt));
|
||||
Assert.Equal([(default(string), false), ("careful", true)], note.FormerRepresentations.Select(r => (r.SpoilerText, r.HasContentWarning)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reads_a_lemmy_page_with_a_title()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"actor": "https://pleroma.test/users/flor_pl97",
|
||||
"attachment": [],
|
||||
"attributedTo": "https://pleroma.test/users/flor_pl97",
|
||||
"cc": [
|
||||
"https://pleroma.test/users/flor_pl97/followers"
|
||||
],
|
||||
"content": "mountain late window map bright postcard (p10) (edited)",
|
||||
"context": "https://pleroma.test/contexts/891653c5-91ba-43f3-b3ef-b066caa3fa4d",
|
||||
"conversation": "https://pleroma.test/contexts/891653c5-91ba-43f3-b3ef-b066caa3fa4d",
|
||||
"formerRepresentations": {
|
||||
"orderedItems": [
|
||||
{
|
||||
"actor": "https://pleroma.test/users/flor_pl97",
|
||||
"attachment": [],
|
||||
"attributedTo": "https://pleroma.test/users/flor_pl97",
|
||||
"cc": [
|
||||
"https://pleroma.test/users/flor_pl97/followers"
|
||||
],
|
||||
"content": "tomorrow library garden bridge the festival bicycle together garden early quiet morning library the postcard tomorrow mountain the small rain postcard warm garden market map mountain map (p10)",
|
||||
"contentMap": {
|
||||
"en": "tomorrow library garden bridge the festival bicycle together garden early quiet morning library the postcard tomorrow mountain the small rain postcard warm garden market map mountain map (p10)"
|
||||
},
|
||||
"context": "https://pleroma.test/contexts/891653c5-91ba-43f3-b3ef-b066caa3fa4d",
|
||||
"conversation": "https://pleroma.test/contexts/891653c5-91ba-43f3-b3ef-b066caa3fa4d",
|
||||
"published": "2026-10-05T00:49:58.860071Z",
|
||||
"repliesCount": 2,
|
||||
"source": {
|
||||
"content": "tomorrow library garden bridge the festival bicycle together garden early quiet morning library the postcard tomorrow mountain the small rain postcard warm garden market map mountain map (p10)",
|
||||
"mediaType": "text/plain"
|
||||
},
|
||||
"summary": "",
|
||||
"tag": [],
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"type": "Note"
|
||||
}
|
||||
],
|
||||
"totalItems": 1,
|
||||
"type": "OrderedCollection"
|
||||
},
|
||||
"id": "https://pleroma.test/objects/6101c4fd-ed46-461c-a3fe-9742852f47da",
|
||||
"published": "2026-10-05T00:49:58.860071Z",
|
||||
"replies": {
|
||||
"first": {
|
||||
"id": "https://pleroma.test/objects/6101c4fd-ed46-461c-a3fe-9742852f47da/replies?page=true",
|
||||
"orderedItems": [
|
||||
"https://pleroma.test/objects/b7b7c066-51a8-46d1-8919-07b87f158887",
|
||||
"https://privapub.test/peasants/vera_pp40/scribbles/6ac2f4520c61eaf3ab241d35"
|
||||
],
|
||||
"partOf": "https://pleroma.test/objects/6101c4fd-ed46-461c-a3fe-9742852f47da/replies",
|
||||
"type": "OrderedCollectionPage"
|
||||
},
|
||||
"id": "https://pleroma.test/objects/6101c4fd-ed46-461c-a3fe-9742852f47da/replies",
|
||||
"totalItems": 2,
|
||||
"type": "OrderedCollection"
|
||||
},
|
||||
"repliesCount": 2,
|
||||
"source": {
|
||||
"content": "mountain late window map bright postcard (p10) (edited)",
|
||||
"mediaType": "text/plain"
|
||||
},
|
||||
"summary": "",
|
||||
"tag": [],
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"type": "Note",
|
||||
"updated": "2026-10-05T00:52:40.105051Z"
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace PrivaPub.Federation.Inbox
|
||||
{
|
||||
public static class RemoteEdits
|
||||
{
|
||||
public const int MaxRevisions = 20;
|
||||
public const int MaxRevisions = NoteParser.MaxRevisions;
|
||||
|
||||
//newer than what we hold; or, for a first edit, no older and actually different, because GoToSocial's timestamps are
|
||||
//whole seconds and an edit made in the second the post was published carries updated == published
|
||||
@@ -47,6 +47,10 @@ namespace PrivaPub.Federation.Inbox
|
||||
return false;
|
||||
}
|
||||
|
||||
//the author's server's own history (Pleroma's, Akkoma's) holds the version we had, and the edits we missed
|
||||
if (note.FormerRepresentations.Count > 0)
|
||||
post.Revisions = note.FormerRepresentations.ToList();
|
||||
else
|
||||
post.Revisions.Add(new PostRevision
|
||||
{
|
||||
Title = post.Title,
|
||||
|
||||
@@ -133,7 +133,8 @@ namespace PrivaPub.Federation.Inbox
|
||||
AuthorGone = author.AccountState == AvatarAccountState.Deleted,
|
||||
CreationDate = note.Published,
|
||||
UpdateDate = note.Updated ?? note.Published,
|
||||
EditedAt = note.Updated
|
||||
EditedAt = note.Updated,
|
||||
Revisions = note.FormerRepresentations.ToList()
|
||||
};
|
||||
await Episode(post, note, author, token);
|
||||
return post;
|
||||
|
||||
@@ -52,6 +52,7 @@ namespace PrivaPub.Federation.Objects
|
||||
public EventDetails Event { get; init; }
|
||||
public PostPlace Place { get; init; }
|
||||
public bool? CommentsEnabled { get; init; }//Lemmy, PieFed and Mbin: false once its moderators lock it; null when not said
|
||||
public IReadOnlyList<PostRevision> FormerRepresentations { get; init; } = Array.Empty<PostRevision>();//its earlier versions, oldest first
|
||||
}
|
||||
|
||||
public static class NoteParser
|
||||
@@ -59,6 +60,7 @@ namespace PrivaPub.Federation.Objects
|
||||
static readonly string[] NoteTypes = { "Note", "Article", "Page", "Question", "Event", "Video", "Audio", "Image", "ChatMessage", "PodcastEpisode" };
|
||||
const int MaxAttachments = 16;
|
||||
const int MaxTags = 64;
|
||||
public const int MaxRevisions = 20;
|
||||
|
||||
public static NoteDocument Parse(JsonNode node)
|
||||
{
|
||||
@@ -75,8 +77,7 @@ namespace PrivaPub.Federation.Objects
|
||||
var summary = Value(note, "summary");
|
||||
var excerpt = ObjectShapes.Text(summary, 1000)
|
||||
?? (note["preview"] is JsonObject preview && Value(preview, "type") == "Note" ? ObjectShapes.Text(Value(preview, "content"), 1000) : default);
|
||||
var sensitive = note["sensitive"] is JsonValue flag && flag.TryGetValue<bool>(out var marked) && marked;
|
||||
var summaryWarns = type is "Note" or "Question" || sensitive;
|
||||
var (sensitive, summaryWarns) = Warns(note, type, summary);
|
||||
return new NoteDocument
|
||||
{
|
||||
Id = id,
|
||||
@@ -87,7 +88,7 @@ namespace PrivaPub.Federation.Objects
|
||||
Title = ObjectShapes.Text(Value(note, "name"), 500),
|
||||
SpoilerText = summaryWarns ? Plain(summary, 500) : default,
|
||||
Excerpt = summaryWarns ? default : excerpt,
|
||||
Sensitive = sensitive || summaryWarns && !string.IsNullOrWhiteSpace(summary),
|
||||
Sensitive = sensitive,
|
||||
Language = language,
|
||||
Url = UrlOf(note["url"]) ?? id,
|
||||
InReplyTo = Id(note["inReplyTo"]),
|
||||
@@ -128,10 +129,45 @@ namespace PrivaPub.Federation.Objects
|
||||
Audio = ObjectShapes.Audio(type == "PodcastEpisode" ? note["audio"] as JsonObject : note),
|
||||
Event = ObjectShapes.Event(note),
|
||||
Place = ObjectShapes.NotePlace(note),
|
||||
CommentsEnabled = note["commentsEnabled"] is JsonValue comments && comments.TryGetValue<bool>(out var enabled) ? enabled : null
|
||||
CommentsEnabled = note["commentsEnabled"] is JsonValue comments && comments.TryGetValue<bool>(out var enabled) ? enabled : null,
|
||||
FormerRepresentations = Former(note, type)
|
||||
};
|
||||
}
|
||||
|
||||
// a summary is a content warning only on a Note or a Question, or a post marked sensitive; elsewhere it is an excerpt
|
||||
static (bool Sensitive, bool SummaryWarns) Warns(JsonObject note, string type, string summary)
|
||||
{
|
||||
var marked = note["sensitive"] is JsonValue flag && flag.TryGetValue<bool>(out var value) && value;
|
||||
var warns = type is "Note" or "Question" || marked;
|
||||
return (marked || warns && !string.IsNullOrWhiteSpace(summary), warns);
|
||||
}
|
||||
|
||||
// Pleroma's and Akkoma's earlier versions of an edited post (`formerRepresentations`, newest first there), each read as
|
||||
// the post itself is: the newest twenty, oldest first, each dated when it was written
|
||||
static List<PostRevision> Former(JsonObject note, string type)
|
||||
{
|
||||
if (note["formerRepresentations"] is not JsonObject former || former["orderedItems"] is not JsonArray items)
|
||||
return [];
|
||||
return items.OfType<JsonObject>().Take(MaxRevisions * 2)
|
||||
.Select(version =>
|
||||
{
|
||||
var summary = Value(version, "summary");
|
||||
var (sensitive, warns) = Warns(version, type, summary);
|
||||
return new PostRevision
|
||||
{
|
||||
Title = ObjectShapes.Text(Value(version, "name"), 500),
|
||||
SpoilerText = warns ? Plain(summary, 500) : default,
|
||||
ContentHtml = ContentSanitizer.Html(Content(version).Html),
|
||||
HasContentWarning = sensitive,
|
||||
EditedAt = Time(Value(version, "updated")) ?? Time(Value(version, "published")) ?? default
|
||||
};
|
||||
})
|
||||
.Reverse()
|
||||
.OrderBy(r => r.EditedAt)
|
||||
.TakeLast(MaxRevisions)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Castopod's PodcastEpisode: its words in `description` (a Note of its own), its sound in `audio`, its title the
|
||||
// sound's name; read as a post with those
|
||||
static JsonObject Episode(JsonObject episode)
|
||||
|
||||
+4
-3
@@ -411,13 +411,13 @@ collections answer HTML to an ActivityPub `Accept`, so PrivaPub shows no counts
|
||||
| Gap | P | Client surface |
|
||||
|---|---|---|
|
||||
| Language from `@context[].@language` | P1 | `Status.language` |
|
||||
| `formerRepresentations` → our revisions | P2 | `/statuses/:id/history` |
|
||||
| `formerRepresentations` → our revisions: **done 2026-10-06** (a post met after its edits has its history; an edit brings the versions missed) | P2 | `/statuses/:id/history` |
|
||||
| `context`/`conversation` threading; parents and quotes we could not fetch, kept as URIs | P2 | `pleroma.context`; `akkoma.in_reply_to_apid`, `akkoma.quote_apid` precedent |
|
||||
| `ChatMessage` in as a direct message (also needed for Lemmy, Mbin and PieFed); advertise `acceptsChatMessages` only once it is answered | P1 (in), P3 (out) | `visibility: direct`, Conversations |
|
||||
| `Listen`, `vcard:bday`, `backgroundUrl` | P3 | own |
|
||||
|
||||
**Pasture evidence (2026-10-03, Akkoma 3.20.1, `tools/pasture/scenarios/akkoma.sh`):** 44 checks pass, three runs in a
|
||||
row. Akkoma publishes no image, so `tools/pasture/images/akkoma` installs its OTP release, pinned by checksum. Covered:
|
||||
row (45 since 2026-10-06). Akkoma publishes no image, so `tools/pasture/images/akkoma` installs its OTP release, pinned by checksum. Covered:
|
||||
- discovery and follows both ways;
|
||||
- posts, CW and followers-only posts;
|
||||
- the `published` time kept;
|
||||
@@ -428,7 +428,8 @@ row. Akkoma publishes no image, so `tools/pasture/images/akkoma` installs its OT
|
||||
- polls and votes both ways;
|
||||
- quotes both ways (`quote_id` out of its API, `quoteUri` in ours);
|
||||
- images with alt text both ways;
|
||||
- edits with history, and deletes, both ways;
|
||||
- edits with history, and deletes, both ways; a post PrivaPub first meets after two edits, by an account no persona
|
||||
follows, has its history from `formerRepresentations` (2026-10-06);
|
||||
- unfollow, block and unblock;
|
||||
- statistics.
|
||||
|
||||
|
||||
+2
-1
@@ -677,7 +677,8 @@ it, raw where it doesn't.
|
||||
receiving server, and a signed roll-call listing only them; honoured: a sender's digest of its followers here,
|
||||
mended from its list; checked live against Mastodon, all four ways a follow can be lost);
|
||||
- `indexable`/`discoverable`/`searchableBy`;
|
||||
- edit history from `formerRepresentations`;
|
||||
- edit history from `formerRepresentations`: **done 2026-10-06** (Pleroma's and Akkoma's earlier versions, for a post
|
||||
met after its edits and for the edits missed in between);
|
||||
- PeerTube reply rules and `ApproveReply`.
|
||||
- **Events:** structured RSVP (`Join`/`Leave` with stable ids): **done 2026-10-05** (owner decision above), checked live
|
||||
against Mobilizon.
|
||||
|
||||
@@ -17,13 +17,19 @@ akkoma_up() {
|
||||
done
|
||||
podman exec pasture-akkoma /opt/akkoma/bin/pleroma_ctl user new akuser akuser@akkoma.test --password Akkoma-Pasture-Pass-1 \
|
||||
--name akuser --admin --assume-yes >/dev/null 2>&1 || true
|
||||
akkoma_token akuser > "$here/.state/akkoma.token" || true
|
||||
echo "akkoma: https://akkoma.test:6443"
|
||||
}
|
||||
|
||||
# akkoma_token <user>: an access token for one of the pasture's users (its password), through a new app and the password
|
||||
# grant
|
||||
akkoma_token() {
|
||||
local app cid cs
|
||||
app=$(site akkoma.test -s -X POST https://akkoma.test:6443/api/v1/apps -d 'client_name=pasture&redirect_uris=urn:ietf:wg:oauth:2.0:oob&scopes=read write follow push admin')
|
||||
cid=$(echo "$app" | python3 -c "import sys,json; print(json.load(sys.stdin)['client_id'])" 2>/dev/null)
|
||||
cs=$(echo "$app" | python3 -c "import sys,json; print(json.load(sys.stdin)['client_secret'])" 2>/dev/null)
|
||||
site akkoma.test -s -X POST https://akkoma.test:6443/oauth/token --data-urlencode grant_type=password --data-urlencode username=akuser \
|
||||
site akkoma.test -s -X POST https://akkoma.test:6443/oauth/token --data-urlencode grant_type=password --data-urlencode "username=$1" \
|
||||
--data-urlencode password=Akkoma-Pasture-Pass-1 --data-urlencode "client_id=$cid" --data-urlencode "client_secret=$cs" \
|
||||
--data-urlencode 'scope=read write follow push admin' \
|
||||
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])" > "$here/.state/akkoma.token" 2>/dev/null || true
|
||||
echo "akkoma: https://akkoma.test:6443"
|
||||
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])" 2>/dev/null
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
# Akkoma 3.20: discovery, follows, posts, CW, replies both ways, likes and boosts with their undos, emoji reactions
|
||||
# both ways, DMs, polls, quotes both ways, edits with their history, deletes, media with alt text, the published time
|
||||
# kept, blocks, statistics. Akkoma speaks the Mastodon API, with reactions under /api/v1/pleroma.
|
||||
# both ways, DMs, polls, quotes both ways, edits with their history (a post met after its edits too), deletes, media
|
||||
# with alt text, the published time kept, blocks, statistics. Akkoma speaks the Mastodon API, with reactions under
|
||||
# /api/v1/pleroma.
|
||||
AK=https://akkoma.test:6443
|
||||
acurl() { curl -sk --resolve akkoma.test:6443:127.0.0.1 "$@"; }
|
||||
. "$here/peers/akkoma.sh"
|
||||
KT=$(cat "$here/.state/akkoma.token" 2>/dev/null)
|
||||
KH="Authorization: Bearer $KT"
|
||||
# what Akkoma holds of an account, newest first; and the one status whose uri is given
|
||||
@@ -122,6 +124,19 @@ curl -s -o /dev/null -X PUT -H "$PH" "$P/api/v1/statuses/$p_post_id" -d 'status=
|
||||
until_true 30 'acurl -H "$KH" "$AK/api/v1/statuses/$p_post_on_a" | grep -q "edited"' && ok "alice_akkoma's edit reaches Akkoma" || ko "edit not applied on Akkoma"
|
||||
acurl -o /dev/null -X PUT -H "$KH" "$AK/api/v1/statuses/$a_post_id" -d 'status=Hello PrivaPub from Akkoma, edited'
|
||||
until_true 30 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$a_post_on_p/history" | j "print(len(d))")" = "2" ]' && ok "akuser's edit reaches PrivaPub with its history" || ko "Akkoma's edit not applied on PrivaPub"
|
||||
# a post PrivaPub first meets after two edits, by an Akkoma account no persona follows, comes with its earlier versions
|
||||
# (formerRepresentations)
|
||||
podman exec pasture-akkoma /opt/akkoma/bin/pleroma_ctl user new akwriter akwriter@akkoma.test --password Akkoma-Pasture-Pass-1 \
|
||||
--name akwriter --assume-yes >/dev/null 2>&1 || true
|
||||
WH="Authorization: Bearer $(akkoma_token akwriter)"
|
||||
written=$(date +%s)
|
||||
w_post=$(acurl -X POST -H "$WH" "$AK/api/v1/statuses" -d "status=written thrice $written&visibility=public" | j "print(d['id'])")
|
||||
acurl -o /dev/null -X PUT -H "$WH" "$AK/api/v1/statuses/$w_post" -d "status=written thrice $written, again"
|
||||
acurl -o /dev/null -X PUT -H "$WH" "$AK/api/v1/statuses/$w_post" -d "status=written thrice $written, at last"
|
||||
w_uri=$(acurl -H "$WH" "$AK/api/v1/statuses/$w_post" | j "print(d['uri'])")
|
||||
w_on_p=$(curl -s -G -H "$PH" "$P/api/v2/search" --data-urlencode "q=$w_uri" -d resolve=true -d type=statuses | j "print(d['statuses'][0]['id'])")
|
||||
[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$w_on_p/history" | j "print([e['content'].split('$written')[-1].strip(' ,</p>') for e in d])")" = "['', 'again', 'at last']" ] \
|
||||
&& ok "a post met after its edits has its history from Akkoma" || ko "a post met after its edits lacks its history ($(curl -s -H "$PH" "$P/api/v1/statuses/$w_on_p/history" | head -c 300))"
|
||||
cw_on_a=$(a_status_by_uri "$alice_on_a" "$cw_uri" | j "print(d['id'])")
|
||||
cw_id=$(curl -s -H "$PH" "$P/api/v1/accounts/$alice_self/statuses" | j "print(next(s['id'] for s in d if s['uri']=='$cw_uri'))")
|
||||
curl -s -o /dev/null -X DELETE -H "$PH" "$P/api/v1/statuses/$cw_id"
|
||||
|
||||
Reference in new issue
Block a user