From 455cce801dcf62ff625d77075ddf004308597789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B2=E3=81=9F=E3=82=8A=E3=82=93?= Date: Sat, 28 Sep 2024 17:15:57 +0900 Subject: [PATCH] Merge pull request #25 from lvpq/yami MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit issueのあれをやった --- locales/index.d.ts | 10 ++ locales/ja-JP.yml | 4 + ...3129062-feat-auto-reject-follow-request.js | 11 ++ .../1709421948931-ScgeduledNoteDelete.js | 11 ++ .../backend/src/core/NoteCreateService.ts | 13 ++ packages/backend/src/core/QueueModule.ts | 11 ++ packages/backend/src/core/QueueService.ts | 1 + .../backend/src/core/UserFollowingService.ts | 18 +- .../src/core/entities/NoteEntityService.ts | 7 + .../src/core/entities/UserEntityService.ts | 1 + packages/backend/src/models/Note.ts | 5 + packages/backend/src/models/UserProfile.ts | 5 + .../backend/src/models/json-schema/note.ts | 5 + .../backend/src/queue/QueueProcessorModule.ts | 2 + .../src/queue/QueueProcessorService.ts | 11 ++ packages/backend/src/queue/const.ts | 1 + .../ScheduledNoteDeleteProcessorService.ts | 43 +++++ packages/backend/src/queue/types.ts | 4 + .../src/server/api/endpoints/i/update.ts | 2 + .../src/server/api/endpoints/notes/create.ts | 25 +++ .../src/server/web/ClientServerService.ts | 4 +- .../src/components/MkDeleteScheduleEditor.vue | 165 ++++++++++++++++++ .../src/components/MkNoteDetailed.vue | 1 + .../frontend/src/components/MkNoteHeader.vue | 2 + .../frontend/src/components/MkPostForm.vue | 27 +++ .../frontend/src/pages/settings/privacy.vue | 6 + packages/misskey-js/src/autogen/types.ts | 8 +- 27 files changed, 400 insertions(+), 3 deletions(-) create mode 100644 packages/backend/migration/1697683129062-feat-auto-reject-follow-request.js create mode 100644 packages/backend/migration/1709421948931-ScgeduledNoteDelete.js create mode 100644 packages/backend/src/queue/processors/ScheduledNoteDeleteProcessorService.ts create mode 100644 packages/frontend/src/components/MkDeleteScheduleEditor.vue diff --git a/locales/index.d.ts b/locales/index.d.ts index 177a3c8160..5c84754a1b 100755 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5266,6 +5266,14 @@ export interface Locale extends ILocale { */ "gameRetry": string; /** + * すぐ消す + */ + "scheduledNoteDelete": string; + /** + * このノートは{time}に消去されます + */ + "noteDeletationAt": ParameterizedString<"time">; + /** * 使用しない場合は空欄にしてください */ "notUsePleaseLeaveBlank": string; @@ -5411,6 +5419,8 @@ export interface Locale extends ILocale { "section3": string; }; }; + "autoRejectFollowRequest": string; + "autoRejectFollowRequestDescription": string; "_announcement": { /** * 既存ユーザーのみ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index ac182c6a7f..737bc49a5e 100755 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1337,6 +1337,8 @@ _delivery: manuallySuspended: "手動停止中" goneSuspended: "サーバー削除のため停止中" autoSuspendedForNotResponding: "サーバー応答なしのため停止中" +scheduledNoteDelete: "すぐ消す" +noteDeletationAt: "このノートは{time}に削除されます" _bubbleGame: howToPlay: "遊び方" @@ -1353,6 +1355,8 @@ _bubbleGame: section1: "位置を調整してハコにモノを落とします。" section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。" section3: "モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう!" +autoRejectFollowRequest: "フォローリクエストを自動で拒否する" +autoRejectFollowRequestDescription: "フォローリクエストを自動で拒否するようにします。「フォロー中ユーザーからのフォロリクを自動承認」がONになっている場合は、フォロー中ユーザーからのフォローリクエストは自動的に承認され、それ以外のユーザーからのフォローリクエストは自動的に拒否されるようになります。" _announcement: forExistingUsers: "既存ユーザーのみ" diff --git a/packages/backend/migration/1697683129062-feat-auto-reject-follow-request.js b/packages/backend/migration/1697683129062-feat-auto-reject-follow-request.js new file mode 100644 index 0000000000..a41ea57856 --- /dev/null +++ b/packages/backend/migration/1697683129062-feat-auto-reject-follow-request.js @@ -0,0 +1,11 @@ +export class FeatAutoRejectFollowRequest1697683129062 { + name = 'FeatAutoRejectFollowRequest1697683129062' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "autoRejectFollowRequest" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "autoRejectFollowRequest"`); + } +} diff --git a/packages/backend/migration/1709421948931-ScgeduledNoteDelete.js b/packages/backend/migration/1709421948931-ScgeduledNoteDelete.js new file mode 100644 index 0000000000..92c8984fee --- /dev/null +++ b/packages/backend/migration/1709421948931-ScgeduledNoteDelete.js @@ -0,0 +1,11 @@ +export class ScheduledNoteDelete1709187210308 { + name = 'ScheduledNoteDelete1709187210308' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "deleteAt" TIMESTAMP WITH TIME ZONE`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "deleteAt"`); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index c252336f99..90813cec00 100755 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -62,6 +62,7 @@ import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { Data } from 'ws'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -148,6 +149,7 @@ type Option = { uri?: string | null; url?: string | null; app?: MiApp | null; + deleteAt?: Date | null; }; @Injectable() @@ -439,6 +441,7 @@ export class NoteCreateService implements OnApplicationShutdown { name: data.name, text: data.text, hasPoll: data.poll != null, + deleteAt: data.deleteAt, cw: data.cw ?? null, tags: tags.map(tag => normalizeForSearch(tag)), emojis, @@ -614,6 +617,16 @@ export class NoteCreateService implements OnApplicationShutdown { }); } + if (data.deleteAt) { + const delay = data.deleteAt.getTime() - Date.now(); + this.queueService.scheduledNoteDeleteQueue.add(note.id, { + noteId: note.id + }, { + delay, + removeOnComplete: true, + }); + } + if (!silent) { if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user); diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index b10b8e5899..cb688c4168 100755 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -21,6 +21,7 @@ import type { Provider } from '@nestjs/common'; export type SystemQueue = Bull.Queue>; export type EndedPollNotificationQueue = Bull.Queue; +export type ScheduledNoteDeleteQueue = Bull.Queue export type DeliverQueue = Bull.Queue; export type InboxQueue = Bull.Queue; export type DbQueue = Bull.Queue; @@ -41,6 +42,12 @@ const $endedPollNotification: Provider = { inject: [DI.config], }; +const $scheduledNoteDeleted: Provider = { + provide: 'queue:scheduledNoteDelete', + useFactory: (config: Config) => new Bull.Queue(QUEUE.SCHEDULED_NOTE_DELETE, baseQueueOptions(config, QUEUE.SCHEDULED_NOTE_DELETE)), + inject: [DI.config], +}; + const $deliver: Provider = { provide: 'queue:deliver', useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)), @@ -89,6 +96,7 @@ const $systemWebhookDeliver: Provider = { providers: [ $system, $endedPollNotification, + $scheduledNoteDeleted, $deliver, $inbox, $db, @@ -100,6 +108,7 @@ const $systemWebhookDeliver: Provider = { exports: [ $system, $endedPollNotification, + $scheduledNoteDeleted, $deliver, $inbox, $db, @@ -113,6 +122,7 @@ export class QueueModule implements OnApplicationShutdown { constructor( @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @@ -129,6 +139,7 @@ export class QueueModule implements OnApplicationShutdown { await Promise.all([ this.systemQueue.close(), this.endedPollNotificationQueue.close(), + this.scheduledNoteDeleteQueue.close(), this.deliverQueue.close(), this.inboxQueue.close(), this.dbQueue.close(), diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index be5f10771a..5080cf908d 100755 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -45,6 +45,7 @@ export class QueueService { @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 6aab8fde70..8cb3c7e720 100755 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -214,6 +214,20 @@ export class UserFollowingService implements OnModuleInit { } if (!autoAccept) { + // autoAcceptが無効かつautoRejectが有効な場合はフォローリクエストを拒否する + if (this.userEntityService.isLocalUser(followee) && followeeProfile.autoRejectFollowRequest) { + if (this.userEntityService.isRemoteUser(follower)) { + // リモートからならRejectを返す + const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee)); + this.queueService.deliver(followee, content, follower.inbox, false); + } + + // ローカルユーザーに対しては敢えてpublishUnfollowせずにお茶を濁す + // フォローできない不具合と勘違いされたりフォローリクエストを連打される可能性があるため + + return; + } + await this.createFollowRequest(follower, followee, requestId, withReplies); return; } @@ -579,7 +593,9 @@ export class UserFollowingService implements OnModuleInit { }); if (!requestExist) { - throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); + // throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); + // 本来ならエラーを返すが、フォローリクエストの自動拒否機能の関係上、エラーを返さずに無視する + return; } await this.followRequestsRepository.delete({ diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 493723ac45..7b5d46e41c 100755 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -395,6 +395,13 @@ export class NoteEntityService implements OnModuleInit { withReactionAndUserPairCache: opts.withReactionAndUserPairCache, _hint_: options?._hint_, }) : undefined, + + poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, + deleteAt: note.deleteAt?.toISOString() ?? undefined, + + ...(meId && Object.keys(note.reactions).length > 0 ? { + myReaction: this.populateMyReaction(note, meId, options?._hint_), + } : {}), } : {}), }); diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 433a72cef0..a83c42ba58 100755 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -620,6 +620,7 @@ export class UserEntityService implements OnModuleInit { autoSensitive: profile!.autoSensitive, carefulBot: profile!.carefulBot, autoAcceptFollowed: profile!.autoAcceptFollowed, + autoRejectFollowRequest: profile!.autoRejectFollowRequest, noCrawle: profile!.noCrawle, preventAiLearning: profile!.preventAiLearning, isExplorable: user.isExplorable, diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index b11e2ec62b..1b9c457d3f 100755 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -188,6 +188,11 @@ export class MiNote { }) public hasPoll: boolean; + @Column('timestamp with time zone', { + nullable: true, + }) + public deleteAt: Date | null; + @Index() @Column({ ...id(), diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 40ea26f610..c6bf2bc017 100755 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -172,6 +172,11 @@ export class MiUserProfile { }) public autoAcceptFollowed: boolean; + @Column('boolean', { + default: false, + }) + public autoRejectFollowRequest: boolean; + @Column('boolean', { default: false, comment: 'Whether reject index by crawler.', diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index 432c096e48..65cedbcf3a 100755 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -152,6 +152,11 @@ export const packedNoteSchema = { }, }, }, + deleteAt: { + type: 'string', + optional: true, nullable: true, + format: 'date-time', + }, emojis: { type: 'object', optional: true, nullable: false, diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 7daca687a1..6ca2eb5a50 100755 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -41,6 +41,7 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js'; import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js'; +import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteDeleteProcessorService.js'; @Module({ imports: [ @@ -79,6 +80,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor UserWebhookDeliverProcessorService, SystemWebhookDeliverProcessorService, EndedPollNotificationProcessorService, + ScheduledNoteDeleteProcessorService, DeliverProcessorService, InboxProcessorService, AggregateRetentionProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 7a6169bf9c..1294e92016 100755 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -45,6 +45,7 @@ import { AggregateRetentionProcessorService } from './processors/AggregateRetent import { QueueLoggerService } from './QueueLoggerService.js'; import { QUEUE, baseQueueOptions } from './const.js'; import { ImportNotesProcessorService } from './processors/ImportNotesProcessorService.js'; +import { ScheduledNoteDeleteProcessorService } from './processors/ScheduledNoteDeleteProcessorService.js'; // ref. https://github.com/misskey-dev/misskey/pull/7635#issue-971097019 function httpRelatedBackoff(attemptsMade: number) { @@ -84,6 +85,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private relationshipQueueWorker: Bull.Worker; private objectStorageQueueWorker: Bull.Worker; private endedPollNotificationQueueWorker: Bull.Worker; + private scheduledNoteDeleteQueueWorker: Bull.Worker; constructor( @Inject(DI.config) @@ -93,6 +95,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService, private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, + private scheduledNoteDeleteProcessorService: ScheduledNoteDeleteProcessorService, private deliverProcessorService: DeliverProcessorService, private inboxProcessorService: InboxProcessorService, private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, @@ -513,6 +516,12 @@ export class QueueProcessorService implements OnApplicationShutdown { }); } //#endregion + + //#region scheduled note delete + this.scheduledNoteDeleteQueueWorker = new Bull.Worker(QUEUE.SCHEDULED_NOTE_DELETE, (job) => this.scheduledNoteDeleteProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.SCHEDULED_NOTE_DELETE), + autorun: false, + }); } @bindThis @@ -527,6 +536,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker.run(), this.objectStorageQueueWorker.run(), this.endedPollNotificationQueueWorker.run(), + this.scheduledNoteDeleteQueueWorker.run(), ]); } @@ -542,6 +552,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker.close(), this.objectStorageQueueWorker.close(), this.endedPollNotificationQueueWorker.close(), + this.scheduledNoteDeleteQueueWorker.close(), ]); } diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts index 67f689b618..ee24c5842c 100755 --- a/packages/backend/src/queue/const.ts +++ b/packages/backend/src/queue/const.ts @@ -11,6 +11,7 @@ export const QUEUE = { INBOX: 'inbox', SYSTEM: 'system', ENDED_POLL_NOTIFICATION: 'endedPollNotification', + SCHEDULED_NOTE_DELETE: 'scheduledNoteDelete', DB: 'db', RELATIONSHIP: 'relationship', OBJECT_STORAGE: 'objectStorage', diff --git a/packages/backend/src/queue/processors/ScheduledNoteDeleteProcessorService.ts b/packages/backend/src/queue/processors/ScheduledNoteDeleteProcessorService.ts new file mode 100644 index 0000000000..6809eb8d8d --- /dev/null +++ b/packages/backend/src/queue/processors/ScheduledNoteDeleteProcessorService.ts @@ -0,0 +1,43 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { NotesRepository, UsersRepository } from '@/models/_.js'; +import type Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; +import { NoteDeleteService } from '@/core/NoteDeleteService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { ScheduledNoteDeleteJobData } from '../types.js'; + +@Injectable() +export class ScheduledNoteDeleteProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private noteDeleteService: NoteDeleteService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('scheduled-note-delete'); + } + + @bindThis + public async process(job: Bull.Job): Promise { + const note = await this.notesRepository.findOneBy({ id: job.data.noteId }); + if (note == null) { + return; + } + + const user = await this.usersRepository.findOneBy({ id: note.userId }); + if (user == null) { + return; + } + + await this.noteDeleteService.delete(user, note); + this.logger.info(`Deleted note ${note.id}`); + } +} diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index c0d246ebbc..a954818128 100755 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -131,6 +131,10 @@ export type EndedPollNotificationJobData = { noteId: MiNote['id']; }; +export type ScheduledNoteDeleteJobData = { + noteId: MiNote['id']; +} + export type SystemWebhookDeliverJobData = { type: string; content: unknown; diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 6cc22e7994..44affc2e00 100755 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -183,6 +183,7 @@ export const paramDef = { publicReactions: { type: 'boolean' }, carefulBot: { type: 'boolean' }, autoAcceptFollowed: { type: 'boolean' }, + autoRejectFollowRequest: { type: 'boolean' }, noCrawle: { type: 'boolean' }, preventAiLearning: { type: 'boolean' }, noindex: { type: 'boolean' }, @@ -337,6 +338,7 @@ export default class extends Endpoint { // eslint- if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; + if (typeof ps.autoRejectFollowRequest === 'boolean') profileUpdates.autoRejectFollowRequest = ps.autoRejectFollowRequest; if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; if (typeof ps.speakAsCat === 'boolean') updates.speakAsCat = ps.speakAsCat; diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 626f03b758..1d55cfe646 100755 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -104,6 +104,12 @@ export const meta = { id: '04da457d-b083-4055-9082-955525eda5a5', }, + cannotScheduleDeleteEarlierThanNow: { + message: 'Scheduled delete time is earlier than now.', + code: 'CANNOT_SCHEDULE_DELETE_EARLIER_THAN_NOW', + id: '9576c3c8-d8f3-11ee-ac15-00155d19d35d', + }, + noSuchChannel: { message: 'No such channel.', code: 'NO_SUCH_CHANNEL', @@ -197,6 +203,14 @@ export const paramDef = { }, required: ['choices'], }, + scheduledDelete: { + type: 'object', + nullable: true, + properties: { + deleteAt: { type: 'integer', nullable: true }, + deleteAfter: { type: 'integer', nullable: true, minimum: 1 }, + }, + }, }, // (re)note with text, files and poll are optional if: { @@ -365,6 +379,16 @@ export default class extends Endpoint { // eslint- } } + if (ps.scheduledDelete) { + if (typeof ps.scheduledDelete.deleteAt === 'number') { + if (ps.scheduledDelete.deleteAt < Date.now()) { + throw new ApiError(meta.errors.cannotScheduleDeleteEarlierThanNow); + } + } else if (typeof ps.scheduledDelete.deleteAfter === 'number') { + ps.scheduledDelete.deleteAt = Date.now() + ps.scheduledDelete.deleteAfter; + } + } + let channel: MiChannel | null = null; if (ps.channelId != null) { channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false }); @@ -396,6 +420,7 @@ export default class extends Endpoint { // eslint- apMentions: ps.noExtractMentions ? [] : undefined, apHashtags: ps.noExtractHashtags ? [] : undefined, apEmojis: ps.noExtractEmojis ? [] : undefined, + deleteAt: ps.scheduledDelete?.deleteAt ? new Date(ps.scheduledDelete.deleteAt) : null, }); return { diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 0af90a844b..70b046b1de 100755 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -30,7 +30,7 @@ import type { DeliverQueue, EndedPollNotificationQueue, InboxQueue, - ObjectStorageQueue, + ObjectStorageQueue, ScheduledNoteDeleteQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue, @@ -116,6 +116,7 @@ export class ClientServerService { @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:scheduledNoteDelete') public scheduledNoteDeleteQueue: ScheduledNoteDeleteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @@ -248,6 +249,7 @@ export class ClientServerService { queues: [ this.systemQueue, this.endedPollNotificationQueue, + this.scheduledNoteDeleteQueue, this.deliverQueue, this.inboxQueue, this.dbQueue, diff --git a/packages/frontend/src/components/MkDeleteScheduleEditor.vue b/packages/frontend/src/components/MkDeleteScheduleEditor.vue new file mode 100644 index 0000000000..352b02145c --- /dev/null +++ b/packages/frontend/src/components/MkDeleteScheduleEditor.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 83bfd12205..f5dfeeb509 100755 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -116,6 +116,7 @@ SPDX-License-Identifier: AGPL-3.0-only + {{ i18n.ts.scheduledNoteDelete }}: + @@ -110,6 +112,7 @@ import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkNotePreview from '@/components/MkNotePreview.vue'; import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue'; +import MkDeleteScheduleEditor, { type DeleteScheduleEditorModelValue } from '@/components/MkDeleteScheduleEditor.vue'; import { host, url } from '@/config.js'; import { erase, unique } from '@/scripts/array.js'; import { extractMentions } from '@/scripts/extract-mentions.js'; @@ -182,6 +185,7 @@ const posted = ref(false); const text = ref(props.initialText ?? ''); const files = ref(props.initialFiles ?? []); const poll = ref(null); +const scheduledNoteDelete = ref(null); const useCw = ref(!!props.initialCw); const showPreview = ref(defaultStore.state.showPreview); watch(showPreview, () => defaultStore.set('showPreview', showPreview.value)); @@ -366,6 +370,7 @@ function watchForDraft() { watch(useCw, () => saveDraft()); watch(cw, () => saveDraft()); watch(poll, () => saveDraft()); + watch(scheduledNoteDelete, () => saveDraft()); watch(files, () => saveDraft(), { deep: true }); watch(visibility, () => saveDraft()); watch(localOnly, () => saveDraft()); @@ -421,6 +426,17 @@ function togglePoll() { } } +function toggleScheduledNoteDelete() { + if (scheduledNoteDelete.value) { + scheduledNoteDelete.value = null; + } else { + scheduledNoteDelete.value = { + deleteAt: null, + deleteAfter: null, + }; + } +} + function addTag(tag: string) { insertTextAtCursor(textareaEl.value, ` #${tag} `); } @@ -718,6 +734,7 @@ function saveDraft() { visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined, quoteId: quoteId.value, reactionAcceptance: reactionAcceptance.value, + scheduledNoteDelete: scheduledNoteDelete.value, }, }; @@ -794,6 +811,7 @@ async function post(ev?: MouseEvent) { renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined, channelId: props.channel ? props.channel.id : undefined, poll: poll.value, + scheduledDelete: scheduledNoteDelete.value, cw: useCw.value ? cw.value ?? '' : null, localOnly: localOnly.value, visibility: visibility.value, @@ -1029,6 +1047,9 @@ onMounted(() => { } quoteId.value = draft.data.quoteId; reactionAcceptance.value = draft.data.reactionAcceptance; + if (draft.data.scheduledNoteDelete) { + scheduledNoteDelete.value = draft.data.scheduledNoteDelete; + } } } @@ -1049,6 +1070,12 @@ onMounted(() => { expiredAfter: null, }; } + if (init.deleteAt) { + scheduledNoteDelete.value = { + deleteAt: init.deleteAt ? (new Date(init.deleteAt)).getTime() : null, + deleteAfter: null, + }; + } if (init.visibleUserIds) { misskeyApi('users/show', { userIds: init.visibleUserIds }).then(users => { users.forEach(u => pushVisibleUser(u)); diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index b155d6e316..66078ff6b5 100755 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -7,6 +7,10 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.makeFollowManuallyApprove }} {{ i18n.ts.autoAcceptFollowed }} + + {{ i18n.ts.autoRejectFollowRequest }}{{ i18n.ts.originalFeature }} + + {{ i18n.ts.makeReactionsPublic }} @@ -87,6 +91,7 @@ const $i = signinRequired(); const isLocked = ref($i.isLocked); const autoAcceptFollowed = ref($i.autoAcceptFollowed); +const autoRejectFollowRequest = ref($i.autoRejectFollowRequest); const noCrawle = ref($i.noCrawle); const noindex = ref($i.noindex); const isExplorable = ref($i.isExplorable); @@ -104,6 +109,7 @@ function save() { misskeyApi('i/update', { isLocked: !!isLocked.value, autoAcceptFollowed: !!autoAcceptFollowed.value, + autoRejectFollowRequest: !!autoRejectFollowRequest, noCrawle: !!noCrawle.value, noindex: !!noindex.value, isExplorable: !!isExplorable.value, diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 562c498190..b443da4882 100755 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4145,7 +4145,7 @@ export type components = { /** @enum {string} */ icon: 'info' | 'warning' | 'error' | 'success'; /** @enum {string} */ - display: 'dialog' | 'normal' | 'banner'; + display: 'dialog' | 'normal' | 'banner' | 'emergency'; needConfirmationToRead: boolean; silence: boolean; forYou: boolean; @@ -4204,6 +4204,8 @@ export type components = { votes: number; }[]; }) | null; + /** Format: date-time */ + deleteAt?: string | null; emojis?: { [key: string]: string; }; @@ -21897,6 +21899,10 @@ export type operations = { expiresAt?: number | null; expiredAfter?: number | null; }) | null; + scheduledDelete?: ({ + deleteAt?: number | null; + deleteAfter?: number | null; + }) | null; }; }; };