diff --git a/packages/backend/src/mfm/to-html.ts b/packages/backend/src/mfm/to-html.ts index b1429f184..af399e81f 100644 --- a/packages/backend/src/mfm/to-html.ts +++ b/packages/backend/src/mfm/to-html.ts @@ -1,17 +1,29 @@ import { JSDOM } from 'jsdom'; import * as mfm from 'mfm-js'; 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 { IMentionedRemoteUsers } from '@/models/entities/note.js'; -export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) { +// 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 +// from the text will be extracted. +export async function toHtml(mfmText: string, mentions?: string[]): string | null { + const nodes = mfm.parse(mfmText); if (nodes == null) { return null; } - const { window } = new JSDOM(''); + const mentionedUsers = await UserProfiles.createQueryBuilder("user_profiles") + .leftJoin('user_profile.user', 'user') + .select('user.username') + .addSelect('user.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: mentions ?? extractMentions(nodes) }) + .getManyRaw(); - const doc = window.document; + const doc = new JSDOM('').window.document; function appendChildren(children: mfm.MfmNode[], targetElement: any): void { if (children) { @@ -106,18 +118,23 @@ export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMenti }, mention(node) { - // Mastodon microformat: span.h-card > a.u-url.mention - const a = doc.createElement('a'); const { username, host, acct } = node.props; - const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); - a.href = remoteUserInfo?.url ?? remoteUserInfo?.uri ?? `${config.url}/${acct}`; - a.className = 'u-url mention'; - a.textContent = acct; + 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; - 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; + } else { + // this user does not actually exist + return doc.createTextNode(acct); + } }, quote(node) { diff --git a/packages/backend/src/remote/activitypub/misc/get-note-html.ts b/packages/backend/src/remote/activitypub/misc/get-note-html.ts deleted file mode 100644 index 389039ebe..000000000 --- a/packages/backend/src/remote/activitypub/misc/get-note-html.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as mfm from 'mfm-js'; -import { Note } from '@/models/entities/note.js'; -import { toHtml } from '../../../mfm/to-html.js'; - -export default function(note: Note) { - if (!note.text) return ''; - return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); -} diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index f705aabac..e662c0e92 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -5,7 +5,7 @@ import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index.js'; import { Emoji } from '@/models/entities/emoji.js'; import { Poll } from '@/models/entities/poll.js'; -import toHtml from '../misc/get-note-html.js'; +import { toHtml } from '@/mfm/to-html.js'; import renderEmoji from './emoji.js'; import renderMention from './mention.js'; import renderHashtag from './hashtag.js'; @@ -97,9 +97,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; - const content = toHtml(Object.assign({}, note, { - text: apText, - })); + const content = await toHtml(apText, note.mentions); const emojis = await getEmojis(note.emojis); const apemojis = emojis.map(emoji => renderEmoji(emoji)); @@ -112,7 +110,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false const asPoll = poll ? { type: 'Question', - content: toHtml(Object.assign({}, note, { text })), + content: await toHtml(text, note.mentions), [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ type: 'Note', diff --git a/packages/backend/src/remote/activitypub/renderer/person.ts b/packages/backend/src/remote/activitypub/renderer/person.ts index 213741143..7de957882 100644 --- a/packages/backend/src/remote/activitypub/renderer/person.ts +++ b/packages/backend/src/remote/activitypub/renderer/person.ts @@ -1,5 +1,4 @@ import { URL } from 'node:url'; -import * as mfm from 'mfm-js'; import config from '@/config/index.js'; import { ILocalUser } from '@/models/entities/user.js'; import { toHtml } from '@/mfm/to-html.js'; @@ -66,7 +65,7 @@ export async function renderPerson(user: ILocalUser) { url: `${config.url}/@${user.username}`, preferredUsername: user.username, name: user.name, - summary: profile.description ? toHtml(mfm.parse(profile.description)) : null, + summary: profile.description ? await toHtml(profile.description) : null, icon: avatar ? renderImage(avatar) : null, image: banner ? renderImage(banner) : null, tag,