diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 335bc165e0..834e22c6a2 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -57,6 +57,18 @@ async function nullIfEntityNotFound(promise: Promise): Promise { } } +type PackSingleOptions = { + detail?: boolean; + skipHide?: boolean; + withReactionAndUserPairCache?: boolean; + _hint_?: { + bufferedReactions: Map; pairs: ([MiUser['id'], string])[] }> | null; + myReactions: Map; + packedFiles: Map | null>; + packedUsers: Map> + }; +}; + @Injectable() export class NoteEntityService implements OnModuleInit { private userEntityService: UserEntityService; @@ -357,21 +369,24 @@ export class NoteEntityService implements OnModuleInit { return fileIds.map(id => packedFiles.get(id)).filter(x => x != null); } + @bindThis + public async packMayDeleted( + src: MiNote | MiDeletedNote, + me?: { id: MiUser['id'] } | null | undefined, + options?: PackSingleOptions, + ): Promise> { + if ('deletedAt' in src) { + return this.packDeletedNote(src, me, options); + } else { + return this.pack(src, me, options); + } + } + @bindThis public async pack( src: MiNote['id'] | MiNote, me?: { id: MiUser['id'] } | null | undefined, - options?: { - detail?: boolean; - skipHide?: boolean; - withReactionAndUserPairCache?: boolean; - _hint_?: { - bufferedReactions: Map; pairs: ([MiUser['id'], string])[] }> | null; - myReactions: Map; - packedFiles: Map | null>; - packedUsers: Map> - }; - }, + options?: PackSingleOptions, ): Promise> { const opts = Object.assign({ detail: true, @@ -462,7 +477,6 @@ export class NoteEntityService implements OnModuleInit { ...(opts.detail ? { clippedCount: note.clippedCount, - // そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, { detail: false, skipHide: opts.skipHide, @@ -470,7 +484,6 @@ export class NoteEntityService implements OnModuleInit { _hint_: options?._hint_, }) : undefined, - // そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, { detail: true, skipHide: opts.skipHide, @@ -502,17 +515,7 @@ export class NoteEntityService implements OnModuleInit { public async packDeletedNote( srcId: MiNote['id'] | MiDeletedNote, me?: { id: MiUser['id'] } | null | undefined, - options?: { - detail?: boolean; - skipHide?: boolean; - withReactionAndUserPairCache?: boolean; - _hint_?: { - bufferedReactions: Map; pairs: ([MiUser['id'], string])[] }> | null; - myReactions: Map; - packedFiles: Map | null>; - packedUsers: Map> - }; - }, + options?: PackSingleOptions, ): Promise> { const opts = Object.assign({ detail: true, @@ -595,7 +598,7 @@ export class NoteEntityService implements OnModuleInit { @bindThis public async packMany( - notes: MiNote[], + notes: (MiNote | MiDeletedNote)[], me?: { id: MiUser['id'] } | null | undefined, options?: { detail?: boolean; @@ -604,7 +607,9 @@ export class NoteEntityService implements OnModuleInit { ) { if (notes.length === 0) return []; - const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany([...getAppearNoteIds(notes)]) : null; + const liveNotes = notes.filter((n): n is MiNote => !('deletedAt' in n)); + + const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany([...getAppearNoteIds(liveNotes)]) : null; const meId = me ? me.id : null; const myReactionsMap = new Map(); @@ -614,7 +619,7 @@ export class NoteEntityService implements OnModuleInit { // パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない const oldId = this.idService.gen(Date.now() - 2000); - for (const note of notes) { + for (const note of liveNotes) { if (isPureRenote(note)) { const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0); if (reactionsCount === 0) { @@ -662,9 +667,9 @@ export class NoteEntityService implements OnModuleInit { } } - await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(notes)); + await this.customEmojiService.prefetchEmojis(this.aggregateNoteEmojis(liveNotes)); // TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく - const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null); + const fileIds = liveNotes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(x => x != null); const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map(); const users = [ ...notes.map(({ user, userId }) => user ?? userId), @@ -674,7 +679,7 @@ export class NoteEntityService implements OnModuleInit { const packedUsers = await this.userEntityService.packMany(users, me) .then(users => new Map(users.map(u => [u.id, u]))); - return await Promise.all(notes.map(n => this.pack(n, me, { + return await Promise.all(notes.map(n => this.packMayDeleted(n, me, { ...options, _hint_: { bufferedReactions, diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index 439396a62f..6c273395ca 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -9,6 +9,7 @@ import type { DeletedNotesRepository, NotesRepository, UsersRepository } from '@ import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; +import type { MiDeletedNote } from '@/models/DeletedNote.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @@ -43,19 +44,17 @@ export class GetterService { } @bindThis - public async getDeletedNote(noteId: MiNote['id']) { - const note = await this.deletedNotesRepository.findOneBy({ id: noteId }); + public async getMayDeletedNoteOrNull(noteId: MiNote['id']): Promise { + let note: MiNote | MiDeletedNote | null = await this.notesRepository.findOneBy({ id: noteId }); - if (note == null) { - throw new IdentifiableError('f2d7e5b8-9d79-4996-b996-89b538a1b71f', 'No such deleted note.'); - } + note ??= await this.deletedNotesRepository.findOneBy({ id: noteId }); return note; } @bindThis - public async getNoteWithRelations(noteId: MiNote['id']) { - const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user', 'reply', 'renote', 'reply.user', 'renote.user'] }); + public async getMayDeletedNote(noteId: MiNote['id']): Promise<(MiNote | MiDeletedNote)> { + const note = await this.getMayDeletedNoteOrNull(noteId); if (note == null) { throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); @@ -65,14 +64,16 @@ export class GetterService { } @bindThis - public async getDeletedNoteWithRelations(noteId: MiNote['id']) { - const note = await this.deletedNotesRepository.findOne({ where: { id: noteId }, relations: ['user', 'reply', 'renote', 'reply.user', 'renote.user'] }); + public async getMayDeletedNoteWithRelations(noteId: MiNote['id']): Promise<(MiNote | MiDeletedNote) & { user: NonNullable }> { + let note: MiNote | MiDeletedNote | null = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user', 'reply', 'renote', 'reply.user', 'renote.user'] }); + + note ??= await this.deletedNotesRepository.findOne({ where: { id: noteId }, relations: ['user', 'reply', 'renote', 'reply.user', 'renote.user'] }); if (note == null) { - throw new IdentifiableError('f2d7e5b8-9d79-4996-b996-89b538a1b71f', 'No such deleted note.'); + throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); } - return note; + return note as (MiNote | MiDeletedNote) & { user: object }; } /** diff --git a/packages/backend/src/server/api/endpoints/notes/conversation.ts b/packages/backend/src/server/api/endpoints/notes/conversation.ts index d13fd5e82e..4091def71f 100644 --- a/packages/backend/src/server/api/endpoints/notes/conversation.ts +++ b/packages/backend/src/server/api/endpoints/notes/conversation.ts @@ -3,12 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import type { MiNote } from '@/models/Note.js'; -import type { NotesRepository } from '@/models/_.js'; +import type { MiDeletedNote } from '@/models/DeletedNote.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { DI } from '@/di-symbols.js'; import { GetterService } from '@/server/api/GetterService.js'; import { ApiError } from '../../error.js'; @@ -49,24 +48,21 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - private noteEntityService: NoteEntityService, private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { - const note = await this.getterService.getNote(ps.noteId).catch(err => { + const note = await this.getterService.getMayDeletedNote(ps.noteId).catch(err => { if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; }); - const conversation: MiNote[] = []; + const conversation: (MiNote | MiDeletedNote)[] = []; let i = 0; - const get = async (id: any) => { + const get = async (id: MiNote['id']) => { i++; - const p = await this.notesRepository.findOneBy({ id }); + const p = await this.getterService.getMayDeletedNoteOrNull(id); if (p == null) return; if (i > ps.offset!) { diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index d8596fe120..f73862d0d5 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -57,28 +57,12 @@ export default class extends Endpoint { // eslint- private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { - let note: MiNote | void; - try { - note = await this.getterService.getNoteWithRelations(ps.noteId); - } catch (err) { - if (err instanceof IdentifiableError && err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') { - try { - const deletedNote = await this.getterService.getDeletedNoteWithRelations(ps.noteId); - - return await this.noteEntityService.packDeletedNote(deletedNote, me, { - detail: true, - }); - } catch (err) { - if (err instanceof IdentifiableError && err.id === 'f2d7e5b8-9d79-4996-b996-89b538a1b71f') { - throw new ApiError(meta.errors.noSuchNote); - } - throw err; - } - } + const note = await this.getterService.getMayDeletedNoteWithRelations(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; - } + }); - if (note.user!.requireSigninToViewContents && me == null) { + if (note.user.requireSigninToViewContents && me == null) { throw new ApiError(meta.errors.signinRequired); } @@ -86,11 +70,11 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.signinRequired); } - if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.userHost != null && me == null) { + if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.user.host != null && me == null) { throw new ApiError(meta.errors.signinRequired); } - return await this.noteEntityService.pack(note, me, { + return await this.noteEntityService.packMayDeleted(note, me, { detail: true, }); });