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
@@ -12,6 +12,7 @@ using PrivaPub.Models.Social;
|
||||
using PrivaPub.Models.User;
|
||||
using PrivaPub.StaticServices;
|
||||
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
|
||||
using GroupEntity = PrivaPub.Models.Group.Group;
|
||||
@@ -192,6 +193,7 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
Visibility = Visibility(post.Visibility),
|
||||
Language = post.Language,
|
||||
MediaAttachments = post.Media.Select(Media).ToList(),
|
||||
Card = Card(post),
|
||||
Mentions = post.Mentions.Select(m => Mention(m, mentionAccounts)).Where(m => m != default).ToList(),
|
||||
Tags = post.Tags.Select(t => new StatusTag { Name = t, Url = ActivityPubRenderer.TagUrl(_localActors.BaseAddress, t) }).ToList()
|
||||
};
|
||||
@@ -227,12 +229,72 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
var content = post.ContentHtml ?? ActivityPubRenderer.Html(post.Text);
|
||||
if (post.IsFederatedCopy && string.IsNullOrWhiteSpace(content) && !string.IsNullOrEmpty(post.Excerpt))
|
||||
content = $"<p>{WebUtility.HtmlEncode(post.Excerpt)}</p>";
|
||||
if (post.Event is { } happening && EventLine(happening) is { } line)
|
||||
content = $"<p>{WebUtility.HtmlEncode(line)}</p>{content}";
|
||||
var titled = !string.IsNullOrEmpty(post.Title) && (!post.IsFederatedCopy || post.ObjectType is not (null or "Note" or "Question"));
|
||||
return titled
|
||||
? $"<p><strong>{WebUtility.HtmlEncode(post.Title)}</strong></p>{content}"
|
||||
: content ?? string.Empty;
|
||||
}
|
||||
|
||||
public static string EventLine(EventDetails happening)
|
||||
{
|
||||
if (happening.Start is not { } start)
|
||||
return default;
|
||||
var zone = Zone(happening.FloatingTime ? default : happening.TimeZone);
|
||||
var local = zone == default ? start : TimeZoneInfo.ConvertTimeFromUtc(start, zone);
|
||||
var when = local.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
|
||||
if (happening.End is { } end && end > start)
|
||||
{
|
||||
var localEnd = zone == default ? end : TimeZoneInfo.ConvertTimeFromUtc(end, zone);
|
||||
when += " – " + localEnd.ToString(localEnd.Date == local.Date ? "HH:mm" : "yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
|
||||
}
|
||||
when += happening.FloatingTime ? " (local time)" : zone == default ? " UTC" : $" ({happening.TimeZone})";
|
||||
var where = happening.Places.Select(p => p.Name ?? p.Address).FirstOrDefault(p => p != default)
|
||||
?? (happening.OnlineURL != default ? "online" : default);
|
||||
return where == default ? when : $"{when} · {where}";
|
||||
}
|
||||
|
||||
static TimeZoneInfo Zone(string id)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
return default;
|
||||
try
|
||||
{
|
||||
return TimeZoneInfo.FindSystemTimeZoneById(id);
|
||||
}
|
||||
catch (Exception ex) when (ex is TimeZoneNotFoundException or InvalidTimeZoneException)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
PreviewCard Card(PostEntity post)
|
||||
{
|
||||
if (!post.IsFederatedCopy)
|
||||
return default;
|
||||
if (post.Link is { } link)
|
||||
return Card(link.Href, link.Title ?? post.Title, link.Description ?? (post.Media.Count == 0 ? post.Excerpt : default), link.ImageURL);
|
||||
if (post.ObjectType is "Article" or "Event" or "Video" or "Audio" && post.Url != default && post.Media.Count == 0)
|
||||
return Card(post.Url, post.Title, post.Excerpt, post.CoverURL);
|
||||
return default;
|
||||
}
|
||||
|
||||
PreviewCard Card(string url, string title, string description, string image)
|
||||
{
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
return default;
|
||||
return new PreviewCard
|
||||
{
|
||||
Url = url,
|
||||
Title = title ?? uri.Host,
|
||||
Description = description ?? string.Empty,
|
||||
ProviderName = uri.Host,
|
||||
ProviderUrl = uri.GetLeftPart(UriPartial.Authority),
|
||||
Image = Proxied(image)
|
||||
};
|
||||
}
|
||||
|
||||
public static string Visibility(PostVisibility visibility) => visibility switch
|
||||
{
|
||||
PostVisibility.Unlisted => "unlisted",
|
||||
@@ -253,6 +315,28 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
return new StatusMention { Id = account.ID, Username = account.UserName, Acct = $"{account.UserName}@{account.Domain}", Url = account.Url ?? account.ActorURI };
|
||||
}
|
||||
|
||||
static Dictionary<string, object> Meta(PostMedia media)
|
||||
{
|
||||
var original = new Dictionary<string, object>();
|
||||
if (media.Width > 0 && media.Height > 0)
|
||||
{
|
||||
original["width"] = media.Width;
|
||||
original["height"] = media.Height;
|
||||
original["size"] = $"{media.Width}x{media.Height}";
|
||||
original["aspect"] = (double)media.Width.Value / media.Height.Value;
|
||||
}
|
||||
if (media.DurationSeconds is { } seconds)
|
||||
original["duration"] = seconds;
|
||||
var meta = new Dictionary<string, object>();
|
||||
if (original.Count > 0)
|
||||
meta["original"] = original;
|
||||
if (media.DurationSeconds is { } length)
|
||||
meta["length"] = TimeSpan.FromSeconds(length).ToString(@"h\:mm\:ss\.ff", CultureInfo.InvariantCulture);
|
||||
if (media.Focus is { Length: 2 })
|
||||
meta["focus"] = new { x = media.Focus[0], y = media.Focus[1] };
|
||||
return meta;
|
||||
}
|
||||
|
||||
MediaAttachment Media(PostMedia media) => new()
|
||||
{
|
||||
Id = media.AttachmentId ?? media.Id.ToString("N"),
|
||||
@@ -265,17 +349,12 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
_ => "unknown"
|
||||
},
|
||||
Url = media.URL ?? Proxied(media.RemoteURL),
|
||||
PreviewUrl = media.PreviewURL ?? media.URL ?? Proxied(media.RemoteURL),
|
||||
PreviewUrl = media.PreviewURL ?? Proxied(media.RemotePreviewURL)
|
||||
?? (media.ContentType?.StartsWith("image/") == true || media.ContentType == default ? media.URL ?? Proxied(media.RemoteURL) : default),
|
||||
RemoteUrl = media.RemoteURL,
|
||||
Description = media.Description,
|
||||
Blurhash = media.Blurhash,
|
||||
Meta = new
|
||||
{
|
||||
original = media.Width.HasValue && media.Height > 0
|
||||
? new { width = media.Width, height = media.Height, size = $"{media.Width}x{media.Height}", aspect = (double)media.Width / media.Height.Value }
|
||||
: default,
|
||||
focus = media.Focus is { Length: 2 } ? new { x = media.Focus[0], y = media.Focus[1] } : default
|
||||
}
|
||||
Meta = Meta(media)
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user