From da43b554f2e033423b0075b38856109c0e955049 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sat, 2 Aug 2025 22:03:00 +0900 Subject: [PATCH] feat: pack and response deleted note as deleted note --- .../backend/src/core/NoteDeleteService.ts | 1 + .../src/core/entities/NoteEntityService.ts | 121 +++++++++++++++++- .../backend/src/server/api/GetterService.ts | 27 +++- .../src/server/api/endpoints/notes/show.ts | 24 +++- 4 files changed, 163 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index a56614d3f2..7003f171e5 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -129,6 +129,7 @@ export class NoteDeleteService { localOnly: note.localOnly, uri: note.uri, url: note.url, + channelId: note.channelId, }); }); diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 6871ba2c72..335bc165e0 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -11,7 +11,7 @@ import type { Packed } from '@/misc/json-schema.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta, DeletedNotesRepository, MiDeletedNote } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; @@ -79,6 +79,9 @@ export class NoteEntityService implements OnModuleInit { @Inject(DI.notesRepository) private notesRepository: NotesRepository, + @Inject(DI.deletedNotesRepository) + private deletedNotesRepository: DeletedNotesRepository, + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @@ -377,7 +380,19 @@ export class NoteEntityService implements OnModuleInit { }, options); const meId = me ? me.id : null; - const note = typeof src === 'object' ? src : await this.noteLoader.load(src); + let note: MiNote; + if (typeof src === 'object') { + note = src; + } else { + try { + note = await this.noteLoader.load(src); + } catch (err) { + if (err instanceof EntityNotFoundError) { + return this.packDeletedNote(src); + } + throw err; + } + } const host = note.userHost; const bufferedReactions = opts._hint_?.bufferedReactions != null @@ -448,20 +463,20 @@ export class NoteEntityService implements OnModuleInit { clippedCount: note.clippedCount, // そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される - reply: (note.replyId && note.reply === null) ? null : note.replyId ? nullIfEntityNotFound(this.pack(note.reply ?? note.replyId, me, { + reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, { detail: false, skipHide: opts.skipHide, withReactionAndUserPairCache: opts.withReactionAndUserPairCache, _hint_: options?._hint_, - })) : undefined, + }) : undefined, // そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される - renote: (note.renoteId && note.renote === null) ? null : note.renoteId ? nullIfEntityNotFound(this.pack(note.renote ?? note.renoteId, me, { + renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, { detail: true, skipHide: opts.skipHide, withReactionAndUserPairCache: opts.withReactionAndUserPairCache, _hint_: options?._hint_, - })) : undefined, + }) : undefined, poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, @@ -484,6 +499,100 @@ export class NoteEntityService implements OnModuleInit { return packed; } + 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> + }; + }, + ): Promise> { + const opts = Object.assign({ + detail: true, + skipHide: false, + withReactionAndUserPairCache: false, + }, options); + + const deletedNote = typeof srcId === 'object' ? srcId : await this.deletedNotesRepository.findOneOrFail({ + where: { + id: srcId, + }, + relations: ['user', 'renote', 'reply', 'channel'], + }); + + const packedUsers = options?._hint_?.packedUsers; + + const channel = deletedNote.channelId + ? deletedNote.channel ?? await this.channelsRepository.findOneBy({ id: deletedNote.channelId }) + : null; + + return await awaitAll({ + id: deletedNote.id, + createdAt: this.idService.parse(deletedNote.id).date.toISOString(), + deletedAt: deletedNote.deletedAt?.toISOString() ?? undefined, + userId: deletedNote.userId, + user: packedUsers?.get(deletedNote.userId) ?? this.userEntityService.pack(deletedNote.user ?? deletedNote.userId, me), + text: deletedNote.deletedAt ? "Deleted note" : "Forgotten remote note. View on remote instance to see contents.", + cw: null, + visibility: 'public', + localOnly: deletedNote.localOnly, + reactionAcceptance: 'likeOnly', + visibleUserIds: undefined, + renoteCount: 0, + repliesCount: 0, + reactionCount: 0, + reactions: {}, + reactionEmojis: {}, + reactionAndUserPairCache: [], + emojis: {}, + tags: undefined, + fileIds: [], + files: [], + replyId: deletedNote.replyId, + renoteId: deletedNote.renoteId, + channelId: deletedNote.channelId ?? undefined, + channel: channel ? { + id: channel.id, + name: channel.name, + color: channel.color, + isSensitive: channel.isSensitive, + allowRenoteToExternal: channel.allowRenoteToExternal, + userId: channel.userId, + } : undefined, + mentions: undefined, + hasPoll: undefined, + uri: deletedNote.uri ?? undefined, + url: deletedNote.url ?? undefined, + + ...(opts.detail ? { + clippedCount: 0, + + reply: deletedNote.replyId ? this.pack(deletedNote.reply ?? deletedNote.replyId, me, { + detail: false, + skipHide: opts.skipHide, + withReactionAndUserPairCache: opts.withReactionAndUserPairCache, + _hint_: options?._hint_, + }) : undefined, + + renote: deletedNote.renoteId ? this.pack(deletedNote.renote ?? deletedNote.renoteId, me, { + detail: true, + skipHide: opts.skipHide, + withReactionAndUserPairCache: opts.withReactionAndUserPairCache, + _hint_: options?._hint_, + }) : undefined, + + poll: undefined, + } : {}), + }); + } + @bindThis public async packMany( notes: MiNote[], diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index 8f4213dfb6..439396a62f 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UsersRepository } from '@/models/_.js'; +import type { DeletedNotesRepository, NotesRepository, UsersRepository } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; @@ -21,6 +21,9 @@ export class GetterService { @Inject(DI.notesRepository) private notesRepository: NotesRepository, + @Inject(DI.deletedNotesRepository) + private deletedNotesRepository: DeletedNotesRepository, + private userEntityService: UserEntityService, ) { } @@ -39,6 +42,17 @@ export class GetterService { return note; } + @bindThis + public async getDeletedNote(noteId: MiNote['id']) { + const note = await this.deletedNotesRepository.findOneBy({ id: noteId }); + + if (note == null) { + throw new IdentifiableError('f2d7e5b8-9d79-4996-b996-89b538a1b71f', 'No such deleted note.'); + } + + 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'] }); @@ -50,6 +64,17 @@ export class GetterService { return note; } + @bindThis + public async getDeletedNoteWithRelations(noteId: MiNote['id']) { + const 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.'); + } + + return note; + } + /** * Get user for API processing */ diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index cae0e752da..d8596fe120 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -9,6 +9,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { GetterService } from '@/server/api/GetterService.js'; import { DI } from '@/di-symbols.js'; import { MiMeta } from '@/models/Meta.js'; +import { MiNote } from '@/models/Note.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -55,10 +57,26 @@ export default class extends Endpoint { // eslint- private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { - const note = await this.getterService.getNoteWithRelations(ps.noteId).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + 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; + } + } throw err; - }); + } if (note.user!.requireSigninToViewContents && me == null) { throw new ApiError(meta.errors.signinRequired);