diff --git a/locales/en-US.yml b/locales/en-US.yml index dfe066af99..1583286ada 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -99,6 +99,7 @@ serverIsDead: "This server is not responding. Please wait for a while and try ag youShouldUpgradeClient: "To view this page, please refresh to update your client." enterListName: "Enter a name for the list" privacy: "Privacy" +autoDeleteNotes: "Self-destructing notes" makeFollowManuallyApprove: "Follow requests require approval" defaultNoteVisibility: "Default visibility" follow: "Follow" diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 103813acf2..96b52c33b4 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -69,6 +69,12 @@ export class QueueService { repeat: { pattern: '*/5 * * * *' }, removeOnComplete: true, }); + + this.systemQueue.add('autoDeleteNotes', { + }, { + repeat: { pattern: '*/5 * * * *' }, + removeOnComplete: true, + }); } @bindThis diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index d7316e19e3..593e1d4b2f 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -13,6 +13,7 @@ import { EndedPollNotificationProcessorService } from './processors/EndedPollNot import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { WebhookDeliverProcessorService } from './processors/WebhookDeliverProcessorService.js'; import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; +import { AutoDeleteNotesProcessorService } from './processors/AutoDeleteNotesProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; @@ -52,6 +53,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor ResyncChartsProcessorService, CleanChartsProcessorService, CheckExpiredMutingsProcessorService, + AutoDeleteNotesProcessorService, CleanProcessorService, DeleteDriveFilesProcessorService, ExportAccountDataProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 76b6d7fb05..d8f39dd5c4 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -38,6 +38,7 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js'; +import { AutoDeleteNotesProcessorService } from './processors/AutoDeleteNotesProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; @@ -118,6 +119,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private cleanChartsProcessorService: CleanChartsProcessorService, private aggregateRetentionProcessorService: AggregateRetentionProcessorService, private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService, + private autoDeleteNotesProcessorService: AutoDeleteNotesProcessorService, private cleanProcessorService: CleanProcessorService, ) { this.logger = this.queueLoggerService.logger; @@ -146,6 +148,7 @@ export class QueueProcessorService implements OnApplicationShutdown { case 'cleanCharts': return this.cleanChartsProcessorService.process(); case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); + case 'autoDeleteNotes': return this.autoDeleteNotesProcessorService.process(); case 'clean': return this.cleanProcessorService.process(); default: throw new Error(`unrecognized job type ${job.name} for system`); } diff --git a/packages/backend/src/queue/processors/AutoDeleteNotesProcessorService.ts b/packages/backend/src/queue/processors/AutoDeleteNotesProcessorService.ts new file mode 100644 index 0000000000..c3a057c3b2 --- /dev/null +++ b/packages/backend/src/queue/processors/AutoDeleteNotesProcessorService.ts @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { UserProfilesRepository, NotesRepository } from '@/models/_.js'; +import type Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; +import { NoteDeleteService } from '@/core/NoteDeleteService.js'; +import { IdService } from '@/core/IdService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; + +@Injectable() +export class AutoDeleteNotesProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + @Inject(DI.userNotePiningsRepository) + private userNotePiningsRepository: UserNotePiningsRepository, + + private idService: IdService, + private noteDeleteService: NoteDeleteService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('auto-delete-notes'); + } + + @bindThis + public async process(): Promise { + this.logger.info('Auto deleting old notes...'); + + const userProfiles = await this.userProfilesRepository.createQueryBuilder('user_profile') + .innerJoinAndSelect('user_profile.user', 'user') + .where('user.host IS NULL') + .andWhere('user_profile.autoDeleteNotes') + .getMany(); + + for (const userProfile of userProfiles) { + const user = userProfile.user; + this.logger.debug(`Deleting old notes of user @${user.username} (id ${user.id})`); + const untilTime = Date.now() - (userProfile.autoDeleteNotesMinutes * 1000 * 60); + const untilId = this.idService.gen(untilTime); + + const pins = await this.userNotePiningsRepository.createQueryBuilder('user_note_pining') + .where('"userId" = :userId', { userId: user.id }) + .getMany(); + + const pinnedNoteIds = pins.map((p) => p.noteId); + + const notes = await this.notesRepository.createQueryBuilder('note') + .where('note."userId" = :userId', { userId: user.id }) + .andWhere('note.id < :untilId', { untilId }) + .andWhere('note.id NOT IN (SELECT "noteId" FROM note_favorite WHERE "userId" = :userId)') + .getMany(); + + for (const note of notes) { + if (pinnedNoteIds.includes(note.id)) { + this.logger.debug(`Skipping note ${note.id} as it is pinned`); + continue; + } + + this.logger.debug(`Deleting note ${note.id}`); + await this.noteDeleteService.delete(user, note, false, user); + } + } + + this.logger.succ('Done with note auto-delete'); + } +} diff --git a/packages/frontend/src/pages/settings/privacy.autodelete.vue b/packages/frontend/src/pages/settings/privacy.autodelete.vue new file mode 100644 index 0000000000..cec6da21d0 --- /dev/null +++ b/packages/frontend/src/pages/settings/privacy.autodelete.vue @@ -0,0 +1,66 @@ + + + diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index 86cf5ab241..36f3319871 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -68,6 +68,15 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.keepCw }} + + +
+ + + + +
+
@@ -82,6 +91,7 @@ import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { signinRequired } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import XAutoDelete from './privacy.autodelete.vue'; const $i = signinRequired();