feat: deliver likes as bookmarks, revert announce cc, add OG images

- Likes are now sent as Create/Note with bookmark-style content (🔖)
  instead of Like activities, ensuring proper display on Mastodon
- Announce activities reverted to upstream addressing (to: Public only,
  no cc:followers)
- Add per-post OG image to both plain JSON-LD and Fedify Note/Article
  objects, derived from the post URL pattern (/og/{date}-{slug}.png)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-19 01:34:59 +01:00
parent b99f5fb73e
commit 45f8ba93c0
+27 -38
View File
@@ -14,7 +14,6 @@ import {
Create, Create,
Hashtag, Hashtag,
Image, Image,
Like,
Mention, Mention,
Note, Note,
Video, Video,
@@ -95,24 +94,7 @@ function linkifyMentions(html, resolvedMentions) {
export function jf2ToActivityStreams(properties, actorUrl, publicationUrl, options = {}) { export function jf2ToActivityStreams(properties, actorUrl, publicationUrl, options = {}) {
const postType = properties["post-type"]; const postType = properties["post-type"];
if (postType === "like") { // Likes are delivered as bookmarks — fall through to bookmark handling below
// Serve like posts as Note objects for AP content negotiation.
// Returning a bare Like activity breaks Mastodon's authorize_interaction
// flow because it expects a content object (Note/Article), not an activity.
const likeOf = properties["like-of"];
const postUrl = resolvePostUrl(properties.url, publicationUrl);
return {
"@context": "https://www.w3.org/ns/activitystreams",
type: "Note",
id: postUrl,
attributedTo: actorUrl,
published: properties.published,
url: postUrl,
to: ["https://www.w3.org/ns/activitystreams#Public"],
cc: [`${actorUrl.replace(/\/$/, "")}/followers`],
content: `\u2764\uFE0F <a href="${likeOf}">${likeOf}</a>`,
};
}
// Reposts are always public — Mastodon and other implementations expect this // Reposts are always public — Mastodon and other implementations expect this
if (postType === "repost") { if (postType === "repost") {
@@ -156,8 +138,8 @@ export function jf2ToActivityStreams(properties, actorUrl, publicationUrl, optio
: [followersUrl], : [followersUrl],
}; };
if (postType === "bookmark") { if (postType === "bookmark" || postType === "like") {
const bookmarkUrl = properties["bookmark-of"]; const bookmarkUrl = properties["bookmark-of"] || properties["like-of"];
const commentary = linkifyUrls(properties.content?.html || properties.content || ""); const commentary = linkifyUrls(properties.content?.html || properties.content || "");
object.content = commentary object.content = commentary
? `${commentary}<br><br>\u{1F516} <a href="${bookmarkUrl}">${bookmarkUrl}</a>` ? `${commentary}<br><br>\u{1F516} <a href="${bookmarkUrl}">${bookmarkUrl}</a>`
@@ -178,6 +160,16 @@ export function jf2ToActivityStreams(properties, actorUrl, publicationUrl, optio
object.content += `<p>\u{1F517} <a href="${postUrl}">${postUrl}</a></p>`; object.content += `<p>\u{1F517} <a href="${postUrl}">${postUrl}</a></p>`;
} }
// OG image for fediverse preview cards
const ogMatch = postUrl && postUrl.match(/\/([\w-]+)\/(\d{4})\/(\d{2})\/(\d{2})\/([\w-]+)\/?$/);
if (ogMatch) {
object.image = {
type: "Image",
url: `${publicationUrl.replace(/\/$/, "")}/og/${ogMatch[2]}-${ogMatch[3]}-${ogMatch[4]}-${ogMatch[5]}.png`,
mediaType: "image/png",
};
}
if (isArticle) { if (isArticle) {
object.name = properties.name; object.name = properties.name;
if (properties.summary) { if (properties.summary) {
@@ -253,28 +245,16 @@ export function jf2ToAS2Activity(properties, actorUrl, publicationUrl, options =
const postType = properties["post-type"]; const postType = properties["post-type"];
const actorUri = new URL(actorUrl); const actorUri = new URL(actorUrl);
if (postType === "like") { // Likes are delivered as bookmarks — fall through to bookmark handling below
const likeOf = properties["like-of"];
if (!likeOf) return null;
const followersUrl = `${actorUrl.replace(/\/$/, "")}/followers`;
return new Like({
actor: actorUri,
object: new URL(likeOf),
to: new URL("https://www.w3.org/ns/activitystreams#Public"),
cc: new URL(followersUrl),
});
}
// Reposts are always public — Mastodon and other implementations expect this // Reposts are always public — upstream @rmdes addressing
if (postType === "repost") { if (postType === "repost") {
const repostOf = properties["repost-of"]; const repostOf = properties["repost-of"];
if (!repostOf) return null; if (!repostOf) return null;
const followersUrl = `${actorUrl.replace(/\/$/, "")}/followers`;
return new Announce({ return new Announce({
actor: actorUri, actor: actorUri,
object: new URL(repostOf), object: new URL(repostOf),
to: new URL("https://www.w3.org/ns/activitystreams#Public"), to: new URL("https://www.w3.org/ns/activitystreams#Public"),
cc: new URL(followersUrl),
}); });
} }
@@ -336,8 +316,8 @@ export function jf2ToAS2Activity(properties, actorUrl, publicationUrl, options =
} }
// Content // Content
if (postType === "bookmark") { if (postType === "bookmark" || postType === "like") {
const bookmarkUrl = properties["bookmark-of"]; const bookmarkUrl = properties["bookmark-of"] || properties["like-of"];
const commentary = linkifyUrls(properties.content?.html || properties.content || ""); const commentary = linkifyUrls(properties.content?.html || properties.content || "");
noteOptions.content = commentary noteOptions.content = commentary
? `${commentary}<br><br>\u{1F516} <a href="${bookmarkUrl}">${bookmarkUrl}</a>` ? `${commentary}<br><br>\u{1F516} <a href="${bookmarkUrl}">${bookmarkUrl}</a>`
@@ -382,6 +362,15 @@ export function jf2ToAS2Activity(properties, actorUrl, publicationUrl, options =
noteOptions.attachments = fedifyAttachments; noteOptions.attachments = fedifyAttachments;
} }
// OG image for fediverse preview cards
const ogMatchF = postUrl && postUrl.match(/\/([\w-]+)\/(\d{4})\/(\d{2})\/(\d{2})\/([\w-]+)\/?$/);
if (ogMatchF) {
noteOptions.image = new Image({
url: new URL(`${publicationUrl.replace(/\/$/, "")}/og/${ogMatchF[2]}-${ogMatchF[3]}-${ogMatchF[4]}-${ogMatchF[5]}.png`),
mediaType: "image/png",
});
}
// Tags: hashtags + Mention for reply addressing + @mentions // Tags: hashtags + Mention for reply addressing + @mentions
const fedifyTags = buildFedifyTags(properties, publicationUrl, postType); const fedifyTags = buildFedifyTags(properties, publicationUrl, postType);
@@ -575,7 +564,7 @@ function buildPlainTags(properties, publicationUrl, existing) {
function buildFedifyTags(properties, publicationUrl, postType) { function buildFedifyTags(properties, publicationUrl, postType) {
const tags = []; const tags = [];
if (postType === "bookmark") { if (postType === "bookmark" || postType === "like") {
tags.push( tags.push(
new Hashtag({ new Hashtag({
name: "#bookmark", name: "#bookmark",