diff --git a/packages/backend/src/mfm/to-html.ts b/packages/backend/src/mfm/to-html.ts index 7c941892f..54e93964b 100644 --- a/packages/backend/src/mfm/to-html.ts +++ b/packages/backend/src/mfm/to-html.ts @@ -3,8 +3,9 @@ import type * as mfm from "mfm-js"; import config from "@/config/index.js"; import { intersperse } from "@/prelude/array.js"; import type { IMentionedRemoteUsers } from "@/models/entities/note.js"; +import { resolveMentionWithFallback } from "@/remote/resolve-user.js"; -export function toHtml( +export async function toHtml( nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], ) { @@ -16,9 +17,9 @@ export function toHtml( const doc = window.document; - function appendChildren(children: mfm.MfmNode[], targetElement: any): void { + async function appendChildren(children: mfm.MfmNode[], targetElement: any): Promise { if (children) { - for (const child of children.map((x) => (handlers as any)[x.type](x))) + for (const child of await Promise.all(children.map(async (x) => await (handlers as any)[x.type](x)))) targetElement.appendChild(child); } } @@ -26,33 +27,33 @@ export function toHtml( const handlers: { [K in mfm.MfmNode["type"]]: (node: mfm.NodeType) => any; } = { - bold(node) { + async bold(node) { const el = doc.createElement("b"); - appendChildren(node.children, el); + await appendChildren(node.children, el); return el; }, - small(node) { + async small(node) { const el = doc.createElement("small"); - appendChildren(node.children, el); + await appendChildren(node.children, el); return el; }, - strike(node) { + async strike(node) { const el = doc.createElement("del"); - appendChildren(node.children, el); + await appendChildren(node.children, el); return el; }, - italic(node) { + async italic(node) { const el = doc.createElement("i"); - appendChildren(node.children, el); + await appendChildren(node.children, el); return el; }, - fn(node) { + async fn(node) { const el = doc.createElement("i"); - appendChildren(node.children, el); + await appendChildren(node.children, el); return el; }, @@ -64,9 +65,9 @@ export function toHtml( return pre; }, - center(node) { + async center(node) { const el = doc.createElement("div"); - appendChildren(node.children, el); + await appendChildren(node.children, el); return el; }, @@ -104,28 +105,20 @@ export function toHtml( return el; }, - link(node) { + async link(node) { const a = doc.createElement("a"); a.href = node.props.url; - appendChildren(node.children, a); + await appendChildren(node.children, a); return a; }, - mention(node) { + async mention(node) { const el = doc.createElement("span"); el.setAttribute("class", "h-card"); el.setAttribute("translate", "no"); const a = doc.createElement("a"); const { username, host, acct } = node.props; - const remoteUserInfo = mentionedRemoteUsers.find( - (remoteUser) => - remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host === host, - ); - a.href = remoteUserInfo - ? remoteUserInfo.url - ? remoteUserInfo.url - : remoteUserInfo.uri - : `${config.url}/${acct}`; + a.href = await resolveMentionWithFallback(username, host, acct, mentionedRemoteUsers); a.className = "u-url mention"; const span = doc.createElement("span"); span.textContent = username; @@ -135,9 +128,9 @@ export function toHtml( return el; }, - quote(node) { + async quote(node) { const el = doc.createElement("blockquote"); - appendChildren(node.children, el); + await appendChildren(node.children, el); return el; }, @@ -168,14 +161,14 @@ export function toHtml( return a; }, - plain(node) { + async plain(node) { const el = doc.createElement("span"); - appendChildren(node.children, el); + await appendChildren(node.children, el); return el; }, }; - appendChildren(nodes, doc.body); + await appendChildren(nodes, doc.body); return `

${doc.body.innerHTML}

`; } diff --git a/packages/backend/src/remote/activitypub/misc/get-note-html.ts b/packages/backend/src/remote/activitypub/misc/get-note-html.ts index cb5294f73..046d60d1a 100644 --- a/packages/backend/src/remote/activitypub/misc/get-note-html.ts +++ b/packages/backend/src/remote/activitypub/misc/get-note-html.ts @@ -2,7 +2,7 @@ import * as mfm from "mfm-js"; import type { Note } from "@/models/entities/note.js"; import { toHtml } from "../../../mfm/to-html.js"; -export default function (note: Note) { +export default async 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 2ad2fec9f..cca291463 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -108,7 +108,7 @@ export default async function renderNote( const summary = note.cw === "" ? String.fromCharCode(0x200b) : note.cw; - const content = toHtml( + const content = await toHtml( Object.assign({}, note, { text: apText, }), diff --git a/packages/backend/src/remote/activitypub/renderer/person.ts b/packages/backend/src/remote/activitypub/renderer/person.ts index 7905653a2..5e010d733 100644 --- a/packages/backend/src/remote/activitypub/renderer/person.ts +++ b/packages/backend/src/remote/activitypub/renderer/person.ts @@ -73,7 +73,7 @@ export async function renderPerson(user: ILocalUser) { preferredUsername: user.username, name: user.name, summary: profile.description - ? toHtml(mfm.parse(profile.description)) + ? await toHtml(mfm.parse(profile.description)) : null, icon: avatar ? renderImage(avatar) : null, image: banner ? renderImage(banner) : null, diff --git a/packages/backend/src/remote/resolve-user.ts b/packages/backend/src/remote/resolve-user.ts index b60106e28..e96f3c316 100644 --- a/packages/backend/src/remote/resolve-user.ts +++ b/packages/backend/src/remote/resolve-user.ts @@ -3,12 +3,13 @@ import chalk from "chalk"; import { IsNull } from "typeorm"; import config from "@/config/index.js"; import type { User, IRemoteUser } from "@/models/entities/user.js"; -import { Users } from "@/models/index.js"; +import { UserProfiles, Users } from "@/models/index.js"; import { toPuny } from "@/misc/convert-host.js"; import webFinger from "./webfinger.js"; import { createPerson, updatePerson } from "./activitypub/models/person.js"; import { remoteLogger } from "./logger.js"; import { Cache } from "@/misc/cache.js"; +import { IMentionedRemoteUsers } from "@/models/entities/note.js"; const logger = remoteLogger.createSubLogger("resolve-user"); const uriHostCache = new Cache("resolveUserUriHost", 60 * 60 * 24); @@ -173,6 +174,21 @@ export async function resolveUser( return user; } +export async function resolveMentionWithFallback(username: string, host: string | null, acct: string, cache: IMentionedRemoteUsers): Promise { + const fallback = `${config.url}/${acct}`; + const cached = cache.find(r => r.username.toLowerCase() === username.toLowerCase() && r.host === host); + if (cached) return cached.url ?? cached.uri; + if (host === null) return fallback; + try { + const user = await resolveUser(username, host); + const profile = await UserProfiles.findOneBy({ userId: user.id }); + return profile?.url ?? user.uri ?? fallback; + } + catch { + return fallback; + } +} + export async function getSubjectHostFromUri(uri: string): Promise { try { const acct = subjectToAcct((await webFinger(uri)).subject); diff --git a/packages/backend/test/mfm.ts b/packages/backend/test/mfm.ts index 5a42e420d..a4f54c550 100644 --- a/packages/backend/test/mfm.ts +++ b/packages/backend/test/mfm.ts @@ -5,16 +5,16 @@ import { toHtml } from "../src/mfm/to-html.js"; import { fromHtml } from "../src/mfm/from-html.js"; describe("toHtml", () => { - it("br", () => { + it("br", async () => { const input = "foo\nbar\nbaz"; const output = "

foo
bar
baz

"; - assert.equal(toHtml(mfm.parse(input)), output); + assert.equal(await toHtml(mfm.parse(input)), output); }); - it("br alt", () => { + it("br alt", async () => { const input = "foo\r\nbar\rbaz"; const output = "

foo
bar
baz

"; - assert.equal(toHtml(mfm.parse(input)), output); + assert.equal(await toHtml(mfm.parse(input)), output); }); });