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,
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 <a href="${likeOf}">${likeOf}</a>`,
};
}
// 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}<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>`;
}
// 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}<br><br>\u{1F516} <a href="${bookmarkUrl}">${bookmarkUrl}</a>`
@@ -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",