From 94473a6172a80bc1afcd6f6b710334cf5487f085 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Wed, 24 Aug 2022 23:16:53 +0200 Subject: [PATCH 1/6] adjust MFM to HTML conversion Removed the misc/get-note-html module which was only used in one place. Instead of it, the general MFM to HTML functionality has been improved to take care of the use cases of that module as well. --- packages/backend/src/mfm/to-html.ts | 45 +++++++++++++------ .../remote/activitypub/misc/get-note-html.ts | 8 ---- .../src/remote/activitypub/renderer/note.ts | 8 ++-- .../src/remote/activitypub/renderer/person.ts | 3 +- 4 files changed, 35 insertions(+), 29 deletions(-) delete mode 100644 packages/backend/src/remote/activitypub/misc/get-note-html.ts diff --git a/packages/backend/src/mfm/to-html.ts b/packages/backend/src/mfm/to-html.ts index b1429f184..f86256a61 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 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..2a0b07ea0 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 = 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: 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..d59beef07 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 ? toHtml(profile.description) : null, icon: avatar ? renderImage(avatar) : null, image: banner ? renderImage(banner) : null, tag, -- 2.34.1 From 491d35b2831b7970a1b2f109700ef96115e159c3 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Wed, 24 Aug 2022 23:57:34 +0200 Subject: [PATCH 2/6] refactor: remove note.mentionedRemoteUsers column The column mentionedRemoteUsers on the note table in the database is firstly in the wrong type since it contains JSON data but is typed as text. Secondly it seems redundant, since that data can be acquired by using the note.mentions column to fetch the respective data instead. --- ...00-remove-mentioned-remote-users-column.js | 12 ++++++++++ packages/backend/src/models/entities/note.ts | 12 ---------- .../src/remote/activitypub/renderer/note.ts | 23 +++++++++++-------- .../backend/src/services/messages/create.ts | 5 ---- packages/backend/src/services/note/create.ts | 12 +--------- packages/backend/src/services/note/delete.ts | 15 ++++++------ 6 files changed, 34 insertions(+), 45 deletions(-) create mode 100644 packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js diff --git a/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js b/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js new file mode 100644 index 000000000..7df859ada --- /dev/null +++ b/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js @@ -0,0 +1,12 @@ +export class removeMentionedRemoteUsersColumn1661376843000 { + name = 'removeMentionedRemoteUsersColumn1661376843000'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "mentionedRemoteUsers"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "mentionedRemoteUsers" TEXT NOT NULL DEFAULT '[]'::text`); + await queryRunner.query(`UPDATE TABLE "note" SET "mentionedRemoteUsers = (SELECT json_agg(row_to_json("data")) FROM (SELECT "url", "uri", "username", "host" FROM "user" JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."id" IN "note"."mentions") AS "data")`); + } +} diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index ee7cbec43..d1e6b9f93 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -155,11 +155,6 @@ export class Note { }) public mentions: User['id'][]; - @Column('text', { - default: '[]', - }) - public mentionedRemoteUsers: string; - @Column('varchar', { length: 128, array: true, default: '{}', }) @@ -233,10 +228,3 @@ export class Note { } } } - -export type IMentionedRemoteUsers = { - uri: string; - url?: string; - username: string; - host: string; -}[]; diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index 2a0b07ea0..640819458 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -1,6 +1,6 @@ import { In, IsNull } from 'typeorm'; import config from '@/config/index.js'; -import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; +import { Note } from '@/models/entities/note.js'; 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'; @@ -55,28 +55,31 @@ export default async function renderNote(note: Note, dive = true, isTalk = false const attributedTo = `${config.url}/users/${note.userId}`; - const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); + const mentionedUsers = note.mentions.length > 0 ? await Users.findBy({ + id: In(note.mentions), + }) : []; + + const mentionUris = mentionedUsers + // only remote users + .filter(user => Users.isRemoteUser(user)) + .map(user => user.uri); let to: string[] = []; let cc: string[] = []; if (note.visibility === 'public') { to = ['https://www.w3.org/ns/activitystreams#Public']; - cc = [`${attributedTo}/followers`].concat(mentions); + cc = [`${attributedTo}/followers`].concat(mentionUris); } else if (note.visibility === 'home') { to = [`${attributedTo}/followers`]; - cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions); + cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentionUris); } else if (note.visibility === 'followers') { to = [`${attributedTo}/followers`]; - cc = mentions; + cc = mentionUris; } else { - to = mentions; + to = mentionUris; } - const mentionedUsers = note.mentions.length > 0 ? await Users.findBy({ - id: In(note.mentions), - }) : []; - const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag)); const mentionTags = mentionedUsers.map(u => renderMention(u)); diff --git a/packages/backend/src/services/messages/create.ts b/packages/backend/src/services/messages/create.ts index b5892a891..4a0ea53a8 100644 --- a/packages/backend/src/services/messages/create.ts +++ b/packages/backend/src/services/messages/create.ts @@ -93,11 +93,6 @@ export async function createMessage(user: { id: User['id']; host: User['host']; userId: message.userId, visibility: 'specified', mentions: [ recipientUser.id ], - mentionedRemoteUsers: JSON.stringify([ recipientUser ].map(u => ({ - uri: u.uri, - username: u.username, - host: u.host, - }))), } as Note; const activity = renderActivity(renderCreate(await renderNote(note, false, true), note)); diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 84bfa89eb..9761de0e5 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -15,7 +15,7 @@ import { insertNoteUnread } from '@/services/note/unread.js'; import { extractMentions } from '@/misc/extract-mentions.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; -import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; +import { Note } from '@/models/entities/note.js'; import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, MutedNotes, Channels, ChannelFollowings, NoteThreadMutings } from '@/models/index.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { App } from '@/models/entities/app.js'; @@ -537,16 +537,6 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O // Append mentions data if (mentionedUsers.length > 0) { insert.mentions = mentionedUsers.map(u => u.id); - const profiles = await UserProfiles.findBy({ userId: In(insert.mentions) }); - insert.mentionedRemoteUsers = JSON.stringify(mentionedUsers.filter(u => Users.isRemoteUser(u)).map(u => { - const profile = profiles.find(p => p.userId === u.id); - return { - uri: u.uri, - url: profile?.url, - username: u.username, - host: u.host, - } as IMentionedRemoteUsers[0]; - })); } // 投稿を作成 diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index fc156b584..61b75c963 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -1,4 +1,4 @@ -import { Brackets, In } from 'typeorm'; +import { Brackets, In, IsNull, Not } from 'typeorm'; import { publishNoteStream } from '@/services/stream.js'; import renderDelete from '@/remote/activitypub/renderer/delete.js'; import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; @@ -7,7 +7,7 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js'; import renderTombstone from '@/remote/activitypub/renderer/tombstone.js'; import config from '@/config/index.js'; import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; -import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; +import { Note } from '@/models/entities/note.js'; import { Notes, Users, Instances } from '@/models/index.js'; import { notesChart, perUserNotesChart, instanceChart } from '@/services/chart/index.js'; import { deliverToFollowers, deliverToUser } from '@/remote/activitypub/deliver-manager.js'; @@ -109,11 +109,12 @@ async function getMentionedRemoteUsers(note: Note): Promise { const where = [] as any[]; // mention / reply / dm - const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); - if (uris.length > 0) { - where.push( - { uri: In(uris) }, - ); + if (note.mentions > 0) { + where.push({ + id: In(note.mentions), + // only remote users, local users are on the server and do not need to be notified + host: Not(IsNull()), + }); } // renote / quote -- 2.34.1 From 39a4eea08e28e566fcf90aedd225315226d19d75 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Thu, 25 Aug 2022 09:22:25 +0200 Subject: [PATCH 3/6] fix migration --- .../1661376843000-remove-mentioned-remote-users-column.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js b/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js index 7df859ada..f4e8d30e5 100644 --- a/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js +++ b/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js @@ -7,6 +7,6 @@ export class removeMentionedRemoteUsersColumn1661376843000 { async down(queryRunner) { await queryRunner.query(`ALTER TABLE "note" ADD "mentionedRemoteUsers" TEXT NOT NULL DEFAULT '[]'::text`); - await queryRunner.query(`UPDATE TABLE "note" SET "mentionedRemoteUsers = (SELECT json_agg(row_to_json("data")) FROM (SELECT "url", "uri", "username", "host" FROM "user" JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."id" IN "note"."mentions") AS "data")`); + await queryRunner.query(`UPDATE TABLE "note" SET "mentionedRemoteUsers" = (SELECT COALESCE(json_agg(row_to_json("data"))::text, '[]') FROM (SELECT "url", "uri", "username", "host" FROM "user" JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL AND "user"."id" = ANY("note"."mentions")) AS "data")`); } } -- 2.34.1 From 9e309f740e5bb4fe21bbd5f1a1077c66248a609f Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sun, 28 Aug 2022 19:48:00 +0200 Subject: [PATCH 4/6] fix: make toHtml async --- packages/backend/src/mfm/to-html.ts | 2 +- packages/backend/src/remote/activitypub/renderer/note.ts | 2 +- packages/backend/src/remote/activitypub/renderer/person.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/mfm/to-html.ts b/packages/backend/src/mfm/to-html.ts index f86256a61..af399e81f 100644 --- a/packages/backend/src/mfm/to-html.ts +++ b/packages/backend/src/mfm/to-html.ts @@ -8,7 +8,7 @@ import { intersperse } from '@/prelude/array.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 // from the text will be extracted. -export function toHtml(mfmText: string, mentions?: string[]): string | null { +export async function toHtml(mfmText: string, mentions?: string[]): string | null { const nodes = mfm.parse(mfmText); if (nodes == null) { return null; diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index 640819458..f79005f0e 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -100,7 +100,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; - const content = toHtml(apText, note.mentions); + const content = await toHtml(apText, note.mentions); const emojis = await getEmojis(note.emojis); const apemojis = emojis.map(emoji => renderEmoji(emoji)); diff --git a/packages/backend/src/remote/activitypub/renderer/person.ts b/packages/backend/src/remote/activitypub/renderer/person.ts index d59beef07..7de957882 100644 --- a/packages/backend/src/remote/activitypub/renderer/person.ts +++ b/packages/backend/src/remote/activitypub/renderer/person.ts @@ -65,7 +65,7 @@ export async function renderPerson(user: ILocalUser) { url: `${config.url}/@${user.username}`, preferredUsername: user.username, name: user.name, - summary: profile.description ? toHtml(profile.description) : null, + summary: profile.description ? await toHtml(profile.description) : null, icon: avatar ? renderImage(avatar) : null, image: banner ? renderImage(banner) : null, tag, -- 2.34.1 From aa80de1de84bf7e1a940e0dbe5216e0e0d2675da Mon Sep 17 00:00:00 2001 From: Francis Dinh Date: Sun, 28 Aug 2022 14:08:04 -0400 Subject: [PATCH 5/6] add one more await for toHtml --- packages/backend/src/remote/activitypub/renderer/note.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index f79005f0e..1bcb5eae2 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -113,7 +113,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false const asPoll = poll ? { type: 'Question', - content: toHtml(text, note.mentions), + 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', -- 2.34.1 From deb7b105f4fc45c89f0ffeea9f216169bdd98ac9 Mon Sep 17 00:00:00 2001 From: Francis Dinh Date: Sun, 28 Aug 2022 15:01:09 -0400 Subject: [PATCH 6/6] fix: migration revert syntax --- .../1661376843000-remove-mentioned-remote-users-column.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js b/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js index f4e8d30e5..42d79b5b5 100644 --- a/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js +++ b/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js @@ -7,6 +7,6 @@ export class removeMentionedRemoteUsersColumn1661376843000 { async down(queryRunner) { await queryRunner.query(`ALTER TABLE "note" ADD "mentionedRemoteUsers" TEXT NOT NULL DEFAULT '[]'::text`); - await queryRunner.query(`UPDATE TABLE "note" SET "mentionedRemoteUsers" = (SELECT COALESCE(json_agg(row_to_json("data"))::text, '[]') FROM (SELECT "url", "uri", "username", "host" FROM "user" JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL AND "user"."id" = ANY("note"."mentions")) AS "data")`); + await queryRunner.query(`UPDATE "note" SET "mentionedRemoteUsers" = (SELECT COALESCE(json_agg(row_to_json("data"))::text, '[]') FROM (SELECT "url", "uri", "username", "host" FROM "user" JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL AND "user"."id" = ANY("note"."mentions")) AS "data")`); } } -- 2.34.1