mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
feat: linkify URLs and extract @mentions in status creation
Mastodon clients send plain text — the server must convert bare URLs and @user@domain mentions into HTML links. Previously, URLs appeared as plain text and mentions were not stored as mention objects. - Bare URLs (http/https) are wrapped in <a> tags - @user@domain patterns are converted to profile links with h-card markup - Mentions are extracted into the mentions[] array with name and URL - Only processes content that doesn't already contain <a> tags (avoids double-linkifying Micropub-rendered content)
This commit is contained in:
@@ -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 <p>, process it
|
||||
// Don't touch HTML that already has links (from Micropub rendering)
|
||||
if (!html.includes("<a ")) {
|
||||
// Linkify bare URLs (http/https)
|
||||
html = html.replace(
|
||||
/(https?:\/\/[^\s<>"')\]]+)/g,
|
||||
'<a href="$1" rel="nofollow noopener noreferrer" target="_blank">$1</a>',
|
||||
);
|
||||
|
||||
// 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,
|
||||
`<span class="h-card"><a href="https://${domain}/@${username}" class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@${username}@${domain}</a></span>`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user