diff --git a/lib/mastodon/routes/statuses.js b/lib/mastodon/routes/statuses.js index 22873d4..47a6789 100644 --- a/lib/mastodon/routes/statuses.js +++ b/lib/mastodon/routes/statuses.js @@ -247,12 +247,17 @@ router.post("/api/v1/statuses", async (req, res, next) => { }); }; + // Process content: linkify URLs and extract @mentions + const rawContent = data.properties.content || { text: statusText || "", html: "" }; + const processedContent = processStatusContent(rawContent, statusText || ""); + const mentions = extractMentions(statusText || ""); + const now = new Date().toISOString(); const timelineItem = await addTimelineItem(collections, { uid: postUrl, url: postUrl, type: data.properties["post-type"] || "note", - content: data.properties.content || { text: statusText || "", html: "" }, + content: processedContent, summary: spoilerText || "", sensitive: sensitive === true || sensitive === "true", visibility: visibility || "public", @@ -274,7 +279,7 @@ router.post("/api/v1/statuses", async (req, res, next) => { category: categories, counts: { replies: 0, boosts: 0, likes: 0 }, linkPreviews: [], - mentions: [], + mentions, emojis: [], }); @@ -636,4 +641,68 @@ async function loadItemInteractions(collections, item) { return { favouritedIds, rebloggedIds, bookmarkedIds }; } +/** + * Process status content: linkify bare URLs and convert @mentions to links. + * + * Mastodon clients send plain text — the server is responsible for + * converting URLs and mentions into HTML links. + * + * @param {object} content - { text, html } from Micropub pipeline + * @param {string} rawText - Original status text from client + * @returns {object} { text, html } with linkified content + */ +function processStatusContent(content, rawText) { + let html = content.html || content.text || rawText || ""; + + // If the HTML is just plain text wrapped in

, process it + // Don't touch HTML that already has links (from Micropub rendering) + if (!html.includes(""')\]]+)/g, + '$1', + ); + + // Convert @user@domain mentions to profile links + html = html.replace( + /(?:^|\s)(@([a-zA-Z0-9_]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,}))/g, + (match, full, username, domain) => + match.replace( + full, + `@${username}@${domain}`, + ), + ); + } + + return { + text: content.text || rawText || "", + html, + }; +} + +/** + * Extract @user@domain mentions from text into mention objects. + * + * @param {string} text - Status text + * @returns {Array<{name: string, url: string}>} Mention objects + */ +function extractMentions(text) { + if (!text) return []; + const mentionRegex = /@([a-zA-Z0-9_]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g; + const mentions = []; + const seen = new Set(); + let match; + while ((match = mentionRegex.exec(text)) !== null) { + const [, username, domain] = match; + const key = `${username}@${domain}`.toLowerCase(); + if (seen.has(key)) continue; + seen.add(key); + mentions.push({ + name: `@${username}@${domain}`, + url: `https://${domain}/@${username}`, + }); + } + return mentions; +} + export default router; diff --git a/package.json b/package.json index 7f719e7..718f1a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-activitypub", - "version": "3.7.2", + "version": "3.7.3", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "keywords": [ "indiekit",