chore: allow resolving conversation relates to deleted note
This commit is contained in:
parent
82e8c5ff25
commit
912cc0a86b
|
@ -57,6 +57,18 @@ async function nullIfEntityNotFound<T>(promise: Promise<T>): Promise<T | null> {
|
|||
}
|
||||
}
|
||||
|
||||
type PackSingleOptions = {
|
||||
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'>>
|
||||
};
|
||||
};
|
||||
|
||||
@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<Packed<'Note'>> {
|
||||
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<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'>>
|
||||
};
|
||||
},
|
||||
options?: PackSingleOptions,
|
||||
): Promise<Packed<'Note'>> {
|
||||
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<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'>>
|
||||
};
|
||||
},
|
||||
options?: PackSingleOptions,
|
||||
): Promise<Packed<'Note'>> {
|
||||
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<MiNote['id'], string | null>();
|
||||
|
@ -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,
|
||||
|
|
|
@ -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<MiNote | MiDeletedNote | null> {
|
||||
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<MiNote['user']> }> {
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<typeof meta, typeof paramDef> { // 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!) {
|
||||
|
|
|
@ -57,28 +57,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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,
|
||||
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;
|
||||
});
|
||||
} 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) {
|
||||
if (note.user.requireSigninToViewContents && me == null) {
|
||||
throw new ApiError(meta.errors.signinRequired);
|
||||
}
|
||||
|
||||
|
@ -86,11 +70,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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,
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue