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..42d79b5b5 --- /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 "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")`); + } +} 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/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/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..1bcb5eae2 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -1,11 +1,11 @@ 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'; 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'; @@ -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)); @@ -97,9 +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(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 +113,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, 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