feat: pack and response deleted note as deleted note

This commit is contained in:
anatawa12 2025-08-02 22:03:00 +09:00
parent 0544f3fc3d
commit da43b554f2
No known key found for this signature in database
GPG Key ID: 9CA909848B8E4EA6
4 changed files with 163 additions and 10 deletions

View File

@ -129,6 +129,7 @@ export class NoteDeleteService {
localOnly: note.localOnly,
uri: note.uri,
url: note.url,
channelId: note.channelId,
});
});

View File

@ -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<MiNote['id'], { deltas: Record<string, number>; pairs: ([MiUser['id'], string])[] }> | null;
myReactions: Map<MiNote['id'], string | null>;
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
};
},
): Promise<Packed<'Note'>> {
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 ? "<small>Deleted note</small>" : "<small>Forgotten remote note. View on remote instance to see contents.</small>",
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[],

View File

@ -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
*/

View File

@ -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<typeof meta, typeof paramDef> { // 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);