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()
|
@Injectable()
|
||||||
export class NoteEntityService implements OnModuleInit {
|
export class NoteEntityService implements OnModuleInit {
|
||||||
private userEntityService: UserEntityService;
|
private userEntityService: UserEntityService;
|
||||||
|
@ -357,21 +369,24 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
return fileIds.map(id => packedFiles.get(id)).filter(x => x != null);
|
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
|
@bindThis
|
||||||
public async pack(
|
public async pack(
|
||||||
src: MiNote['id'] | MiNote,
|
src: MiNote['id'] | MiNote,
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
me?: { id: MiUser['id'] } | null | undefined,
|
||||||
options?: {
|
options?: 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'>>
|
|
||||||
};
|
|
||||||
},
|
|
||||||
): Promise<Packed<'Note'>> {
|
): Promise<Packed<'Note'>> {
|
||||||
const opts = Object.assign({
|
const opts = Object.assign({
|
||||||
detail: true,
|
detail: true,
|
||||||
|
@ -462,7 +477,6 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
...(opts.detail ? {
|
...(opts.detail ? {
|
||||||
clippedCount: note.clippedCount,
|
clippedCount: note.clippedCount,
|
||||||
|
|
||||||
// そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される
|
|
||||||
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
|
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
|
||||||
detail: false,
|
detail: false,
|
||||||
skipHide: opts.skipHide,
|
skipHide: opts.skipHide,
|
||||||
|
@ -470,7 +484,6 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
_hint_: options?._hint_,
|
_hint_: options?._hint_,
|
||||||
}) : undefined,
|
}) : undefined,
|
||||||
|
|
||||||
// そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される
|
|
||||||
renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, {
|
renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, {
|
||||||
detail: true,
|
detail: true,
|
||||||
skipHide: opts.skipHide,
|
skipHide: opts.skipHide,
|
||||||
|
@ -502,17 +515,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
public async packDeletedNote(
|
public async packDeletedNote(
|
||||||
srcId: MiNote['id'] | MiDeletedNote,
|
srcId: MiNote['id'] | MiDeletedNote,
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
me?: { id: MiUser['id'] } | null | undefined,
|
||||||
options?: {
|
options?: 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'>>
|
|
||||||
};
|
|
||||||
},
|
|
||||||
): Promise<Packed<'Note'>> {
|
): Promise<Packed<'Note'>> {
|
||||||
const opts = Object.assign({
|
const opts = Object.assign({
|
||||||
detail: true,
|
detail: true,
|
||||||
|
@ -595,7 +598,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async packMany(
|
public async packMany(
|
||||||
notes: MiNote[],
|
notes: (MiNote | MiDeletedNote)[],
|
||||||
me?: { id: MiUser['id'] } | null | undefined,
|
me?: { id: MiUser['id'] } | null | undefined,
|
||||||
options?: {
|
options?: {
|
||||||
detail?: boolean;
|
detail?: boolean;
|
||||||
|
@ -604,7 +607,9 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
) {
|
) {
|
||||||
if (notes.length === 0) return [];
|
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 meId = me ? me.id : null;
|
||||||
const myReactionsMap = new Map<MiNote['id'], string | null>();
|
const myReactionsMap = new Map<MiNote['id'], string | null>();
|
||||||
|
@ -614,7 +619,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
// パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない
|
// パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない
|
||||||
const oldId = this.idService.gen(Date.now() - 2000);
|
const oldId = this.idService.gen(Date.now() - 2000);
|
||||||
|
|
||||||
for (const note of notes) {
|
for (const note of liveNotes) {
|
||||||
if (isPureRenote(note)) {
|
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);
|
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) {
|
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 があったらここで解決しておく
|
// 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 packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
|
||||||
const users = [
|
const users = [
|
||||||
...notes.map(({ user, userId }) => user ?? userId),
|
...notes.map(({ user, userId }) => user ?? userId),
|
||||||
|
@ -674,7 +679,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
const packedUsers = await this.userEntityService.packMany(users, me)
|
const packedUsers = await this.userEntityService.packMany(users, me)
|
||||||
.then(users => new Map(users.map(u => [u.id, u])));
|
.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,
|
...options,
|
||||||
_hint_: {
|
_hint_: {
|
||||||
bufferedReactions,
|
bufferedReactions,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type { DeletedNotesRepository, NotesRepository, UsersRepository } from '@
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
|
import type { MiDeletedNote } from '@/models/DeletedNote.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
|
@ -43,19 +44,17 @@ export class GetterService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getDeletedNote(noteId: MiNote['id']) {
|
public async getMayDeletedNoteOrNull(noteId: MiNote['id']): Promise<MiNote | MiDeletedNote | null> {
|
||||||
const note = await this.deletedNotesRepository.findOneBy({ id: noteId });
|
let note: MiNote | MiDeletedNote | null = await this.notesRepository.findOneBy({ id: noteId });
|
||||||
|
|
||||||
if (note == null) {
|
note ??= await this.deletedNotesRepository.findOneBy({ id: noteId });
|
||||||
throw new IdentifiableError('f2d7e5b8-9d79-4996-b996-89b538a1b71f', 'No such deleted note.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getNoteWithRelations(noteId: MiNote['id']) {
|
public async getMayDeletedNote(noteId: MiNote['id']): Promise<(MiNote | MiDeletedNote)> {
|
||||||
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user', 'reply', 'renote', 'reply.user', 'renote.user'] });
|
const note = await this.getMayDeletedNoteOrNull(noteId);
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
|
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');
|
||||||
|
@ -65,14 +64,16 @@ export class GetterService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getDeletedNoteWithRelations(noteId: MiNote['id']) {
|
public async getMayDeletedNoteWithRelations(noteId: MiNote['id']): Promise<(MiNote | MiDeletedNote) & { user: NonNullable<MiNote['user']> }> {
|
||||||
const note = await this.deletedNotesRepository.findOne({ where: { id: noteId }, relations: ['user', 'reply', 'renote', 'reply.user', 'renote.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) {
|
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
|
* 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 { 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 { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
@ -49,24 +48,21 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.notesRepository)
|
|
||||||
private notesRepository: NotesRepository,
|
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
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);
|
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
const conversation: MiNote[] = [];
|
const conversation: (MiNote | MiDeletedNote)[] = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
const get = async (id: any) => {
|
const get = async (id: MiNote['id']) => {
|
||||||
i++;
|
i++;
|
||||||
const p = await this.notesRepository.findOneBy({ id });
|
const p = await this.getterService.getMayDeletedNoteOrNull(id);
|
||||||
if (p == null) return;
|
if (p == null) return;
|
||||||
|
|
||||||
if (i > ps.offset!) {
|
if (i > ps.offset!) {
|
||||||
|
|
|
@ -57,28 +57,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private getterService: GetterService,
|
private getterService: GetterService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
let note: MiNote | void;
|
const note = await this.getterService.getMayDeletedNoteWithRelations(ps.noteId).catch(err => {
|
||||||
try {
|
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||||
note = await this.getterService.getNoteWithRelations(ps.noteId);
|
throw err;
|
||||||
} 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) {
|
if (note.user.requireSigninToViewContents && me == null) {
|
||||||
throw new ApiError(meta.errors.signinRequired);
|
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);
|
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);
|
throw new ApiError(meta.errors.signinRequired);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.noteEntityService.pack(note, me, {
|
return await this.noteEntityService.packMayDeleted(note, me, {
|
||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue