mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
merge: upstream 42c0959..6436763 — own posts in ap_timeline with synthesized content for interactions
-42c0959: feat: add own posts to ap_timeline after syndication -f1ad18d: fix: synthesize timeline content for likes/bookmarks/reposts -6436763: fix: include interaction target URL in timeline content Upstream's implementation supersedes our42f8c2d: uses buildTimelineContent() for synthesized display content on interaction types, reads real profile data, normalizes media, idempotent via $setOnInsert.
This commit is contained in:
@@ -220,46 +220,52 @@ export function createSyndicator(plugin) {
|
||||
`[ActivityPub] Syndication queued: ${typeName} for ${properties.url}${replyNote}`,
|
||||
);
|
||||
|
||||
// Mirror own Micropub-created posts into ap_timeline so the Mastodon
|
||||
// Client API (context, statuses, etc.) can find them by ID.
|
||||
if (typeName === "Create" && properties.url) {
|
||||
try {
|
||||
const rawHtml = properties.content?.html || (typeof properties.content === "string" ? properties.content : "") || "";
|
||||
const now = new Date().toISOString();
|
||||
const postType = properties["post-type"] || "note";
|
||||
const asArray = (v) => Array.isArray(v) ? v : v ? [v] : [];
|
||||
await addTimelineItem(plugin._collections, {
|
||||
uid: properties.url,
|
||||
url: properties.url,
|
||||
type: postType,
|
||||
content: { html: rawHtml, text: rawHtml.replace(/<[^>]*>/g, "") },
|
||||
summary: properties["content-warning"] || properties.summary || "",
|
||||
sensitive: !!(properties.sensitive || properties["content-warning"]),
|
||||
visibility: properties.visibility || plugin.options.defaultVisibility || "public",
|
||||
language: properties.lang || properties.language || null,
|
||||
inReplyTo: properties["in-reply-to"] || null,
|
||||
published: properties.published || now,
|
||||
createdAt: now,
|
||||
author: {
|
||||
name: plugin.options.actor.name || handle,
|
||||
url: actorUrl,
|
||||
photo: plugin.options.actor.icon || "",
|
||||
handle: `@${handle}`,
|
||||
emojis: [],
|
||||
bot: false,
|
||||
},
|
||||
photo: asArray(properties.photo),
|
||||
video: asArray(properties.video),
|
||||
audio: asArray(properties.audio),
|
||||
category: asArray(properties.category),
|
||||
counts: { replies: 0, boosts: 0, likes: 0 },
|
||||
linkPreviews: [],
|
||||
mentions: [],
|
||||
// Add own post to ap_timeline so it appears in Mastodon Client API
|
||||
// timelines (Phanpy/Moshidon). Uses $setOnInsert — idempotent.
|
||||
try {
|
||||
const profile = await plugin._collections.ap_profile?.findOne({});
|
||||
const content = buildTimelineContent(properties);
|
||||
const timelineItem = {
|
||||
uid: properties.url,
|
||||
url: properties.url,
|
||||
type: mapPostType(properties["post-type"]),
|
||||
content,
|
||||
author: {
|
||||
name: profile?.name || handle,
|
||||
url: profile?.url || plugin._publicationUrl,
|
||||
photo: profile?.icon || "",
|
||||
handle: `@${handle}`,
|
||||
emojis: [],
|
||||
});
|
||||
} catch (timelineError) {
|
||||
console.warn("[ActivityPub] Failed to mirror syndicated post to ap_timeline:", timelineError.message);
|
||||
bot: false,
|
||||
},
|
||||
published: properties.published || new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
visibility: properties.visibility || "public",
|
||||
sensitive: properties.sensitive === "true",
|
||||
category: Array.isArray(properties.category)
|
||||
? properties.category
|
||||
: properties.category ? [properties.category] : [],
|
||||
photo: normalizeMedia(properties.photo, plugin._publicationUrl),
|
||||
video: normalizeMedia(properties.video, plugin._publicationUrl),
|
||||
audio: normalizeMedia(properties.audio, plugin._publicationUrl),
|
||||
counts: { replies: 0, boosts: 0, likes: 0 },
|
||||
};
|
||||
if (properties.name) timelineItem.name = properties.name;
|
||||
if (properties.summary) timelineItem.summary = properties.summary;
|
||||
if (properties["content-warning"]) {
|
||||
timelineItem.summary = properties["content-warning"];
|
||||
timelineItem.sensitive = true;
|
||||
}
|
||||
if (properties["in-reply-to"]) {
|
||||
timelineItem.inReplyTo = Array.isArray(properties["in-reply-to"])
|
||||
? properties["in-reply-to"][0]
|
||||
: properties["in-reply-to"];
|
||||
}
|
||||
await addTimelineItem(plugin._collections, timelineItem);
|
||||
} catch (tlError) {
|
||||
console.warn(
|
||||
`[ActivityPub] Failed to add own post to timeline: ${tlError.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return properties.url || undefined;
|
||||
@@ -280,3 +286,94 @@ export function createSyndicator(plugin) {
|
||||
update: async (properties) => plugin.update(properties),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Timeline helpers ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Build content from JF2 properties for the ap_timeline entry.
|
||||
* For interaction types (likes, bookmarks, reposts), always includes
|
||||
* the target URL — even when there's comment text alongside it.
|
||||
*/
|
||||
function buildTimelineContent(properties) {
|
||||
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
|
||||
// Extract any existing body content
|
||||
const raw = properties.content;
|
||||
let bodyText = "";
|
||||
let bodyHtml = "";
|
||||
if (raw) {
|
||||
if (typeof raw === "string") {
|
||||
bodyText = raw;
|
||||
bodyHtml = `<p>${raw}</p>`;
|
||||
} else {
|
||||
bodyText = raw.text || raw.value || "";
|
||||
bodyHtml = raw.html || raw.text || raw.value || "";
|
||||
}
|
||||
}
|
||||
|
||||
// Interaction types: prepend label + target URL, append any comment
|
||||
const likeOf = properties["like-of"];
|
||||
if (likeOf) {
|
||||
const prefix = `Liked: ${likeOf}`;
|
||||
const prefixHtml = `<p>Liked: <a href="${esc(likeOf)}">${esc(likeOf)}</a></p>`;
|
||||
return {
|
||||
text: bodyText ? `${prefix}\n\n${bodyText}` : prefix,
|
||||
html: bodyText ? `${prefixHtml}\n${bodyHtml}` : prefixHtml,
|
||||
};
|
||||
}
|
||||
|
||||
const bookmarkOf = properties["bookmark-of"];
|
||||
if (bookmarkOf) {
|
||||
const label = properties.name || bookmarkOf;
|
||||
const prefix = `Bookmarked: ${label}`;
|
||||
const prefixHtml = `<p>Bookmarked: <a href="${esc(bookmarkOf)}">${esc(label)}</a></p>`;
|
||||
return {
|
||||
text: bodyText ? `${prefix}\n\n${bodyText}` : prefix,
|
||||
html: bodyText ? `${prefixHtml}\n${bodyHtml}` : prefixHtml,
|
||||
};
|
||||
}
|
||||
|
||||
const repostOf = properties["repost-of"];
|
||||
if (repostOf) {
|
||||
const label = properties.name || repostOf;
|
||||
const prefix = `Reposted: ${label}`;
|
||||
const prefixHtml = `<p>Reposted: <a href="${esc(repostOf)}">${esc(label)}</a></p>`;
|
||||
return {
|
||||
text: bodyText ? `${prefix}\n\n${bodyText}` : prefix,
|
||||
html: bodyText ? `${prefixHtml}\n${bodyHtml}` : prefixHtml,
|
||||
};
|
||||
}
|
||||
|
||||
// Regular post — return body content as-is
|
||||
if (bodyText || bodyHtml) {
|
||||
return { text: bodyText, html: bodyHtml };
|
||||
}
|
||||
|
||||
// Article with title but no body
|
||||
if (properties.name) {
|
||||
return { text: properties.name, html: `<p>${esc(properties.name)}</p>` };
|
||||
}
|
||||
|
||||
return { text: "", html: "" };
|
||||
}
|
||||
|
||||
function mapPostType(postType) {
|
||||
if (postType === "article") return "article";
|
||||
if (postType === "repost") return "boost";
|
||||
return "note";
|
||||
}
|
||||
|
||||
function normalizeMedia(value, siteUrl) {
|
||||
if (!value) return [];
|
||||
const base = siteUrl?.replace(/\/$/, "") || "";
|
||||
const arr = Array.isArray(value) ? value : [value];
|
||||
return arr.map((item) => {
|
||||
if (typeof item === "string") {
|
||||
return item.startsWith("http") ? item : `${base}/${item.replace(/^\//, "")}`;
|
||||
}
|
||||
if (item?.url && !item.url.startsWith("http")) {
|
||||
return { ...item, url: `${base}/${item.url.replace(/^\//, "")}` };
|
||||
}
|
||||
return item;
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
||||
"version": "3.10.0",
|
||||
"version": "3.10.3",
|
||||
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
||||
"keywords": [
|
||||
"indiekit",
|
||||
|
||||
Reference in New Issue
Block a user