From 45f8ba93c0202fabf460bdd03e9ee758faa0a457 Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:34:59 +0100 Subject: [PATCH] feat: deliver likes as bookmarks, revert announce cc, add OG images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- lib/jf2-to-as2.js | 65 ++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/lib/jf2-to-as2.js b/lib/jf2-to-as2.js index 03932a7..00b4824 100644 --- a/lib/jf2-to-as2.js +++ b/lib/jf2-to-as2.js @@ -14,7 +14,6 @@ import { Create, Hashtag, Image, - Like, Mention, Note, Video, @@ -95,24 +94,7 @@ function linkifyMentions(html, resolvedMentions) { export function jf2ToActivityStreams(properties, actorUrl, publicationUrl, options = {}) { const postType = properties["post-type"]; - if (postType === "like") { - // 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 ${likeOf}`, - }; - } + // Likes are delivered as bookmarks — fall through to bookmark handling below // Reposts are always public — Mastodon and other implementations expect this if (postType === "repost") { @@ -156,8 +138,8 @@ export function jf2ToActivityStreams(properties, actorUrl, publicationUrl, optio : [followersUrl], }; - if (postType === "bookmark") { - const bookmarkUrl = properties["bookmark-of"]; + if (postType === "bookmark" || postType === "like") { + const bookmarkUrl = properties["bookmark-of"] || properties["like-of"]; const commentary = linkifyUrls(properties.content?.html || properties.content || ""); object.content = commentary ? `${commentary}

\u{1F516} ${bookmarkUrl}` @@ -178,6 +160,16 @@ export function jf2ToActivityStreams(properties, actorUrl, publicationUrl, optio object.content += `

\u{1F517} ${postUrl}

`; } + // 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) { object.name = properties.name; if (properties.summary) { @@ -253,28 +245,16 @@ export function jf2ToAS2Activity(properties, actorUrl, publicationUrl, options = const postType = properties["post-type"]; const actorUri = new URL(actorUrl); - if (postType === "like") { - 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), - }); - } + // Likes are delivered as bookmarks — fall through to bookmark handling below - // Reposts are always public — Mastodon and other implementations expect this + // Reposts are always public — upstream @rmdes addressing if (postType === "repost") { const repostOf = properties["repost-of"]; if (!repostOf) return null; - const followersUrl = `${actorUrl.replace(/\/$/, "")}/followers`; return new Announce({ actor: actorUri, object: new URL(repostOf), 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 - if (postType === "bookmark") { - const bookmarkUrl = properties["bookmark-of"]; + if (postType === "bookmark" || postType === "like") { + const bookmarkUrl = properties["bookmark-of"] || properties["like-of"]; const commentary = linkifyUrls(properties.content?.html || properties.content || ""); noteOptions.content = commentary ? `${commentary}

\u{1F516} ${bookmarkUrl}` @@ -382,6 +362,15 @@ export function jf2ToAS2Activity(properties, actorUrl, publicationUrl, options = 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 const fedifyTags = buildFedifyTags(properties, publicationUrl, postType); @@ -575,7 +564,7 @@ function buildPlainTags(properties, publicationUrl, existing) { function buildFedifyTags(properties, publicationUrl, postType) { const tags = []; - if (postType === "bookmark") { + if (postType === "bookmark" || postType === "like") { tags.push( new Hashtag({ name: "#bookmark",