From 4f653f2fbc9f48f2d3069dd587907ebee667386c Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:30:35 +0900 Subject: enhance(frontend): typed nirax (#16309) * enhance(frontend): typed nirax * migrate router.replace * fix --- packages/frontend/src/components/MkEmojiPicker.vue | 2 +- packages/frontend/src/components/MkPageWindow.vue | 4 ++-- packages/frontend/src/components/MkSuperMenu.vue | 2 +- packages/frontend/src/components/global/MkA.vue | 4 ++-- packages/frontend/src/components/global/StackingRouterView.vue | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) (limited to 'packages/frontend/src/components') diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 68da098439..654aceb8f5 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -495,7 +495,7 @@ function done(query?: string): boolean | void { function settings() { emit('esc'); - router.push('settings/emoji-palette'); + router.push('/settings/emoji-palette'); } onMounted(() => { diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index 1310ea6a77..cf60c1ca3e 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -151,7 +151,7 @@ const contextmenu = computed(() => ([{ function back() { history.value.pop(); - windowRouter.replace(history.value.at(-1)!.path); + windowRouter.replaceByPath(history.value.at(-1)!.path); } function reload() { @@ -163,7 +163,7 @@ function close() { } function expand() { - mainRouter.push(windowRouter.getCurrentFullPath(), 'forcePage'); + mainRouter.pushByPath(windowRouter.getCurrentFullPath(), 'forcePage'); windowEl.value?.close(); } diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 3f8d92a61d..5c89a6530d 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -186,7 +186,7 @@ function searchOnKeyDown(ev: KeyboardEvent) { if (ev.key === 'Enter' && searchSelectedIndex.value != null) { ev.preventDefault(); - router.push(searchResult.value[searchSelectedIndex.value].path + '#' + searchResult.value[searchSelectedIndex.value].id); + router.pushByPath(searchResult.value[searchSelectedIndex.value].path + '#' + searchResult.value[searchSelectedIndex.value].id); } else if (ev.key === 'ArrowDown') { ev.preventDefault(); const current = searchSelectedIndex.value ?? -1; diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue index 4004db5b12..ae1b4549ec 100644 --- a/packages/frontend/src/components/global/MkA.vue +++ b/packages/frontend/src/components/global/MkA.vue @@ -64,7 +64,7 @@ function onContextmenu(ev) { icon: 'ti ti-player-eject', text: i18n.ts.showInPage, action: () => { - router.push(props.to, 'forcePage'); + router.pushByPath(props.to, 'forcePage'); }, }, { type: 'divider' }, { icon: 'ti ti-external-link', @@ -99,6 +99,6 @@ function nav(ev: MouseEvent) { return openWindow(); } - router.push(props.to, ev.ctrlKey ? 'forcePage' : null); + router.pushByPath(props.to, ev.ctrlKey ? 'forcePage' : null); } diff --git a/packages/frontend/src/components/global/StackingRouterView.vue b/packages/frontend/src/components/global/StackingRouterView.vue index c95c74aef3..9e47517244 100644 --- a/packages/frontend/src/components/global/StackingRouterView.vue +++ b/packages/frontend/src/components/global/StackingRouterView.vue @@ -76,7 +76,7 @@ function mount() { function back() { const prev = tabs.value[tabs.value.length - 2]; tabs.value = [...tabs.value.slice(0, tabs.value.length - 1)]; - router.replace(prev.fullPath); + router?.replaceByPath(prev.fullPath); } router.useListener('change', ({ resolved }) => { -- cgit v1.2.3-freya From 1dec8b2329c5b82bdd4a55e0ffd9997709feca61 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:12:59 +0900 Subject: fix(frontend/test): Cypressが失敗する問題を修正 (#16307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * attempt to fix test * fix(frontend/test): Cypressが失敗する問題を修正 --- packages/frontend/src/components/MkImgWithBlurhash.vue | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'packages/frontend/src/components') diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue index 361aeff4d0..983a0932c3 100644 --- a/packages/frontend/src/components/MkImgWithBlurhash.vue +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -52,15 +52,20 @@ import TestWebGL2 from '@/workers/test-webgl2?worker'; import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js'; import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js'; +// テスト環境で Web Worker インスタンスは作成できない +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-expect-error +const isTest = (import.meta.env.MODE === 'test' || window.Cypress != null); + const canvasPromise = new Promise(resolve => { - // テスト環境で Web Worker インスタンスは作成できない - if (import.meta.env.MODE === 'test') { + if (isTest) { const canvas = window.document.createElement('canvas'); canvas.width = 64; canvas.height = 64; resolve(canvas); return; } + const testWorker = new TestWebGL2(); testWorker.addEventListener('message', event => { if (event.data.result) { @@ -189,7 +194,7 @@ function drawAvg() { } async function draw() { - if (import.meta.env.MODE === 'test' && props.hash == null) return; + if (isTest && props.hash == null) return; drawAvg(); -- cgit v1.2.3-freya From f2a23fb55ef2100bd26e3f2bcd7f939052c2ea09 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:40:51 +0900 Subject: ノートの脱CASCADE削除 (#16332) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * Update CHANGELOG.md * Update QueryService.ts * Update QueryService.ts * wip * Update MkNoteDetailed.vue * Update NoteEntityService.ts * wip * Update antennas.ts * Update create.ts * Update NoteEntityService.ts * wip * Update CHANGELOG.md * Update NoteEntityService.ts * Update NoteCreateService.ts * Update note.test.ts * Update note.test.ts * Update ClientServerService.ts * Update ClientServerService.ts * add error handling * Update NoteDeleteService.ts * Update CHANGELOG.md * Update entities.ts * Update entities.ts * Update misskey-js.api.md --- CHANGELOG.md | 6 ++-- locales/index.d.ts | 4 +-- locales/ja-JP.yml | 4 +-- .../1753868431598-remove_note_constraints.js | 18 +++++++++++ packages/backend/src/core/NoteCreateService.ts | 8 +++-- packages/backend/src/core/NoteDeleteService.ts | 36 ---------------------- packages/backend/src/core/QueryService.ts | 4 +-- .../backend/src/core/entities/NoteEntityService.ts | 25 +++++++++++---- packages/backend/src/models/Note.ts | 4 +-- packages/backend/src/server/api/GetterService.ts | 4 +-- .../src/server/api/endpoints/notes/create.ts | 10 ++++-- .../backend/src/server/api/endpoints/notes/show.ts | 2 +- .../backend/src/server/web/ClientServerService.ts | 9 ++++-- packages/backend/test-federation/test/note.test.ts | 2 -- packages/backend/test/e2e/antennas.ts | 1 - packages/frontend/src/components/MkNote.vue | 6 ++-- .../frontend/src/components/MkNoteDetailed.vue | 2 +- packages/frontend/src/components/MkNoteSimple.vue | 18 +++++++++-- packages/frontend/src/components/MkNoteSub.vue | 21 ++++++++++--- packages/misskey-js/etc/misskey-js.api.md | 4 +-- packages/misskey-js/src/entities.ts | 3 +- packages/misskey-js/src/note.ts | 4 +-- 22 files changed, 115 insertions(+), 80 deletions(-) create mode 100644 packages/backend/migration/1753868431598-remove_note_constraints.js (limited to 'packages/frontend/src/components') diff --git a/CHANGELOG.md b/CHANGELOG.md index 161a336a8b..af5c0da4a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,16 @@ ## Unreleased ### General -- +- ノートを削除した際、関連するノートが同時に削除されないようになりました + - APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります ### Client - Fix: 一部の設定検索結果が存在しないパスになる問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171) ### Server -- +- Enhance: ノートの削除処理の効率化 +- Enhance: 全体的なパフォーマンスの向上 ## 2025.7.0 diff --git a/locales/index.d.ts b/locales/index.d.ts index 8d757ff579..088b89b79f 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2567,11 +2567,11 @@ export interface Locale extends ILocale { */ "serviceworkerInfo": string; /** - * 削除された投稿 + * 削除されたノート */ "deletedNote": string; /** - * 非公開の投稿 + * 非公開のノート */ "invisibleNote": string; /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 161edfe8bb..5bd2fc6e17 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -637,8 +637,8 @@ addRelay: "リレーの追加" inboxUrl: "inboxのURL" addedRelays: "追加済みのリレー" serviceworkerInfo: "プッシュ通知を行うには有効にする必要があります。" -deletedNote: "削除された投稿" -invisibleNote: "非公開の投稿" +deletedNote: "削除されたノート" +invisibleNote: "非公開のノート" enableInfiniteScroll: "自動でもっと見る" visibility: "公開範囲" poll: "アンケート" diff --git a/packages/backend/migration/1753868431598-remove_note_constraints.js b/packages/backend/migration/1753868431598-remove_note_constraints.js new file mode 100644 index 0000000000..29540cf9de --- /dev/null +++ b/packages/backend/migration/1753868431598-remove_note_constraints.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RemoveNoteConstraints1753868431598 { + name = 'RemoveNoteConstraints1753868431598' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_52ccc804d7c69037d558bac4c96"`); + await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_52ccc804d7c69037d558bac4c96" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 469426f87e..1eefcfa054 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -421,7 +421,7 @@ export class NoteCreateService implements OnApplicationShutdown { emojis, userId: user.id, localOnly: data.localOnly!, - reactionAcceptance: data.reactionAcceptance, + reactionAcceptance: data.reactionAcceptance ?? null, visibility: data.visibility as any, visibleUserIds: data.visibility === 'specified' ? data.visibleUsers @@ -483,7 +483,11 @@ export class NoteCreateService implements OnApplicationShutdown { await this.notesRepository.insert(insert); } - return insert; + return { + ...insert, + reply: data.reply ?? null, + renote: data.renote ?? null, + }; } catch (e) { // duplicate key error if (isDuplicateKeyValueError(e)) { diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index e394506a44..af1f0eda9a 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -62,7 +62,6 @@ export class NoteDeleteService { */ async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) { const deletedAt = new Date(); - const cascadingNotes = await this.findCascadingNotes(note); if (note.replyId) { await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1); @@ -90,15 +89,6 @@ export class NoteDeleteService { this.deliverToConcerned(user, note, content); } - - // also deliver delete activity to cascaded notes - const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes - for (const cascadingNote of federatedLocalCascadingNotes) { - if (!cascadingNote.user) continue; - if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue; - const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user)); - this.deliverToConcerned(cascadingNote.user, cascadingNote, content); - } //#endregion this.notesChart.update(note, false); @@ -118,9 +108,6 @@ export class NoteDeleteService { } } - for (const cascadingNote of cascadingNotes) { - this.searchService.unindexNote(cascadingNote); - } this.searchService.unindexNote(note); await this.notesRepository.delete({ @@ -140,29 +127,6 @@ export class NoteDeleteService { } } - @bindThis - private async findCascadingNotes(note: MiNote): Promise { - const recursive = async (noteId: string): Promise => { - const query = this.notesRepository.createQueryBuilder('note') - .where('note.replyId = :noteId', { noteId }) - .orWhere(new Brackets(q => { - q.where('note.renoteId = :noteId', { noteId }) - .andWhere('note.text IS NOT NULL'); - })) - .leftJoinAndSelect('note.user', 'user'); - const replies = await query.getMany(); - - return [ - replies, - ...await Promise.all(replies.map(reply => recursive(reply.id))), - ].flat(); - }; - - const cascadingNotes: MiNote[] = await recursive(note.id); - - return cascadingNotes; - } - @bindThis private async getMentionedRemoteUsers(note: MiNote) { const where = [] as any[]; diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index d398e83230..49f93ad108 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -360,7 +360,7 @@ export class QueryService { public generateSuspendedUserQueryForNote(q: SelectQueryBuilder, excludeAuthor?: boolean): void { if (excludeAuthor) { const brakets = (user: string) => new Brackets(qb => qb - .where(`note.${user}Id IS NULL`) + .where(`${user}.id IS NULL`) // そもそもreplyやrenoteではない、もしくはleftjoinなどでuserが存在しなかった場合を考慮 .orWhere(`user.id = ${user}.id`) .orWhere(`${user}.isSuspended = FALSE`)); q @@ -368,7 +368,7 @@ export class QueryService { .andWhere(brakets('renoteUser')); } else { const brakets = (user: string) => new Brackets(qb => qb - .where(`note.${user}Id IS NULL`) + .where(`${user}.id IS NULL`) // そもそもreplyやrenoteではない、もしくはleftjoinなどでuserが存在しなかった場合を考慮 .orWhere(`${user}.isSuspended = FALSE`)); q .andWhere('user.isSuspended = FALSE') diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 92caad908c..6871ba2c72 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; +import { EntityNotFoundError, In } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { Packed } from '@/misc/json-schema.js'; @@ -46,6 +46,17 @@ function getAppearNoteIds(notes: MiNote[]): Set { return appearNoteIds; } +async function nullIfEntityNotFound(promise: Promise): Promise { + try { + return await promise; + } catch (err) { + if (err instanceof EntityNotFoundError) { + return null; + } + throw err; + } +} + @Injectable() export class NoteEntityService implements OnModuleInit { private userEntityService: UserEntityService; @@ -436,19 +447,21 @@ export class NoteEntityService implements OnModuleInit { ...(opts.detail ? { clippedCount: note.clippedCount, - reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, { + // そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される + reply: (note.replyId && note.reply === null) ? null : note.replyId ? nullIfEntityNotFound(this.pack(note.reply ?? note.replyId, me, { detail: false, skipHide: opts.skipHide, withReactionAndUserPairCache: opts.withReactionAndUserPairCache, _hint_: options?._hint_, - }) : undefined, + })) : undefined, - renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, { + // そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される + renote: (note.renoteId && note.renote === null) ? null : note.renoteId ? nullIfEntityNotFound(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, @@ -591,7 +604,7 @@ export class NoteEntityService implements OnModuleInit { private findNoteOrFail(id: string): Promise { return this.notesRepository.findOneOrFail({ where: { id }, - relations: ['user'], + relations: ['user', 'renote', 'reply'], }); } diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index 9822ec94e4..ff46615729 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -36,7 +36,7 @@ export class MiNote { public replyId: MiNote['id'] | null; @ManyToOne(type => MiNote, { - onDelete: 'CASCADE', + createForeignKeyConstraints: false, }) @JoinColumn() public reply: MiNote | null; @@ -50,7 +50,7 @@ export class MiNote { public renoteId: MiNote['id'] | null; @ManyToOne(type => MiNote, { - onDelete: 'CASCADE', + createForeignKeyConstraints: false, }) @JoinColumn() public renote: MiNote | null; diff --git a/packages/backend/src/server/api/GetterService.ts b/packages/backend/src/server/api/GetterService.ts index 444e6db744..8f4213dfb6 100644 --- a/packages/backend/src/server/api/GetterService.ts +++ b/packages/backend/src/server/api/GetterService.ts @@ -40,8 +40,8 @@ export class GetterService { } @bindThis - public async getNoteWithUser(noteId: MiNote['id']) { - const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] }); + public async getNoteWithRelations(noteId: MiNote['id']) { + const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user', 'reply', 'renote', 'reply.user', 'renote.user'] }); if (note == null) { throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 253a360815..7caea8eedc 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -269,7 +269,10 @@ export default class extends Endpoint { // eslint- let renote: MiNote | null = null; if (ps.renoteId != null) { // Fetch renote to note - renote = await this.notesRepository.findOneBy({ id: ps.renoteId }); + renote = await this.notesRepository.findOne({ + where: { id: ps.renoteId }, + relations: ['user', 'renote', 'reply'], + }); if (renote == null) { throw new ApiError(meta.errors.noSuchRenoteTarget); @@ -315,7 +318,10 @@ export default class extends Endpoint { // eslint- let reply: MiNote | null = null; if (ps.replyId != null) { // Fetch reply - reply = await this.notesRepository.findOneBy({ id: ps.replyId }); + reply = await this.notesRepository.findOne({ + where: { id: ps.replyId }, + relations: ['user'], + }); if (reply == null) { throw new ApiError(meta.errors.noSuchReplyTarget); diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index b93c73b0c5..cae0e752da 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -55,7 +55,7 @@ export default class extends Endpoint { // eslint- private getterService: GetterService, ) { super(meta, paramDef, async (ps, me) => { - const note = await this.getterService.getNoteWithUser(ps.noteId).catch(err => { + const note = await this.getterService.getNoteWithRelations(ps.noteId).catch(err => { if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; }); diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 8ca61a497d..4d122b0fcf 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -580,7 +580,7 @@ export class ClientServerService { id: request.params.note, visibility: In(['public', 'home']), }, - relations: ['user'], + relations: ['user', 'reply', 'renote'], }); if ( @@ -821,8 +821,11 @@ export class ClientServerService { fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => { reply.removeHeader('X-Frame-Options'); - const note = await this.notesRepository.findOneBy({ - id: request.params.note, + const note = await this.notesRepository.findOne({ + where: { + id: request.params.note, + }, + relations: ['user', 'reply', 'renote'], }); if (note == null) return; diff --git a/packages/backend/test-federation/test/note.test.ts b/packages/backend/test-federation/test/note.test.ts index 1584f9587e..a339cd86d2 100644 --- a/packages/backend/test-federation/test/note.test.ts +++ b/packages/backend/test-federation/test/note.test.ts @@ -63,7 +63,6 @@ describe('Note', () => { deepStrictEqualWithExcludedFields(note, resolvedNote, [ 'id', 'emojis', - 'reactionAcceptance', 'replyId', 'reply', 'userId', @@ -105,7 +104,6 @@ describe('Note', () => { deepStrictEqualWithExcludedFields(note, resolvedNote, [ 'id', 'emojis', - 'reactionAcceptance', 'renoteId', 'renote', 'userId', diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index 4dbeacf925..1bbacd065b 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -673,7 +673,6 @@ describe('アンテナ', () => { assert.deepStrictEqual(response, expected); }); - test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { }); test.each([ { label: 'ID指定', offsetBy: 'id' }, diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 0605030d5b..b9cb37e99a 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]" tabindex="0" > - +
{{ i18n.ts.pinnedNote }}
@@ -99,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -282,7 +282,7 @@ let note = deepClone(props.note); //} const isRenote = Misskey.note.isPureRenote(note); -const appearNote = getAppearNote(note); +const appearNote = getAppearNote(note) ?? note; const { $note: $appearNote, subscribe: subscribeManuallyToNoteCapture } = useNoteCapture({ note: appearNote, parentNote: note, diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index fb37bb1ae6..c04959b97a 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index e684cf2a30..f1107527b7 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> + + diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index f6a2eb1c27..d079b4cb0c 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -287,6 +287,10 @@ SPDX-License-Identifier: AGPL-3.0-only
+ + + Open setup wizard + @@ -425,6 +429,20 @@ const proxyAccountForm = useForm({ fetchInstance(true); }); +async function openSetupWizard() { + const { canceled } = await os.confirm({ + type: 'warning', + title: i18n.ts._serverSettings.restartServerSetupWizardConfirm_title, + text: i18n.ts._serverSettings.restartServerSetupWizardConfirm_text, + }); + if (canceled) return; + + const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkServerSetupWizardDialog.vue').then(x => x.default), { + }, { + closed: () => dispose(), + }); +} + const headerTabs = computed(() => []); definePage(() => ({ diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index 3e2d086858..393ba98d30 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -87,7 +87,14 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._serverSetupWizard.settingsYouMakeHereCanBeChangedLater }}
- + + + + {{ i18n.ts._serverSetupWizard.skipSettings }} -- cgit v1.2.3-freya From d624da9c1aac731bd49a7bbb949744ebf4986479 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 1 Aug 2025 11:49:12 +0900 Subject: feat: remote notes cleaning (#16292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * wip * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update CleanRemoteNotesProcessorService.ts * Update job-queue.job.vue * wip * Update CleanRemoteNotesProcessorService.ts * wip * wip * wip * Update CleanRemoteNotesProcessorService.ts * wip * Update CHANGELOG.md * Revert "wip" This reverts commit 89d455d302c1106c421bcec309fd7bf02509465e. * wip * woip * Update QueueService.ts * Update QueueService.ts * ピン留め考慮 * Update CleanRemoteNotesProcessorService.ts * Update QueueService.ts * Update CleanRemoteNotesProcessorService.ts * add log * Update CHANGELOG.md * wip * Update MkServerSetupWizard.vue --- CHANGELOG.md | 3 + locales/index.d.ts | 32 ++++ locales/ja-JP.yml | 8 + .../migration/1753863104203-remoteNotesCleaning.js | 20 +++ packages/backend/src/core/QueueService.ts | 4 + packages/backend/src/models/Meta.ts | 15 ++ packages/backend/src/queue/QueueProcessorModule.ts | 4 +- .../backend/src/queue/QueueProcessorService.ts | 3 + .../processors/CleanRemoteNotesProcessorService.ts | 174 +++++++++++++++++++++ .../backend/src/server/api/endpoints/admin/meta.ts | 15 ++ .../src/server/api/endpoints/admin/update-meta.ts | 15 ++ .../src/components/MkServerSetupWizard.vue | 15 +- .../frontend/src/pages/admin/job-queue.job.vue | 4 +- packages/frontend/src/pages/admin/performance.vue | 42 +++++ packages/misskey-js/src/autogen/types.ts | 6 + 15 files changed, 356 insertions(+), 4 deletions(-) create mode 100644 packages/backend/migration/1753863104203-remoteNotesCleaning.js create mode 100644 packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts (limited to 'packages/frontend/src/components') diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e2a82e574..4025f8ab44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ ### General - ノートを削除した際、関連するノートが同時に削除されないようになりました - APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります +- 定期的に参照されていない古いリモートの投稿を削除する機能が実装されました(コントロールパネル→パフォーマンス→Remote Notes Cleaning) + - **デフォルトでオン**になっています + - データベースの肥大化を防止することが可能です ### Client - Fix: 一部の設定検索結果が存在しないパスになる問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index 1be8811a25..6bb6d59476 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5493,6 +5493,14 @@ export interface Locale extends ILocale { * 低くすると画質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、画質は低下します。 */ "defaultImageCompressionLevel_description": string; + /** + * 分 + */ + "inMinutes": string; + /** + * 日 + */ + "inDays": string; "_order": { /** * 新しい順 @@ -6486,6 +6494,22 @@ export interface Locale extends ILocale { * 有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。 */ "reactionsBufferingDescription": string; + /** + * リモート投稿の自動クリーニング + */ + "remoteNotesCleaning": string; + /** + * 有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。 + */ + "remoteNotesCleaning_description": string; + /** + * 最大クリーニング処理継続時間 + */ + "remoteNotesCleaningMaxProcessingDuration": string; + /** + * 最低ノート保持日数 + */ + "remoteNotesCleaningExpiryDaysForEachNotes": string; /** * 問い合わせ先URL */ @@ -11951,6 +11975,14 @@ export interface Locale extends ILocale { * 連合可能なサーバーの指定など、高度な設定も後ほど可能です。 */ "youCanConfigureMoreFederationSettingsLater": string; + /** + * 受信コンテンツの自動クリーニング + */ + "remoteContentsCleaning": string; + /** + * 連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。 + */ + "remoteContentsCleaning_description": string; /** * 管理者情報 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index d4edfc5aab..f141d23ecc 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1368,6 +1368,8 @@ redisplayAllTips: "全ての「ヒントとコツ」を再表示" hideAllTips: "全ての「ヒントとコツ」を非表示" defaultImageCompressionLevel: "デフォルトの画像圧縮度" defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、画質は低下します。" +inMinutes: "分" +inDays: "日" _order: newest: "新しい順" @@ -1649,6 +1651,10 @@ _serverSettings: fanoutTimelineDbFallback: "データベースへのフォールバック" fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。" reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。" + remoteNotesCleaning: "リモート投稿の自動クリーニング" + remoteNotesCleaning_description: "有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。" + remoteNotesCleaningMaxProcessingDuration: "最大クリーニング処理継続時間" + remoteNotesCleaningExpiryDaysForEachNotes: "最低ノート保持日数" inquiryUrl: "問い合わせ先URL" inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。" openRegistration: "アカウントの作成をオープンにする" @@ -3196,6 +3202,8 @@ _serverSetupWizard: doYouConnectToFediverse_description1: "分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。" doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。" youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。" + remoteContentsCleaning: "受信コンテンツの自動クリーニング" + remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。" adminInfo: "管理者情報" adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。" adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。" diff --git a/packages/backend/migration/1753863104203-remoteNotesCleaning.js b/packages/backend/migration/1753863104203-remoteNotesCleaning.js new file mode 100644 index 0000000000..37d42a571d --- /dev/null +++ b/packages/backend/migration/1753863104203-remoteNotesCleaning.js @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RemoteNotesCleaning1753863104203 { + name = 'RemoteNotesCleaning1753863104203' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableRemoteNotesCleaning" boolean NOT NULL DEFAULT true`); + await queryRunner.query('ALTER TABLE "meta" ADD "remoteNotesCleaningMaxProcessingDurationInMinutes" integer NOT NULL DEFAULT \'60\''); + await queryRunner.query('ALTER TABLE "meta" ADD "remoteNotesCleaningExpiryDaysForEachNotes" integer NOT NULL DEFAULT \'90\''); + } + + async down(queryRunner) { + await queryRunner.query('ALTER TABLE "meta" DROP COLUMN "remoteNotesCleaningExpiryDaysForEachNotes"'); + await queryRunner.query('ALTER TABLE "meta" DROP COLUMN "remoteNotesCleaningMaxProcessingDurationInMinutes"'); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableRemoteNotesCleaning"`); + } +} diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 2e49f8cf5e..06170b242a 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -78,6 +78,10 @@ const REPEATABLE_SYSTEM_JOB_DEF = [{ name: 'checkModeratorsActivity', // 毎時30分に起動 pattern: '30 * * * *', +}, { + name: 'cleanRemoteNotes', + // 毎日午前4時に起動(最も人の少ない時間帯) + pattern: '0 4 * * *', }]; @Injectable() diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 85c10ab666..c97fcd8dfc 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -701,6 +701,21 @@ export class MiMeta { default: true, }) public allowExternalApRedirect: boolean; + + @Column('boolean', { + default: true, + }) + public enableRemoteNotesCleaning: boolean; + + @Column('integer', { + default: 60, // minutes + }) + public remoteNotesCleaningMaxProcessingDurationInMinutes: number; + + @Column('integer', { + default: 90, // days + }) + public remoteNotesCleaningExpiryDaysForEachNotes: number; } export type SoftwareSuspension = { diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 9044285bf6..e01414cd53 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -6,7 +6,6 @@ import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; import { GlobalModule } from '@/GlobalModule.js'; -import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueProcessorService } from './QueueProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; @@ -18,6 +17,8 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; +import { CheckModeratorsActivityProcessorService } from './processors/CheckModeratorsActivityProcessorService.js'; +import { CleanRemoteNotesProcessorService } from './processors/CleanRemoteNotesProcessorService.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; @@ -83,6 +84,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor AggregateRetentionProcessorService, CheckExpiredMutingsProcessorService, CheckModeratorsActivityProcessorService, + CleanRemoteNotesProcessorService, QueueProcessorService, ], exports: [ diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index c98ebcdcd9..7b64182754 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -43,6 +43,7 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; +import { CleanRemoteNotesProcessorService } from './processors/CleanRemoteNotesProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; import { QUEUE, baseWorkerOptions } from './const.js'; @@ -123,6 +124,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService, private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService, private cleanProcessorService: CleanProcessorService, + private cleanRemoteNotesProcessorService: CleanRemoteNotesProcessorService, ) { this.logger = this.queueLoggerService.logger; @@ -164,6 +166,7 @@ export class QueueProcessorService implements OnApplicationShutdown { case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process(); case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process(); case 'clean': return this.cleanProcessorService.process(); + case 'cleanRemoteNotes': return this.cleanRemoteNotesProcessorService.process(job); default: throw new Error(`unrecognized job type ${job.name} for system`); } }; diff --git a/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts b/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts new file mode 100644 index 0000000000..5b682e20b8 --- /dev/null +++ b/packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts @@ -0,0 +1,174 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { setTimeout } from 'node:timers/promises'; +import { Inject, Injectable } from '@nestjs/common'; +import { And, In, IsNull, LessThan, MoreThan, Not } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { MiMeta, MiNote, NoteFavoritesRepository, NotesRepository, UserNotePiningsRepository } from '@/models/_.js'; +import type Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; +import { IdService } from '@/core/IdService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; + +@Injectable() +export class CleanRemoteNotesProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.meta) + private meta: MiMeta, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.noteFavoritesRepository) + private noteFavoritesRepository: NoteFavoritesRepository, + + @Inject(DI.userNotePiningsRepository) + private userNotePiningsRepository: UserNotePiningsRepository, + + private idService: IdService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('clean-remote-notes'); + } + + @bindThis + public async process(job: Bull.Job>): Promise<{ + deletedCount: number; + oldest: number | null; + newest: number | null; + skipped?: boolean; + }> { + if (!this.meta.enableRemoteNotesCleaning) { + this.logger.info('Remote notes cleaning is disabled, skipping...'); + return { + deletedCount: 0, + oldest: null, + newest: null, + skipped: true, + }; + } + + this.logger.info('cleaning remote notes...'); + + const maxDuration = this.meta.remoteNotesCleaningMaxProcessingDurationInMinutes * 60 * 1000; // Convert minutes to milliseconds + const startAt = Date.now(); + + const MAX_NOTE_COUNT_PER_QUERY = 50; + + const stats = { + deletedCount: 0, + oldest: null as number | null, + newest: null as number | null, + }; + + let cursor: MiNote['id'] = this.idService.gen(Date.now() - (1000 * 60 * 60 * 24 * this.meta.remoteNotesCleaningExpiryDaysForEachNotes)); + + while (true) { + const batchBeginAt = Date.now(); + + let notes: Pick[] = await this.notesRepository.find({ + where: { + id: LessThan(cursor), + userHost: Not(IsNull()), + clippedCount: 0, + renoteCount: 0, + }, + take: MAX_NOTE_COUNT_PER_QUERY, + order: { + // 新しい順 + // https://github.com/misskey-dev/misskey/pull/16292#issuecomment-3139376314 + id: -1, + }, + select: ['id'], + }); + + const fetchedCount = notes.length; + + for (const note of notes) { + if (note.id < cursor) { + cursor = note.id; + } + } + + const pinings = notes.length === 0 ? [] : await this.userNotePiningsRepository.find({ + where: { + noteId: In(notes.map(note => note.id)), + }, + select: ['noteId'], + }); + + notes = notes.filter(note => { + return !pinings.some(pining => pining.noteId === note.id); + }); + + const favorites = notes.length === 0 ? [] : await this.noteFavoritesRepository.find({ + where: { + noteId: In(notes.map(note => note.id)), + }, + select: ['noteId'], + }); + + notes = notes.filter(note => { + return !favorites.some(favorite => favorite.noteId === note.id); + }); + + const replies = notes.length === 0 ? [] : await this.notesRepository.find({ + where: { + replyId: In(notes.map(note => note.id)), + userHost: IsNull(), + }, + select: ['replyId'], + }); + + notes = notes.filter(note => { + return !replies.some(reply => reply.replyId === note.id); + }); + + if (notes.length > 0) { + await this.notesRepository.delete(notes.map(note => note.id)); + + for (const note of notes) { + const t = this.idService.parse(note.id).date.getTime(); + if (stats.oldest === null || t < stats.oldest) { + stats.oldest = t; + } + if (stats.newest === null || t > stats.newest) { + stats.newest = t; + } + } + + stats.deletedCount += notes.length; + } + + job.log(`Deleted ${notes.length} of ${fetchedCount}; ${Date.now() - batchBeginAt}ms`); + + const elapsed = Date.now() - startAt; + + if (elapsed >= maxDuration) { + this.logger.info(`Reached maximum duration of ${maxDuration}ms, stopping...`); + job.log('Reached maximum duration, stopping cleaning.'); + job.updateProgress(100); + break; + } + + job.updateProgress((elapsed / maxDuration) * 100); + + await setTimeout(1000 * 5); // Wait a moment to avoid overwhelming the db + } + + this.logger.succ('cleaning of remote notes completed.'); + + return { + deletedCount: stats.deletedCount, + oldest: stats.oldest, + newest: stats.newest, + skipped: false, + }; + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 924163afbb..4d3f6d6cd8 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -571,6 +571,18 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + enableRemoteNotesCleaning: { + type: 'boolean', + optional: false, nullable: false, + }, + remoteNotesCleaningExpiryDaysForEachNotes: { + type: 'number', + optional: false, nullable: false, + }, + remoteNotesCleaningMaxProcessingDurationInMinutes: { + type: 'number', + optional: false, nullable: false, + }, }, }, } as const; @@ -722,6 +734,9 @@ export default class extends Endpoint { // eslint- proxyRemoteFiles: instance.proxyRemoteFiles, signToActivityPubGet: instance.signToActivityPubGet, allowExternalApRedirect: instance.allowExternalApRedirect, + enableRemoteNotesCleaning: instance.enableRemoteNotesCleaning, + remoteNotesCleaningExpiryDaysForEachNotes: instance.remoteNotesCleaningExpiryDaysForEachNotes, + remoteNotesCleaningMaxProcessingDurationInMinutes: instance.remoteNotesCleaningMaxProcessingDurationInMinutes, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 578aa2b662..08cea23119 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -205,6 +205,9 @@ export const paramDef = { proxyRemoteFiles: { type: 'boolean' }, signToActivityPubGet: { type: 'boolean' }, allowExternalApRedirect: { type: 'boolean' }, + enableRemoteNotesCleaning: { type: 'boolean' }, + remoteNotesCleaningExpiryDaysForEachNotes: { type: 'number' }, + remoteNotesCleaningMaxProcessingDurationInMinutes: { type: 'number' }, }, required: [], } as const; @@ -723,6 +726,18 @@ export default class extends Endpoint { // eslint- set.allowExternalApRedirect = ps.allowExternalApRedirect; } + if (ps.enableRemoteNotesCleaning !== undefined) { + set.enableRemoteNotesCleaning = ps.enableRemoteNotesCleaning; + } + + if (ps.remoteNotesCleaningExpiryDaysForEachNotes !== undefined) { + set.remoteNotesCleaningExpiryDaysForEachNotes = ps.remoteNotesCleaningExpiryDaysForEachNotes; + } + + if (ps.remoteNotesCleaningMaxProcessingDurationInMinutes !== undefined) { + set.remoteNotesCleaningMaxProcessingDurationInMinutes = ps.remoteNotesCleaningMaxProcessingDurationInMinutes; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/frontend/src/components/MkServerSetupWizard.vue b/packages/frontend/src/components/MkServerSetupWizard.vue index e5614d63d7..23e0e85bc9 100644 --- a/packages/frontend/src/components/MkServerSetupWizard.vue +++ b/packages/frontend/src/components/MkServerSetupWizard.vue @@ -55,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}
{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}
+
{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}
{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}
{{ i18n.ts.learnMore }}
@@ -63,6 +63,11 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }} + + + + +
@@ -110,6 +115,10 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.federation }}:
{{ serverSettings.federation === 'none' ? i18n.ts.no : i18n.ts.all }}
+
+
{{ i18n.ts._serverSettings.remoteNotesCleaning }}:
+
{{ serverSettings.enableRemoteNotesCleaning ? i18n.ts.yes : i18n.ts.no }}
+
FTT:
{{ serverSettings.enableFanoutTimeline ? i18n.ts.yes : i18n.ts.no }}
@@ -185,7 +194,9 @@ import { misskeyApi } from '@/utility/misskey-api.js'; import { i18n } from '@/i18n.js'; import MkFolder from '@/components/MkFolder.vue'; import MkRadios from '@/components/MkRadios.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; import MkInfo from '@/components/MkInfo.vue'; +import MkLink from '@/components/MkLink.vue'; const emit = defineEmits<{ (ev: 'finished'): void; @@ -202,6 +213,7 @@ const q_name = ref(currentMeta.name ?? ''); const q_use = ref('single'); const q_scale = ref('small'); const q_federation = ref(currentMeta.federation === 'none' ? 'no' : 'yes'); +const q_remoteContentsCleaning = ref(currentMeta.enableRemoteNotesCleaning); const q_adminName = ref(currentMeta.maintainerName ?? ''); const q_adminEmail = ref(currentMeta.maintainerEmail ?? ''); @@ -219,6 +231,7 @@ const serverSettings = computed(() => { emailRequiredForSignup: q_use.value === 'open', enableIpLogging: q_use.value === 'open', federation: q_federation.value === 'yes' ? 'all' : 'none', + enableRemoteNotesCleaning: q_remoteContentsCleaning.value, enableFanoutTimeline: true, enableFanoutTimelineDbFallback: q_use.value === 'single', enableReactionsBuffering, diff --git a/packages/frontend/src/pages/admin/job-queue.job.vue b/packages/frontend/src/pages/admin/job-queue.job.vue index 659aa02b50..4ecdb74199 100644 --- a/packages/frontend/src/pages/admin/job-queue.job.vue +++ b/packages/frontend/src/pages/admin/job-queue.job.vue @@ -98,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
@@ -150,7 +150,7 @@ SPDX-License-Identifier: AGPL-3.0-only Update
- +
diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue index c28621b11e..ff3a5b9d7f 100644 --- a/packages/frontend/src/pages/admin/performance.vue +++ b/packages/frontend/src/pages/admin/performance.vue @@ -101,6 +101,35 @@ SPDX-License-Identifier: AGPL-3.0-only
+ + + + + + + + +
+ + + + + + +
+
@@ -196,6 +225,19 @@ const rbtForm = useForm({ fetchInstance(true); }); +const remoteNotesCleaningForm = useForm({ + enableRemoteNotesCleaning: meta.enableRemoteNotesCleaning, + remoteNotesCleaningExpiryDaysForEachNotes: meta.remoteNotesCleaningExpiryDaysForEachNotes, + remoteNotesCleaningMaxProcessingDurationInMinutes: meta.remoteNotesCleaningMaxProcessingDurationInMinutes, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableRemoteNotesCleaning: state.enableRemoteNotesCleaning, + remoteNotesCleaningExpiryDaysForEachNotes: state.remoteNotesCleaningExpiryDaysForEachNotes, + remoteNotesCleaningMaxProcessingDurationInMinutes: state.remoteNotesCleaningMaxProcessingDurationInMinutes, + }); + fetchInstance(true); +}); + const headerActions = computed(() => []); const headerTabs = computed(() => []); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 7594117deb..c0a6dca67e 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -9370,6 +9370,9 @@ export interface operations { proxyRemoteFiles: boolean; signToActivityPubGet: boolean; allowExternalApRedirect: boolean; + enableRemoteNotesCleaning: boolean; + remoteNotesCleaningExpiryDaysForEachNotes: number; + remoteNotesCleaningMaxProcessingDurationInMinutes: number; }; }; }; @@ -12599,6 +12602,9 @@ export interface operations { proxyRemoteFiles?: boolean; signToActivityPubGet?: boolean; allowExternalApRedirect?: boolean; + enableRemoteNotesCleaning?: boolean; + remoteNotesCleaningExpiryDaysForEachNotes?: number; + remoteNotesCleaningMaxProcessingDurationInMinutes?: number; }; }; }; -- cgit v1.2.3-freya From b2b07e5f21f10faa59ce60bec788306438415b65 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:36:25 +0900 Subject: enhance(backend): 連合関係のサーバー設定のデフォルト値をウィザード側に移動 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - サーバー初期設定ウィザードでデフォルト値を設定できるため、データベース上のデフォルト値でオンにしておく必要がない - 連合は初期設定が終わるまで閉じられている方が安全 --- CHANGELOG.md | 2 +- .../1754019326356-tweakDefaultFederationSettings.js | 18 ++++++++++++++++++ packages/backend/src/models/Meta.ts | 4 ++-- .../frontend/src/components/MkServerSetupWizard.vue | 12 +++++------- 4 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 packages/backend/migration/1754019326356-tweakDefaultFederationSettings.js (limited to 'packages/frontend/src/components') diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ddad2ec2f..ab8a93d873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - ノートを削除した際、関連するノートが同時に削除されないようになりました - APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります - 定期的に参照されていない古いリモートの投稿を削除する機能が実装されました(コントロールパネル→パフォーマンス→Remote Notes Cleaning) - - **デフォルトでオン**になっています + - 既存のサーバーでは**デフォルトでオフ**、新規サーバーでは**デフォルトでオン**になります - データベースの肥大化を防止することが可能です ### Client diff --git a/packages/backend/migration/1754019326356-tweakDefaultFederationSettings.js b/packages/backend/migration/1754019326356-tweakDefaultFederationSettings.js new file mode 100644 index 0000000000..12c723f80d --- /dev/null +++ b/packages/backend/migration/1754019326356-tweakDefaultFederationSettings.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class TweakDefaultFederationSettings1754019326356 { + name = 'TweakDefaultFederationSettings1754019326356' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "federation" SET DEFAULT 'none'`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "enableRemoteNotesCleaning" SET DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "enableRemoteNotesCleaning" SET DEFAULT true`); + await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "federation" SET DEFAULT 'all'`); + } +} diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index c97fcd8dfc..1fc50cbd07 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -654,7 +654,7 @@ export class MiMeta { @Column('varchar', { length: 128, - default: 'all', + default: 'none', }) public federation: 'all' | 'specified' | 'none'; @@ -703,7 +703,7 @@ export class MiMeta { public allowExternalApRedirect: boolean; @Column('boolean', { - default: true, + default: false, }) public enableRemoteNotesCleaning: boolean; diff --git a/packages/frontend/src/components/MkServerSetupWizard.vue b/packages/frontend/src/components/MkServerSetupWizard.vue index 23e0e85bc9..d2f56b55c4 100644 --- a/packages/frontend/src/components/MkServerSetupWizard.vue +++ b/packages/frontend/src/components/MkServerSetupWizard.vue @@ -207,15 +207,13 @@ const props = withDefaults(defineProps<{ }>(), { }); -const currentMeta = await misskeyApi('admin/meta'); - -const q_name = ref(currentMeta.name ?? ''); +const q_name = ref(''); const q_use = ref('single'); const q_scale = ref('small'); -const q_federation = ref(currentMeta.federation === 'none' ? 'no' : 'yes'); -const q_remoteContentsCleaning = ref(currentMeta.enableRemoteNotesCleaning); -const q_adminName = ref(currentMeta.maintainerName ?? ''); -const q_adminEmail = ref(currentMeta.maintainerEmail ?? ''); +const q_federation = ref('yes'); +const q_remoteContentsCleaning = ref(true); +const q_adminName = ref(''); +const q_adminEmail = ref(''); const serverSettings = computed(() => { let enableReactionsBuffering; -- cgit v1.2.3-freya From c2c58982218866f54421e1793a8265ed1de4405a Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 1 Aug 2025 19:55:55 +0900 Subject: refactor --- packages/frontend/src/components/MkMediaAudio.vue | 4 ++-- packages/frontend/src/components/MkMediaBanner.vue | 4 ++-- packages/frontend/src/components/MkMediaImage.vue | 6 +++--- packages/frontend/src/components/MkMediaVideo.vue | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) (limited to 'packages/frontend/src/components') diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue index b7052ad918..e3bb39549f 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only @contextmenu.stop @keydown.stop > -