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
+288
-24
No files matched your search
@@ -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,14 +47,18 @@ namespace PrivaPub.Federation.Inbox
|
||||
return false;
|
||||
}
|
||||
|
||||
post.Revisions.Add(new PostRevision
|
||||
{
|
||||
Title = post.Title,
|
||||
SpoilerText = post.SpoilerText,
|
||||
ContentHtml = post.ContentHtml,
|
||||
HasContentWarning = post.HasContentWarning,
|
||||
EditedAt = post.EditedAt ?? post.CreationDate
|
||||
});
|
||||
//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,
|
||||
SpoilerText = post.SpoilerText,
|
||||
ContentHtml = post.ContentHtml,
|
||||
HasContentWarning = post.HasContentWarning,
|
||||
EditedAt = post.EditedAt ?? post.CreationDate
|
||||
});
|
||||
if (post.Revisions.Count > MaxRevisions)
|
||||
post.Revisions.RemoveRange(0, post.Revisions.Count - MaxRevisions);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user