From ce29c1dce201841f0077d362e90ce3cecb4481e3 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sat, 14 Oct 2023 16:39:16 +0200 Subject: [PATCH] [mastodon-client] Use improved mention parsing in mfm-to-html --- packages/backend/src/remote/resolve-user.ts | 2 +- .../api/mastodon/converters/announcement.ts | 4 +- .../server/api/mastodon/converters/note.ts | 2 +- .../server/api/mastodon/converters/user.ts | 8 +-- .../src/server/api/mastodon/helpers/mfm.ts | 67 +++++++------------ .../src/server/api/mastodon/helpers/misc.ts | 4 +- .../src/server/api/mastodon/helpers/note.ts | 2 +- .../api/mastodon/streaming/channels/user.ts | 2 +- 8 files changed, 38 insertions(+), 53 deletions(-) diff --git a/packages/backend/src/remote/resolve-user.ts b/packages/backend/src/remote/resolve-user.ts index e96f3c316..7855c4372 100644 --- a/packages/backend/src/remote/resolve-user.ts +++ b/packages/backend/src/remote/resolve-user.ts @@ -178,7 +178,7 @@ export async function resolveMentionWithFallback(username: string, host: string 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; + if (host === null || host === config.domain) return fallback; try { const user = await resolveUser(username, host); const profile = await UserProfiles.findOneBy({ userId: user.id }); diff --git a/packages/backend/src/server/api/mastodon/converters/announcement.ts b/packages/backend/src/server/api/mastodon/converters/announcement.ts index 6098cf1f3..a1adaa466 100644 --- a/packages/backend/src/server/api/mastodon/converters/announcement.ts +++ b/packages/backend/src/server/api/mastodon/converters/announcement.ts @@ -3,10 +3,10 @@ import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js"; import mfm from "mfm-js"; export class AnnouncementConverter { - public static encode(announcement: Announcement, isRead: boolean): MastodonEntity.Announcement { + public static async encode(announcement: Announcement, isRead: boolean): Promise { return { id: announcement.id, - content: `

${MfmHelpers.toHtml(mfm.parse(announcement.title), [], null) ?? 'Announcement'}

${MfmHelpers.toHtml(mfm.parse(announcement.text), [], null) ?? ''}`, + content: `

${await MfmHelpers.toHtml(mfm.parse(announcement.title), []) ?? 'Announcement'}

${await MfmHelpers.toHtml(mfm.parse(announcement.text), []) ?? ''}`, starts_at: null, ends_at: null, published: true, diff --git a/packages/backend/src/server/api/mastodon/converters/note.ts b/packages/backend/src/server/api/mastodon/converters/note.ts index 4c8c77c39..45b109c4f 100644 --- a/packages/backend/src/server/api/mastodon/converters/note.ts +++ b/packages/backend/src/server/api/mastodon/converters/note.ts @@ -117,7 +117,7 @@ export class NoteConverter { in_reply_to_id: note.replyId, in_reply_to_account_id: note.replyUserId, reblog: reblog.then(reblog => note.text === null ? reblog : null), - content: text.then(async text => text !== null ? await host.then(host => MfmHelpers.toHtml(mfm.parse(text), JSON.parse(note.mentionedRemoteUsers), host)) ?? escapeMFM(text) : ""), + content: text.then(async text => text !== null ? MfmHelpers.toHtml(mfm.parse(text), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? escapeMFM(text)) : ""), text: text, created_at: note.createdAt.toISOString(), emojis: noteEmoji, diff --git a/packages/backend/src/server/api/mastodon/converters/user.ts b/packages/backend/src/server/api/mastodon/converters/user.ts index 9abaebb72..4bc2150c3 100644 --- a/packages/backend/src/server/api/mastodon/converters/user.ts +++ b/packages/backend/src/server/api/mastodon/converters/user.ts @@ -31,7 +31,7 @@ export class UserConverter { acctUrl = `https://${u.host}/@${u.username}`; } const profile = UserProfiles.findOneBy({ userId: u.id }); - const bio = profile.then(profile => MfmHelpers.toHtml(mfm.parse(profile?.description ?? ""), [], u.host) ?? escapeMFM(profile?.description ?? "")); + const bio = profile.then(profile => MfmHelpers.toHtml(mfm.parse(profile?.description ?? ""), []).then(p => p ?? escapeMFM(profile?.description ?? ""))); const avatar = u.avatarId ? (DriveFiles.findOneBy({ id: u.avatarId })) .then(p => p?.url ?? Users.getIdenticonUrl(u.id)) @@ -92,7 +92,7 @@ export class UserConverter { header_static: banner, emojis: populateEmojis(u.emojis, u.host).then(emoji => emoji.map((e) => EmojiConverter.encode(e))), moved: null, //FIXME - fields: profile.then(profile => profile?.fields.map(p => this.encodeField(p, u.host)) ?? []), + fields: profile.then(profile => Promise.all(profile?.fields.map(async p => this.encodeField(p, u.host)) ?? [])), bot: u.isBot, discoverable: u.isExplorable }).then(p => { @@ -107,10 +107,10 @@ export class UserConverter { return Promise.all(encoded); } - private static encodeField(f: Field, host: string | null): MastodonEntity.Field { + private static async encodeField(f: Field, host: string | null): Promise { return { name: f.name, - value: MfmHelpers.toHtml(mfm.parse(f.value), [], host, true) ?? escapeMFM(f.value), + value: await MfmHelpers.toHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value), verified_at: f.verified ? (new Date()).toISOString() : null, } } diff --git a/packages/backend/src/server/api/mastodon/helpers/mfm.ts b/packages/backend/src/server/api/mastodon/helpers/mfm.ts index 378fa831a..390f5ff7a 100644 --- a/packages/backend/src/server/api/mastodon/helpers/mfm.ts +++ b/packages/backend/src/server/api/mastodon/helpers/mfm.ts @@ -3,12 +3,12 @@ import { JSDOM } from "jsdom"; import config from "@/config/index.js"; import { intersperse } from "@/prelude/array.js"; import mfm from "mfm-js"; +import { resolveMentionWithFallback } from "@/remote/resolve-user.js"; export class MfmHelpers { - public static toHtml( + public static async toHtml( nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = [], - objectHost: string | null, inline: boolean = false ) { if (nodes == null) { @@ -19,9 +19,9 @@ export class MfmHelpers { 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); } } @@ -29,40 +29,40 @@ export class MfmHelpers { const handlers: { [K in mfm.MfmNode["type"]]: (node: mfm.NodeType) => any; } = { - bold(node) { + async bold(node) { const el = doc.createElement("span"); el.textContent = '**'; - appendChildren(node.children, el); + await appendChildren(node.children, el); el.textContent += '**'; 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("span"); el.textContent = '~~'; - appendChildren(node.children, el); + await appendChildren(node.children, el); el.textContent += '~~'; return el; }, - italic(node) { + async italic(node) { const el = doc.createElement("span"); el.textContent = '*'; - appendChildren(node.children, el); + await appendChildren(node.children, el); el.textContent += '*'; return el; }, - fn(node) { + async fn(node) { const el = doc.createElement("span"); el.textContent = '*'; - appendChildren(node.children, el); + await appendChildren(node.children, el); el.textContent += '*'; return el; }, @@ -83,9 +83,9 @@ export class MfmHelpers { return pre; }, - center(node) { + async center(node) { const el = doc.createElement("div"); - appendChildren(node.children, el); + await appendChildren(node.children, el); return el; }, @@ -123,37 +123,22 @@ export class MfmHelpers { return el; }, - link(node) { + async link(node) { const a = doc.createElement("a"); a.setAttribute("rel", "nofollow noopener noreferrer"); a.setAttribute("target", "_blank"); 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 } = node.props; - const remoteUserInfo = mentionedRemoteUsers.find( - (remoteUser) => - remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host === host, - ); - const localpart = `@${username}`; - const isLocal = host === config.domain || (host == null && objectHost == null); - const acct = isLocal ? localpart : node.props.acct; - a.href = remoteUserInfo - ? remoteUserInfo.url - ? remoteUserInfo.url - : remoteUserInfo.uri - : isLocal - ? `${config.url}/${acct}` - : host == null - ? `https://${objectHost}/${localpart}` - : `https://${host}/${localpart}`; + const { username, host, acct } = node.props; + a.href = await resolveMentionWithFallback(username, host, acct, mentionedRemoteUsers); a.className = "u-url mention"; const span = doc.createElement("span"); span.textContent = username; @@ -163,9 +148,9 @@ export class MfmHelpers { return el; }, - quote(node) { + async quote(node) { const el = doc.createElement("blockquote"); - appendChildren(node.children, el); + await appendChildren(node.children, el); return el; }, @@ -198,14 +183,14 @@ export class MfmHelpers { 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 inline ? doc.body.innerHTML : `

${doc.body.innerHTML}

`; } diff --git a/packages/backend/src/server/api/mastodon/helpers/misc.ts b/packages/backend/src/server/api/mastodon/helpers/misc.ts index ddb46afa5..ab3e65947 100644 --- a/packages/backend/src/server/api/mastodon/helpers/misc.ts +++ b/packages/backend/src/server/api/mastodon/helpers/misc.ts @@ -107,7 +107,7 @@ export class MiscHelpers { .then(p => p.map(x => x.announcementId)) ]); - return announcements.map(p => AnnouncementConverter.encode(p, reads.includes(p.id))); + return Promise.all(announcements.map(async p => AnnouncementConverter.encode(p, reads.includes(p.id)))); } const sq = AnnouncementReads.createQueryBuilder("reads") @@ -120,7 +120,7 @@ export class MiscHelpers { .setParameter("userId", user.id); return query.getMany() - .then(p => p.map(x => AnnouncementConverter.encode(x, false))); + .then(p => Promise.all(p.map(async x => AnnouncementConverter.encode(x, false)))); } public static async dismissAnnouncement(announcement: Announcement, ctx: MastoContext): Promise { diff --git a/packages/backend/src/server/api/mastodon/helpers/note.ts b/packages/backend/src/server/api/mastodon/helpers/note.ts index 3e571d9fd..5580f92d1 100644 --- a/packages/backend/src/server/api/mastodon/helpers/note.ts +++ b/packages/backend/src/server/api/mastodon/helpers/note.ts @@ -201,7 +201,7 @@ export class NoteHelpers { const files = DriveFiles.packMany(edit.fileIds); const item = { account: account, - content: user.then(user => MfmHelpers.toHtml(mfm.parse(edit.text ?? ''), JSON.parse(note.mentionedRemoteUsers), user.host) ?? ''), + content: MfmHelpers.toHtml(mfm.parse(edit.text ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''), created_at: lastDate.toISOString(), emojis: [], sensitive: files.then(files => files.length > 0 ? files.some((f) => f.isSensitive) : false), diff --git a/packages/backend/src/server/api/mastodon/streaming/channels/user.ts b/packages/backend/src/server/api/mastodon/streaming/channels/user.ts index 3f92df5e5..f9f4a46c3 100644 --- a/packages/backend/src/server/api/mastodon/streaming/channels/user.ts +++ b/packages/backend/src/server/api/mastodon/streaming/channels/user.ts @@ -76,7 +76,7 @@ export class MastodonStreamUser extends MastodonStream { case "announcementAdded": // This shouldn't be necessary but is for some reason data.body.createdAt = new Date(data.body.createdAt); - this.connection.send(this.chName, "announcement", AnnouncementConverter.encode(data.body, false)); + this.connection.send(this.chName, "announcement", await AnnouncementConverter.encode(data.body, false)); break; case "announcementDeleted": this.connection.send(this.chName, "announcement.delete", data.body);