@rmdes/indiekit-endpoint-activitypub 3.7.1 → 3.7.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.
package/index.js CHANGED
@@ -721,13 +721,19 @@ export default class ActivityPubEndpoint {
721
721
  );
722
722
 
723
723
  // Resolve the remote actor to get their inbox
724
- // Use authenticated document loader for servers requiring Authorized Fetch
724
+ // Try authenticated document loader first (for Authorized Fetch servers),
725
+ // fall back to unsigned if that fails (some servers reject signed GETs)
725
726
  const documentLoader = await ctx.getDocumentLoader({
726
727
  identifier: handle,
727
728
  });
728
- const remoteActor = await lookupWithSecurity(ctx,actorUrl, {
729
+ let remoteActor = await lookupWithSecurity(ctx, actorUrl, {
729
730
  documentLoader,
730
731
  });
732
+ if (!remoteActor) {
733
+ // Retry without authentication — some servers (e.g., tags.pub)
734
+ // may reject or mishandle signed GET requests
735
+ remoteActor = await lookupWithSecurity(ctx, actorUrl);
736
+ }
731
737
  if (!remoteActor) {
732
738
  return { ok: false, error: "Could not resolve remote actor" };
733
739
  }
@@ -247,12 +247,17 @@ router.post("/api/v1/statuses", async (req, res, next) => {
247
247
  });
248
248
  };
249
249
 
250
+ // Process content: linkify URLs and extract @mentions
251
+ const rawContent = data.properties.content || { text: statusText || "", html: "" };
252
+ const processedContent = processStatusContent(rawContent, statusText || "");
253
+ const mentions = extractMentions(statusText || "");
254
+
250
255
  const now = new Date().toISOString();
251
256
  const timelineItem = await addTimelineItem(collections, {
252
257
  uid: postUrl,
253
258
  url: postUrl,
254
259
  type: data.properties["post-type"] || "note",
255
- content: data.properties.content || { text: statusText || "", html: "" },
260
+ content: processedContent,
256
261
  summary: spoilerText || "",
257
262
  sensitive: sensitive === true || sensitive === "true",
258
263
  visibility: visibility || "public",
@@ -274,7 +279,7 @@ router.post("/api/v1/statuses", async (req, res, next) => {
274
279
  category: categories,
275
280
  counts: { replies: 0, boosts: 0, likes: 0 },
276
281
  linkPreviews: [],
277
- mentions: [],
282
+ mentions,
278
283
  emojis: [],
279
284
  });
280
285
 
@@ -636,4 +641,68 @@ async function loadItemInteractions(collections, item) {
636
641
  return { favouritedIds, rebloggedIds, bookmarkedIds };
637
642
  }
638
643
 
644
+ /**
645
+ * Process status content: linkify bare URLs and convert @mentions to links.
646
+ *
647
+ * Mastodon clients send plain text — the server is responsible for
648
+ * converting URLs and mentions into HTML links.
649
+ *
650
+ * @param {object} content - { text, html } from Micropub pipeline
651
+ * @param {string} rawText - Original status text from client
652
+ * @returns {object} { text, html } with linkified content
653
+ */
654
+ function processStatusContent(content, rawText) {
655
+ let html = content.html || content.text || rawText || "";
656
+
657
+ // If the HTML is just plain text wrapped in <p>, process it
658
+ // Don't touch HTML that already has links (from Micropub rendering)
659
+ if (!html.includes("<a ")) {
660
+ // Linkify bare URLs (http/https)
661
+ html = html.replace(
662
+ /(https?:\/\/[^\s<>"')\]]+)/g,
663
+ '<a href="$1" rel="nofollow noopener noreferrer" target="_blank">$1</a>',
664
+ );
665
+
666
+ // Convert @user@domain mentions to profile links
667
+ html = html.replace(
668
+ /(?:^|\s)(@([a-zA-Z0-9_]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,}))/g,
669
+ (match, full, username, domain) =>
670
+ match.replace(
671
+ full,
672
+ `<span class="h-card"><a href="https://${domain}/@${username}" class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@${username}@${domain}</a></span>`,
673
+ ),
674
+ );
675
+ }
676
+
677
+ return {
678
+ text: content.text || rawText || "",
679
+ html,
680
+ };
681
+ }
682
+
683
+ /**
684
+ * Extract @user@domain mentions from text into mention objects.
685
+ *
686
+ * @param {string} text - Status text
687
+ * @returns {Array<{name: string, url: string}>} Mention objects
688
+ */
689
+ function extractMentions(text) {
690
+ if (!text) return [];
691
+ const mentionRegex = /@([a-zA-Z0-9_]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
692
+ const mentions = [];
693
+ const seen = new Set();
694
+ let match;
695
+ while ((match = mentionRegex.exec(text)) !== null) {
696
+ const [, username, domain] = match;
697
+ const key = `${username}@${domain}`.toLowerCase();
698
+ if (seen.has(key)) continue;
699
+ seen.add(key);
700
+ mentions.push({
701
+ name: `@${username}@${domain}`,
702
+ url: `https://${domain}/@${username}`,
703
+ });
704
+ }
705
+ return mentions;
706
+ }
707
+
639
708
  export default router;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "3.7.1",
3
+ "version": "3.7.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",