summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-08-01 11:49:12 +0900
committerGitHub <noreply@github.com>2025-08-01 11:49:12 +0900
commitd624da9c1aac731bd49a7bbb949744ebf4986479 (patch)
tree94306c63e3452e77fcf100b5a61eb243705463e4
parentenhance(frontend): サーバーの初期設定ウィザードをやり直せ... (diff)
downloadmisskey-d624da9c1aac731bd49a7bbb949744ebf4986479.tar.gz
misskey-d624da9c1aac731bd49a7bbb949744ebf4986479.tar.bz2
misskey-d624da9c1aac731bd49a7bbb949744ebf4986479.zip
feat: remote notes cleaning (#16292)
* 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
-rw-r--r--CHANGELOG.md3
-rw-r--r--locales/index.d.ts32
-rw-r--r--locales/ja-JP.yml8
-rw-r--r--packages/backend/migration/1753863104203-remoteNotesCleaning.js20
-rw-r--r--packages/backend/src/core/QueueService.ts4
-rw-r--r--packages/backend/src/models/Meta.ts15
-rw-r--r--packages/backend/src/queue/QueueProcessorModule.ts4
-rw-r--r--packages/backend/src/queue/QueueProcessorService.ts3
-rw-r--r--packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts174
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts15
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts15
-rw-r--r--packages/frontend/src/components/MkServerSetupWizard.vue15
-rw-r--r--packages/frontend/src/pages/admin/job-queue.job.vue4
-rw-r--r--packages/frontend/src/pages/admin/performance.vue42
-rw-r--r--packages/misskey-js/src/autogen/types.ts6
15 files changed, 356 insertions, 4 deletions
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 {
* 低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。
*/
"defaultImageCompressionLevel_description": string;
+ /**
+ * 分
+ */
+ "inMinutes": string;
+ /**
+ * 日
+ */
+ "inDays": string;
"_order": {
/**
* 新しい順
@@ -6487,6 +6495,22 @@ export interface Locale extends ILocale {
*/
"reactionsBufferingDescription": string;
/**
+ * リモート投稿の自動クリーニング
+ */
+ "remoteNotesCleaning": string;
+ /**
+ * 有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。
+ */
+ "remoteNotesCleaning_description": string;
+ /**
+ * 最大クリーニング処理継続時間
+ */
+ "remoteNotesCleaningMaxProcessingDuration": string;
+ /**
+ * 最低ノート保持日数
+ */
+ "remoteNotesCleaningExpiryDaysForEachNotes": string;
+ /**
* 問い合わせ先URL
*/
"inquiryUrl": string;
@@ -11952,6 +11976,14 @@ export interface Locale extends ILocale {
*/
"youCanConfigureMoreFederationSettingsLater": string;
/**
+ * 受信コンテンツの自動クリーニング
+ */
+ "remoteContentsCleaning": string;
+ /**
+ * 連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。
+ */
+ "remoteContentsCleaning_description": string;
+ /**
* 管理者情報
*/
"adminInfo": 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: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
+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<Record<string, unknown>>): 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<MiNote, 'id'>[] = 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> { // 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
<template #icon><i class="ti ti-planet"></i></template>
<div class="_gaps_s">
- <div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}</div>
+ <div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}<br><MkLink target="_blank" url="https://wikipedia.org/wiki/Fediverse">{{ i18n.ts.learnMore }}</MkLink></div>
<MkRadios v-model="q_federation" :vertical="true">
<option value="yes">{{ i18n.ts.yes }}</option>
@@ -63,6 +63,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkRadios>
<MkInfo v-if="q_federation === 'yes'">{{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }}</MkInfo>
+
+ <MkSwitch v-if="q_federation === 'yes'" v-model="q_remoteContentsCleaning">
+ <template #label>{{ i18n.ts._serverSetupWizard.remoteContentsCleaning }}</template>
+ <template #caption>{{ i18n.ts._serverSetupWizard.remoteContentsCleaning_description }}</template>
+ </MkSwitch>
</div>
</MkFolder>
@@ -111,6 +116,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>{{ serverSettings.federation === 'none' ? i18n.ts.no : i18n.ts.all }}</div>
</div>
<div>
+ <div><b>{{ i18n.ts._serverSettings.remoteNotesCleaning }}:</b></div>
+ <div>{{ serverSettings.enableRemoteNotesCleaning ? i18n.ts.yes : i18n.ts.no }}</div>
+ </div>
+ <div>
<div><b>FTT:</b></div>
<div>{{ serverSettings.enableFanoutTimeline ? i18n.ts.yes : i18n.ts.no }}</div>
</div>
@@ -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<Misskey.entities.AdminUpdateMetaRequest>(() => {
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
</MkKeyValue>
<MkKeyValue v-if="job.progress != null && typeof job.progress === 'number' && job.progress > 0">
<template #key>Progress</template>
- <template #value>{{ Math.floor(job.progress * 100) }}%</template>
+ <template #value>{{ Math.floor(job.progress) }}%</template>
</MkKeyValue>
</div>
<MkFolder :withSpacer="false">
@@ -150,7 +150,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton><i class="ti ti-device-floppy"></i> Update</MkButton>
</div>
<div v-else-if="tab === 'result'">
- <MkCode :code="String(job.returnValue)"/>
+ <MkCode :code="JSON5.stringify(job.returnValue, null, '\t')" lang="json5"/>
</div>
<div v-else-if="tab === 'error'" class="_gaps_s">
<MkCode v-for="log in job.stacktrace" :code="log" lang="stacktrace"/>
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
</MkSwitch>
</div>
</MkFolder>
+
+ <MkFolder :defaultOpen="true">
+ <template #icon><i class="ti ti-recycle"></i></template>
+ <template #label>Remote Notes Cleaning (仮)</template>
+ <template v-if="remoteNotesCleaningForm.savedState.enableRemoteNotesCleaning" #suffix>Enabled</template>
+ <template v-else #suffix>Disabled</template>
+ <template v-if="remoteNotesCleaningForm.modified.value" #footer>
+ <MkFormFooter :form="remoteNotesCleaningForm"/>
+ </template>
+
+ <div class="_gaps_m">
+ <MkSwitch v-model="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
+ <template #label>{{ i18n.ts.enable }}<span v-if="remoteNotesCleaningForm.modifiedStates.enableRemoteNotesCleaning" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #caption>{{ i18n.ts._serverSettings.remoteNotesCleaning_description }}</template>
+ </MkSwitch>
+
+ <template v-if="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
+ <MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningExpiryDaysForEachNotes" type="number">
+ <template #label>{{ i18n.ts._serverSettings.remoteNotesCleaningExpiryDaysForEachNotes }} ({{ i18n.ts.inDays }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningExpiryDaysForEachNotes" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #suffix>{{ i18n.ts._time.day }}</template>
+ </MkInput>
+
+ <MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningMaxProcessingDurationInMinutes" type="number">
+ <template #label>{{ i18n.ts._serverSettings.remoteNotesCleaningMaxProcessingDuration }} ({{ i18n.ts.inMinutes }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningMaxProcessingDurationInMinutes" class="_modified">{{ i18n.ts.modified }}</span></template>
+ <template #suffix>{{ i18n.ts._time.minute }}</template>
+ </MkInput>
+ </template>
+ </div>
+ </MkFolder>
</div>
</div>
</PageWithHeader>
@@ -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;
};
};
};