diff --git a/packages/backend/src/mfm/to-html.ts b/packages/backend/src/mfm/to-html.ts index 830bfac52..06acaeceb 100644 --- a/packages/backend/src/mfm/to-html.ts +++ b/packages/backend/src/mfm/to-html.ts @@ -4,6 +4,7 @@ import config from '@/config/index.js'; import { UserProfiles } from '@/models/index.js'; import { extractMentions } from '@/misc/extract-mentions.js'; import { intersperse } from '@/prelude/array.js'; +import { toPunyNullable } from '@/misc/convert-host.js'; // Transforms MFM to HTML, given the MFM text and a list of user IDs that are // mentioned in the text. If the list of mentions is not given, all mentions @@ -14,6 +15,19 @@ export async function toHtml(mfmText: string, mentions?: string[]): Promise 0) { + mentionedUsers = await UserProfiles.createQueryBuilder('user_profile') + .leftJoin('user_profile.user', 'user') + .select('user.usernameLower', 'username') + .addSelect('user.host', 'host') + // links should preferably use user friendly urls, only fall back to AP ids + .addSelect('COALESCE(user_profile.url, user.uri)', 'url') + .where('"userId" IN (:...ids)', { ids }) + .getRawMany(); + } + const doc = new JSDOM('').window.document; const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType) => Promise } = { @@ -103,30 +117,28 @@ export async function toHtml(mfmText: string, mentions?: string[]): Promise { - const { username, host, acct } = node.props; - const ids = mentions ?? extractMentions(nodes); - if (ids.length > 0) { - const mentionedUsers = await UserProfiles.createQueryBuilder('user_profile') - .leftJoin('user_profile.user', 'user') - .select('user.username', 'username') - .addSelect('user.host', 'host') - // links should preferably use user friendly urls, only fall back to AP ids - .addSelect('COALESCE(user_profile.url, user.uri)', 'url') - .where('"userId" IN (:...ids)', { ids }) - .getRawMany(); - const userInfo = mentionedUsers.find(user => user.username === username && user.host === host); - if (userInfo != null) { - // Mastodon microformat: span.h-card > a.u-url.mention - const a = doc.createElement('a'); - a.href = userInfo.url ?? `${config.url}/${acct}`; - a.className = 'u-url mention'; - a.textContent = acct; + let { username, host, acct } = node.props; + // normalize username and host for searching the user + username = username.toLowerCase(); + host = toPunyNullable(host); + // Discard host if it is the local host. Otherwise mentions of local users where the + // hostname is not omitted are not handled correctly. + if (host == config.hostname) { + host = null; + } + const userInfo = mentionedUsers.find(user => user.username === username && user.host === host); + if (userInfo != null) { + // Mastodon microformat: span.h-card > a.u-url.mention + const a = doc.createElement('a'); + // The fallback will only be used for local users, so the host part can be discarded. + a.href = userInfo.url ?? `${config.url}/@${username}`; + a.className = 'u-url mention'; + a.textContent = acct; - const card = doc.createElement('span'); - card.className = 'h-card'; - card.appendChild(a); - return card; - } + const card = doc.createElement('span'); + card.className = 'h-card'; + card.appendChild(a); + return card; } // this user does not actually exist return doc.createTextNode(acct);