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,