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 diff --git a/locales/index.d.ts b/locales/index.d.ts index 4071d5c373..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": { /** @@ -10328,6 +10332,10 @@ export interface Locale extends ILocale { * アンケートの結果が出ました */ "pollEnded": string; + /** + * 予約ノートが投稿されました + */ + "scheduledNotePosted": string; /** * 新しい投稿 */ @@ -12641,6 +12649,10 @@ export interface Locale extends ILocale { * 下書き一覧 */ "listDrafts": string; + /** + * 投稿予約 + */ + "schedule": string; }; /** * 二次元コード diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c0e598ef7b..514181b925 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1383,6 +1383,7 @@ customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カ themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。" thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!" createUserSpecifiedNote: "ユーザー指定ノートを作成" +schedulePost: "投稿を予約" _compression: _quality: @@ -2728,6 +2729,7 @@ _notification: youReceivedFollowRequest: "フォローリクエストが来ました" yourFollowRequestAccepted: "フォローリクエストが承認されました" pollEnded: "アンケートの結果が出ました" + scheduledNotePosted: "予約ノートが投稿されました" newNote: "新しい投稿" unreadAntennaNote: "アンテナ {name}" roleAssigned: "ロールが付与されました" @@ -3385,6 +3387,7 @@ _drafts: restoreFromDraft: "下書きから復元" restore: "復元" listDrafts: "下書き一覧" + schedule: "投稿予約" qr: "二次元コード" _qr: 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"`); + } +} diff --git a/packages/backend/src/core/NoteDraftService.ts b/packages/backend/src/core/NoteDraftService.ts index c43be96efa..2925aa9ea1 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; @@ -30,6 +31,7 @@ export type NoteDraftOptions = { hashtag?: string; channelId?: MiChannel['id'] | null; poll?: (IPoll & { expiredAfter?: number | null }) | null; + scheduledAt?: Date | null; }; @Injectable() @@ -56,6 +58,7 @@ export class NoteDraftService { private roleService: RoleService, private idService: IdService, private noteEntityService: NoteEntityService, + private queueService: QueueService, ) { } @@ -95,7 +98,11 @@ export class NoteDraftService { appliedDraft.id = this.idService.gen(); appliedDraft.userId = me.id; - const draft = this.noteDraftsRepository.save(appliedDraft); + const draft = await this.noteDraftsRepository.insertOne(appliedDraft); + + if (draft.scheduledAt) { + this.schedule(draft); + } return draft; } @@ -123,7 +130,18 @@ export class NoteDraftService { const appliedDraft = await this.checkAndSetDraftNoteOptions(me, draft, data); - return await this.noteDraftsRepository.save(appliedDraft); + await this.noteDraftsRepository.update(draftId, appliedDraft); + + this.clearSchedule(draft).then(() => { + if (appliedDraft.scheduledAt) { + this.schedule(draft); + } + }); + + return { + ...draft, + ...appliedDraft, + }; } @bindThis @@ -138,6 +156,8 @@ export class NoteDraftService { } await this.noteDraftsRepository.delete(draft.id); + + this.clearSchedule(draft); } @bindThis @@ -307,8 +327,40 @@ export class NoteDraftService { visibleUserIds: data.visibleUserIds ?? [], localOnly: data.localOnly, reactionAcceptance: data.reactionAcceptance, + scheduledAt: data.scheduledAt ?? null, } satisfies MiNoteDraft; return appliedDraft; } + + @bindThis + 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, + }, { + 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, + }, + }); + } + + @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(); + } + } + } } 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/core/entities/NoteDraftEntityService.ts b/packages/backend/src/core/entities/NoteDraftEntityService.ts index 3ef8cdaa12..cdbe2cf643 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?.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/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/NoteDraft.ts b/packages/backend/src/models/NoteDraft.ts index 6483748bc2..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,13 @@ export class MiNoteDraft { }) public pollExpiredAfter: number | null; - // ここまで追加 + //#endregion + + // 予約投稿 + @Column('timestamp with time zone', { + nullable: true, + }) + public scheduledAt: Date | null; constructor(data: Partial) { if (data == null) return; 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; diff --git a/packages/backend/src/models/json-schema/note-draft.ts b/packages/backend/src/models/json-schema/note-draft.ts index 504b263a6d..03c32b6548 100644 --- a/packages/backend/src/models/json-schema/note-draft.ts +++ b/packages/backend/src/models/json-schema/note-draft.ts @@ -167,5 +167,9 @@ export const packedNoteDraftSchema = { optional: false, nullable: true, enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null], }, + scheduledAt: { + type: 'number', + optional: false, nullable: true, + }, }, } as const; 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/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..82f1d029c7 --- /dev/null +++ b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts @@ -0,0 +1,47 @@ +/* + * 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 { NoteCreateService } from '@/core/NoteCreateService.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 noteCreateService: NoteCreateService, + 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.findOne({ where: { id: job.data.noteDraftId }, relations: ['user'] }); + if (draft == null || draft.user == null || draft.scheduledAt == null) { + return; + } + + const note = await this.noteCreateService.create(draft.user, draft); + + this.noteDraftsRepository.remove(draft); + + 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..b69699c338 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('queue: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/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/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) { 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/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 }} + 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/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/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index d07710fc67..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; @@ -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..74ea5fbe5f 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'; @@ -4453,6 +4462,7 @@ export type components = { localOnly?: boolean; /** @enum {string|null} */ reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null; + scheduledAt: number | null; }; NoteReaction: { /** Format: id */ @@ -4561,6 +4571,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; @@ -9553,7 +9571,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 +9758,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 +9826,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 +9889,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 +9902,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 +9992,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 +10056,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 +10120,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 +10184,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 +10251,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; }; }; @@ -11653,6 +11671,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'; @@ -25954,8 +25981,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')[]; }; }; }; @@ -26039,8 +26066,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')[]; }; }; }; @@ -27314,6 +27341,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'; @@ -29168,6 +29204,7 @@ export interface operations { expiresAt?: number | null; expiredAfter?: number | null; } | null; + scheduledAt?: number | null; }; }; }; @@ -29409,6 +29446,7 @@ export interface operations { expiresAt?: number | null; expiredAfter?: number | null; } | null; + scheduledAt?: number | null; }; }; }; diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 9afd1f8be6..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', @@ -233,6 +234,7 @@ export const rolePolicies = [ export const queueTypes = [ 'system', 'endedPollNotification', + 'postScheduledNote', 'deliver', 'inbox', 'db',