P5: remote objects are read in every shape, and videos, events, audio and links keep their own details
- ObjectShapes reads `url`, `icon` and `image` as a value, a Link or an array; Markdown `content` (PeerTube) is rendered; a missing `mediaType` is inferred from the extension or the attachment type (Bridgy); alt text comes from `name`, `summary` or their language maps; thumbnails come from attachment `icon`, object `icon[]`, `image` or a Loops `preview`; durations are ISO 8601 or seconds; per-attachment `sensitive` is kept; the language falls back to `@context` `@language` (Pleroma); titles and excerpts are plain text; hashtags normalise with NFKC like Mastodon. - Typed details on Post: Link (Lemmy link posts, Mbin `source` URLs, Mastodon 4.7 Link attachments with an FEP-8967 publisher preview), Video (PeerTube files with their streams, HLS playlist, poster, captions, chapters, licence, channel, live state, comment policy), Audio (Funkwhale), Event (Mobilizon, Gancio, Hubzilla: times, zone, floating time, places and addresses, online link, capacity, status), the cover image, and the author's own source text. - Mastodon API: a card built from those details without fetching anything, an event's "when · where" line, attachment thumbnails and durations, and no media file offered as a preview image. 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
dcd024100a
commit
fc111d8c46
12 files changed
+751
-38
No files matched your search
@@ -1,6 +1,7 @@
|
||||
using PrivaPub.Models.Post;
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using static PrivaPub.Federation.Objects.ActivityJson;
|
||||
@@ -31,6 +32,12 @@ namespace PrivaPub.Federation.Objects
|
||||
public IReadOnlyList<PostMention> Mentions { get; init; } = Array.Empty<PostMention>();
|
||||
public IReadOnlyList<string> Tags { get; init; } = Array.Empty<string>();
|
||||
public IReadOnlyList<PostMedia> Attachments { get; init; } = Array.Empty<PostMedia>();
|
||||
public PostSource Source { get; init; }
|
||||
public string CoverURL { get; init; }
|
||||
public PostLink Link { get; init; }
|
||||
public VideoDetails Video { get; init; }
|
||||
public AudioDetails Audio { get; init; }
|
||||
public EventDetails Event { get; init; }
|
||||
}
|
||||
|
||||
public static class NoteParser
|
||||
@@ -50,6 +57,8 @@ namespace PrivaPub.Federation.Objects
|
||||
|
||||
var (contentHtml, language) = Content(note);
|
||||
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;
|
||||
return new NoteDocument
|
||||
@@ -59,9 +68,9 @@ namespace PrivaPub.Federation.Objects
|
||||
Raw = note,
|
||||
AttributedTo = Attribution(note["attributedTo"]),
|
||||
ContentHtml = ContentSanitizer.Html(contentHtml),
|
||||
Title = Plain(Value(note, "name"), 500),
|
||||
Title = ObjectShapes.Text(Value(note, "name"), 500),
|
||||
SpoilerText = summaryWarns ? Plain(summary, 500) : default,
|
||||
Excerpt = summaryWarns ? default : Plain(summary, 1000),
|
||||
Excerpt = summaryWarns ? default : excerpt,
|
||||
Sensitive = sensitive || summaryWarns && !string.IsNullOrWhiteSpace(summary),
|
||||
Language = language,
|
||||
Url = UrlOf(note["url"]) ?? id,
|
||||
@@ -78,12 +87,18 @@ namespace PrivaPub.Federation.Objects
|
||||
.DistinctBy(m => m.ActorURI)
|
||||
.Take(MaxTags)
|
||||
.ToList(),
|
||||
Tags = Tags(note, "Hashtag").Select(t => Value(t, "name")?.TrimStart('#').ToLowerInvariant())
|
||||
Tags = Tags(note, "Hashtag").Select(t => Value(t, "name")?.TrimStart('#').Normalize(NormalizationForm.FormKC).ToLowerInvariant())
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.Distinct()
|
||||
.Take(MaxTags)
|
||||
.ToList(),
|
||||
Attachments = Attachments(note)
|
||||
Attachments = Attachments(note),
|
||||
Source = ObjectShapes.Source(note),
|
||||
CoverURL = ObjectShapes.Cover(note),
|
||||
Link = ObjectShapes.Link(note),
|
||||
Video = ObjectShapes.Video(note),
|
||||
Audio = ObjectShapes.Audio(note),
|
||||
Event = ObjectShapes.Event(note)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,7 +115,9 @@ namespace PrivaPub.Federation.Objects
|
||||
}
|
||||
if (string.IsNullOrEmpty(content) && Value(note, "_misskey_content") is { } misskey)
|
||||
content = System.Net.WebUtility.HtmlEncode(misskey).Replace("\n", "<br>");
|
||||
return (content, language);
|
||||
if (!string.IsNullOrEmpty(content) && Value(note, "mediaType")?.StartsWith("text/markdown", StringComparison.OrdinalIgnoreCase) == true)
|
||||
content = Rendering.ActivityPubRenderer.Html(content);
|
||||
return (content, language ?? ObjectShapes.Language(note));
|
||||
}
|
||||
|
||||
static string Attribution(JsonNode node) => node switch
|
||||
@@ -120,33 +137,37 @@ namespace PrivaPub.Federation.Objects
|
||||
_ => Enumerable.Empty<JsonObject>()
|
||||
}).Where(t => Value(t, "type") == type);
|
||||
|
||||
static List<PostMedia> Attachments(JsonObject note)
|
||||
{
|
||||
var items = note["attachment"] switch
|
||||
{
|
||||
JsonArray array => array.OfType<JsonObject>(),
|
||||
JsonObject single => new[] { single },
|
||||
_ => Enumerable.Empty<JsonObject>()
|
||||
};
|
||||
return items
|
||||
static List<PostMedia> Attachments(JsonObject note) =>
|
||||
ObjectShapes.Objects(note["attachment"])
|
||||
.Where(a => Value(a, "type") is "Document" or "Image" or "Video" or "Audio")
|
||||
.Select(a => new PostMedia
|
||||
{
|
||||
RemoteURL = UrlOf(a["url"]),
|
||||
ContentType = Value(a, "mediaType"),
|
||||
Description = Plain(Value(a, "name") ?? Value(a, "summary"), 1500),
|
||||
Blurhash = Value(a, "blurhash"),
|
||||
Width = Int(a["width"]),
|
||||
Height = Int(a["height"]),
|
||||
Focus = a["focalPoint"] is JsonArray focus && focus.Count == 2
|
||||
? focus.Select(f => f is JsonValue v && v.TryGetValue<double>(out var d) ? (float)Math.Clamp(d, -1, 1) : 0f).ToArray()
|
||||
: default
|
||||
})
|
||||
.Select(Attachment)
|
||||
.Where(m => Origin.Of(m.RemoteURL) != default)
|
||||
.Take(MaxAttachments)
|
||||
.ToList();
|
||||
|
||||
static PostMedia Attachment(JsonObject attachment)
|
||||
{
|
||||
var (url, linkMediaType) = ObjectShapes.MediaLink(attachment["url"] ?? attachment["href"]);
|
||||
return new PostMedia
|
||||
{
|
||||
RemoteURL = url,
|
||||
ContentType = ObjectShapes.InferMediaType(Value(attachment, "mediaType") ?? linkMediaType, url, Value(attachment, "type")),
|
||||
RemotePreviewURL = ObjectShapes.Image(attachment["icon"]),
|
||||
DurationSeconds = ObjectShapes.Seconds(attachment["duration"]),
|
||||
Sensitive = attachment["sensitive"] is JsonValue flag && flag.TryGetValue<bool>(out var marked) ? marked : default(bool?),
|
||||
Description = Plain(Value(attachment, "name") ?? Value(attachment, "summary") ?? FirstOf(attachment["nameMap"]) ?? FirstOf(attachment["summaryMap"]), 1500),
|
||||
Blurhash = Value(attachment, "blurhash"),
|
||||
Width = Int(attachment["width"]),
|
||||
Height = Int(attachment["height"]),
|
||||
Focus = attachment["focalPoint"] is JsonArray focus && focus.Count == 2
|
||||
? focus.Select(f => f is JsonValue v && v.TryGetValue<double>(out var d) ? (float)Math.Clamp(d, -1, 1) : 0f).ToArray()
|
||||
: default
|
||||
};
|
||||
}
|
||||
|
||||
static string FirstOf(JsonNode map) =>
|
||||
map is JsonObject languages ? languages.Select(l => l.Value is JsonValue v && v.TryGetValue<string>(out var text) ? text : default).FirstOrDefault(t => t != default) : default;
|
||||
|
||||
static string UrlOf(JsonNode node) => node switch
|
||||
{
|
||||
JsonValue value when value.TryGetValue<string>(out var text) => text,
|
||||
|
||||
Reference in new issue
Block a user