From ccb78b3bfc1a28009b5dd6175d2ca7a63c2c9406 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:07:20 +0900 Subject: [PATCH 01/53] Update NoteDraft.ts --- packages/backend/src/models/NoteDraft.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/backend/src/models/NoteDraft.ts b/packages/backend/src/models/NoteDraft.ts index 6483748bc2..03bbb7a194 100644 --- a/packages/backend/src/models/NoteDraft.ts +++ b/packages/backend/src/models/NoteDraft.ts @@ -153,6 +153,12 @@ export class MiNoteDraft { // ここまで追加 + // 予約投稿 + @Column('timestamp with time zone', { + nullable: true, + }) + public scheduledAt: Date | null; + constructor(data: Partial) { if (data == null) return; From 9dba657ffa42e07e8d84858950af04d4f310a4af Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:13:54 +0900 Subject: [PATCH 02/53] Update NoteDraft.ts --- packages/backend/src/models/NoteDraft.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/models/NoteDraft.ts b/packages/backend/src/models/NoteDraft.ts index 03bbb7a194..4f07f4a8be 100644 --- a/packages/backend/src/models/NoteDraft.ts +++ b/packages/backend/src/models/NoteDraft.ts @@ -126,7 +126,7 @@ export class MiNoteDraft { @JoinColumn() public channel: MiChannel | null; - // 以下、Pollについて追加 + //#region 以下、Pollについて追加 @Column('boolean', { default: false, @@ -151,7 +151,7 @@ export class MiNoteDraft { }) public pollExpiredAfter: number | null; - // ここまで追加 + //#endregion // 予約投稿 @Column('timestamp with time zone', { From bf2cb2d0b7bc50d4bb7573d5599e85d0e33bc17f Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:31:02 +0900 Subject: [PATCH 03/53] wip --- packages/backend/src/core/NoteDraftService.ts | 20 +++++++++ packages/backend/src/core/QueueModule.ts | 12 ++++++ packages/backend/src/core/QueueService.ts | 4 ++ .../backend/src/queue/QueueProcessorModule.ts | 2 + .../src/queue/QueueProcessorService.ts | 20 +++++++++ packages/backend/src/queue/const.ts | 1 + .../PostScheduledNoteProcessorService.ts | 41 +++++++++++++++++++ packages/backend/src/queue/types.ts | 4 ++ .../server/api/endpoints/admin/queue/stats.ts | 3 +- packages/misskey-js/src/consts.ts | 1 + 10 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts diff --git a/packages/backend/src/core/NoteDraftService.ts b/packages/backend/src/core/NoteDraftService.ts index c43be96efa..9274b18570 100644 --- a/packages/backend/src/core/NoteDraftService.ts +++ b/packages/backend/src/core/NoteDraftService.ts @@ -16,6 +16,7 @@ import { IPoll } from '@/models/Poll.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { isRenote, isQuote } from '@/misc/is-renote.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { QueueService } from '@/core/QueueService.js'; export type NoteDraftOptions = { replyId?: MiNote['id'] | null; @@ -56,6 +57,7 @@ export class NoteDraftService { private roleService: RoleService, private idService: IdService, private noteEntityService: NoteEntityService, + private queueService: QueueService, ) { } @@ -311,4 +313,22 @@ export class NoteDraftService { return appliedDraft; } + + @bindThis + public async schedule(draft: MiNoteDraft, scheduledAt: Date): Promise { + const delay = scheduledAt.getTime() - Date.now(); + this.queueService.deleteUserMutingQueue.add(draft.id, { + noteDraftId: draft.id, + }, { + delay, + removeOnComplete: { + age: 3600 * 24 * 7, // keep up to 7 days + count: 30, + }, + removeOnFail: { + age: 3600 * 24 * 7, // keep up to 7 days + count: 100, + }, + }); + } } diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index b10b8e5899..ecd96261e0 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -16,11 +16,13 @@ import { RelationshipJobData, UserWebhookDeliverJobData, SystemWebhookDeliverJobData, + PostScheduledNoteJobData, } from '../queue/types.js'; import type { Provider } from '@nestjs/common'; export type SystemQueue = Bull.Queue>; export type EndedPollNotificationQueue = Bull.Queue; +export type PostScheduledNoteQueue = Bull.Queue; export type DeliverQueue = Bull.Queue; export type InboxQueue = Bull.Queue; export type DbQueue = Bull.Queue; @@ -41,6 +43,12 @@ const $endedPollNotification: Provider = { inject: [DI.config], }; +const $postScheduledNote: Provider = { + provide: 'queue:postScheduledNote', + useFactory: (config: Config) => new Bull.Queue(QUEUE.POST_SCHEDULED_NOTE, baseQueueOptions(config, QUEUE.POST_SCHEDULED_NOTE)), + inject: [DI.config], +}; + const $deliver: Provider = { provide: 'queue:deliver', useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)), @@ -89,6 +97,7 @@ const $systemWebhookDeliver: Provider = { providers: [ $system, $endedPollNotification, + $postScheduledNote, $deliver, $inbox, $db, @@ -100,6 +109,7 @@ const $systemWebhookDeliver: Provider = { exports: [ $system, $endedPollNotification, + $postScheduledNote, $deliver, $inbox, $db, @@ -113,6 +123,7 @@ export class QueueModule implements OnApplicationShutdown { constructor( @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @@ -129,6 +140,7 @@ export class QueueModule implements OnApplicationShutdown { await Promise.all([ this.systemQueue.close(), this.endedPollNotificationQueue.close(), + this.postScheduledNoteQueue.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 2d0e7b5d83..42782167bb 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -31,6 +31,7 @@ import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, + PostScheduledNoteQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, @@ -44,6 +45,7 @@ import type * as Bull from 'bullmq'; export const QUEUE_TYPES = [ 'system', 'endedPollNotification', + 'postScheduledNote', 'deliver', 'inbox', 'db', @@ -92,6 +94,7 @@ export class QueueService { @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @@ -717,6 +720,7 @@ export class QueueService { switch (type) { case 'system': return this.systemQueue; case 'endedPollNotification': return this.endedPollNotificationQueue; + case 'postScheduledNote': return this.postScheduledNoteQueue; case 'deliver': return this.deliverQueue; case 'inbox': return this.inboxQueue; case 'db': return this.dbQueue; diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index e01414cd53..e64882c4df 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -10,6 +10,7 @@ import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueProcessorService } from './QueueProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; +import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; @@ -79,6 +80,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor UserWebhookDeliverProcessorService, SystemWebhookDeliverProcessorService, EndedPollNotificationProcessorService, + PostScheduledNoteProcessorService, DeliverProcessorService, InboxProcessorService, AggregateRetentionProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 7b64182754..642d3fc8ad 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -14,6 +14,7 @@ import { CheckModeratorsActivityProcessorService } from '@/queue/processors/Chec import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; +import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; @@ -85,6 +86,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private relationshipQueueWorker: Bull.Worker; private objectStorageQueueWorker: Bull.Worker; private endedPollNotificationQueueWorker: Bull.Worker; + private postScheduledNoteQueueWorker: Bull.Worker; constructor( @Inject(DI.config) @@ -94,6 +96,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService, private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, + private postScheduledNoteProcessorService: PostScheduledNoteProcessorService, private deliverProcessorService: DeliverProcessorService, private inboxProcessorService: InboxProcessorService, private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, @@ -520,6 +523,21 @@ export class QueueProcessorService implements OnApplicationShutdown { }); } //#endregion + + //#region post scheduled note + { + this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, async (job) => { + if (this.config.sentryForBackend) { + return Sentry.startSpan({ name: 'Queue: PostScheduledNote' }, () => this.postScheduledNoteProcessorService.process(job)); + } else { + return this.postScheduledNoteProcessorService.process(job); + } + }, { + ...baseWorkerOptions(this.config, QUEUE.POST_SCHEDULED_NOTE), + autorun: false, + }); + } + //#endregion } @bindThis @@ -534,6 +552,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker.run(), this.objectStorageQueueWorker.run(), this.endedPollNotificationQueueWorker.run(), + this.postScheduledNoteQueueWorker.run(), ]); } @@ -549,6 +568,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker.close(), this.objectStorageQueueWorker.close(), this.endedPollNotificationQueueWorker.close(), + this.postScheduledNoteQueueWorker.close(), ]); } diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts index 7e146a7e03..625204b7ad 100644 --- a/packages/backend/src/queue/const.ts +++ b/packages/backend/src/queue/const.ts @@ -12,6 +12,7 @@ export const QUEUE = { INBOX: 'inbox', SYSTEM: 'system', ENDED_POLL_NOTIFICATION: 'endedPollNotification', + POST_SCHEDULED_NOTE: 'postScheduledNote', DB: 'db', RELATIONSHIP: 'relationship', OBJECT_STORAGE: 'objectStorage', diff --git a/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts new file mode 100644 index 0000000000..f29ddd7161 --- /dev/null +++ b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { NoteDraftsRepository } from '@/models/_.js'; +import type Logger from '@/logger.js'; +import { NotificationService } from '@/core/NotificationService.js'; +import { bindThis } from '@/decorators.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { PostScheduledNoteJobData } from '../types.js'; + +@Injectable() +export class PostScheduledNoteProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.noteDraftsRepository) + private noteDraftsRepository: NoteDraftsRepository, + + private notificationService: NotificationService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('post-scheduled-note'); + } + + @bindThis + public async process(job: Bull.Job): Promise { + const draft = await this.noteDraftsRepository.findOneBy({ id: job.data.noteDraftId }); + if (draft == null) { + return; + } + + this.notificationService.createNotification(draft.userId, 'scheduledNotePosted', { + noteId: note.id, + }); + } +} diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 757daea88b..1cb2b93918 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -109,6 +109,10 @@ export type EndedPollNotificationJobData = { noteId: MiNote['id']; }; +export type PostScheduledNoteJobData = { + noteDraftId: string; +}; + export type SystemWebhookDeliverJobData = { type: T; content: SystemWebhookPayload; diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index d7f9e4eaa3..e05f0ce9b1 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js'; +import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, PostScheduledNoteQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js'; export const meta = { tags: ['admin'], @@ -49,6 +49,7 @@ export default class extends Endpoint { // eslint- constructor( @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 9afd1f8be6..69ce6a853d 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -233,6 +233,7 @@ export const rolePolicies = [ export const queueTypes = [ 'system', 'endedPollNotification', + 'postScheduledNote', 'deliver', 'inbox', 'db', From 3f5d1d35d6b0826563480c6db3fb11d1e11fb53a Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:31:25 +0900 Subject: [PATCH 04/53] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3672772665..4f189d69cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - pnpm 10.16.0 が必要です ### General +- Feat: 予約投稿ができるようになりました - Enhance: 広告ごとにセンシティブフラグを設定できるようになりました ### Client From 0a018ab7085fb6a4fc72548945888910f23f37d1 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:34:37 +0900 Subject: [PATCH 05/53] wip --- .../core/entities/NoteDraftEntityService.ts | 1 + .../src/models/json-schema/note-draft.ts | 5 +++++ packages/misskey-js/etc/misskey-js.api.md | 2 +- packages/misskey-js/src/autogen/types.ts | 22 ++++++++++--------- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/core/entities/NoteDraftEntityService.ts b/packages/backend/src/core/entities/NoteDraftEntityService.ts index 3ef8cdaa12..926c526e87 100644 --- a/packages/backend/src/core/entities/NoteDraftEntityService.ts +++ b/packages/backend/src/core/entities/NoteDraftEntityService.ts @@ -105,6 +105,7 @@ export class NoteDraftEntityService implements OnModuleInit { const packed: Packed<'NoteDraft'> = await awaitAll({ id: noteDraft.id, createdAt: this.idService.parse(noteDraft.id).date.toISOString(), + scheduledAt: noteDraft.scheduledAt?.toISOString() ?? undefined, userId: noteDraft.userId, user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me), text: text, diff --git a/packages/backend/src/models/json-schema/note-draft.ts b/packages/backend/src/models/json-schema/note-draft.ts index 504b263a6d..00622fa588 100644 --- a/packages/backend/src/models/json-schema/note-draft.ts +++ b/packages/backend/src/models/json-schema/note-draft.ts @@ -167,5 +167,10 @@ export const packedNoteDraftSchema = { optional: false, nullable: true, enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null], }, + scheduledAt: { + type: 'string', + optional: true, nullable: true, + format: 'date-time', + }, }, } as const; diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index d07710fc67..20872719d0 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -3339,7 +3339,7 @@ type QueueStats = { type QueueStatsLog = QueueStats[]; // @public (undocumented) -export const queueTypes: readonly ["system", "endedPollNotification", "deliver", "inbox", "db", "relationship", "objectStorage", "userWebhookDeliver", "systemWebhookDeliver"]; +export const queueTypes: readonly ["system", "endedPollNotification", "postScheduledNote", "deliver", "inbox", "db", "relationship", "objectStorage", "userWebhookDeliver", "systemWebhookDeliver"]; // @public (undocumented) type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 22a3733fcb..5d0ae23e5e 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4453,6 +4453,8 @@ export type components = { localOnly?: boolean; /** @enum {string|null} */ reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null; + /** Format: date-time */ + scheduledAt?: string | null; }; NoteReaction: { /** Format: id */ @@ -9553,7 +9555,7 @@ export interface operations { content: { 'application/json': { /** @enum {string} */ - queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + queue: 'system' | 'endedPollNotification' | 'postScheduledNote' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; /** @enum {string} */ state: '*' | 'completed' | 'wait' | 'active' | 'paused' | 'prioritized' | 'delayed' | 'failed'; }; @@ -9740,7 +9742,7 @@ export interface operations { content: { 'application/json': { /** @enum {string} */ - queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + queue: 'system' | 'endedPollNotification' | 'postScheduledNote' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; state: ('active' | 'wait' | 'delayed' | 'completed' | 'failed' | 'paused')[]; search?: string; }; @@ -9808,7 +9810,7 @@ export interface operations { content: { 'application/json': { /** @enum {string} */ - queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + queue: 'system' | 'endedPollNotification' | 'postScheduledNote' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; }; }; }; @@ -9871,7 +9873,7 @@ export interface operations { content: { 'application/json': { /** @enum {string} */ - queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + queue: 'system' | 'endedPollNotification' | 'postScheduledNote' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; }; }; }; @@ -9884,7 +9886,7 @@ export interface operations { content: { 'application/json': { /** @enum {string} */ - name: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + name: 'system' | 'endedPollNotification' | 'postScheduledNote' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; qualifiedName: string; counts: { [key: string]: number; @@ -9974,7 +9976,7 @@ export interface operations { content: { 'application/json': { /** @enum {string} */ - name: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + name: 'system' | 'endedPollNotification' | 'postScheduledNote' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; counts: { [key: string]: number; }; @@ -10038,7 +10040,7 @@ export interface operations { content: { 'application/json': { /** @enum {string} */ - queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + queue: 'system' | 'endedPollNotification' | 'postScheduledNote' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; jobId: string; }; }; @@ -10102,7 +10104,7 @@ export interface operations { content: { 'application/json': { /** @enum {string} */ - queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + queue: 'system' | 'endedPollNotification' | 'postScheduledNote' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; jobId: string; }; }; @@ -10166,7 +10168,7 @@ export interface operations { content: { 'application/json': { /** @enum {string} */ - queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + queue: 'system' | 'endedPollNotification' | 'postScheduledNote' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; jobId: string; }; }; @@ -10233,7 +10235,7 @@ export interface operations { content: { 'application/json': { /** @enum {string} */ - queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; + queue: 'system' | 'endedPollNotification' | 'postScheduledNote' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; jobId: string; }; }; From 92e24125db5bc48cb7f1ff9f356c77dc41124086 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:39:53 +0900 Subject: [PATCH 06/53] Update PostScheduledNoteProcessorService.ts --- .../queue/processors/PostScheduledNoteProcessorService.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts index f29ddd7161..e203189f0d 100644 --- a/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts +++ b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts @@ -9,6 +9,7 @@ import type { NoteDraftsRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; +import { NoteCreateService } from '@/core/NoteCreateService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { PostScheduledNoteJobData } from '../types.js'; @@ -21,6 +22,7 @@ export class PostScheduledNoteProcessorService { @Inject(DI.noteDraftsRepository) private noteDraftsRepository: NoteDraftsRepository, + private noteCreateService: NoteCreateService, private notificationService: NotificationService, private queueLoggerService: QueueLoggerService, ) { @@ -34,6 +36,10 @@ export class PostScheduledNoteProcessorService { return; } + const note = await this.noteCreateService.create(draft.user, draft); + + this.noteDraftsRepository.remove(draft); + this.notificationService.createNotification(draft.userId, 'scheduledNotePosted', { noteId: note.id, }); From 8b41db3f8ff971c2ad9760dde0147dec6fd82011 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:45:38 +0900 Subject: [PATCH 07/53] Update PostScheduledNoteProcessorService.ts --- .../src/queue/processors/PostScheduledNoteProcessorService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts index e203189f0d..82f1d029c7 100644 --- a/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts +++ b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts @@ -31,8 +31,8 @@ export class PostScheduledNoteProcessorService { @bindThis public async process(job: Bull.Job): Promise { - const draft = await this.noteDraftsRepository.findOneBy({ id: job.data.noteDraftId }); - if (draft == null) { + const draft = await this.noteDraftsRepository.findOne({ where: { id: job.data.noteDraftId }, relations: ['user'] }); + if (draft == null || draft.user == null || draft.scheduledAt == null) { return; } From 3b401816694b6b433addd8d9c0098623220ea8c7 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:57:35 +0900 Subject: [PATCH 08/53] Update Notification.ts --- packages/backend/src/models/Notification.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 0b4eeb3455..fffd7fb4c3 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -60,6 +60,11 @@ export type MiNotification = { createdAt: string; notifierId: MiUser['id']; noteId: MiNote['id']; +} | { + type: 'scheduledNotePosted'; + id: string; + createdAt: string; + noteId: MiNote['id']; } | { type: 'receiveFollowRequest'; id: string; From efebadfa652b5e9f3af1722003517ab3c8f8ecc2 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:04:17 +0900 Subject: [PATCH 09/53] wip --- locales/ja-JP.yml | 1 + .../entities/NotificationEntityService.ts | 13 +++++- .../src/models/json-schema/notification.ts | 15 +++++++ .../backend/src/models/json-schema/user.ts | 1 + .../server/api/endpoints/admin/show-user.ts | 1 + .../src/server/api/endpoints/i/update.ts | 1 + packages/backend/src/types.ts | 2 + .../src/components/MkNotification.vue | 13 ++++++ packages/misskey-js/etc/misskey-js.api.md | 2 +- packages/misskey-js/src/autogen/types.ts | 43 +++++++++++++++++-- packages/misskey-js/src/consts.ts | 1 + 11 files changed, 87 insertions(+), 6 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c0e598ef7b..1a68846b94 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2728,6 +2728,7 @@ _notification: youReceivedFollowRequest: "フォローリクエストが来ました" yourFollowRequestAccepted: "フォローリクエストが承認されました" pollEnded: "アンケートの結果が出ました" + scheduledNotePosted: "予約ノートが投稿されました" newNote: "新しい投稿" unreadAntennaNote: "アンテナ {name}" roleAssigned: "ロールが付与されました" diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index e91fb9eb51..0e96237d32 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -21,7 +21,18 @@ import type { OnModuleInit } from '@nestjs/common'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; -const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded'] as (typeof groupedNotificationTypes[number])[]); +const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set([ + 'note', + 'mention', + 'reply', + 'renote', + 'renote:grouped', + 'quote', + 'reaction', + 'reaction:grouped', + 'pollEnded', + 'scheduledNotePosted', +] as (typeof groupedNotificationTypes[number])[]); @Injectable() export class NotificationEntityService implements OnModuleInit { diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 6de120c8d7..8abe385164 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -207,6 +207,21 @@ export const packedNotificationSchema = { optional: false, nullable: false, }, }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['scheduledNotePosted'], + }, + note: { + type: 'object', + ref: 'Note', + optional: false, nullable: false, + }, + }, }, { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index c507d8d5c6..a35b336017 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -609,6 +609,7 @@ export const packedMeDetailedOnlySchema = { quote: { optional: true, ...notificationRecieveConfig }, reaction: { optional: true, ...notificationRecieveConfig }, pollEnded: { optional: true, ...notificationRecieveConfig }, + scheduledNotePosted: { optional: true, ...notificationRecieveConfig }, receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, followRequestAccepted: { optional: true, ...notificationRecieveConfig }, roleAssigned: { optional: true, ...notificationRecieveConfig }, diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 1ba6853dbe..7264d10137 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -103,6 +103,7 @@ export const meta = { quote: { optional: true, ...notificationRecieveConfig }, reaction: { optional: true, ...notificationRecieveConfig }, pollEnded: { optional: true, ...notificationRecieveConfig }, + scheduledNotePosted: { optional: true, ...notificationRecieveConfig }, receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, followRequestAccepted: { optional: true, ...notificationRecieveConfig }, roleAssigned: { optional: true, ...notificationRecieveConfig }, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 082d97f5d4..96619247e0 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -209,6 +209,7 @@ export const paramDef = { quote: notificationRecieveConfig, reaction: notificationRecieveConfig, pollEnded: notificationRecieveConfig, + scheduledNotePosted: notificationRecieveConfig, receiveFollowRequest: notificationRecieveConfig, followRequestAccepted: notificationRecieveConfig, roleAssigned: notificationRecieveConfig, diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index b20f2a2179..c3ede7afb1 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -12,6 +12,7 @@ * quote - 投稿が引用Renoteされた * reaction - 投稿にリアクションされた * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した + * scheduledNotePosted - 予約したノートが投稿された * receiveFollowRequest - フォローリクエストされた * followRequestAccepted - 自分の送ったフォローリクエストが承認された * roleAssigned - ロールが付与された @@ -32,6 +33,7 @@ export const notificationTypes = [ 'quote', 'reaction', 'pollEnded', + 'scheduledNotePosted', 'receiveFollowRequest', 'followRequestAccepted', 'roleAssigned', diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 21104b41df..ce8bea736c 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.t_mention]: notification.type === 'mention', [$style.t_quote]: notification.type === 'quote', [$style.t_pollEnded]: notification.type === 'pollEnded', + [$style.t_scheduledNotePosted]: notification.type === 'scheduledNotePosted', [$style.t_achievementEarned]: notification.type === 'achievementEarned', [$style.t_exportCompleted]: notification.type === 'exportCompleted', [$style.t_login]: notification.type === 'login', @@ -39,6 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only + @@ -60,6 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._notification.pollEnded }} + {{ i18n.ts._notification.scheduledNotePosted }} {{ i18n.ts._notification.newNote }}: {{ i18n.ts._notification.roleAssigned }} {{ i18n.ts._notification.chatRoomInvitationReceived }} @@ -103,6 +106,11 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + +
{{ notification.role.name }}
@@ -338,6 +346,11 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) pointer-events: none; } +.t_scheduledNotePosted { + background: var(--eventOther); + pointer-events: none; +} + .t_achievementEarned { background: var(--eventAchievement); pointer-events: none; diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 20872719d0..b117ff4747 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -3226,7 +3226,7 @@ type Notification_2 = components['schemas']['Notification']; type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json']; // @public (undocumented) -export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned", "exportCompleted", "test", "login", "createToken"]; +export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollEnded", "scheduledNotePosted", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned", "exportCompleted", "test", "login", "createToken"]; // @public (undocumented) export function nyaize(text: string): string; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 5d0ae23e5e..afff4c9301 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4159,6 +4159,15 @@ export type components = { /** Format: misskey:id */ userListId: string; }; + scheduledNotePosted?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; + } | { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }; receiveFollowRequest?: { /** @enum {string} */ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; @@ -4563,6 +4572,14 @@ export type components = { /** Format: id */ userId: string; note: components['schemas']['Note']; + } | { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** @enum {string} */ + type: 'scheduledNotePosted'; + note: components['schemas']['Note']; } | { /** Format: id */ id: string; @@ -11655,6 +11672,15 @@ export interface operations { /** Format: misskey:id */ userListId: string; }; + scheduledNotePosted?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; + } | { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }; receiveFollowRequest?: { /** @enum {string} */ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; @@ -25956,8 +25982,8 @@ export interface operations { untilDate?: number; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; }; }; }; @@ -26041,8 +26067,8 @@ export interface operations { untilDate?: number; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'scheduledNotePosted' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; }; }; }; @@ -27316,6 +27342,15 @@ export interface operations { /** Format: misskey:id */ userListId: string; }; + scheduledNotePosted?: { + /** @enum {string} */ + type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; + } | { + /** @enum {string} */ + type: 'list'; + /** Format: misskey:id */ + userListId: string; + }; receiveFollowRequest?: { /** @enum {string} */ type: 'all' | 'following' | 'follower' | 'mutualFollow' | 'followingOrFollower' | 'never'; diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 69ce6a853d..b68522cd30 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -26,6 +26,7 @@ export const notificationTypes = [ 'quote', 'reaction', 'pollEnded', + 'scheduledNotePosted', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', From 7c12fb87f6391433d4b341d6d6f48f4b673cb9c1 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:11:20 +0900 Subject: [PATCH 10/53] Update NoteDraftService.ts --- packages/backend/src/core/NoteDraftService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/NoteDraftService.ts b/packages/backend/src/core/NoteDraftService.ts index 9274b18570..531e4406fd 100644 --- a/packages/backend/src/core/NoteDraftService.ts +++ b/packages/backend/src/core/NoteDraftService.ts @@ -31,6 +31,7 @@ export type NoteDraftOptions = { hashtag?: string; channelId?: MiChannel['id'] | null; poll?: (IPoll & { expiredAfter?: number | null }) | null; + scheduledAt?: Date | null; }; @Injectable() @@ -309,6 +310,7 @@ export class NoteDraftService { visibleUserIds: data.visibleUserIds ?? [], localOnly: data.localOnly, reactionAcceptance: data.reactionAcceptance, + scheduledAt: data.scheduledAt ?? null, } satisfies MiNoteDraft; return appliedDraft; @@ -317,7 +319,7 @@ export class NoteDraftService { @bindThis public async schedule(draft: MiNoteDraft, scheduledAt: Date): Promise { const delay = scheduledAt.getTime() - Date.now(); - this.queueService.deleteUserMutingQueue.add(draft.id, { + this.queueService.postScheduledNoteQueue.add(draft.id, { noteDraftId: draft.id, }, { delay, From 8783334d42f9b5278746aec14a3ac68d749cdadb Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:17:54 +0900 Subject: [PATCH 11/53] Update NoteDraftService.ts --- packages/backend/src/core/NoteDraftService.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/NoteDraftService.ts b/packages/backend/src/core/NoteDraftService.ts index 531e4406fd..165fac05f5 100644 --- a/packages/backend/src/core/NoteDraftService.ts +++ b/packages/backend/src/core/NoteDraftService.ts @@ -98,7 +98,7 @@ export class NoteDraftService { appliedDraft.id = this.idService.gen(); appliedDraft.userId = me.id; - const draft = this.noteDraftsRepository.save(appliedDraft); + const draft = this.noteDraftsRepository.insertOne(appliedDraft); return draft; } @@ -126,7 +126,12 @@ export class NoteDraftService { const appliedDraft = await this.checkAndSetDraftNoteOptions(me, draft, data); - return await this.noteDraftsRepository.save(appliedDraft); + await this.noteDraftsRepository.update(draftId, appliedDraft); + + return { + ...draft, + ...appliedDraft, + }; } @bindThis From ee5b9ba1a9001cedb7a4f767b7f2214b4b131cdd Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:26:01 +0900 Subject: [PATCH 12/53] Update NoteDraftService.ts --- packages/backend/src/core/NoteDraftService.ts | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/NoteDraftService.ts b/packages/backend/src/core/NoteDraftService.ts index 165fac05f5..2925aa9ea1 100644 --- a/packages/backend/src/core/NoteDraftService.ts +++ b/packages/backend/src/core/NoteDraftService.ts @@ -98,7 +98,11 @@ export class NoteDraftService { appliedDraft.id = this.idService.gen(); appliedDraft.userId = me.id; - const draft = this.noteDraftsRepository.insertOne(appliedDraft); + const draft = await this.noteDraftsRepository.insertOne(appliedDraft); + + if (draft.scheduledAt) { + this.schedule(draft); + } return draft; } @@ -128,6 +132,12 @@ export class NoteDraftService { await this.noteDraftsRepository.update(draftId, appliedDraft); + this.clearSchedule(draft).then(() => { + if (appliedDraft.scheduledAt) { + this.schedule(draft); + } + }); + return { ...draft, ...appliedDraft, @@ -146,6 +156,8 @@ export class NoteDraftService { } await this.noteDraftsRepository.delete(draft.id); + + this.clearSchedule(draft); } @bindThis @@ -322,8 +334,11 @@ export class NoteDraftService { } @bindThis - public async schedule(draft: MiNoteDraft, scheduledAt: Date): Promise { - const delay = scheduledAt.getTime() - Date.now(); + public async schedule(draft: MiNoteDraft): Promise { + if (draft.scheduledAt == null) return; + if (draft.scheduledAt.getTime() <= Date.now()) return; + + const delay = draft.scheduledAt.getTime() - Date.now(); this.queueService.postScheduledNoteQueue.add(draft.id, { noteDraftId: draft.id, }, { @@ -338,4 +353,14 @@ export class NoteDraftService { }, }); } + + @bindThis + public async clearSchedule(draft: MiNoteDraft): Promise { + const jobs = await this.queueService.postScheduledNoteQueue.getJobs(['delayed', 'waiting', 'active']); + for (const job of jobs) { + if (job.data.noteDraftId === draft.id) { + await job.remove(); + } + } + } } From 182d1471562ed875bf964699c22c8b0bad823ce7 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:33:14 +0900 Subject: [PATCH 13/53] wip --- .../backend/src/server/api/endpoints/notes/drafts/create.ts | 2 ++ .../backend/src/server/api/endpoints/notes/drafts/update.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts index 1c28ec22d0..75c1c577d1 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts @@ -183,6 +183,7 @@ export const paramDef = { }, required: ['choices'], }, + scheduledAt: { type: 'integer', nullable: true }, }, required: [], } as const; @@ -212,6 +213,7 @@ export default class extends Endpoint { // eslint- visibility: ps.visibility, visibleUserIds: ps.visibleUserIds ?? [], channelId: ps.channelId ?? undefined, + scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, }).catch((err) => { if (err instanceof IdentifiableError) { switch (err.id) { diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts index ee221fb765..2692b279a4 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts @@ -215,6 +215,7 @@ export const paramDef = { }, required: ['choices'], }, + scheduledAt: { type: 'integer', nullable: true }, }, required: ['draftId'], } as const; @@ -244,6 +245,7 @@ export default class extends Endpoint { // eslint- visibility: ps.visibility, visibleUserIds: ps.visibleUserIds ?? [], channelId: ps.channelId ?? undefined, + scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, }).catch((err) => { if (err instanceof IdentifiableError) { switch (err.id) { From 1df5826a7e85ede619fc0e3526083f11fb112278 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:34:11 +0900 Subject: [PATCH 14/53] Create 1758677617888-scheduled-post.js --- .../migration/1758677617888-scheduled-post.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 packages/backend/migration/1758677617888-scheduled-post.js diff --git a/packages/backend/migration/1758677617888-scheduled-post.js b/packages/backend/migration/1758677617888-scheduled-post.js new file mode 100644 index 0000000000..f8a5d1495d --- /dev/null +++ b/packages/backend/migration/1758677617888-scheduled-post.js @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ScheduledPost1758677617888 { + name = 'ScheduledPost1758677617888' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note_draft" ADD "scheduledAt" TIMESTAMP WITH TIME ZONE`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "scheduledAt"`); + } +} From c644616205ea39c34c99ccd94b2906a26a6ed67e Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:34:18 +0900 Subject: [PATCH 15/53] Update index.d.ts --- locales/index.d.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/locales/index.d.ts b/locales/index.d.ts index 4071d5c373..b6f26d6a1d 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -10328,6 +10328,10 @@ export interface Locale extends ILocale { * アンケートの結果が出ました */ "pollEnded": string; + /** + * 予約ノートが投稿されました + */ + "scheduledNotePosted": string; /** * 新しい投稿 */ From d2faac80d85048ee8b0c82c77ed4c71b73c3a5ba Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:36:48 +0900 Subject: [PATCH 16/53] Update stats.ts --- packages/backend/src/server/api/endpoints/admin/queue/stats.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index e05f0ce9b1..b69699c338 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -49,7 +49,7 @@ export default class extends Endpoint { // eslint- constructor( @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, - @Inject('postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue, + @Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, From e129b09160ca9c331222b636d8580370b9145a95 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:05:44 +0900 Subject: [PATCH 17/53] wip --- locales/index.d.ts | 4 ++++ locales/ja-JP.yml | 1 + packages/frontend/src/components/MkNoteDraftsDialog.vue | 9 +++++++++ 3 files changed, 14 insertions(+) diff --git a/locales/index.d.ts b/locales/index.d.ts index b6f26d6a1d..5877cc8432 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -12645,6 +12645,10 @@ export interface Locale extends ILocale { * 下書き一覧 */ "listDrafts": string; + /** + * 投稿予約 + */ + "schedule": string; }; /** * 二次元コード diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1a68846b94..1381c1c65b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -3386,6 +3386,7 @@ _drafts: restoreFromDraft: "下書きから復元" restore: "復元" listDrafts: "下書き一覧" + schedule: "投稿予約" qr: "二次元コード" _qr: diff --git a/packages/frontend/src/components/MkNoteDraftsDialog.vue b/packages/frontend/src/components/MkNoteDraftsDialog.vue index 5b8211b715..304a0f55ac 100644 --- a/packages/frontend/src/components/MkNoteDraftsDialog.vue +++ b/packages/frontend/src/components/MkNoteDraftsDialog.vue @@ -94,12 +94,21 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._drafts.restore }} + + + {{ i18n.ts._drafts.schedule }} + From 8b1f889d1dd30a535d7753d799b4f6ddf98015f5 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:08:03 +0900 Subject: [PATCH 18/53] wip --- locales/index.d.ts | 4 ++++ locales/ja-JP.yml | 1 + .../core/entities/NoteDraftEntityService.ts | 2 +- .../src/models/json-schema/note-draft.ts | 5 ++--- .../frontend/src/components/MkPostForm.vue | 21 ++++++++++++++++++- packages/misskey-js/src/autogen/types.ts | 5 +++-- 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index 5877cc8432..ddbb9e3335 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5553,6 +5553,10 @@ export interface Locale extends ILocale { * ユーザー指定ノートを作成 */ "createUserSpecifiedNote": string; + /** + * 投稿を予約 + */ + "schedulePost": string; "_compression": { "_quality": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1381c1c65b..514181b925 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1383,6 +1383,7 @@ customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カ themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。" thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!" createUserSpecifiedNote: "ユーザー指定ノートを作成" +schedulePost: "投稿を予約" _compression: _quality: diff --git a/packages/backend/src/core/entities/NoteDraftEntityService.ts b/packages/backend/src/core/entities/NoteDraftEntityService.ts index 926c526e87..cdbe2cf643 100644 --- a/packages/backend/src/core/entities/NoteDraftEntityService.ts +++ b/packages/backend/src/core/entities/NoteDraftEntityService.ts @@ -105,7 +105,7 @@ export class NoteDraftEntityService implements OnModuleInit { const packed: Packed<'NoteDraft'> = await awaitAll({ id: noteDraft.id, createdAt: this.idService.parse(noteDraft.id).date.toISOString(), - scheduledAt: noteDraft.scheduledAt?.toISOString() ?? undefined, + scheduledAt: noteDraft.scheduledAt?.getTime() ?? null, userId: noteDraft.userId, user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me), text: text, diff --git a/packages/backend/src/models/json-schema/note-draft.ts b/packages/backend/src/models/json-schema/note-draft.ts index 00622fa588..03c32b6548 100644 --- a/packages/backend/src/models/json-schema/note-draft.ts +++ b/packages/backend/src/models/json-schema/note-draft.ts @@ -168,9 +168,8 @@ export const packedNoteDraftSchema = { enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null], }, scheduledAt: { - type: 'string', - optional: true, nullable: true, - format: 'date-time', + type: 'number', + optional: false, nullable: true, }, }, } as const; diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 17f93a4ec8..197530d260 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -199,6 +199,7 @@ if (props.initialVisibleUsers) { props.initialVisibleUsers.forEach(u => pushVisibleUser(u)); } const reactionAcceptance = ref(store.s.reactionAcceptance); +const scheduledAt = ref(null); const draghover = ref(false); const quoteId = ref(null); const hasNotSpecifiedMentions = ref(false); @@ -414,6 +415,7 @@ function watchForDraft() { watch(localOnly, () => saveDraft()); watch(quoteId, () => saveDraft()); watch(reactionAcceptance, () => saveDraft()); + watch(scheduledAt, () => saveDraft()); } function checkMissingMention() { @@ -605,6 +607,12 @@ function showOtherSettings() { action: () => { toggleReactionAcceptance(); }, + }, { + icon: 'ti ti-calendar-time', + text: i18n.ts.schedulePost + '...', + action: () => { + schedule(); + }, }, { type: 'divider' }, { type: 'switch', icon: 'ti ti-eye', @@ -809,6 +817,7 @@ function saveDraft() { ...( visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}), quoteId: quoteId.value, reactionAcceptance: reactionAcceptance.value, + scheduledAt: scheduledAt.value, }, }; @@ -838,6 +847,7 @@ async function saveServerDraft(clearLocal = false) { replyId: replyTargetNote.value ? replyTargetNote.value.id : undefined, channelId: targetChannel.value ? targetChannel.value.id : undefined, reactionAcceptance: reactionAcceptance.value, + scheduledAt: scheduledAt.value, }).then(() => { if (clearLocal) { clear(); @@ -1175,6 +1185,7 @@ function showDraftMenu(ev: MouseEvent) { renoteTargetNote.value = draft.renote; replyTargetNote.value = draft.reply; reactionAcceptance.value = draft.reactionAcceptance; + scheduledAt.value = draft.scheduledAt ?? null; if (draft.channel) targetChannel.value = draft.channel as unknown as Misskey.entities.Channel; visibleUsers.value = []; @@ -1220,6 +1231,13 @@ function showDraftMenu(ev: MouseEvent) { }], (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); } +async function schedule() { + const { canceled, result } = await os.inputDate({ + title: i18n.ts.schedulePost, + }); + if (canceled) return; +} + onMounted(() => { if (props.autofocus) { focus(); @@ -1255,6 +1273,7 @@ onMounted(() => { } quoteId.value = draft.data.quoteId; reactionAcceptance.value = draft.data.reactionAcceptance; + scheduledAt.value = draft.data.scheduledAt ?? null; } } diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index afff4c9301..74ea5fbe5f 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4462,8 +4462,7 @@ export type components = { localOnly?: boolean; /** @enum {string|null} */ reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null; - /** Format: date-time */ - scheduledAt?: string | null; + scheduledAt: number | null; }; NoteReaction: { /** Format: id */ @@ -29205,6 +29204,7 @@ export interface operations { expiresAt?: number | null; expiredAfter?: number | null; } | null; + scheduledAt?: number | null; }; }; }; @@ -29446,6 +29446,7 @@ export interface operations { expiresAt?: number | null; expiredAfter?: number | null; } | null; + scheduledAt?: number | null; }; }; }; From d9d90a04e2b9fe20f37378c282941f3b046c02c2 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:10:29 +0900 Subject: [PATCH 19/53] wip --- locales/index.d.ts | 8 ++++++ locales/ja-JP.yml | 2 ++ .../frontend/src/components/MkPostForm.vue | 27 ++++++++++++++----- packages/frontend/src/os.ts | 4 +-- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index ddbb9e3335..7ae60a77c4 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5557,6 +5557,14 @@ export interface Locale extends ILocale { * 投稿を予約 */ "schedulePost": string; + /** + * {x}に投稿を予約します + */ + "scheduleToPostOnX": ParameterizedString<"x">; + /** + * 予約 + */ + "schedule": string; "_compression": { "_quality": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 514181b925..e78b17096f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1384,6 +1384,8 @@ themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォル thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!" createUserSpecifiedNote: "ユーザー指定ノートを作成" schedulePost: "投稿を予約" +scheduleToPostOnX: "{x}に投稿を予約します" +schedule: "予約" _compression: _quality: diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 197530d260..60eeb7bb81 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
@@ -61,6 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only + {{ i18n.tsx.scheduleToPostOnX({ x: new Date(scheduledAt).toLocaleString() }) }} - {{ i18n.ts.notSpecifiedMentionWarning }} -
@@ -263,11 +264,17 @@ const placeholder = computed((): string => { }); const submitText = computed((): string => { - return renoteTargetNote.value - ? i18n.ts.quote - : replyTargetNote.value - ? i18n.ts.reply - : i18n.ts.note; + return scheduledAt.value != null + ? i18n.ts.schedule + : renoteTargetNote.value + ? i18n.ts.quote + : replyTargetNote.value + ? i18n.ts.reply + : i18n.ts.note; +}); + +const submitIcon = computed((): string => { + return posted.value ? 'ti ti-check' : scheduledAt.value != null ? 'ti ti-calendar-time' : replyTargetNote.value ? 'ti ti-arrow-back-up' : renoteTargetNote.value ? 'ti ti-quote' : 'ti ti-send'; }); const textLength = computed((): number => { @@ -1232,10 +1239,12 @@ function showDraftMenu(ev: MouseEvent) { } async function schedule() { - const { canceled, result } = await os.inputDate({ + const { canceled, result } = await os.inputDatetime({ title: i18n.ts.schedulePost, }); if (canceled) return; + + scheduledAt.value = result.getTime(); } onMounted(() => { @@ -1538,6 +1547,10 @@ html[data-color-scheme=light] .preview { margin: 0 20px 16px 20px; } +.scheduledAt { + margin: 0 20px 16px 20px; +} + .cw, .hashtags, .text { diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 6c5f04c6b5..a14c24746a 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -460,7 +460,7 @@ export function inputNumber(props: { }); } -export function inputDate(props: { +export function inputDatetime(props: { title?: string; text?: string; placeholder?: string | null; @@ -475,7 +475,7 @@ export function inputDate(props: { title: props.title, text: props.text, input: { - type: 'date', + type: 'datetime-local', placeholder: props.placeholder, default: props.default ?? null, }, From 8fede2d6706f47d50cc72a59603062debb85df13 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:02:52 +0900 Subject: [PATCH 20/53] wip --- .../migration/1758677617888-scheduled-post.js | 2 + packages/backend/src/core/NoteDraftService.ts | 7 +++- .../core/entities/NoteDraftEntityService.ts | 1 + packages/backend/src/models/NoteDraft.ts | 6 ++- .../src/models/json-schema/note-draft.ts | 4 ++ .../PostScheduledNoteProcessorService.ts | 2 +- .../api/endpoints/notes/drafts/create.ts | 2 + .../api/endpoints/notes/drafts/update.ts | 2 + .../frontend/src/components/MkPostForm.vue | 39 +++++++++++++++---- packages/misskey-js/src/autogen/types.ts | 5 +++ 10 files changed, 59 insertions(+), 11 deletions(-) diff --git a/packages/backend/migration/1758677617888-scheduled-post.js b/packages/backend/migration/1758677617888-scheduled-post.js index f8a5d1495d..b31313d9db 100644 --- a/packages/backend/migration/1758677617888-scheduled-post.js +++ b/packages/backend/migration/1758677617888-scheduled-post.js @@ -11,12 +11,14 @@ export class ScheduledPost1758677617888 { */ async up(queryRunner) { await queryRunner.query(`ALTER TABLE "note_draft" ADD "scheduledAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`ALTER TABLE "note_draft" ADD "isActuallyScheduled" boolean NOT NULL DEFAULT false`); } /** * @param {QueryRunner} queryRunner */ async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "isActuallyScheduled"`); await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "scheduledAt"`); } } diff --git a/packages/backend/src/core/NoteDraftService.ts b/packages/backend/src/core/NoteDraftService.ts index 2925aa9ea1..e01feb2545 100644 --- a/packages/backend/src/core/NoteDraftService.ts +++ b/packages/backend/src/core/NoteDraftService.ts @@ -32,6 +32,7 @@ export type NoteDraftOptions = { channelId?: MiChannel['id'] | null; poll?: (IPoll & { expiredAfter?: number | null }) | null; scheduledAt?: Date | null; + isActuallyScheduled?: boolean; }; @Injectable() @@ -100,7 +101,7 @@ export class NoteDraftService { appliedDraft.userId = me.id; const draft = await this.noteDraftsRepository.insertOne(appliedDraft); - if (draft.scheduledAt) { + if (draft.scheduledAt && draft.isActuallyScheduled) { this.schedule(draft); } @@ -133,7 +134,7 @@ export class NoteDraftService { await this.noteDraftsRepository.update(draftId, appliedDraft); this.clearSchedule(draft).then(() => { - if (appliedDraft.scheduledAt) { + if (appliedDraft.scheduledAt != null && appliedDraft.isActuallyScheduled) { this.schedule(draft); } }); @@ -328,6 +329,7 @@ export class NoteDraftService { localOnly: data.localOnly, reactionAcceptance: data.reactionAcceptance, scheduledAt: data.scheduledAt ?? null, + isActuallyScheduled: data.isActuallyScheduled ?? false, } satisfies MiNoteDraft; return appliedDraft; @@ -335,6 +337,7 @@ export class NoteDraftService { @bindThis public async schedule(draft: MiNoteDraft): Promise { + if (!draft.isActuallyScheduled) return; if (draft.scheduledAt == null) return; if (draft.scheduledAt.getTime() <= Date.now()) return; diff --git a/packages/backend/src/core/entities/NoteDraftEntityService.ts b/packages/backend/src/core/entities/NoteDraftEntityService.ts index cdbe2cf643..338b0411e6 100644 --- a/packages/backend/src/core/entities/NoteDraftEntityService.ts +++ b/packages/backend/src/core/entities/NoteDraftEntityService.ts @@ -106,6 +106,7 @@ export class NoteDraftEntityService implements OnModuleInit { id: noteDraft.id, createdAt: this.idService.parse(noteDraft.id).date.toISOString(), scheduledAt: noteDraft.scheduledAt?.getTime() ?? null, + isActuallyScheduled: noteDraft.isActuallyScheduled, userId: noteDraft.userId, user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me), text: text, diff --git a/packages/backend/src/models/NoteDraft.ts b/packages/backend/src/models/NoteDraft.ts index 4f07f4a8be..411374fc96 100644 --- a/packages/backend/src/models/NoteDraft.ts +++ b/packages/backend/src/models/NoteDraft.ts @@ -153,12 +153,16 @@ export class MiNoteDraft { //#endregion - // 予約投稿 @Column('timestamp with time zone', { nullable: true, }) public scheduledAt: Date | null; + @Column('boolean', { + default: false, + }) + public isActuallyScheduled: boolean; + constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/json-schema/note-draft.ts b/packages/backend/src/models/json-schema/note-draft.ts index 03c32b6548..c6519bad0b 100644 --- a/packages/backend/src/models/json-schema/note-draft.ts +++ b/packages/backend/src/models/json-schema/note-draft.ts @@ -171,5 +171,9 @@ export const packedNoteDraftSchema = { type: 'number', optional: false, nullable: true, }, + isActuallyScheduled: { + type: 'boolean', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts index 82f1d029c7..4793385990 100644 --- a/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts +++ b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts @@ -32,7 +32,7 @@ export class PostScheduledNoteProcessorService { @bindThis public async process(job: Bull.Job): Promise { const draft = await this.noteDraftsRepository.findOne({ where: { id: job.data.noteDraftId }, relations: ['user'] }); - if (draft == null || draft.user == null || draft.scheduledAt == null) { + if (draft == null || draft.user == null || draft.scheduledAt == null || !draft.isActuallyScheduled) { return; } diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts index 75c1c577d1..5d18a2e42c 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts @@ -184,6 +184,7 @@ export const paramDef = { required: ['choices'], }, scheduledAt: { type: 'integer', nullable: true }, + isActuallyScheduled: { type: 'boolean', default: false }, }, required: [], } as const; @@ -214,6 +215,7 @@ export default class extends Endpoint { // eslint- visibleUserIds: ps.visibleUserIds ?? [], channelId: ps.channelId ?? undefined, scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, + isActuallyScheduled: ps.isActuallyScheduled ?? false, }).catch((err) => { if (err instanceof IdentifiableError) { switch (err.id) { diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts index 2692b279a4..7457a82dee 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts @@ -216,6 +216,7 @@ export const paramDef = { required: ['choices'], }, scheduledAt: { type: 'integer', nullable: true }, + isActuallyScheduled: { type: 'boolean', default: false }, }, required: ['draftId'], } as const; @@ -246,6 +247,7 @@ export default class extends Endpoint { // eslint- visibleUserIds: ps.visibleUserIds ?? [], channelId: ps.channelId ?? undefined, scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, + isActuallyScheduled: ps.isActuallyScheduled ?? false, }).catch((err) => { if (err instanceof IdentifiableError) { switch (err.id) { diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 60eeb7bb81..bc490cdeb2 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -669,6 +669,7 @@ function clear() { files.value = []; poll.value = null; quoteId.value = null; + scheduledAt.value = null; } function onKeydown(ev: KeyboardEvent) { @@ -839,7 +840,9 @@ function deleteDraft() { miLocalStorage.setItem('drafts', JSON.stringify(draftData)); } -async function saveServerDraft(clearLocal = false) { +async function saveServerDraft(options: { + isActuallyScheduled?: boolean; +} = {}) { return await os.apiWithDialog(serverDraftId.value == null ? 'notes/drafts/create' : 'notes/drafts/update', { ...(serverDraftId.value == null ? {} : { draftId: serverDraftId.value }), text: text.value, @@ -855,12 +858,7 @@ async function saveServerDraft(clearLocal = false) { channelId: targetChannel.value ? targetChannel.value.id : undefined, reactionAcceptance: reactionAcceptance.value, scheduledAt: scheduledAt.value, - }).then(() => { - if (clearLocal) { - clear(); - deleteDraft(); - } - }).catch((err) => { + isActuallyScheduled: options.isActuallyScheduled ?? false, }); } @@ -895,6 +893,21 @@ async function post(ev?: MouseEvent) { } } + if (scheduledAt.value != null) { + if (uploader.items.value.some(x => x.uploaded == null)) { + await uploadFiles(); + + // アップロード失敗したものがあったら中止 + if (uploader.items.value.some(x => x.uploaded == null)) { + return; + } + } + + await postAsScheduled(); + clear(); + return; + } + if (props.mock) return; if (visibility.value === 'public' && ( @@ -1066,6 +1079,14 @@ async function post(ev?: MouseEvent) { }); } +async function postAsScheduled() { + if (props.mock) return; + + await saveServerDraft({ + isActuallyScheduled: true, + }); +} + function cancel() { emit('cancel'); } @@ -1247,6 +1268,10 @@ async function schedule() { scheduledAt.value = result.getTime(); } +function cancelSchedule() { + scheduledAt.value = null; +} + onMounted(() => { if (props.autofocus) { focus(); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 74ea5fbe5f..d187b56090 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4463,6 +4463,7 @@ export type components = { /** @enum {string|null} */ reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null; scheduledAt: number | null; + isActuallyScheduled: boolean; }; NoteReaction: { /** Format: id */ @@ -29205,6 +29206,8 @@ export interface operations { expiredAfter?: number | null; } | null; scheduledAt?: number | null; + /** @default false */ + isActuallyScheduled?: boolean; }; }; }; @@ -29447,6 +29450,8 @@ export interface operations { expiredAfter?: number | null; } | null; scheduledAt?: number | null; + /** @default false */ + isActuallyScheduled?: boolean; }; }; }; From db14012bbff35d37ebad905d5f10722c70fc8636 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:23:59 +0900 Subject: [PATCH 21/53] wip --- locales/index.d.ts | 8 ++++++++ locales/ja-JP.yml | 2 ++ packages/frontend/src/components/MkPostForm.vue | 9 ++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index 7ae60a77c4..fa5275878f 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5286,6 +5286,10 @@ export interface Locale extends ILocale { * 下書き */ "draft": string; + /** + * 下書きと予約投稿 + */ + "draftsAndScheduledNotes": string; /** * リアクションする際に確認する */ @@ -12661,6 +12665,10 @@ export interface Locale extends ILocale { * 投稿予約 */ "schedule": string; + /** + * 予約投稿一覧 + */ + "listScheduledNotes": string; }; /** * 二次元コード diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index e78b17096f..d8fa726adb 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1317,6 +1317,7 @@ acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。" federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。" draft: "下書き" +draftsAndScheduledNotes: "下書きと予約投稿" confirmOnReact: "リアクションする際に確認する" reactAreYouSure: "\" {emoji} \" をリアクションしますか?" markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?" @@ -3390,6 +3391,7 @@ _drafts: restore: "復元" listDrafts: "下書き一覧" schedule: "投稿予約" + listScheduledNotes: "予約投稿一覧" qr: "二次元コード" _qr: diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index bc490cdeb2..e587cf9320 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
@@ -134,6 +157,11 @@ import * as os from '@/os.js'; import { $i } from '@/i.js'; import { misskeyApi } from '@/utility/misskey-api'; import { Paginator } from '@/utility/paginator.js'; +import MkTabs from '@/components/MkTabs.vue'; + +const props = defineProps<{ + scheduled?: boolean; +}>(); const emit = defineEmits<{ (ev: 'restore', draft: Misskey.entities.NoteDraft): void; @@ -141,8 +169,20 @@ const emit = defineEmits<{ (ev: 'closed'): void; }>(); -const paginator = markRaw(new Paginator('notes/drafts/list', { +const tab = ref<'drafts' | 'scheduled'>(props.scheduled ? 'scheduled' : 'drafts'); + +const draftsPaginator = markRaw(new Paginator('notes/drafts/list', { limit: 10, + params: { + scheduled: false, + }, +})); + +const scheduledPaginator = markRaw(new Paginator('notes/drafts/list', { + limit: 10, + params: { + scheduled: true, + }, })); const currentDraftsCount = ref(0); @@ -171,7 +211,7 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) { if (canceled) return; os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => { - paginator.reload(); + draftsPaginator.reload(); }); } @@ -229,4 +269,11 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) { padding-top: 16px; border-top: solid 1px var(--MI_THEME-divider); } + +.tabs { + background: color(from var(--MI_THEME-bg) srgb r g b / 0.75); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + border-bottom: solid 0.5px var(--MI_THEME-divider); +} diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index d187b56090..0431ee4872 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -29354,6 +29354,7 @@ export interface operations { untilId?: string; sinceDate?: number; untilDate?: number; + scheduled?: boolean | null; }; }; }; From f6e481d8fbff5e68b7c241457590f50961f67666 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 24 Sep 2025 18:02:06 +0900 Subject: [PATCH 24/53] wip --- locales/index.d.ts | 8 +++++++ locales/ja-JP.yml | 2 ++ .../src/components/MkNoteDraftsDialog.vue | 21 ++++++++++++------- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index f4064c542d..50b2028bbe 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5565,6 +5565,10 @@ export interface Locale extends ILocale { * {x}に投稿を予約します */ "scheduleToPostOnX": ParameterizedString<"x">; + /** + * {x}に投稿が予約されています + */ + "scheduledToPostOnX": ParameterizedString<"x">; /** * 予約 */ @@ -12673,6 +12677,10 @@ export interface Locale extends ILocale { * 予約投稿一覧 */ "listScheduledNotes": string; + /** + * 予約解除 + */ + "cancelSchedule": string; }; /** * 二次元コード diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c52128534a..1767e3da52 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1386,6 +1386,7 @@ thankYouForTestingBeta: "ベータ版の検証にご協力いただきありが createUserSpecifiedNote: "ユーザー指定ノートを作成" schedulePost: "投稿を予約" scheduleToPostOnX: "{x}に投稿を予約します" +scheduledToPostOnX: "{x}に投稿が予約されています" schedule: "予約" scheduled: "予約" @@ -3393,6 +3394,7 @@ _drafts: listDrafts: "下書き一覧" schedule: "投稿予約" listScheduledNotes: "予約投稿一覧" + cancelSchedule: "予約解除" qr: "二次元コード" _qr: diff --git a/packages/frontend/src/components/MkNoteDraftsDialog.vue b/packages/frontend/src/components/MkNoteDraftsDialog.vue index 14cff56c0d..6e35ff82e2 100644 --- a/packages/frontend/src/components/MkNoteDraftsDialog.vue +++ b/packages/frontend/src/components/MkNoteDraftsDialog.vue @@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only :class="[$style.draft]" >
+ {{ i18n.tsx.scheduledToPostOnX({ x: new Date(draft.scheduledAt).toLocaleString() }) }}
@@ -107,8 +108,19 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ + {{ i18n.ts._drafts.cancelSchedule }} + + {{ i18n.ts._drafts.restore }} - - - {{ i18n.ts._drafts.schedule }} - Date: Wed, 24 Sep 2025 18:30:39 +0900 Subject: [PATCH 25/53] wip --- packages/backend/src/core/NoteDraftService.ts | 43 +++++++++---------- packages/backend/src/models/NoteDraft.ts | 8 ---- .../api/endpoints/notes/drafts/create.ts | 22 +++++----- .../api/endpoints/notes/drafts/update.ts | 24 +++++------ .../src/components/MkNoteDraftsDialog.vue | 10 +++++ .../frontend/src/components/MkPostForm.vue | 10 ++--- packages/misskey-js/src/autogen/types.ts | 40 +++++++---------- 7 files changed, 74 insertions(+), 83 deletions(-) diff --git a/packages/backend/src/core/NoteDraftService.ts b/packages/backend/src/core/NoteDraftService.ts index e01feb2545..ff010a38ff 100644 --- a/packages/backend/src/core/NoteDraftService.ts +++ b/packages/backend/src/core/NoteDraftService.ts @@ -17,22 +17,23 @@ import { IdentifiableError } from '@/misc/identifiable-error.js'; import { isRenote, isQuote } from '@/misc/is-renote.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { QueueService } from '@/core/QueueService.js'; +import { deepClone } from '@/misc/clone.js'; export type NoteDraftOptions = { - replyId?: MiNote['id'] | null; - renoteId?: MiNote['id'] | null; - text?: string | null; - cw?: string | null; - localOnly?: boolean | null; - reactionAcceptance?: typeof noteReactionAcceptances[number]; - visibility?: typeof noteVisibilities[number]; - fileIds?: MiDriveFile['id'][]; - visibleUserIds?: MiUser['id'][]; - hashtag?: string; - channelId?: MiChannel['id'] | null; - poll?: (IPoll & { expiredAfter?: number | null }) | null; - scheduledAt?: Date | null; - isActuallyScheduled?: boolean; + replyId: MiNote['id'] | null; + renoteId: MiNote['id'] | null; + text: string | null; + cw: string | null; + localOnly: boolean | null; + reactionAcceptance: typeof noteReactionAcceptances[number]; + visibility: typeof noteVisibilities[number]; + fileIds: MiDriveFile['id'][]; + visibleUserIds: MiUser['id'][]; + hashtag: string | null; + channelId: MiChannel['id'] | null; + poll: (IPoll & { expiredAfter?: number | null }) | null; + scheduledAt: Date | null; + isActuallyScheduled: boolean; }; @Injectable() @@ -109,7 +110,7 @@ export class NoteDraftService { } @bindThis - public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: NoteDraftOptions): Promise { + public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: Partial): Promise { const draft = await this.noteDraftsRepository.findOneBy({ id: draftId, userId: me.id, @@ -180,7 +181,7 @@ export class NoteDraftService { public async checkAndSetDraftNoteOptions( me: MiLocalUser, draft: MiNoteDraft, - data: NoteDraftOptions, + data: Partial, ): Promise { data.visibility ??= 'public'; data.localOnly ??= false; @@ -191,8 +192,6 @@ export class NoteDraftService { data.localOnly = true; } - let appliedDraft = draft; - //#region visibleUsers let visibleUsers: MiUser[] = []; if (data.visibleUserIds != null) { @@ -205,7 +204,7 @@ export class NoteDraftService { //#region files let files: MiDriveFile[] = []; const fileIds = data.fileIds ?? null; - if (fileIds != null) { + if (fileIds != null && fileIds.length > 0) { files = await this.driveFilesRepository.createQueryBuilder('file') .where('file.userId = :userId AND file.id IN (:...fileIds)', { userId: me.id, @@ -310,8 +309,8 @@ export class NoteDraftService { } //#endregion - appliedDraft = { - ...appliedDraft, + return { + ...draft, visibility: data.visibility, cw: data.cw ?? null, fileIds: fileIds ?? [], @@ -331,8 +330,6 @@ export class NoteDraftService { scheduledAt: data.scheduledAt ?? null, isActuallyScheduled: data.isActuallyScheduled ?? false, } satisfies MiNoteDraft; - - return appliedDraft; } @bindThis diff --git a/packages/backend/src/models/NoteDraft.ts b/packages/backend/src/models/NoteDraft.ts index 411374fc96..0ece02c943 100644 --- a/packages/backend/src/models/NoteDraft.ts +++ b/packages/backend/src/models/NoteDraft.ts @@ -162,12 +162,4 @@ export class MiNoteDraft { default: false, }) public isActuallyScheduled: boolean; - - constructor(data: Partial) { - if (data == null) return; - - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } } diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts index 5d18a2e42c..0529d0bcdc 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts @@ -162,7 +162,7 @@ export const paramDef = { fileIds: { type: 'array', uniqueItems: true, - minItems: 1, + minItems: 0, maxItems: 16, items: { type: 'string', format: 'misskey:id' }, }, @@ -186,7 +186,7 @@ export const paramDef = { scheduledAt: { type: 'integer', nullable: true }, isActuallyScheduled: { type: 'boolean', default: false }, }, - required: [], + required: ['visibility', 'visibleUserIds', 'cw', 'hashtag', 'localOnly', 'reactionAcceptance', 'replyId', 'renoteId', 'channelId', 'text', 'fileIds', 'poll', 'scheduledAt', 'isActuallyScheduled'], } as const; @Injectable() @@ -203,19 +203,19 @@ export default class extends Endpoint { // eslint- multiple: ps.poll.multiple ?? false, expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, expiredAfter: ps.poll.expiredAfter ?? null, - } : undefined, - text: ps.text ?? null, - replyId: ps.replyId ?? undefined, - renoteId: ps.renoteId ?? undefined, - cw: ps.cw ?? null, - ...(ps.hashtag ? { hashtag: ps.hashtag } : {}), + } : null, + text: ps.text, + replyId: ps.replyId, + renoteId: ps.renoteId, + cw: ps.cw, + hashtag: ps.hashtag, localOnly: ps.localOnly, reactionAcceptance: ps.reactionAcceptance, visibility: ps.visibility, - visibleUserIds: ps.visibleUserIds ?? [], - channelId: ps.channelId ?? undefined, + visibleUserIds: ps.visibleUserIds, + channelId: ps.channelId, scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, - isActuallyScheduled: ps.isActuallyScheduled ?? false, + isActuallyScheduled: ps.isActuallyScheduled, }).catch((err) => { if (err instanceof IdentifiableError) { switch (err.id) { diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts index 7457a82dee..9755c857f6 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts @@ -171,14 +171,14 @@ export const paramDef = { type: 'object', properties: { draftId: { type: 'string', nullable: false, format: 'misskey:id' }, - visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' }, + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'] }, visibleUserIds: { type: 'array', uniqueItems: true, items: { type: 'string', format: 'misskey:id', } }, cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, hashtag: { type: 'string', nullable: true, maxLength: 200 }, - localOnly: { type: 'boolean', default: false }, - reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, + localOnly: { type: 'boolean' }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'] }, replyId: { type: 'string', format: 'misskey:id', nullable: true }, renoteId: { type: 'string', format: 'misskey:id', nullable: true }, channelId: { type: 'string', format: 'misskey:id', nullable: true }, @@ -194,7 +194,7 @@ export const paramDef = { fileIds: { type: 'array', uniqueItems: true, - minItems: 1, + minItems: 0, maxItems: 16, items: { type: 'string', format: 'misskey:id' }, }, @@ -216,7 +216,7 @@ export const paramDef = { required: ['choices'], }, scheduledAt: { type: 'integer', nullable: true }, - isActuallyScheduled: { type: 'boolean', default: false }, + isActuallyScheduled: { type: 'boolean' }, }, required: ['draftId'], } as const; @@ -236,18 +236,18 @@ export default class extends Endpoint { // eslint- expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, expiredAfter: ps.poll.expiredAfter ?? null, } : undefined, - text: ps.text ?? null, - replyId: ps.replyId ?? undefined, - renoteId: ps.renoteId ?? undefined, - cw: ps.cw ?? null, + text: ps.text, + replyId: ps.replyId, + renoteId: ps.renoteId, + cw: ps.cw, ...(ps.hashtag ? { hashtag: ps.hashtag } : {}), localOnly: ps.localOnly, reactionAcceptance: ps.reactionAcceptance, visibility: ps.visibility, - visibleUserIds: ps.visibleUserIds ?? [], - channelId: ps.channelId ?? undefined, + visibleUserIds: ps.visibleUserIds, + channelId: ps.channelId, scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, - isActuallyScheduled: ps.isActuallyScheduled ?? false, + isActuallyScheduled: ps.isActuallyScheduled, }).catch((err) => { if (err instanceof IdentifiableError) { switch (err.id) { diff --git a/packages/frontend/src/components/MkNoteDraftsDialog.vue b/packages/frontend/src/components/MkNoteDraftsDialog.vue index 6e35ff82e2..4b2c2229b1 100644 --- a/packages/frontend/src/components/MkNoteDraftsDialog.vue +++ b/packages/frontend/src/components/MkNoteDraftsDialog.vue @@ -219,6 +219,16 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) { draftsPaginator.reload(); }); } + +async function cancelSchedule(draft: Misskey.entities.NoteDraft) { + os.apiWithDialog('notes/drafts/update', { + draftId: draft.id, + isActuallyScheduled: false, + scheduledAt: null, + }).then(() => { + scheduledPaginator.reload(); + }); +}