From 18920032e8b8990009521a31ff3ad1cfc8ce0a1e Mon Sep 17 00:00:00 2001 From: mattyatea Date: Mon, 6 Nov 2023 18:15:29 +0900 Subject: [PATCH 01/22] =?UTF-8?q?=E4=BA=88=E7=B4=84=E6=8A=95=E7=A8=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cherrypick from https://github.com/Type4ny-Project/Type4ny/commit/e133a6b6a4bee78c2598deaad087712b2e8e26ed # Conflicts: # locales/index.d.ts # misskey-assets # packages/backend/src/core/NoteCreateService.ts # packages/backend/src/core/QueueModule.ts # packages/backend/src/core/QueueService.ts # packages/backend/src/di-symbols.ts # packages/backend/src/models/_.ts # packages/backend/src/queue/types.ts # packages/backend/src/server/api/endpoints/admin/queue/stats.ts # packages/backend/src/server/web/ClientServerService.ts # packages/frontend/src/components/MkPostForm.vue Co-authored-by: まっちゃてぃー。 <56515516+mattyatea@users.noreply.github.com>Signed-off-by: mattyatea --- locales/index.d.ts | 4 + locales/ja-JP.yml | 1 + .../backend/src/core/NoteCreateService.ts | 3 +- packages/backend/src/core/QueueModule.ts | 12 + packages/backend/src/core/QueueService.ts | 2 + packages/backend/src/di-symbols.ts | 1 + packages/backend/src/models/NoteSchedule.ts | 29 ++ packages/backend/src/models/_.ts | 3 + .../backend/src/queue/QueueProcessorModule.ts | 2 + .../src/queue/QueueProcessorService.ts | 12 + packages/backend/src/queue/const.ts | 1 + .../ScheduleNotePostProcessorService.ts | 30 ++ packages/backend/src/queue/types.ts | 38 +- .../backend/src/server/api/EndpointsModule.ts | 4 + packages/backend/src/server/api/endpoints.ts | 2 + .../server/api/endpoints/admin/queue/stats.ts | 3 +- .../api/endpoints/notes/create-schedule.ts | 367 ++++++++++++++++++ .../src/server/web/ClientServerService.ts | 3 + .../cherrypick-js/etc/cherrypick-js.api.md | 14 +- .../src/autogen/apiClientJSDoc.ts | 11 + .../cherrypick-js/src/autogen/endpoint.ts | 3 + .../cherrypick-js/src/autogen/entities.ts | 1 + packages/cherrypick-js/src/autogen/types.ts | 103 +++++ packages/cherrypick-js/src/note.ts | 12 +- .../frontend/src/components/MkPostForm.vue | 19 +- .../src/components/MkScheduleEditor.vue | 58 +++ 26 files changed, 731 insertions(+), 7 deletions(-) create mode 100644 packages/backend/src/models/NoteSchedule.ts create mode 100644 packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts create mode 100644 packages/backend/src/server/api/endpoints/notes/create-schedule.ts create mode 100644 packages/frontend/src/components/MkScheduleEditor.vue diff --git a/locales/index.d.ts b/locales/index.d.ts index 8d1cc35645..5b5c46abcd 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2946,6 +2946,10 @@ export interface Locale extends ILocale { * アンケート */ "poll": string; + /** + * 予約 + */ + "schedule": string; /** * 内容を隠す */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 79a18a4a2e..7cec7014dd 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -731,6 +731,7 @@ invisibleNote: "非公開の投稿" enableInfiniteScroll: "自動でもっと見る" visibility: "公開範囲" poll: "アンケート" +schedule: "予約" useCw: "内容を隠す" enablePlayer: "プレイヤーを開く" disablePlayer: "プレイヤーを閉じる" diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 64a9d20918..c1b5cea6b0 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -63,6 +63,7 @@ import { UserBlockingService } from '@/core/UserBlockingService.js'; import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { MiNoteSchedule } from '@/models/_.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -139,6 +140,7 @@ type Option = { files?: MiDriveFile[] | null; poll?: IPoll | null; event?: IEvent | null; + schedule?: MiNoteSchedule | null; localOnly?: boolean | null; reactionAcceptance?: MiNote['reactionAcceptance']; disableRightClick?: boolean | null; @@ -400,7 +402,6 @@ export class NoteCreateService implements OnApplicationShutdown { if (mentionedUsers.length > 0 && mentionedUsers.length > (await this.roleService.getUserPolicies(user.id)).mentionLimit) { throw new IdentifiableError('9f466dab-c856-48cd-9e65-ff90ff750580', 'Note contains too many mentions'); } - const note = await this.insertNote(user, data, tags, emojis, mentionedUsers); setImmediate('post created', { signal: this.#shutdownController.signal }).then( diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index 98a25b0ff5..911df83822 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -17,11 +17,13 @@ import { RelationshipJobData, UserWebhookDeliverJobData, SystemWebhookDeliverJobData, + ScheduleNotePostJobData, } from '../queue/types.js'; import type { Provider } from '@nestjs/common'; export type SystemQueue = Bull.Queue>; export type EndedPollNotificationQueue = Bull.Queue; +export type ScheduleNotePostQueue = Bull.Queue; export type DeliverQueue = Bull.Queue; export type InboxQueue = Bull.Queue; export type DbQueue = Bull.Queue; @@ -42,6 +44,12 @@ const $endedPollNotification: Provider = { inject: [DI.config, DI.redisForJobQueue], }; +const $scheduleNotePost: Provider = { + provide: 'queue:scheduleNotePost', + useFactory: (config: Config, redisForJobQueue: Redis.Redis) => new Bull.Queue(QUEUE.SCHEDULE_NOTE_POST, baseQueueOptions(config, QUEUE.SCHEDULE_NOTE_POST, redisForJobQueue)), + inject: [DI.config, DI.redisForJobQueue], +}; + const $deliver: Provider = { provide: 'queue:deliver', useFactory: (config: Config, redisForJobQueue: Redis.Redis) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER, redisForJobQueue)), @@ -90,6 +98,7 @@ const $systemWebhookDeliver: Provider = { providers: [ $system, $endedPollNotification, + $scheduleNotePost, $deliver, $inbox, $db, @@ -101,6 +110,7 @@ const $systemWebhookDeliver: Provider = { exports: [ $system, $endedPollNotification, + $scheduleNotePost, $deliver, $inbox, $db, @@ -114,6 +124,7 @@ export class QueueModule implements OnApplicationShutdown { constructor( @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @@ -130,6 +141,7 @@ export class QueueModule implements OnApplicationShutdown { await Promise.all([ this.systemQueue.close(), this.endedPollNotificationQueue.close(), + this.scheduleNotePostQueue.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 5a6b4f0c68..87b90c41b9 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -33,6 +33,7 @@ import type { SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue, + ScheduleNotePostQueue, } from './QueueModule.js'; import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; @@ -45,6 +46,7 @@ export class QueueService { @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:scheduleNotePost') public ScheduleNotePostQueue: ScheduleNotePostQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 3b585c78d0..bb1dfd915f 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -19,6 +19,7 @@ export const DI = { //#region Repositories usersRepository: Symbol('usersRepository'), notesRepository: Symbol('notesRepository'), + noteScheduleRepository: Symbol('noteScheduleRepository'), abuseReportResolversRepository: Symbol('abuseReportResolversRepository'), announcementsRepository: Symbol('announcementsRepository'), announcementReadsRepository: Symbol('announcementReadsRepository'), diff --git a/packages/backend/src/models/NoteSchedule.ts b/packages/backend/src/models/NoteSchedule.ts new file mode 100644 index 0000000000..557133e8fa --- /dev/null +++ b/packages/backend/src/models/NoteSchedule.ts @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { noteVisibilities } from '@/types.js'; +import { MiNote } from '@/models/Note.js'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiChannel } from './Channel.js'; +import type { MiDriveFile } from './DriveFile.js'; + +@Entity('note_schedule') +export class MiNoteSchedule { + @PrimaryColumn(id()) + public noteId: MiNote['id']; + + @Column('string') + public note:{ apEmojis: any[] | undefined; visibility: any; apMentions: any[] | undefined; visibleUsers: MiUser[]; channel: null | MiChannel; poll: { multiple: any; choices: any; expiresAt: Date | null } | undefined; renote: null | MiNote; localOnly: any; cw: any; apHashtags: any[] | undefined; reactionAcceptance: any; files: MiDriveFile[]; text: any; reply: null | MiNote }; + + @Column('string') + public user: MiUser & {host: null;uri: null;}; + + @Column('timestamp with time zone', { + nullable: true, + }) + public expiresAt: Date; +} diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index a199efd94c..108c5d493b 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -86,6 +86,7 @@ import { MiFlash } from '@/models/Flash.js'; import { MiFlashLike } from '@/models/FlashLike.js'; import { MiFlashLikeRemote } from '@/models/FlashLikeRemote.js'; import { MiUserListFavorite } from '@/models/UserListFavorite.js'; +import { MiNoteSchedule } from './NoteSchedule.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; @@ -173,6 +174,7 @@ export { MiNoteThreadMuting, MiNoteUnread, MiOfficialTag, + MiNoteSchedule, MiPage, MiPageLike, MiPasswordResetRequest, @@ -253,6 +255,7 @@ export type NoteReactionsRepository = Repository & MiRepository< export type NoteThreadMutingsRepository = Repository & MiRepository; export type NoteUnreadsRepository = Repository & MiRepository; export type OfficialTagRepository = Repository & MiRepository; +export type NoteScheduleRepository = Repository; export type PagesRepository = Repository & MiRepository; export type PageLikesRepository = Repository & MiRepository; export type PasswordResetRequestsRepository = Repository & MiRepository; diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index cea0f46e46..076a1396c2 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 { ScheduleNotePostProcessorService } from './processors/ScheduleNotePostProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; @@ -77,6 +78,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor UserWebhookDeliverProcessorService, SystemWebhookDeliverProcessorService, EndedPollNotificationProcessorService, + ScheduleNotePostProcessorService, DeliverProcessorService, InboxProcessorService, AggregateRetentionProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 2fc82e2d16..fce2edbb19 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -14,6 +14,7 @@ import { bindThis } from '@/decorators.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; +import { ScheduleNotePostProcessorService } from './processors/ScheduleNotePostProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; @@ -84,6 +85,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private relationshipQueueWorker: Bull.Worker; private objectStorageQueueWorker: Bull.Worker; private endedPollNotificationQueueWorker: Bull.Worker; + private schedulerNotePostQueueWorker: Bull.Worker; constructor( @Inject(DI.config) @@ -96,6 +98,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService, private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, + private scheduleNotePostProcessorService: ScheduleNotePostProcessorService, private deliverProcessorService: DeliverProcessorService, private inboxProcessorService: InboxProcessorService, private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, @@ -494,6 +497,13 @@ export class QueueProcessorService implements OnApplicationShutdown { } //#endregion + //#region schedule note post + this.schedulerNotePostQueueWorker = new Bull.Worker(QUEUE.SCHEDULE_NOTE_POST, (job) => this.scheduleNotePostProcessorService.process(job), { + ...baseQueueOptions(this.config, QUEUE.SCHEDULE_NOTE_POST, this.redisForJobQueue), + autorun: false, + }); + //#endregion + //#region ended poll notification { this.endedPollNotificationQueueWorker = new Bull.Worker(QUEUE.ENDED_POLL_NOTIFICATION, (job) => { @@ -522,6 +532,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker.run(), this.objectStorageQueueWorker.run(), this.endedPollNotificationQueueWorker.run(), + this.schedulerNotePostQueueWorker.run(), ]); } @@ -537,6 +548,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker.close(), this.objectStorageQueueWorker.close(), this.endedPollNotificationQueueWorker.close(), + this.schedulerNotePostQueueWorker.close(), ]); } diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts index 488c76e024..14880f6ff1 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', + SCHEDULE_NOTE_POST: 'scheduleNotePost', DB: 'db', RELATIONSHIP: 'relationship', OBJECT_STORAGE: 'objectStorage', diff --git a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts new file mode 100644 index 0000000000..b4e3e0686f --- /dev/null +++ b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type Logger from '@/logger.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 { ScheduleNotePostJobData } from '../types.js'; + +@Injectable() +export class ScheduleNotePostProcessorService { + private logger: Logger; + + constructor( + private noteCreateService: NoteCreateService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('ended-poll-notification'); + } + + @bindThis + public async process(job: Bull.Job): Promise { + job.data.note.createdAt = new Date(); + await this.noteCreateService.create(job.data.user, job.data.note); + } +} diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 8c0a87bfe5..146c0d8824 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -6,10 +6,14 @@ import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiNote } from '@/models/Note.js'; -import type { MiUser } from '@/models/User.js'; +import type { MiLocalUser, MiUser } from '@/models/User.js'; import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import type { MiWebhook } from '@/models/Webhook.js'; import type { IActivity } from '@/core/activitypub/type.js'; +import { IPoll } from '@/models/Poll.js'; +import { MiNoteSchedule } from '@/models/NoteSchedule.js'; +import { MiChannel } from '@/models/Channel.js'; +import { MiApp } from '@/models/App.js'; import type httpSignature from '@peertube/http-signature'; export type DeliverJobData = { @@ -109,6 +113,38 @@ export type EndedPollNotificationJobData = { noteId: MiNote['id']; }; +export type ScheduleNotePostJobData = { + note: { + name?: string | null; + text?: string | null; + reply?: MiNote | null; + renote?: MiNote | null; + files?: MiDriveFile[] | null; + poll?: IPoll | null; + schedule?: MiNoteSchedule | null; + localOnly?: boolean | null; + reactionAcceptance?: MiNote['reactionAcceptance']; + cw?: string | null; + visibility?: string; + visibleUsers?: MinimumUser[] | null; + channel?: MiChannel | null; + apMentions?: MinimumUser[] | null; + apHashtags?: string[] | null; + apEmojis?: string[] | null; + uri?: string | null; + url?: string | null; + app?: MiApp | null; + }; + user: MiUser & {host: null, uri: null}; +} + +type MinimumUser = { + id: MiUser['id']; + host: MiUser['host']; + username: MiUser['username']; + uri: MiUser['uri']; +}; + export type SystemWebhookDeliverJobData = { type: string; content: unknown; diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 5e6867354f..a51d2ea2fd 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -285,6 +285,7 @@ import * as ep___notes_children from './endpoints/notes/children.js'; import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; +import * as ep___notes_schedule_create from './endpoints/notes/create-schedule.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; import * as ep___notes_update from './endpoints/notes/update.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; @@ -695,6 +696,7 @@ const $notes_children: Provider = { provide: 'ep:notes/children', useClass: ep__ const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes_clips.default }; const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default }; const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default }; +const $notes_schedule_create: Provider = { provide: 'ep:notes/create-schedule', useClass: ep___notes_schedule_create.default }; const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default }; const $notes_update: Provider = { provide: 'ep:notes/update', useClass: ep___notes_update.default }; const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default }; @@ -1110,6 +1112,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_clips, $notes_conversation, $notes_create, + $notes_schedule_create, $notes_delete, $notes_update, $notes_favorites_create, @@ -1517,6 +1520,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_clips, $notes_conversation, $notes_create, + $notes_schedule_create, $notes_delete, $notes_update, $notes_favorites_create, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 556a4e7e50..a856fc274b 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -290,6 +290,7 @@ import * as ep___notes_children from './endpoints/notes/children.js'; import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; +import * as ep___notes_schedule_create from './endpoints/notes/create-schedule.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; import * as ep___notes_update from './endpoints/notes/update.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; @@ -698,6 +699,7 @@ const eps = [ ['notes/clips', ep___notes_clips], ['notes/conversation', ep___notes_conversation], ['notes/create', ep___notes_create], + ['notes/create-schedule', ep___notes_schedule_create], ['notes/delete', ep___notes_delete], ['notes/update', ep___notes_update], ['notes/favorites/create', ep___notes_favorites_create], 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..8c600a3202 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, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue, ScheduleNotePostQueue } 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:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue, @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/notes/create-schedule.ts b/packages/backend/src/server/api/endpoints/notes/create-schedule.ts new file mode 100644 index 0000000000..7cc2d5ea84 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/create-schedule.ts @@ -0,0 +1,367 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { DataSource, In } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import { isPureRenote } from 'cherrypick-js/note.js'; +import type { MiUser } from '@/models/User.js'; +import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/_.js'; +import type { MiDriveFile } from '@/models/DriveFile.js'; +import type { MiNote } from '@/models/Note.js'; +import type { MiChannel } from '@/models/Channel.js'; +import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { NoteCreateService } from '@/core/NoteCreateService.js'; +import { DI } from '@/di-symbols.js'; +import { QueueService } from '@/core/QueueService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + + prohibitMoved: true, + + limit: { + duration: ms('1hour'), + max: 300, + }, + + kind: 'write:notes', + + errors: { + noSuchRenoteTarget: { + message: 'No such renote target.', + code: 'NO_SUCH_RENOTE_TARGET', + id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4', + }, + + cannotReRenote: { + message: 'You can not Renote a pure Renote.', + code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE', + id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a', + }, + + cannotRenoteDueToVisibility: { + message: 'You can not Renote due to target visibility.', + code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY', + id: 'be9529e9-fe72-4de0-ae43-0b363c4938af', + }, + + noSuchReplyTarget: { + message: 'No such reply target.', + code: 'NO_SUCH_REPLY_TARGET', + id: '749ee0f6-d3da-459a-bf02-282e2da4292c', + }, + + cannotReplyToPureRenote: { + message: 'You can not reply to a pure Renote.', + code: 'CANNOT_REPLY_TO_A_PURE_RENOTE', + id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15', + }, + + cannotCreateAlreadyExpiredPoll: { + message: 'Poll is already expired.', + code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', + id: '04da457d-b083-4055-9082-955525eda5a5', + }, + + cannotCreateAlreadyExpiredSchedule: { + message: 'Schedule is already expired.', + code: 'CANNOT_CREATE_ALREADY_EXPIRED_SCHEDULE', + id: '8a9bfb90-fc7e-4878-a3e8-d97faaf5fb07', + }, + + noSuchChannel: { + message: 'No such channel.', + code: 'NO_SUCH_CHANNEL', + id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb', + }, + noSuchSchedule: { + message: 'No such schedule.', + code: 'NO_SUCH_SCHEDULE', + id: '44dee229-8da1-4a61-856d-e3a4bbc12032', + }, + youHaveBeenBlocked: { + message: 'You have been blocked by this user.', + code: 'YOU_HAVE_BEEN_BLOCKED', + id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3', + }, + + noSuchFile: { + message: 'Some files are not found.', + code: 'NO_SUCH_FILE', + id: 'b6992544-63e7-67f0-fa7f-32444b1b5306', + }, + + cannotRenoteOutsideOfChannel: { + message: 'Cannot renote outside of channel.', + code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL', + id: '33510210-8452-094c-6227-4a6c05d99f00', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' }, + visibleUserIds: { type: 'array', uniqueItems: true, items: { + type: 'string', format: 'misskey:id', + } }, + cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, + localOnly: { type: 'boolean', default: false }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, + noExtractMentions: { type: 'boolean', default: false }, + noExtractHashtags: { type: 'boolean', default: false }, + noExtractEmojis: { type: 'boolean', default: false }, + replyId: { type: 'string', format: 'misskey:id', nullable: true }, + renoteId: { type: 'string', format: 'misskey:id', nullable: true }, + channelId: { type: 'string', format: 'misskey:id', nullable: true }, + + // anyOf内にバリデーションを書いても最初の一つしかチェックされない + // See https://github.com/misskey-dev/misskey/pull/10082 + text: { + type: 'string', + minLength: 1, + maxLength: MAX_NOTE_TEXT_LENGTH, + nullable: true, + }, + fileIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + mediaIds: { + type: 'array', + uniqueItems: true, + minItems: 1, + maxItems: 16, + items: { type: 'string', format: 'misskey:id' }, + }, + poll: { + type: 'object', + nullable: true, + properties: { + choices: { + type: 'array', + uniqueItems: true, + minItems: 2, + maxItems: 10, + items: { type: 'string', minLength: 1, maxLength: 50 }, + }, + multiple: { type: 'boolean' }, + expiresAt: { type: 'integer', nullable: true }, + expiredAfter: { type: 'integer', nullable: true, minimum: 1 }, + }, + required: ['choices'], + }, + schedule: { + type: 'object', + nullable: false, + properties: { + expiresAt: { type: 'integer', nullable: false }, + }, + }, + }, + // (re)note with text, files and poll are optional + anyOf: [ + { required: ['text'] }, + { required: ['renoteId'] }, + { required: ['fileIds'] }, + { required: ['mediaIds'] }, + { required: ['poll'] }, + { required: ['schedule'] }, + ], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + private noteEntityService: NoteEntityService, + private queueService: QueueService, + private noteCreateService: NoteCreateService, + ) { + super(meta, paramDef, async (ps, me) => { + let visibleUsers: MiUser[] = []; + if (ps.visibleUserIds) { + visibleUsers = await this.usersRepository.findBy({ + id: In(ps.visibleUserIds), + }); + } + + let files: MiDriveFile[] = []; + const fileIds = ps.fileIds ?? ps.mediaIds ?? null; + if (fileIds != null) { + files = await this.driveFilesRepository.createQueryBuilder('file') + .where('file.userId = :userId AND file.id IN (:...fileIds)', { + userId: me.id, + fileIds, + }) + .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') + .setParameters({ fileIds }) + .getMany(); + + if (files.length !== fileIds.length) { + throw new ApiError(meta.errors.noSuchFile); + } + } + + let renote: MiNote | null = null; + if (ps.renoteId != null) { + // Fetch renote to note + renote = await this.notesRepository.findOneBy({ id: ps.renoteId }); + + if (renote == null) { + throw new ApiError(meta.errors.noSuchRenoteTarget); + } else if (isPureRenote(renote)) { + throw new ApiError(meta.errors.cannotReRenote); + } + + // Check blocking + if (renote.userId !== me.id) { + const blockExist = await this.blockingsRepository.exist({ + where: { + blockerId: renote.userId, + blockeeId: me.id, + }, + }); + if (blockExist) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } + + if (renote.visibility === 'followers' && renote.userId !== me.id) { + // 他人のfollowers noteはreject + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } else if (renote.visibility === 'specified') { + // specified / direct noteはreject + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } + + if (renote.channelId && renote.channelId !== ps.channelId) { + // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック + // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する + const renoteChannel = await this.channelsRepository.findOneById(renote.channelId); + if (renoteChannel == null) { + // リノートしたいノートが書き込まれているチャンネルが無い + throw new ApiError(meta.errors.noSuchChannel); + } else if (!renoteChannel.allowRenoteToExternal) { + // リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合 + throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel); + } + } + } + + let reply: MiNote | null = null; + if (ps.replyId != null) { + // Fetch reply + reply = await this.notesRepository.findOneBy({ id: ps.replyId }); + + if (reply == null) { + throw new ApiError(meta.errors.noSuchReplyTarget); + } else if (isPureRenote(reply)) { + throw new ApiError(meta.errors.cannotReplyToPureRenote); + } + + // Check blocking + if (reply.userId !== me.id) { + const blockExist = await this.blockingsRepository.exist({ + where: { + blockerId: reply.userId, + blockeeId: me.id, + }, + }); + if (blockExist) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } + } + + if (ps.poll) { + if (typeof ps.poll.expiresAt === 'number') { + if (ps.poll.expiresAt < Date.now()) { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); + } + } else if (typeof ps.poll.expiredAfter === 'number') { + ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter; + } + } + + let channel: MiChannel | null = null; + if (ps.channelId != null) { + channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false }); + + if (channel == null) { + throw new ApiError(meta.errors.noSuchChannel); + } + } + if (ps.schedule) { + if (typeof ps.schedule.expiresAt === 'number') { + if (ps.schedule.expiresAt < Date.now()) { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredSchedule); + } + } + } + + const note = { + files: files, + poll: ps.poll ? { + choices: ps.poll.choices, + multiple: ps.poll.multiple ?? false, + expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, + } : undefined, + text: ps.text ?? undefined, + reply, + renote, + cw: ps.cw, + localOnly: ps.localOnly, + reactionAcceptance: ps.reactionAcceptance, + visibility: ps.visibility, + visibleUsers, + channel, + apMentions: ps.noExtractMentions ? [] : undefined, + apHashtags: ps.noExtractHashtags ? [] : undefined, + apEmojis: ps.noExtractEmojis ? [] : undefined, + }; + + if (ps.schedule && ps.schedule.expiresAt) { + const delay = new Date(ps.schedule.expiresAt).getTime() - Date.now(); + await this.queueService.ScheduleNotePostQueue.add(String(delay), { + note: note, + user: me, + }, { + delay, + removeOnComplete: true, + }); + } + + return ''; + }); + } +} diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 46a999218e..c0d5860638 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -31,6 +31,7 @@ import type { EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, + ScheduleNotePostQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue, @@ -116,6 +117,7 @@ export class ClientServerService { @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @@ -245,6 +247,7 @@ export class ClientServerService { queues: [ this.systemQueue, this.endedPollNotificationQueue, + this.scheduleNotePostQueue, this.deliverQueue, this.inboxQueue, this.dbQueue, diff --git a/packages/cherrypick-js/etc/cherrypick-js.api.md b/packages/cherrypick-js/etc/cherrypick-js.api.md index 002dd540ce..0bfb2f7973 100644 --- a/packages/cherrypick-js/etc/cherrypick-js.api.md +++ b/packages/cherrypick-js/etc/cherrypick-js.api.md @@ -1609,6 +1609,7 @@ declare namespace entities { NotesConversationResponse, NotesCreateRequest, NotesCreateResponse, + NotesCreateScheduleRequest, NotesDeleteRequest, NotesUpdateRequest, NotesFavoritesCreateRequest, @@ -2327,7 +2328,15 @@ type ISigninHistoryRequest = operations['i___signin-history']['requestBody']['co type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json']; // @public (undocumented) -function isPureRenote(note: Note): note is PureRenote; +function isPureRenote(note: { + renote?: object | null; + reply?: object | null; + text: string | null; + cw?: string | null; + fileIds?: string[]; + poll?: object | null; + event?: Record | null; +}): note is PureRenote; // @public (undocumented) type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json']; @@ -2659,6 +2668,9 @@ type NotesCreateRequest = operations['notes___create']['requestBody']['content'] // @public (undocumented) type NotesCreateResponse = operations['notes___create']['responses']['200']['content']['application/json']; +// @public (undocumented) +type NotesCreateScheduleRequest = operations['notes___create-schedule']['requestBody']['content']['application/json']; + // @public (undocumented) type NotesDeleteRequest = operations['notes___delete']['requestBody']['content']['application/json']; diff --git a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts index 30389fcfec..97b8ab3f9d 100644 --- a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts +++ b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts @@ -3111,6 +3111,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/cherrypick-js/src/autogen/endpoint.ts b/packages/cherrypick-js/src/autogen/endpoint.ts index 8eedc1e72e..15684d450c 100644 --- a/packages/cherrypick-js/src/autogen/endpoint.ts +++ b/packages/cherrypick-js/src/autogen/endpoint.ts @@ -413,6 +413,7 @@ import type { NotesConversationResponse, NotesCreateRequest, NotesCreateResponse, + NotesCreateScheduleRequest, NotesDeleteRequest, NotesUpdateRequest, NotesFavoritesCreateRequest, @@ -887,6 +888,7 @@ export type Endpoints = { 'notes/clips': { req: NotesClipsRequest; res: NotesClipsResponse }; 'notes/conversation': { req: NotesConversationRequest; res: NotesConversationResponse }; 'notes/create': { req: NotesCreateRequest; res: NotesCreateResponse }; + 'notes/create-schedule': { req: NotesCreateScheduleRequest; res: EmptyResponse }; 'notes/delete': { req: NotesDeleteRequest; res: EmptyResponse }; 'notes/update': { req: NotesUpdateRequest; res: EmptyResponse }; 'notes/favorites/create': { req: NotesFavoritesCreateRequest; res: EmptyResponse }; @@ -1296,6 +1298,7 @@ export const endpointReqTypes: Record | null, +}): note is PureRenote { return ( note.renote != null && note.reply == null && diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 5d5d94de4c..37bad6aa39 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -72,6 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only +
@@ -79,6 +80,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+ @@ -132,6 +134,7 @@ import { emojiPicker } from '@/scripts/emoji-picker.js'; import { vibrate } from '@/scripts/vibrate.js'; import * as sound from '@/scripts/sound.js'; import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js'; +import MkScheduleEditor from '@/components/MkScheduleEditor.vue'; const $i = signinRequired(); @@ -188,6 +191,9 @@ const event = ref<{ end: string | null; metadata: Record; } | null>(null); +let schedule = ref<{ + expiresAt: string | null; +}| null>(null); const useCw = ref(!!props.initialCw); const showPreview = ref(defaultStore.state.showPreview); const showProfilePreview = ref(defaultStore.state.showProfilePreview); @@ -437,6 +443,16 @@ function toggleEvent() { } } +function toggleSchedule() { + if (schedule.value) { + schedule.value = null; + } else { + schedule.value = { + expiresAt: null, + }; + } +} + function addTag(tag: string) { insertTextAtCursor(textareaEl.value, ` #${tag} `); } @@ -770,6 +786,7 @@ async function post(ev?: MouseEvent) { replyId: props.reply ? props.reply.id : undefined, renoteId: props.renote ? props.renote.id : quoteId.value ? quoteId.value : undefined, channelId: props.channel ? props.channel.id : undefined, + schedule: schedule.value ?? undefined, poll: poll.value, event: event.value, cw: useCw.value ? cw.value ?? '' : null, @@ -814,7 +831,7 @@ async function post(ev?: MouseEvent) { } posting.value = true; - misskeyApi(props.updateMode ? 'notes/update' : 'notes/create', postData, token).then(() => { + misskeyApi(props.updateMode ? 'notes/update' : (postData.schedule ? 'notes/create-schedule' : 'notes/create'), postData, token).then(() => { if (props.freezeAfterPosted) { posted.value = true; } else { diff --git a/packages/frontend/src/components/MkScheduleEditor.vue b/packages/frontend/src/components/MkScheduleEditor.vue new file mode 100644 index 0000000000..f6c1bdde46 --- /dev/null +++ b/packages/frontend/src/components/MkScheduleEditor.vue @@ -0,0 +1,58 @@ + + + + + From 3bc778799c847ae787495efbd29a0d3855f32c02 Mon Sep 17 00:00:00 2001 From: kozakura913 <98575220+kozakura913@users.noreply.github.com> Date: Sat, 28 Sep 2024 22:58:20 +0900 Subject: [PATCH 02/22] =?UTF-8?q?=E4=BA=88=E7=B4=84=E6=8A=95=E7=A8=BF?= =?UTF-8?q?=E3=82=92db=E3=81=AB=E4=BF=9D=E5=AD=98=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cherrypick from https://github.com/Type4ny-Project/Type4ny/commit/387faf55cf8b98918bc6c83fd8377d75408d7d1a # Conflicts: # package.json # packages/backend/src/models/RepositoryModule.ts # packages/backend/src/postgres.ts Co-authored-by: まっちゃてぃー。 <56515516+mattyatea@users.noreply.github.com>Signed-off-by: mattyatea --- .../migration/1699337454434-schedulenote.js | 11 ++++ packages/backend/src/models/NoteSchedule.ts | 15 +++--- .../backend/src/models/RepositoryModule.ts | 9 ++++ packages/backend/src/postgres.ts | 2 + .../ScheduleNotePostProcessorService.ts | 20 ++++++- packages/backend/src/queue/types.ts | 23 +------- .../api/endpoints/notes/create-schedule.ts | 52 ++++++++++++++++--- 7 files changed, 92 insertions(+), 40 deletions(-) create mode 100644 packages/backend/migration/1699337454434-schedulenote.js diff --git a/packages/backend/migration/1699337454434-schedulenote.js b/packages/backend/migration/1699337454434-schedulenote.js new file mode 100644 index 0000000000..213da993b0 --- /dev/null +++ b/packages/backend/migration/1699337454434-schedulenote.js @@ -0,0 +1,11 @@ +export class Schedulenote1699337454434 { + name = 'Schedulenote1699337454434' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "note_schedule" ("id" character varying(32) NOT NULL, "note" jsonb NOT NULL, "userId" character varying(260) NOT NULL, CONSTRAINT "PK_3a1ae2db41988f4994268218436" PRIMARY KEY ("id"))`); + } + + async down(queryRunner) { + await queryRunner.query(`DROP TABLE "note_schedule"`); + } +} diff --git a/packages/backend/src/models/NoteSchedule.ts b/packages/backend/src/models/NoteSchedule.ts index 557133e8fa..aec1bb52b9 100644 --- a/packages/backend/src/models/NoteSchedule.ts +++ b/packages/backend/src/models/NoteSchedule.ts @@ -14,16 +14,13 @@ import type { MiDriveFile } from './DriveFile.js'; @Entity('note_schedule') export class MiNoteSchedule { @PrimaryColumn(id()) - public noteId: MiNote['id']; + public id: string; - @Column('string') - public note:{ apEmojis: any[] | undefined; visibility: any; apMentions: any[] | undefined; visibleUsers: MiUser[]; channel: null | MiChannel; poll: { multiple: any; choices: any; expiresAt: Date | null } | undefined; renote: null | MiNote; localOnly: any; cw: any; apHashtags: any[] | undefined; reactionAcceptance: any; files: MiDriveFile[]; text: any; reply: null | MiNote }; + @Column('jsonb') + public note:{createdAt?: Date | undefined ; apEmojis: any[] | undefined; visibility: any; apMentions: any[] | undefined; visibleUsers: MiUser[]; channel: null | MiChannel; poll: { multiple: any; choices: any; expiresAt: Date | null } | undefined; renote: null | MiNote; localOnly: any; cw: any; apHashtags: any[] | undefined; reactionAcceptance: any; files: MiDriveFile[]; text: any; reply: null | MiNote }; - @Column('string') - public user: MiUser & {host: null;uri: null;}; - - @Column('timestamp with time zone', { - nullable: true, + @Column('varchar', { + length: 260, }) - public expiresAt: Date; + public userId: MiUser['id']; } diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 6534c8a959..d55cda0a74 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -86,6 +86,7 @@ import { MiUserSecurityKey, MiWebhook, MiOfficialTag, + MiNoteSchedule, } from './_.js'; import type { Provider } from '@nestjs/common'; import type { DataSource } from 'typeorm'; @@ -102,6 +103,12 @@ const $notesRepository: Provider = { inject: [DI.db], }; +const $noteScheduleRepository: Provider = { + provide: DI.noteScheduleRepository, + useFactory: (db: DataSource) => db.getRepository(MiNoteSchedule).extend(miRepository as MiRepository), + inject: [DI.db], +}; + const $announcementsRepository: Provider = { provide: DI.announcementsRepository, useFactory: (db: DataSource) => db.getRepository(MiAnnouncement).extend(miRepository as MiRepository), @@ -563,6 +570,7 @@ const $officialTagRepository: Provider = { providers: [ $usersRepository, $notesRepository, + $noteScheduleRepository, $announcementsRepository, $announcementReadsRepository, $appsRepository, @@ -643,6 +651,7 @@ const $officialTagRepository: Provider = { exports: [ $usersRepository, $notesRepository, + $noteScheduleRepository, $announcementsRepository, $announcementReadsRepository, $appsRepository, diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 70fb1e5a28..5f084167fd 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -87,6 +87,7 @@ import { MiFlashLikeRemote } from '@/models/FlashLikeRemote.js'; import { MiUserMemo } from '@/models/UserMemo.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; +import { MiNoteSchedule } from '@/models/NoteSchedule.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; @@ -166,6 +167,7 @@ export const entities = [ MiRenoteMuting, MiBlocking, MiNote, + MiNoteSchedule, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, diff --git a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts index b4e3e0686f..6e4432f9c3 100644 --- a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts +++ b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts @@ -7,6 +7,8 @@ import { Inject, Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; +import type { NoteScheduleRepository, UsersRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { ScheduleNotePostJobData } from '../types.js'; @@ -16,6 +18,12 @@ export class ScheduleNotePostProcessorService { private logger: Logger; constructor( + @Inject(DI.noteScheduleRepository) + private noteScheduleRepository: NoteScheduleRepository, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + private noteCreateService: NoteCreateService, private queueLoggerService: QueueLoggerService, ) { @@ -24,7 +32,15 @@ export class ScheduleNotePostProcessorService { @bindThis public async process(job: Bull.Job): Promise { - job.data.note.createdAt = new Date(); - await this.noteCreateService.create(job.data.user, job.data.note); + this.noteScheduleRepository.findOneBy({ id: job.data.scheduleNoteId }).then(async (data) => { + if (!data) { + this.logger.warn(`Schedule note ${job.data.scheduleNoteId} not found`); + } else { + data.note.createdAt = new Date(); + const me = await this.usersRepository.findOneByOrFail({ id: data.userId }); + await this.noteCreateService.create(me, data.note); + await this.noteScheduleRepository.remove(data); + } + }); } } diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 146c0d8824..c1f6c6532a 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -114,28 +114,7 @@ export type EndedPollNotificationJobData = { }; export type ScheduleNotePostJobData = { - note: { - name?: string | null; - text?: string | null; - reply?: MiNote | null; - renote?: MiNote | null; - files?: MiDriveFile[] | null; - poll?: IPoll | null; - schedule?: MiNoteSchedule | null; - localOnly?: boolean | null; - reactionAcceptance?: MiNote['reactionAcceptance']; - cw?: string | null; - visibility?: string; - visibleUsers?: MinimumUser[] | null; - channel?: MiChannel | null; - apMentions?: MinimumUser[] | null; - apHashtags?: string[] | null; - apEmojis?: string[] | null; - uri?: string | null; - url?: string | null; - app?: MiApp | null; - }; - user: MiUser & {host: null, uri: null}; + scheduleNoteId: MiNote['id']; } type MinimumUser = { diff --git a/packages/backend/src/server/api/endpoints/notes/create-schedule.ts b/packages/backend/src/server/api/endpoints/notes/create-schedule.ts index 7cc2d5ea84..9d749018a4 100644 --- a/packages/backend/src/server/api/endpoints/notes/create-schedule.ts +++ b/packages/backend/src/server/api/endpoints/notes/create-schedule.ts @@ -8,7 +8,14 @@ import { DataSource, In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { isPureRenote } from 'cherrypick-js/note.js'; import type { MiUser } from '@/models/User.js'; -import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/_.js'; +import type { + UsersRepository, + NotesRepository, + BlockingsRepository, + DriveFilesRepository, + ChannelsRepository, + NoteScheduleRepository, +} from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiNote } from '@/models/Note.js'; import type { MiChannel } from '@/models/Channel.js'; @@ -18,6 +25,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; import { DI } from '@/di-symbols.js'; import { QueueService } from '@/core/QueueService.js'; +import { MiNoteSchedule } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -194,6 +203,9 @@ export default class extends Endpoint { // eslint- @Inject(DI.notesRepository) private notesRepository: NotesRepository, + @Inject(DI.noteScheduleRepository) + private noteScheduleRepository: NoteScheduleRepository, + @Inject(DI.blockingsRepository) private blockingsRepository: BlockingsRepository, @@ -203,9 +215,8 @@ export default class extends Endpoint { // eslint- @Inject(DI.channelsRepository) private channelsRepository: ChannelsRepository, - private noteEntityService: NoteEntityService, private queueService: QueueService, - private noteCreateService: NoteCreateService, + private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { let visibleUsers: MiUser[] = []; @@ -328,8 +339,28 @@ export default class extends Endpoint { // eslint- } } } - - const note = { + type NoteType = { + createdAt?: Date | undefined; + apEmojis: any[] | undefined; + visibility: any; + apMentions: any[] | undefined; + visibleUsers: MiUser[]; + channel: null | MiChannel; + poll: { + multiple: any; + choices: any; + expiresAt: Date | null; + } | undefined; + renote: null | MiNote; + localOnly: any; + cw: any; + apHashtags: any[] | undefined; + reactionAcceptance: any; + files: MiDriveFile[]; + text: any; + reply: null | MiNote; + }; + const note:NoteType = { files: files, poll: ps.poll ? { choices: ps.poll.choices, @@ -351,10 +382,17 @@ export default class extends Endpoint { // eslint- }; if (ps.schedule && ps.schedule.expiresAt) { + me.token = null; + const noteId = this.idService.gen(new Date().getTime()); + await this.noteScheduleRepository.insert({ + id: noteId, + note: note, + userId: me.id, + }); + const delay = new Date(ps.schedule.expiresAt).getTime() - Date.now(); await this.queueService.ScheduleNotePostQueue.add(String(delay), { - note: note, - user: me, + scheduleNoteId: noteId, }, { delay, removeOnComplete: true, From 00283def9d049e50253758ca4ae16175609c8305 Mon Sep 17 00:00:00 2001 From: kozakura913 <98575220+kozakura913@users.noreply.github.com> Date: Sat, 28 Sep 2024 23:40:02 +0900 Subject: [PATCH 03/22] =?UTF-8?q?=E4=BA=88=E7=B4=84=E6=8A=95=E7=A8=BF?= =?UTF-8?q?=E3=81=AE=E4=B8=80=E8=A6=A7=E8=A1=A8=E7=A4=BA=E3=80=81=E5=89=8A?= =?UTF-8?q?=E9=99=A4=E3=82=92=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cherrypick from https://github.com/Type4ny-Project/Type4ny/commit/540f531b6d10e5419e63012ad63a637d4d7d084a # Conflicts: # locales/index.d.ts # packages/backend/src/server/api/EndpointsModule.ts # packages/backend/src/server/api/endpoints.ts # packages/frontend/src/components/MkNoteHeader.vue # packages/frontend/src/components/MkNoteSimple.vue # packages/frontend/src/components/MkPostForm.vue Co-authored-by: まっちゃてぃー。 <56515516+mattyatea@users.noreply.github.com>Signed-off-by: mattyatea --- locales/index.d.ts | 8 +- locales/ja-JP.yml | 3 +- .../migration/1699337454434-schedulenote.js | 11 -- .../migration/1699437894737-schedulenote.js | 12 ++ packages/backend/src/models/NoteSchedule.ts | 4 + .../backend/src/server/api/EndpointsModule.ts | 8 ++ packages/backend/src/server/api/endpoints.ts | 4 + .../api/endpoints/notes/create-schedule.ts | 4 +- .../api/endpoints/notes/delete-schedule.ts | 49 +++++++ .../api/endpoints/notes/list-schedule.ts | 61 +++++++++ .../cherrypick-js/etc/cherrypick-js.api.md | 8 ++ .../src/autogen/apiClientJSDoc.ts | 22 +++ .../cherrypick-js/src/autogen/endpoint.ts | 6 + .../cherrypick-js/src/autogen/entities.ts | 2 + packages/cherrypick-js/src/autogen/types.ts | 128 ++++++++++++++++++ .../frontend/src/components/MkNoteHeader.vue | 6 +- .../frontend/src/components/MkNoteSimple.vue | 29 +++- .../frontend/src/components/MkPostForm.vue | 9 +- .../components/MkSchedulePostListDialog.vue | 49 +++++++ packages/frontend/src/os.ts | 8 +- 20 files changed, 407 insertions(+), 24 deletions(-) delete mode 100644 packages/backend/migration/1699337454434-schedulenote.js create mode 100644 packages/backend/migration/1699437894737-schedulenote.js create mode 100644 packages/backend/src/server/api/endpoints/notes/delete-schedule.ts create mode 100644 packages/backend/src/server/api/endpoints/notes/list-schedule.ts create mode 100644 packages/frontend/src/components/MkSchedulePostListDialog.vue diff --git a/locales/index.d.ts b/locales/index.d.ts index 5b5c46abcd..5d1df697ab 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2947,13 +2947,17 @@ export interface Locale extends ILocale { */ "poll": string; /** - * 予約 + * 予約投稿 */ - "schedule": string; + "schedulePost": string; /** * 内容を隠す */ "useCw": string; + /** + * 予約投稿一覧 + */ + "schedulePostList": string; /** * プレイヤーを開く */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7cec7014dd..1a34d0a5e1 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -731,8 +731,9 @@ invisibleNote: "非公開の投稿" enableInfiniteScroll: "自動でもっと見る" visibility: "公開範囲" poll: "アンケート" -schedule: "予約" +schedulePost: "予約投稿" useCw: "内容を隠す" +schedulePostList: "予約投稿一覧" enablePlayer: "プレイヤーを開く" disablePlayer: "プレイヤーを閉じる" expandTweet: "ポストを展開する" diff --git a/packages/backend/migration/1699337454434-schedulenote.js b/packages/backend/migration/1699337454434-schedulenote.js deleted file mode 100644 index 213da993b0..0000000000 --- a/packages/backend/migration/1699337454434-schedulenote.js +++ /dev/null @@ -1,11 +0,0 @@ -export class Schedulenote1699337454434 { - name = 'Schedulenote1699337454434' - - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "note_schedule" ("id" character varying(32) NOT NULL, "note" jsonb NOT NULL, "userId" character varying(260) NOT NULL, CONSTRAINT "PK_3a1ae2db41988f4994268218436" PRIMARY KEY ("id"))`); - } - - async down(queryRunner) { - await queryRunner.query(`DROP TABLE "note_schedule"`); - } -} diff --git a/packages/backend/migration/1699437894737-schedulenote.js b/packages/backend/migration/1699437894737-schedulenote.js new file mode 100644 index 0000000000..d0188a45ee --- /dev/null +++ b/packages/backend/migration/1699437894737-schedulenote.js @@ -0,0 +1,12 @@ +export class Schedulenote1699437894737 { + name = 'Schedulenote1699437894737' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "note_schedule" ("id" character varying(32) NOT NULL, "note" jsonb NOT NULL, "userId" character varying(260) NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_3a1ae2db41988f4994268218436" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_e798958c40009bf0cdef4f28b5" ON "note_schedule" ("userId") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP TABLE "note_schedule"`); + } +} diff --git a/packages/backend/src/models/NoteSchedule.ts b/packages/backend/src/models/NoteSchedule.ts index aec1bb52b9..f622893ecf 100644 --- a/packages/backend/src/models/NoteSchedule.ts +++ b/packages/backend/src/models/NoteSchedule.ts @@ -19,8 +19,12 @@ export class MiNoteSchedule { @Column('jsonb') public note:{createdAt?: Date | undefined ; apEmojis: any[] | undefined; visibility: any; apMentions: any[] | undefined; visibleUsers: MiUser[]; channel: null | MiChannel; poll: { multiple: any; choices: any; expiresAt: Date | null } | undefined; renote: null | MiNote; localOnly: any; cw: any; apHashtags: any[] | undefined; reactionAcceptance: any; files: MiDriveFile[]; text: any; reply: null | MiNote }; + @Index() @Column('varchar', { length: 260, }) public userId: MiUser['id']; + + @Column('timestamp with time zone') + public expiresAt: Date; } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index a51d2ea2fd..5c89606051 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -286,8 +286,10 @@ import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; import * as ep___notes_schedule_create from './endpoints/notes/create-schedule.js'; +import * as ep___notes_schedule_list from './endpoints/notes/list-schedule.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; import * as ep___notes_update from './endpoints/notes/update.js'; +import * as ep___notes_schedule_delete from './endpoints/notes/delete-schedule.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; @@ -697,8 +699,10 @@ const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default }; const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default }; const $notes_schedule_create: Provider = { provide: 'ep:notes/create-schedule', useClass: ep___notes_schedule_create.default }; +const $notes_schedule_list: Provider = { provide: 'ep:notes/list-schedule', useClass: ep___notes_schedule_list.default }; const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default }; const $notes_update: Provider = { provide: 'ep:notes/update', useClass: ep___notes_update.default }; +const $notes_schedule_delete: Provider = { provide: 'ep:notes/delete-schedule', useClass: ep___notes_schedule_delete.default }; const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default }; const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default }; const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; @@ -1113,8 +1117,10 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_conversation, $notes_create, $notes_schedule_create, + $notes_schedule_list, $notes_delete, $notes_update, + $notes_schedule_delete, $notes_favorites_create, $notes_favorites_delete, $notes_featured, @@ -1521,8 +1527,10 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_conversation, $notes_create, $notes_schedule_create, + $notes_schedule_list, $notes_delete, $notes_update, + $notes_schedule_delete, $notes_favorites_create, $notes_favorites_delete, $notes_featured, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index a856fc274b..12e3a29fbb 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -291,8 +291,10 @@ import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; import * as ep___notes_schedule_create from './endpoints/notes/create-schedule.js'; +import * as ep___notes_schedule_list from './endpoints/notes/list-schedule.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; import * as ep___notes_update from './endpoints/notes/update.js'; +import * as ep___notes_schedule_delete from './endpoints/notes/delete-schedule.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; @@ -700,8 +702,10 @@ const eps = [ ['notes/conversation', ep___notes_conversation], ['notes/create', ep___notes_create], ['notes/create-schedule', ep___notes_schedule_create], + ['notes/list-schedule', ep___notes_schedule_list], ['notes/delete', ep___notes_delete], ['notes/update', ep___notes_update], + ['notes/delete-schedule', ep___notes_schedule_delete], ['notes/favorites/create', ep___notes_favorites_create], ['notes/favorites/delete', ep___notes_favorites_delete], ['notes/featured', ep___notes_featured], diff --git a/packages/backend/src/server/api/endpoints/notes/create-schedule.ts b/packages/backend/src/server/api/endpoints/notes/create-schedule.ts index 9d749018a4..9c7418e6b7 100644 --- a/packages/backend/src/server/api/endpoints/notes/create-schedule.ts +++ b/packages/backend/src/server/api/endpoints/notes/create-schedule.ts @@ -197,9 +197,6 @@ export default class extends Endpoint { // eslint- @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.db) - private db: DataSource, - @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -388,6 +385,7 @@ export default class extends Endpoint { // eslint- id: noteId, note: note, userId: me.id, + expiresAt: new Date(ps.schedule.expiresAt), }); const delay = new Date(ps.schedule.expiresAt).getTime() - Date.now(); diff --git a/packages/backend/src/server/api/endpoints/notes/delete-schedule.ts b/packages/backend/src/server/api/endpoints/notes/delete-schedule.ts new file mode 100644 index 0000000000..f9398576d3 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/delete-schedule.ts @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { Inject, Injectable } from '@nestjs/common'; +import type { NoteScheduleRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + + limit: { + duration: ms('1hour'), + max: 300, + }, + + errors: { + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: '490be23f-8c1f-4796-819f-94cb4f9d1630', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + noteId: { type: 'string', format: 'misskey:id' }, + }, + required: ['noteId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.noteScheduleRepository) + private noteScheduleRepository: NoteScheduleRepository, + ) { + super(meta, paramDef, async (ps, me) => { + await this.noteScheduleRepository.delete({ id: ps.noteId }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/list-schedule.ts b/packages/backend/src/server/api/endpoints/notes/list-schedule.ts new file mode 100644 index 0000000000..8cd670e421 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/list-schedule.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import type { NoteScheduleRepository } from '@/models/_.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Note', + }, + }, + limit: { + duration: ms('1hour'), + max: 300, + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.noteScheduleRepository) + private noteScheduleRepository: NoteScheduleRepository, + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const scheduleNotes = await this.noteScheduleRepository.findBy({ userId: me.id }); + const user = await this.userEntityService.pack(me, me); + scheduleNotes.forEach((item: any) => { + item.note.user = user; + item.note.createdAt = new Date(item.expiresAt); + item.note.isSchedule = true; + item.note.id = item.id; + }); + return scheduleNotes; + }); + } +} diff --git a/packages/cherrypick-js/etc/cherrypick-js.api.md b/packages/cherrypick-js/etc/cherrypick-js.api.md index 0bfb2f7973..4cbc80ed59 100644 --- a/packages/cherrypick-js/etc/cherrypick-js.api.md +++ b/packages/cherrypick-js/etc/cherrypick-js.api.md @@ -1610,8 +1610,10 @@ declare namespace entities { NotesCreateRequest, NotesCreateResponse, NotesCreateScheduleRequest, + NotesListScheduleResponse, NotesDeleteRequest, NotesUpdateRequest, + NotesDeleteScheduleRequest, NotesFavoritesCreateRequest, NotesFavoritesDeleteRequest, NotesFeaturedRequest, @@ -2674,6 +2676,9 @@ type NotesCreateScheduleRequest = operations['notes___create-schedule']['request // @public (undocumented) type NotesDeleteRequest = operations['notes___delete']['requestBody']['content']['application/json']; +// @public (undocumented) +type NotesDeleteScheduleRequest = operations['notes___delete-schedule']['requestBody']['content']['application/json']; + // @public (undocumented) type NotesEventsSearchRequest = operations['notes___events___search']['requestBody']['content']['application/json']; @@ -2704,6 +2709,9 @@ type NotesHybridTimelineRequest = operations['notes___hybrid-timeline']['request // @public (undocumented) type NotesHybridTimelineResponse = operations['notes___hybrid-timeline']['responses']['200']['content']['application/json']; +// @public (undocumented) +type NotesListScheduleResponse = operations['notes___list-schedule']['responses']['200']['content']['application/json']; + // @public (undocumented) type NotesLocalTimelineRequest = operations['notes___local-timeline']['requestBody']['content']['application/json']; diff --git a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts index 97b8ab3f9d..a302794dbe 100644 --- a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts +++ b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts @@ -3122,6 +3122,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * @@ -3144,6 +3155,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/cherrypick-js/src/autogen/endpoint.ts b/packages/cherrypick-js/src/autogen/endpoint.ts index 15684d450c..2d44f99550 100644 --- a/packages/cherrypick-js/src/autogen/endpoint.ts +++ b/packages/cherrypick-js/src/autogen/endpoint.ts @@ -414,8 +414,10 @@ import type { NotesCreateRequest, NotesCreateResponse, NotesCreateScheduleRequest, + NotesListScheduleResponse, NotesDeleteRequest, NotesUpdateRequest, + NotesDeleteScheduleRequest, NotesFavoritesCreateRequest, NotesFavoritesDeleteRequest, NotesFeaturedRequest, @@ -889,8 +891,10 @@ export type Endpoints = { 'notes/conversation': { req: NotesConversationRequest; res: NotesConversationResponse }; 'notes/create': { req: NotesCreateRequest; res: NotesCreateResponse }; 'notes/create-schedule': { req: NotesCreateScheduleRequest; res: EmptyResponse }; + 'notes/list-schedule': { req: EmptyRequest; res: NotesListScheduleResponse }; 'notes/delete': { req: NotesDeleteRequest; res: EmptyResponse }; 'notes/update': { req: NotesUpdateRequest; res: EmptyResponse }; + 'notes/delete-schedule': { req: NotesDeleteScheduleRequest; res: EmptyResponse }; 'notes/favorites/create': { req: NotesFavoritesCreateRequest; res: EmptyResponse }; 'notes/favorites/delete': { req: NotesFavoritesDeleteRequest; res: EmptyResponse }; 'notes/featured': { req: NotesFeaturedRequest; res: NotesFeaturedResponse }; @@ -1299,8 +1303,10 @@ export const endpointReqTypes: Record
+ @@ -63,7 +64,8 @@ import MkInstanceTicker from '@/components/MkInstanceTicker.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; const props = defineProps<{ - note: Misskey.entities.Note; + note: Misskey.entities.Note & {isSchedule? : boolean}; + scheduled?: boolean; notificationId?: string; }>(); @@ -74,7 +76,7 @@ const router = useRouter(); function showOnRemote() { if (props.note.user.instance === undefined) router.push(notePage(props.note)); - + else window.open(props.note.url ?? props.note.uri, '_blank', 'noopener'); } diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index 07260fefe3..1414b7f6dd 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> - + From cd42e21e33ba1b28b82f6cfecf072ebdd59c597b Mon Sep 17 00:00:00 2001 From: kozakura913 <98575220+kozakura913@users.noreply.github.com> Date: Sun, 29 Sep 2024 21:31:33 +0900 Subject: [PATCH 07/22] =?UTF-8?q?=E4=BA=88=E7=B4=84=E6=8A=95=E7=A8=BF?= =?UTF-8?q?=E3=81=AE=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=82=92=E7=A7=BB=E5=8B=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/server/api/EndpointsModule.ts | 12 +-- packages/backend/src/server/api/endpoints.ts | 12 +-- .../create.ts} | 2 +- .../delete.ts} | 1 + .../{list-schedule.ts => schedule/list.ts} | 0 .../cherrypick-js/etc/cherrypick-js.api.md | 32 ++++---- .../src/autogen/apiClientJSDoc.ts | 12 +-- .../cherrypick-js/src/autogen/endpoint.ts | 20 ++--- .../cherrypick-js/src/autogen/entities.ts | 8 +- packages/cherrypick-js/src/autogen/types.ts | 76 +++++++++---------- .../frontend/src/components/MkNoteSimple.vue | 4 +- .../frontend/src/components/MkPostForm.vue | 2 +- .../components/MkSchedulePostListDialog.vue | 2 +- 13 files changed, 92 insertions(+), 91 deletions(-) rename packages/backend/src/server/api/endpoints/notes/{create-schedule.ts => schedule/create.ts} (99%) rename packages/backend/src/server/api/endpoints/notes/{delete-schedule.ts => schedule/delete.ts} (98%) rename packages/backend/src/server/api/endpoints/notes/{list-schedule.ts => schedule/list.ts} (100%) diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 5c89606051..61dd7a70de 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -285,11 +285,11 @@ import * as ep___notes_children from './endpoints/notes/children.js'; import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; -import * as ep___notes_schedule_create from './endpoints/notes/create-schedule.js'; -import * as ep___notes_schedule_list from './endpoints/notes/list-schedule.js'; +import * as ep___notes_schedule_create from './endpoints/notes/schedule/create.js'; +import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js'; +import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; import * as ep___notes_update from './endpoints/notes/update.js'; -import * as ep___notes_schedule_delete from './endpoints/notes/delete-schedule.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; @@ -698,11 +698,11 @@ const $notes_children: Provider = { provide: 'ep:notes/children', useClass: ep__ const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes_clips.default }; const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default }; const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default }; -const $notes_schedule_create: Provider = { provide: 'ep:notes/create-schedule', useClass: ep___notes_schedule_create.default }; -const $notes_schedule_list: Provider = { provide: 'ep:notes/list-schedule', useClass: ep___notes_schedule_list.default }; +const $notes_schedule_create: Provider = { provide: 'ep:notes/schedule/create', useClass: ep___notes_schedule_create.default }; +const $notes_schedule_list: Provider = { provide: 'ep:notes/schedule/list', useClass: ep___notes_schedule_list.default }; +const $notes_schedule_delete: Provider = { provide: 'ep:notes/schedule/delete', useClass: ep___notes_schedule_delete.default }; const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default }; const $notes_update: Provider = { provide: 'ep:notes/update', useClass: ep___notes_update.default }; -const $notes_schedule_delete: Provider = { provide: 'ep:notes/delete-schedule', useClass: ep___notes_schedule_delete.default }; const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default }; const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default }; const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 12e3a29fbb..31653f37e3 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -290,11 +290,11 @@ import * as ep___notes_children from './endpoints/notes/children.js'; import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; -import * as ep___notes_schedule_create from './endpoints/notes/create-schedule.js'; -import * as ep___notes_schedule_list from './endpoints/notes/list-schedule.js'; +import * as ep___notes_schedule_create from './endpoints/notes/schedule/create.js'; +import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js'; +import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; import * as ep___notes_update from './endpoints/notes/update.js'; -import * as ep___notes_schedule_delete from './endpoints/notes/delete-schedule.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; @@ -701,11 +701,11 @@ const eps = [ ['notes/clips', ep___notes_clips], ['notes/conversation', ep___notes_conversation], ['notes/create', ep___notes_create], - ['notes/create-schedule', ep___notes_schedule_create], - ['notes/list-schedule', ep___notes_schedule_list], + ['notes/schedule/create', ep___notes_schedule_create], + ['notes/schedule/list', ep___notes_schedule_list], + ['notes/schedule/delete', ep___notes_schedule_delete], ['notes/delete', ep___notes_delete], ['notes/update', ep___notes_update], - ['notes/delete-schedule', ep___notes_schedule_delete], ['notes/favorites/create', ep___notes_favorites_create], ['notes/favorites/delete', ep___notes_favorites_delete], ['notes/featured', ep___notes_featured], diff --git a/packages/backend/src/server/api/endpoints/notes/create-schedule.ts b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts similarity index 99% rename from packages/backend/src/server/api/endpoints/notes/create-schedule.ts rename to packages/backend/src/server/api/endpoints/notes/schedule/create.ts index 9c7418e6b7..15f313458d 100644 --- a/packages/backend/src/server/api/endpoints/notes/create-schedule.ts +++ b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts @@ -27,7 +27,7 @@ import { DI } from '@/di-symbols.js'; import { QueueService } from '@/core/QueueService.js'; import { MiNoteSchedule } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; -import { ApiError } from '../../error.js'; +import { ApiError } from '../../../error.js'; export const meta = { tags: ['notes'], diff --git a/packages/backend/src/server/api/endpoints/notes/delete-schedule.ts b/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts similarity index 98% rename from packages/backend/src/server/api/endpoints/notes/delete-schedule.ts rename to packages/backend/src/server/api/endpoints/notes/schedule/delete.ts index f9398576d3..bcc05bb84d 100644 --- a/packages/backend/src/server/api/endpoints/notes/delete-schedule.ts +++ b/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts @@ -13,6 +13,7 @@ export const meta = { tags: ['notes'], requireCredential: true, + kind: 'write:notes', limit: { duration: ms('1hour'), diff --git a/packages/backend/src/server/api/endpoints/notes/list-schedule.ts b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts similarity index 100% rename from packages/backend/src/server/api/endpoints/notes/list-schedule.ts rename to packages/backend/src/server/api/endpoints/notes/schedule/list.ts diff --git a/packages/cherrypick-js/etc/cherrypick-js.api.md b/packages/cherrypick-js/etc/cherrypick-js.api.md index 9470b7cade..23c8b0ec07 100644 --- a/packages/cherrypick-js/etc/cherrypick-js.api.md +++ b/packages/cherrypick-js/etc/cherrypick-js.api.md @@ -1609,12 +1609,12 @@ declare namespace entities { NotesConversationResponse, NotesCreateRequest, NotesCreateResponse, - NotesCreateScheduleRequest, - NotesListScheduleRequest, - NotesListScheduleResponse, + NotesScheduleCreateRequest, + NotesScheduleListRequest, + NotesScheduleListResponse, + NotesScheduleDeleteRequest, NotesDeleteRequest, NotesUpdateRequest, - NotesDeleteScheduleRequest, NotesFavoritesCreateRequest, NotesFavoritesDeleteRequest, NotesFeaturedRequest, @@ -2671,15 +2671,9 @@ type NotesCreateRequest = operations['notes___create']['requestBody']['content'] // @public (undocumented) type NotesCreateResponse = operations['notes___create']['responses']['200']['content']['application/json']; -// @public (undocumented) -type NotesCreateScheduleRequest = operations['notes___create-schedule']['requestBody']['content']['application/json']; - // @public (undocumented) type NotesDeleteRequest = operations['notes___delete']['requestBody']['content']['application/json']; -// @public (undocumented) -type NotesDeleteScheduleRequest = operations['notes___delete-schedule']['requestBody']['content']['application/json']; - // @public (undocumented) type NotesEventsSearchRequest = operations['notes___events___search']['requestBody']['content']['application/json']; @@ -2710,12 +2704,6 @@ type NotesHybridTimelineRequest = operations['notes___hybrid-timeline']['request // @public (undocumented) type NotesHybridTimelineResponse = operations['notes___hybrid-timeline']['responses']['200']['content']['application/json']; -// @public (undocumented) -type NotesListScheduleRequest = operations['notes___list-schedule']['requestBody']['content']['application/json']; - -// @public (undocumented) -type NotesListScheduleResponse = operations['notes___list-schedule']['responses']['200']['content']['application/json']; - // @public (undocumented) type NotesLocalTimelineRequest = operations['notes___local-timeline']['requestBody']['content']['application/json']; @@ -2767,6 +2755,18 @@ type NotesRequest = operations['notes']['requestBody']['content']['application/j // @public (undocumented) type NotesResponse = operations['notes']['responses']['200']['content']['application/json']; +// @public (undocumented) +type NotesScheduleCreateRequest = operations['notes___schedule___create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesScheduleDeleteRequest = operations['notes___schedule___delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesScheduleListRequest = operations['notes___schedule___list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type NotesScheduleListResponse = operations['notes___schedule___list']['responses']['200']['content']['application/json']; + // @public (undocumented) type NotesSearchByTagRequest = operations['notes___search-by-tag']['requestBody']['content']['application/json']; diff --git a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts index 3028d67f71..c5732953e2 100644 --- a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts +++ b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts @@ -3116,7 +3116,7 @@ declare module '../api.js' { * * **Credential required**: *Yes* / **Permission**: *write:notes* */ - request( + request( endpoint: E, params: P, credential?: string | null, @@ -3127,7 +3127,7 @@ declare module '../api.js' { * * **Credential required**: *Yes* / **Permission**: *read:account* */ - request( + request( endpoint: E, params: P, credential?: string | null, @@ -3138,7 +3138,7 @@ declare module '../api.js' { * * **Credential required**: *Yes* / **Permission**: *write:notes* */ - request( + request( endpoint: E, params: P, credential?: string | null, @@ -3149,7 +3149,7 @@ declare module '../api.js' { * * **Credential required**: *Yes* / **Permission**: *write:notes* */ - request( + request( endpoint: E, params: P, credential?: string | null, @@ -3158,9 +3158,9 @@ declare module '../api.js' { /** * No description provided. * - * **Credential required**: *Yes* + * **Credential required**: *Yes* / **Permission**: *write:notes* */ - request( + request( endpoint: E, params: P, credential?: string | null, diff --git a/packages/cherrypick-js/src/autogen/endpoint.ts b/packages/cherrypick-js/src/autogen/endpoint.ts index 88efd0f4d0..1f4e3aa5df 100644 --- a/packages/cherrypick-js/src/autogen/endpoint.ts +++ b/packages/cherrypick-js/src/autogen/endpoint.ts @@ -413,12 +413,12 @@ import type { NotesConversationResponse, NotesCreateRequest, NotesCreateResponse, - NotesCreateScheduleRequest, - NotesListScheduleRequest, - NotesListScheduleResponse, + NotesScheduleCreateRequest, + NotesScheduleListRequest, + NotesScheduleListResponse, + NotesScheduleDeleteRequest, NotesDeleteRequest, NotesUpdateRequest, - NotesDeleteScheduleRequest, NotesFavoritesCreateRequest, NotesFavoritesDeleteRequest, NotesFeaturedRequest, @@ -891,11 +891,11 @@ export type Endpoints = { 'notes/clips': { req: NotesClipsRequest; res: NotesClipsResponse }; 'notes/conversation': { req: NotesConversationRequest; res: NotesConversationResponse }; 'notes/create': { req: NotesCreateRequest; res: NotesCreateResponse }; - 'notes/create-schedule': { req: NotesCreateScheduleRequest; res: EmptyResponse }; - 'notes/list-schedule': { req: NotesListScheduleRequest; res: NotesListScheduleResponse }; + 'notes/schedule/create': { req: NotesScheduleCreateRequest; res: EmptyResponse }; + 'notes/schedule/list': { req: NotesScheduleListRequest; res: NotesScheduleListResponse }; + 'notes/schedule/delete': { req: NotesScheduleDeleteRequest; res: EmptyResponse }; 'notes/delete': { req: NotesDeleteRequest; res: EmptyResponse }; 'notes/update': { req: NotesUpdateRequest; res: EmptyResponse }; - 'notes/delete-schedule': { req: NotesDeleteScheduleRequest; res: EmptyResponse }; 'notes/favorites/create': { req: NotesFavoritesCreateRequest; res: EmptyResponse }; 'notes/favorites/delete': { req: NotesFavoritesDeleteRequest; res: EmptyResponse }; 'notes/featured': { req: NotesFeaturedRequest; res: NotesFeaturedResponse }; @@ -1303,11 +1303,11 @@ export const endpointReqTypes: Record(); async function deleteScheduleNote() { - await os.apiWithDialog('notes/delete-schedule', { noteId: props.note.id }) + await os.apiWithDialog('notes/schedule/delete', { noteId: props.note.id }) .then(() => { isDeleted.value = true; }); } async function editScheduleNote() { - await misskeyApi('notes/delete-schedule', { noteId: props.note.id }) + await misskeyApi('notes/schedule/delete', { noteId: props.note.id }) .then(() => { isDeleted.value = true; }); diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 83fa076e3a..b07385e74a 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -834,7 +834,7 @@ async function post(ev?: MouseEvent) { } posting.value = true; - misskeyApi(props.updateMode ? 'notes/update' : (postData.schedule ? 'notes/create-schedule' : 'notes/create'), postData, token).then(() => { + misskeyApi(props.updateMode ? 'notes/update' : (postData.schedule ? 'notes/schedule/create' : 'notes/create'), postData, token).then(() => { if (props.freezeAfterPosted) { posted.value = true; } else { diff --git a/packages/frontend/src/components/MkSchedulePostListDialog.vue b/packages/frontend/src/components/MkSchedulePostListDialog.vue index 5ad15f06e2..efa1e07f55 100644 --- a/packages/frontend/src/components/MkSchedulePostListDialog.vue +++ b/packages/frontend/src/components/MkSchedulePostListDialog.vue @@ -50,7 +50,7 @@ const cancel = () => { }; const paginationEl = ref(); const pagination: Paging = { - endpoint: 'notes/list-schedule', + endpoint: 'notes/schedule/list', limit: 10, }; From 38e9c51f1f8408001d9a78607dec754b9941b9cc Mon Sep 17 00:00:00 2001 From: kozakura913 <98575220+kozakura913@users.noreply.github.com> Date: Sun, 29 Sep 2024 23:16:10 +0900 Subject: [PATCH 08/22] =?UTF-8?q?=E4=BA=88=E7=B4=84=E6=8A=95=E7=A8=BFAPI?= =?UTF-8?q?=E3=81=AE=E5=9E=8B=E3=82=92=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration/1699437894737-schedulenote.js | 5 +++ packages/backend/src/models/NoteSchedule.ts | 22 +++++++++- .../ScheduleNotePostProcessorService.ts | 1 - .../api/endpoints/notes/schedule/create.ts | 37 +++++++--------- .../api/endpoints/notes/schedule/list.ts | 44 ++++++++++++------- packages/cherrypick-js/src/autogen/types.ts | 25 ++++++----- 6 files changed, 84 insertions(+), 50 deletions(-) diff --git a/packages/backend/migration/1699437894737-schedulenote.js b/packages/backend/migration/1699437894737-schedulenote.js index d0188a45ee..d3e6c42eb1 100644 --- a/packages/backend/migration/1699437894737-schedulenote.js +++ b/packages/backend/migration/1699437894737-schedulenote.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + export class Schedulenote1699437894737 { name = 'Schedulenote1699437894737' diff --git a/packages/backend/src/models/NoteSchedule.ts b/packages/backend/src/models/NoteSchedule.ts index f622893ecf..01268bfa1f 100644 --- a/packages/backend/src/models/NoteSchedule.ts +++ b/packages/backend/src/models/NoteSchedule.ts @@ -17,7 +17,27 @@ export class MiNoteSchedule { public id: string; @Column('jsonb') - public note:{createdAt?: Date | undefined ; apEmojis: any[] | undefined; visibility: any; apMentions: any[] | undefined; visibleUsers: MiUser[]; channel: null | MiChannel; poll: { multiple: any; choices: any; expiresAt: Date | null } | undefined; renote: null | MiNote; localOnly: any; cw: any; apHashtags: any[] | undefined; reactionAcceptance: any; files: MiDriveFile[]; text: any; reply: null | MiNote }; + public note:{ + id: MiNote['id']; + apEmojis?: any[]; + visibility: 'public' | 'home' | 'followers' | 'specified'; + apMentions?: any[]; + visibleUsers: MiUser[]; + channel: null | MiChannel; + poll: { + multiple: boolean; + choices: string[]; + expiresAt: Date | null + } | undefined; + renote: null | MiNote; + localOnly: boolean; + cw?: string|null; + apHashtags?: string[]; + reactionAcceptance: 'likeOnly'|'likeOnlyForRemote'| 'nonSensitiveOnly'| 'nonSensitiveOnlyForLocalLikeOnlyForRemote'| null; + files: MiDriveFile[]; + text: string; + reply: null | MiNote + }; @Index() @Column('varchar', { diff --git a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts index d1312bea8e..793f19e265 100644 --- a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts +++ b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts @@ -36,7 +36,6 @@ export class ScheduleNotePostProcessorService { if (!data) { this.logger.warn(`Schedule note ${job.data.scheduleNoteId} not found`); } else { - data.note.createdAt = new Date(); const me = await this.usersRepository.findOneByOrFail({ id: data.userId }); await this.noteCreateService.create(me, data.note); await this.noteScheduleRepository.remove(data); diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts index 15f313458d..77932c9ad4 100644 --- a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts @@ -4,7 +4,7 @@ */ import ms from 'ms'; -import { DataSource, In } from 'typeorm'; +import { In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import { isPureRenote } from 'cherrypick-js/note.js'; import type { MiUser } from '@/models/User.js'; @@ -21,11 +21,8 @@ import type { MiNote } from '@/models/Note.js'; import type { MiChannel } from '@/models/Channel.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { NoteCreateService } from '@/core/NoteCreateService.js'; import { DI } from '@/di-symbols.js'; import { QueueService } from '@/core/QueueService.js'; -import { MiNoteSchedule } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { ApiError } from '../../../error.js'; @@ -124,7 +121,6 @@ export const paramDef = { type: 'string', format: 'misskey:id', } }, cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, - localOnly: { type: 'boolean', default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, noExtractMentions: { type: 'boolean', default: false }, noExtractHashtags: { type: 'boolean', default: false }, @@ -187,8 +183,8 @@ export const paramDef = { { required: ['fileIds'] }, { required: ['mediaIds'] }, { required: ['poll'] }, - { required: ['schedule'] }, ], + required: ['schedule'], } as const; @Injectable() @@ -329,35 +325,34 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchChannel); } } - if (ps.schedule) { - if (typeof ps.schedule.expiresAt === 'number') { - if (ps.schedule.expiresAt < Date.now()) { - throw new ApiError(meta.errors.cannotCreateAlreadyExpiredSchedule); - } + if (typeof ps.schedule.expiresAt === 'number') { + if (ps.schedule.expiresAt < Date.now()) { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredSchedule); } } type NoteType = { - createdAt?: Date | undefined; + id:MiNote['id']; apEmojis: any[] | undefined; - visibility: any; + visibility: 'public' | 'home' | 'followers' | 'specified'; apMentions: any[] | undefined; visibleUsers: MiUser[]; channel: null | MiChannel; poll: { - multiple: any; - choices: any; + multiple: boolean; + choices: string[]; expiresAt: Date | null; } | undefined; renote: null | MiNote; - localOnly: any; - cw: any; + localOnly: boolean; + cw?: string|null; apHashtags: any[] | undefined; - reactionAcceptance: any; + reactionAcceptance: 'likeOnly'|'likeOnlyForRemote'| 'nonSensitiveOnly'| 'nonSensitiveOnlyForLocalLikeOnlyForRemote'| null; files: MiDriveFile[]; - text: any; + text?: string; reply: null | MiNote; }; const note:NoteType = { + id: this.idService.gen(ps.schedule.expiresAt), files: files, poll: ps.poll ? { choices: ps.poll.choices, @@ -368,7 +363,7 @@ export default class extends Endpoint { // eslint- reply, renote, cw: ps.cw, - localOnly: ps.localOnly, + localOnly: false, reactionAcceptance: ps.reactionAcceptance, visibility: ps.visibility, visibleUsers, @@ -378,7 +373,7 @@ export default class extends Endpoint { // eslint- apEmojis: ps.noExtractEmojis ? [] : undefined, }; - if (ps.schedule && ps.schedule.expiresAt) { + if (ps.schedule.expiresAt) { me.token = null; const noteId = this.idService.gen(new Date().getTime()); await this.noteScheduleRepository.insert({ diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts index 7d7d9ffeef..ed38f1c832 100644 --- a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts +++ b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts @@ -7,9 +7,10 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import type { NoteScheduleRepository } from '@/models/_.js'; +import type { MiNoteSchedule, NoteScheduleRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { QueryService } from '@/core/QueryService.js'; +import { Packed } from '@/misc/json-schema.js'; export const meta = { tags: ['notes'], @@ -23,24 +24,30 @@ export const meta = { type: 'object', optional: false, nullable: false, properties: { - id: { type: 'string', optional: false, nullable: false }, + id: { type: 'string', format: 'misskey:id', optional: false, nullable: false }, note: { type: 'object', optional: false, nullable: false, properties: { id: { type: 'string', optional: false, nullable: false }, text: { type: 'string', optional: false, nullable: false }, - files: { type: 'array', optional: false, nullable: false, items: { type: 'any' } }, - localOnly: { type: 'boolean', optional: false, nullable: false }, - visibility: { type: 'string', optional: false, nullable: false }, - visibleUsers: { type: 'array', optional: false, nullable: false, items: { type: 'any' } }, - reactionAcceptance: { type: 'string', optional: false, nullable: false }, + cw: { type: 'string', optional: true, nullable: true }, + fileIds: { type: 'array', optional: false, nullable: false, items: { type: 'string', format: 'misskey:id', optional: false, nullable: false } }, + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], optional: false, nullable: false }, + visibleUsers: { + type: 'array', optional: false, nullable: false, items: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, + }, user: { type: 'object', optional: false, nullable: false, ref: 'User', }, - createdAt: { type: 'string', optional: false, nullable: false }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, + createdAt: { type: 'string', format: 'misskey:id', optional: false, nullable: false }, isSchedule: { type: 'boolean', optional: false, nullable: false }, }, }, @@ -86,29 +93,32 @@ export default class extends Endpoint { // eslint- note: { id: string; text: string; - files: any[]; - localOnly: boolean; - visibility: string; - visibleUsers: any[]; - reactionAcceptance: string; - user: any; + cw?: string|null; + fileIds: string[]; + visibility: 'public' | 'home' | 'followers' | 'specified'; + visibleUsers: Packed<'UserLite'>[]; + reactionAcceptance: 'likeOnly'|'likeOnlyForRemote'| 'nonSensitiveOnly'| 'nonSensitiveOnlyForLocalLikeOnlyForRemote'| null; + user: Packed<'User'>; createdAt: string; isSchedule: boolean; }; userId: string; expiresAt: string; - }[] = scheduleNotes.map((item: any) => { + }[] = await Promise.all(scheduleNotes.map(async (item: MiNoteSchedule) => { return { ...item, + expiresAt: item.expiresAt.toISOString(), note: { ...item.note, user: user, - createdAt: new Date(item.expiresAt), + visibleUsers: await userEntityService.packMany(item.note.visibleUsers, me), + fileIds: item.note.files.map(f => f.id), + createdAt: item.expiresAt.toISOString(), isSchedule: true, id: item.id, }, }; - }); + })); return scheduleNotesPack; }); diff --git a/packages/cherrypick-js/src/autogen/types.ts b/packages/cherrypick-js/src/autogen/types.ts index a4bae36519..a069ae5d3c 100644 --- a/packages/cherrypick-js/src/autogen/types.ts +++ b/packages/cherrypick-js/src/autogen/types.ts @@ -22135,8 +22135,6 @@ export type operations = { visibility?: 'public' | 'home' | 'followers' | 'specified'; visibleUserIds?: string[]; cw?: string | null; - /** @default false */ - localOnly?: boolean; /** * @default null * @enum {string|null} @@ -22163,7 +22161,7 @@ export type operations = { expiresAt?: number | null; expiredAfter?: number | null; }) | null; - schedule?: { + schedule: { expiresAt?: number; }; }; @@ -22235,23 +22233,30 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': { + 'application/json': ({ + /** Format: misskey:id */ id: string; note: { id: string; text: string; - files: Record[]; - localOnly: boolean; - visibility: string; - visibleUsers: Record[]; - reactionAcceptance: string; + cw?: string | null; + fileIds: string[]; + /** @enum {string} */ + visibility: 'public' | 'home' | 'followers' | 'specified'; + visibleUsers: components['schemas']['UserLite'][]; user: components['schemas']['User']; + /** + * @default null + * @enum {string|null} + */ + reactionAcceptance: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote'; + /** Format: misskey:id */ createdAt: string; isSchedule: boolean; }; userId: string; expiresAt: string; - }[]; + })[]; }; }; /** @description Client error */ From 4919bf00054933e685b25fbc1ca14196cd02d281 Mon Sep 17 00:00:00 2001 From: kozakura913 <98575220+kozakura913@users.noreply.github.com> Date: Mon, 30 Sep 2024 00:03:34 +0900 Subject: [PATCH 09/22] =?UTF-8?q?=E4=BA=88=E7=B4=84=E6=8A=95=E7=A8=BF?= =?UTF-8?q?=E3=81=AE=E6=A8=A9=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 8 ++++++++ locales/ja-JP.yml | 2 ++ packages/backend/src/core/RoleService.ts | 3 +++ .../backend/src/models/json-schema/role.ts | 4 ++++ .../api/endpoints/notes/schedule/create.ts | 6 +++++- .../api/endpoints/notes/schedule/list.ts | 2 +- .../cherrypick-js/etc/cherrypick-js.api.md | 2 +- .../src/autogen/apiClientJSDoc.ts | 2 +- packages/cherrypick-js/src/autogen/types.ts | 5 +++-- packages/cherrypick-js/src/consts.ts | 1 + .../frontend/src/components/MkPostForm.vue | 4 ++-- packages/frontend/src/const.ts | 1 + .../frontend/src/pages/admin/roles.editor.vue | 20 +++++++++++++++++++ packages/frontend/src/pages/admin/roles.vue | 8 ++++++++ 14 files changed, 60 insertions(+), 8 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index 5d1df697ab..d2bc34bad6 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -7676,6 +7676,10 @@ export interface Locale extends ILocale { * ノートの編集 */ "canEditNote": string; + /** + * ノートの予約投稿 + */ + "canScheduleNote": string; /** * ノート内の最大メンション数 */ @@ -9331,6 +9335,10 @@ export interface Locale extends ILocale { * ノートを作成・削除する */ "write:notes": string; + /** + * 予約投稿を見る + */ + "read:notes-schedule": string; /** * 通知を見る */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1a34d0a5e1..a74b5d9865 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1993,6 +1993,7 @@ _role: ltlAvailable: "ローカルタイムラインの閲覧" canPublicNote: "パブリック投稿の許可" canEditNote: "ノートの編集" + canScheduleNote: "ノートの予約投稿" mentionMax: "ノート内の最大メンション数" canInvite: "サーバー招待コードの発行" inviteLimit: "招待コードの作成可能数" @@ -2455,6 +2456,7 @@ _permissions: "read:mutes": "ミュートを見る" "write:mutes": "ミュートを操作する" "write:notes": "ノートを作成・削除する" + "read:notes-schedule": "予約投稿を見る" "read:notifications": "通知を見る" "write:notifications": "通知を操作する" "read:reactions": "リアクションを見る" diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 203d67e6dc..2548816874 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -36,6 +36,7 @@ export type RolePolicies = { ltlAvailable: boolean; canPublicNote: boolean; canEditNote: boolean; + canScheduleNote: boolean; mentionLimit: number; canInvite: boolean; inviteLimit: number; @@ -70,6 +71,7 @@ export const DEFAULT_POLICIES: RolePolicies = { ltlAvailable: true, canPublicNote: true, canEditNote: true, + canScheduleNote: true, mentionLimit: 20, canInvite: false, inviteLimit: 0, @@ -377,6 +379,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), canEditNote: calc('canEditNote', vs => vs.some(v => v === true)), + canScheduleNote: calc('canScheduleNote', vs => vs.some(v => v === true)), mentionLimit: calc('mentionLimit', vs => Math.max(...vs)), canInvite: calc('canInvite', vs => vs.some(v => v === true)), inviteLimit: calc('inviteLimit', vs => Math.max(...vs)), diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index b3b15d5993..f8ff766635 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -284,6 +284,10 @@ export const packedRolePoliciesSchema = { type: 'boolean', optional: false, nullable: false, }, + canScheduleNote: { + type: 'boolean', + optional: false, nullable: false, + }, mutualLinkSectionLimit: { type: 'integer', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts index 77932c9ad4..cb1735ae7e 100644 --- a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts @@ -30,6 +30,7 @@ export const meta = { tags: ['notes'], requireCredential: true, + requireRolePolicy: 'canScheduleNote', prohibitMoved: true, @@ -211,7 +212,10 @@ export default class extends Endpoint { // eslint- private queueService: QueueService, private idService: IdService, ) { - super(meta, paramDef, async (ps, me) => { + super({ + ...meta, + requireRolePolicy: 'canScheduleNote', + }, paramDef, async (ps, me) => { let visibleUsers: MiUser[] = []; if (ps.visibleUserIds) { visibleUsers = await this.usersRepository.findBy({ diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts index ed38f1c832..3329dd6251 100644 --- a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts +++ b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts @@ -16,7 +16,7 @@ export const meta = { tags: ['notes'], requireCredential: true, - kind: 'read:account', + kind: 'read:notes-schedule', res: { type: 'array', optional: false, nullable: false, diff --git a/packages/cherrypick-js/etc/cherrypick-js.api.md b/packages/cherrypick-js/etc/cherrypick-js.api.md index 23c8b0ec07..dd251861f6 100644 --- a/packages/cherrypick-js/etc/cherrypick-js.api.md +++ b/packages/cherrypick-js/etc/cherrypick-js.api.md @@ -2900,7 +2900,7 @@ type PartialRolePolicyOverride = Partial<{ }>; // @public (undocumented) -export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:admin:official-tags", "write:admin:reindex"]; +export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notes-schedule", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:admin:official-tags", "write:admin:reindex"]; // @public (undocumented) type PingResponse = operations['ping']['responses']['200']['content']['application/json']; diff --git a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts index c5732953e2..c2a36abf07 100644 --- a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts +++ b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts @@ -3125,7 +3125,7 @@ declare module '../api.js' { /** * No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:account* + * **Credential required**: *Yes* / **Permission**: *read:notes-schedule* */ request( endpoint: E, diff --git a/packages/cherrypick-js/src/autogen/types.ts b/packages/cherrypick-js/src/autogen/types.ts index a069ae5d3c..43ef3f3106 100644 --- a/packages/cherrypick-js/src/autogen/types.ts +++ b/packages/cherrypick-js/src/autogen/types.ts @@ -2704,7 +2704,7 @@ export type paths = { * notes/schedule/list * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:account* + * **Credential required**: *Yes* / **Permission**: *read:notes-schedule* */ post: operations['notes___schedule___list']; }; @@ -5167,6 +5167,7 @@ export type components = { avatarDecorationLimit: number; fileSizeLimit: number; canEditNote: boolean; + canScheduleNote: boolean; mutualLinkSectionLimit: number; mutualLinkLimit: number; }; @@ -22214,7 +22215,7 @@ export type operations = { * notes/schedule/list * @description No description provided. * - * **Credential required**: *Yes* / **Permission**: *read:account* + * **Credential required**: *Yes* / **Permission**: *read:notes-schedule* */ notes___schedule___list: { requestBody: { diff --git a/packages/cherrypick-js/src/consts.ts b/packages/cherrypick-js/src/consts.ts index 6d110d3ec3..2aa86d90af 100644 --- a/packages/cherrypick-js/src/consts.ts +++ b/packages/cherrypick-js/src/consts.ts @@ -42,6 +42,7 @@ export const permissions = [ 'read:mutes', 'write:mutes', 'write:notes', + 'read:notes-schedule', 'read:notifications', 'write:notifications', 'read:reactions', diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index b07385e74a..24c275f479 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -19,8 +19,8 @@ SPDX-License-Identifier: AGPL-3.0-only
- - + +