From 32bf01e7bb4bf175c5df167271c0d0cde9331719 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Tue, 31 Mar 2026 12:07:40 +0200 Subject: [PATCH] fix: resolve and linkify @mentions in ap_timeline content When the syndicator adds own posts to ap_timeline, it now: 1. Linkifies @user@domain patterns using WebFinger-resolved profile URLs (matching the federated AS2 content from jf2-to-as2.js) 2. Stores resolved mentions with actorUrl for proper Mastodon API serialization (deterministic account IDs via remoteActorId) serializeStatus now parses mention handles into proper username/acct fields with real account IDs instead of placeholder "0". --- lib/mastodon/entities/status.js | 19 ++++++++++++------- lib/syndicator.js | 28 ++++++++++++++++++++++++++++ package.json | 2 +- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/lib/mastodon/entities/status.js b/lib/mastodon/entities/status.js index 6ce2909..c38fcdd 100644 --- a/lib/mastodon/entities/status.js +++ b/lib/mastodon/entities/status.js @@ -15,6 +15,7 @@ */ import { serializeAccount } from "./account.js"; import { sanitizeHtml } from "./sanitize.js"; +import { remoteActorId } from "../helpers/id-mapping.js"; // Module-level defaults set once at startup via setLocalIdentity() let _localPublicationUrl = ""; @@ -178,13 +179,17 @@ export function serializeStatus(item, { baseUrl, favouritedIds, rebloggedIds, bo url: `${baseUrl}/tags/${encodeURIComponent(tag)}`, })); - // Mentions - const mentions = (item.mentions || []).map((m) => ({ - id: "0", // We don't have stable IDs for mentioned accounts - username: m.name || "", - url: m.url || "", - acct: m.name || "", - })); + // Mentions — use actorUrl for deterministic ID, parse acct from handle + const mentions = (item.mentions || []).map((m) => { + const handle = (m.name || "").replace(/^@/, ""); + const parts = handle.split("@"); + return { + id: m.actorUrl ? remoteActorId(m.actorUrl) : "0", + username: parts[0] || handle, + url: m.url || m.actorUrl || "", + acct: handle, + }; + }); // Custom emojis const emojis = (item.emojis || []).map((e) => ({ diff --git a/lib/syndicator.js b/lib/syndicator.js index 6739ccb..7cf70b1 100644 --- a/lib/syndicator.js +++ b/lib/syndicator.js @@ -227,11 +227,39 @@ export function createSyndicator(plugin) { const content = buildTimelineContent(properties); // Permalink is appended at read time by serializeStatus, not here. + // Linkify @mentions in content using resolved WebFinger data. + // This ensures the ap_timeline HTML has proper links for + // mentions, matching what the federated AS2 activity contains. + if (resolvedMentions.length > 0 && content.html) { + const { default: jf2Mod } = await import("./jf2-to-as2.js"); + // Import linkifyMentions — it's not exported, so inline the logic + for (const { handle, profileUrl, actorUrl } of resolvedMentions) { + const escaped = handle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`(?@${handle}`, + ); + } + } + + // Store resolved mentions for Mastodon API serialization + const timelineMentions = resolvedMentions + .filter(m => m.actorUrl) + .map(m => ({ + name: `@${m.handle}`, + url: m.profileUrl || m.actorUrl, + actorUrl: m.actorUrl, + })); + const timelineItem = { uid: properties.url, url: properties.url, type: mapPostType(properties["post-type"]), content, + mentions: timelineMentions, author: { name: profile?.name || handle, url: profile?.url || plugin._publicationUrl, diff --git a/package.json b/package.json index 423515b..cbacbd1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-activitypub", - "version": "3.12.2", + "version": "3.12.3", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "keywords": [ "indiekit",