@rmdes/indiekit-endpoint-activitypub 3.12.1 → 3.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { serializeAccount } from "./account.js";
17
17
  import { sanitizeHtml } from "./sanitize.js";
18
+ import { remoteActorId } from "../helpers/id-mapping.js";
18
19
 
19
20
  // Module-level defaults set once at startup via setLocalIdentity()
20
21
  let _localPublicationUrl = "";
@@ -99,7 +100,17 @@ export function serializeStatus(item, { baseUrl, favouritedIds, rebloggedIds, bo
99
100
  }
100
101
 
101
102
  // Regular status (note, article, question)
102
- const content = item.content?.html || item.content?.text || "";
103
+ let content = item.content?.html || item.content?.text || "";
104
+
105
+ // Append permalink for own posts at read time — matches what fediverse
106
+ // users see via federation (jf2-to-as2 appends the same link).
107
+ // Done here instead of at write time so it survives backfills and cleanups.
108
+ const isOwnPost = _localPublicationUrl && item.author?.url === _localPublicationUrl;
109
+ const postUrl = item.uid || item.url;
110
+ if (isOwnPost && postUrl && !content.includes(postUrl)) {
111
+ const escaped = postUrl.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
112
+ content += `\n<p>\u{1F517} <a href="${escaped}">${escaped}</a></p>`;
113
+ }
103
114
  const spoilerText = item.summary || "";
104
115
  const sensitive = item.sensitive || false;
105
116
  const visibility = item.visibility || "public";
@@ -168,13 +179,17 @@ export function serializeStatus(item, { baseUrl, favouritedIds, rebloggedIds, bo
168
179
  url: `${baseUrl}/tags/${encodeURIComponent(tag)}`,
169
180
  }));
170
181
 
171
- // Mentions
172
- const mentions = (item.mentions || []).map((m) => ({
173
- id: "0", // We don't have stable IDs for mentioned accounts
174
- username: m.name || "",
175
- url: m.url || "",
176
- acct: m.name || "",
177
- }));
182
+ // Mentions — use actorUrl for deterministic ID, parse acct from handle
183
+ const mentions = (item.mentions || []).map((m) => {
184
+ const handle = (m.name || "").replace(/^@/, "");
185
+ const parts = handle.split("@");
186
+ return {
187
+ id: m.actorUrl ? remoteActorId(m.actorUrl) : "0",
188
+ username: parts[0] || handle,
189
+ url: m.url || m.actorUrl || "",
190
+ acct: handle,
191
+ };
192
+ });
178
193
 
179
194
  // Custom emojis
180
195
  const emojis = (item.emojis || []).map((e) => ({
package/lib/syndicator.js CHANGED
@@ -225,20 +225,41 @@ export function createSyndicator(plugin) {
225
225
  try {
226
226
  const profile = await plugin._collections.ap_profile?.findOne({});
227
227
  const content = buildTimelineContent(properties);
228
-
229
- // Append permalink to ALL post types so the Mastodon API timeline
230
- // matches what fediverse users see via federation (jf2-to-as2 appends it too).
231
- if (properties.url) {
232
- const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
233
- content.text = `${content.text}\n\n\u{1F517} ${properties.url}`;
234
- content.html = `${content.html}\n<p>\u{1F517} <a href="${esc(properties.url)}">${esc(properties.url)}</a></p>`;
228
+ // Permalink is appended at read time by serializeStatus, not here.
229
+
230
+ // Linkify @mentions in content using resolved WebFinger data.
231
+ // This ensures the ap_timeline HTML has proper <a> links for
232
+ // mentions, matching what the federated AS2 activity contains.
233
+ if (resolvedMentions.length > 0 && content.html) {
234
+ const { default: jf2Mod } = await import("./jf2-to-as2.js");
235
+ // Import linkifyMentions — it's not exported, so inline the logic
236
+ for (const { handle, profileUrl, actorUrl } of resolvedMentions) {
237
+ const escaped = handle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
238
+ const pattern = new RegExp(`(?<!["\\/\\w])@${escaped}(?![\\w])`, "gi");
239
+ const parts = handle.split("@");
240
+ const url = profileUrl || (actorUrl ? actorUrl : `https://${parts[1]}/@${parts[0]}`);
241
+ content.html = content.html.replace(
242
+ pattern,
243
+ `<a href="${url}" class="mention" rel="nofollow noopener" target="_blank">@${handle}</a>`,
244
+ );
245
+ }
235
246
  }
236
247
 
248
+ // Store resolved mentions for Mastodon API serialization
249
+ const timelineMentions = resolvedMentions
250
+ .filter(m => m.actorUrl)
251
+ .map(m => ({
252
+ name: `@${m.handle}`,
253
+ url: m.profileUrl || m.actorUrl,
254
+ actorUrl: m.actorUrl,
255
+ }));
256
+
237
257
  const timelineItem = {
238
258
  uid: properties.url,
239
259
  url: properties.url,
240
260
  type: mapPostType(properties["post-type"]),
241
261
  content,
262
+ mentions: timelineMentions,
242
263
  author: {
243
264
  name: profile?.name || handle,
244
265
  url: profile?.url || plugin._publicationUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "3.12.1",
3
+ "version": "3.12.3",
4
4
  "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
5
5
  "keywords": [
6
6
  "indiekit",