From 70a1c308f05d21dfcc2bfb72dbfb30ce2f81510c Mon Sep 17 00:00:00 2001 From: Nya Candy Date: Fri, 26 Jan 2024 13:41:37 +0800 Subject: [PATCH] chore(edit): only send to relative users to prevent possible data leak --- .../backend/src/core/NoteUpdateService.ts | 96 +++++++++++++------ 1 file changed, 68 insertions(+), 28 deletions(-) diff --git a/packages/backend/src/core/NoteUpdateService.ts b/packages/backend/src/core/NoteUpdateService.ts index 4500d671b..400bf7f1b 100644 --- a/packages/backend/src/core/NoteUpdateService.ts +++ b/packages/backend/src/core/NoteUpdateService.ts @@ -5,6 +5,7 @@ import { Brackets, In } from 'typeorm'; import { Injectable, Inject } from '@nestjs/common'; +import * as mfm from 'mfm-js'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/_.js'; @@ -24,8 +25,10 @@ import { bindThis } from '@/decorators.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { isPureRenote } from '@/misc/is-pure-renote.js'; import { IdService } from '@/core/IdService.js'; +import { trackPromise } from '@/misc/promise-tracker.js'; +import { extractMentions } from '@/misc/extract-mentions.js'; +import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; @Injectable() export class NoteUpdateService { @@ -47,6 +50,7 @@ export class NoteUpdateService { private globalEventService: GlobalEventService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, + private remoteUserResolveService: RemoteUserResolveService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, private metaService: MetaService, @@ -82,7 +86,7 @@ export class NoteUpdateService { await this.apRendererService.renderNote(newNote, false), newNote, ), ); - + this.deliverToConcerned(user, newNote, content); } } @@ -114,38 +118,74 @@ export class NoteUpdateService { } @bindThis - private async getMentionedRemoteUsers(note: MiNote) { - const where = [] as any[]; + private async extractMentionedUsers(user: { host: MiUser['host']; }, tokens: mfm.MfmNode[]): Promise { + if (tokens == null) return []; - // mention / reply / dm - const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); - if (uris.length > 0) { - where.push( - { uri: In(uris) }, - ); - } + const mentions = extractMentions(tokens); + let mentionedUsers = (await Promise.all(mentions.map(m => + this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null), + ))).filter(x => x != null) as MiUser[]; - // renote / quote - if (note.renoteUserId) { - where.push({ - id: note.renoteUserId, - }); - } + // Drop duplicate users + mentionedUsers = mentionedUsers.filter((u, i, self) => + i === self.findIndex(u2 => u.id === u2.id), + ); - if (where.length === 0) return []; - - return await this.usersRepository.find({ - where, - }) as MiRemoteUser[]; + return mentionedUsers; } @bindThis - private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) { - this.apDeliverManagerService.deliverToFollowers(user, content); - this.relayService.deliverToRelays(user, content); - const remoteUsers = await this.getMentionedRemoteUsers(note); - for (const remoteUser of remoteUsers) { - this.apDeliverManagerService.deliverToUser(user, content, remoteUser); + private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, noteActivity: any) { + const dm = this.apDeliverManagerService.createDeliverManager(user, noteActivity); + + // Parse MFM if needed + const tokens = (note.text ? mfm.parse(note.text)! : []); + const cwTokens = note.cw ? mfm.parse(note.cw)! : []; + + const combinedTokens = tokens.concat(cwTokens); + + const mentionedUsers = await this.extractMentionedUsers(user, combinedTokens); + + if (note.reply && (user.id !== note.reply.userId) && !mentionedUsers.some(u => u.id === note.reply!.userId)) { + mentionedUsers.push(await this.usersRepository.findOneByOrFail({ id: note.reply.userId })); } + + if (note.visibility === 'specified') { + if (note.visibleUserIds == null) throw new Error('invalid param'); + + for (const u of note.visibleUserIds) { + if (!mentionedUsers.some(x => x.id === u)) { + mentionedUsers.push(await this.usersRepository.findOneByOrFail({ id: u })); + } + } + } + + // メンションされたリモートユーザーに配送 + for (const u of mentionedUsers.filter(u => this.userEntityService.isRemoteUser(u))) { + dm.addDirectRecipe(u as MiRemoteUser); + } + + // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 + if (note.reply && note.reply.userHost !== null) { + const u = await this.usersRepository.findOneBy({ id: note.reply.userId }); + if (u && this.userEntityService.isRemoteUser(u)) dm.addDirectRecipe(u); + } + + // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送 + if (note.renote && note.renote.userHost !== null) { + const u = await this.usersRepository.findOneBy({ id: note.renote.userId }); + if (u && this.userEntityService.isRemoteUser(u)) dm.addDirectRecipe(u); + } + + // フォロワーに配送 + if (['public', 'home', 'followers'].includes(note.visibility)) { + dm.addFollowersRecipe(); + } + + if (['public'].includes(note.visibility)) { + this.relayService.deliverToRelays(user, noteActivity); + } + + trackPromise(dm.execute()); } }