From 2528508cff9d8c90abd33e46b15220a49a00e2e2 Mon Sep 17 00:00:00 2001 From: NoriDev <m1nthing2322@gmail.com> Date: Thu, 31 Oct 2024 13:52:01 +0900 Subject: [PATCH 01/18] =?UTF-8?q?feat:=20=EB=85=B8=ED=8A=B8=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EB=A5=BC=20=EC=98=88=EC=95=BD=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EC=9D=8C=20(yojo-art/cherrypick#483,=20[Type4ny-Proje?= =?UTF-8?q?ct/Type4ny@271c872c](https://github.com/Type4ny-Project/Type4ny?= =?UTF-8?q?/commit/271c872c97f215ef5d8e0be62251dd422a52e5b1))?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en-US.yml | 2 + locales/index.d.ts | 8 + locales/ja-JP.yml | 2 + locales/ko-KR.yml | 2 + .../migration/1699437894737-scheduleNote.js | 17 + packages/backend/src/core/QueueModule.ts | 12 + packages/backend/src/core/QueueService.ts | 2 + packages/backend/src/core/RoleService.ts | 3 + packages/backend/src/di-symbols.ts | 1 + packages/backend/src/models/NoteSchedule.ts | 60 +++ .../backend/src/models/RepositoryModule.ts | 9 + packages/backend/src/models/_.ts | 3 + .../backend/src/models/json-schema/role.ts | 4 + packages/backend/src/postgres.ts | 2 + .../backend/src/queue/QueueProcessorModule.ts | 2 + .../src/queue/QueueProcessorService.ts | 14 + packages/backend/src/queue/const.ts | 1 + .../ScheduleNotePostProcessorService.ts | 94 +++++ packages/backend/src/queue/types.ts | 4 + .../backend/src/server/api/EndpointsModule.ts | 12 + packages/backend/src/server/api/endpoints.ts | 6 + .../server/api/endpoints/admin/queue/stats.ts | 3 +- .../api/endpoints/notes/schedule/create.ts | 393 ++++++++++++++++++ .../api/endpoints/notes/schedule/delete.ts | 67 +++ .../api/endpoints/notes/schedule/list.ts | 128 ++++++ .../src/server/web/ClientServerService.ts | 3 + packages/frontend-shared/js/const.ts | 1 + .../frontend/src/components/MkNoteHeader.vue | 5 +- .../frontend/src/components/MkNoteSimple.vue | 55 ++- .../frontend/src/components/MkPostForm.vue | 59 ++- .../src/components/MkScheduleEditor.vue | 69 +++ .../components/MkSchedulePostListDialog.vue | 60 +++ packages/frontend/src/os.ts | 1 + .../frontend/src/pages/admin/roles.editor.vue | 19 + packages/frontend/src/pages/admin/roles.vue | 7 + packages/misskey-js/etc/misskey-js.api.md | 16 + .../misskey-js/src/autogen/apiClientJSDoc.ts | 33 ++ packages/misskey-js/src/autogen/endpoint.ts | 7 + packages/misskey-js/src/autogen/entities.ts | 4 + packages/misskey-js/src/autogen/types.ts | 269 ++++++++++++ packages/misskey-js/src/consts.ts | 2 + 41 files changed, 1455 insertions(+), 6 deletions(-) create mode 100644 packages/backend/migration/1699437894737-scheduleNote.js 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/schedule/create.ts create mode 100644 packages/backend/src/server/api/endpoints/notes/schedule/delete.ts create mode 100644 packages/backend/src/server/api/endpoints/notes/schedule/list.ts create mode 100644 packages/frontend/src/components/MkScheduleEditor.vue create mode 100644 packages/frontend/src/components/MkSchedulePostListDialog.vue diff --git a/locales/en-US.yml b/locales/en-US.yml index 6ea7fb4f8d..38e9b03acb 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -2073,6 +2073,8 @@ _permissions: "read:mutes": "View your list of muted users" "write:mutes": "Edit your list of muted users" "write:notes": "Compose or delete notes" + "read:notes-schedule": "View your list of scheduled notes" + "write:notes-schedule": "Compose or delete scheduled notes" "read:notifications": "View your notifications" "write:notifications": "Manage your notifications" "read:reactions": "View your reactions" diff --git a/locales/index.d.ts b/locales/index.d.ts index 5caebcfa02..92c09ffe12 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -8140,6 +8140,14 @@ export interface Locale extends ILocale { * ノートを作æˆãƒ»å‰Šé™¤ã™ã‚‹ */ "write:notes": string; + /** + * 予約投稿を見る + */ + "read:notes-schedule": string; + /** + * 予約投稿を作æˆãƒ»å‰Šé™¤ã™ã‚‹ + */ + "write:notes-schedule": string; /** * 通知を見る */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c448d4d50a..0d2229ac20 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2121,6 +2121,8 @@ _permissions: "read:mutes": "ミュートを見る" "write:mutes": "ミュートをæ“作ã™ã‚‹" "write:notes": "ノートを作æˆãƒ»å‰Šé™¤ã™ã‚‹" + "read:notes-schedule": "予約投稿を見る" + "write:notes-schedule": "予約投稿を作æˆãƒ»å‰Šé™¤ã™ã‚‹" "read:notifications": "通知を見る" "write:notifications": "通知をæ“作ã™ã‚‹" "read:reactions": "リアクションを見る" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 414202adab..351d5a23ce 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -2080,6 +2080,8 @@ _permissions: "read:mutes": "뮤트 여부를 확ì¸í•©ë‹ˆë‹¤" "write:mutes": "뮤트를 하거나 í•´ì œí•©ë‹ˆë‹¤" "write:notes": "노트를 작성하거나 ì‚ì œí•©ë‹ˆë‹¤" + "read:notes-schedule": "게시를 예약한 노트를 봅니다" + "write:notes-schedule": "노트 게시를 예약하거나 ì‚ì œí•©ë‹ˆë‹¤" "read:notifications": "ì•Œë¦¼ì„ í™•ì¸í•©ë‹ˆë‹¤" "write:notifications": "ì•Œë¦¼ì„ ëª¨ë‘ ì½ìŒ 처리합니다" "read:reactions": "ë¦¬ì•¡ì…˜ì„ í™•ì¸í•©ë‹ˆë‹¤" diff --git a/packages/backend/migration/1699437894737-scheduleNote.js b/packages/backend/migration/1699437894737-scheduleNote.js new file mode 100644 index 0000000000..28dc290f25 --- /dev/null +++ b/packages/backend/migration/1699437894737-scheduleNote.js @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +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, "scheduledAt" 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/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index b10b8e5899..6dd48927c1 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -16,6 +16,7 @@ import { RelationshipJobData, UserWebhookDeliverJobData, SystemWebhookDeliverJobData, + ScheduleNotePostJobData, } from '../queue/types.js'; import type { Provider } from '@nestjs/common'; @@ -28,6 +29,7 @@ export type RelationshipQueue = Bull.Queue<RelationshipJobData>; export type ObjectStorageQueue = Bull.Queue; export type UserWebhookDeliverQueue = Bull.Queue<UserWebhookDeliverJobData>; export type SystemWebhookDeliverQueue = Bull.Queue<SystemWebhookDeliverJobData>; +export type ScheduleNotePostQueue = Bull.Queue<ScheduleNotePostJobData>; const $system: Provider = { provide: 'queue:system', @@ -83,6 +85,12 @@ const $systemWebhookDeliver: Provider = { inject: [DI.config], }; +const $scheduleNotePost: Provider = { + provide: 'queue:scheduleNotePost', + useFactory: (config: Config) => new Bull.Queue(QUEUE.SCHEDULE_NOTE_POST, baseQueueOptions(config, QUEUE.SCHEDULE_NOTE_POST)), + inject: [DI.config], +}; + @Module({ imports: [ ], @@ -96,6 +104,7 @@ const $systemWebhookDeliver: Provider = { $objectStorage, $userWebhookDeliver, $systemWebhookDeliver, + $scheduleNotePost, ], exports: [ $system, @@ -107,6 +116,7 @@ const $systemWebhookDeliver: Provider = { $objectStorage, $userWebhookDeliver, $systemWebhookDeliver, + $scheduleNotePost, ], }) export class QueueModule implements OnApplicationShutdown { @@ -120,6 +130,7 @@ export class QueueModule implements OnApplicationShutdown { @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, + @Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue, ) {} public async dispose(): Promise<void> { @@ -136,6 +147,7 @@ export class QueueModule implements OnApplicationShutdown { this.objectStorageQueue.close(), this.userWebhookDeliverQueue.close(), this.systemWebhookDeliverQueue.close(), + this.scheduleNotePostQueue.close(), ]); } diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index dc13aa21bf..d9d282a168 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -32,6 +32,7 @@ import type { SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue, + ScheduleNotePostQueue, } from './QueueModule.js'; import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; @@ -52,6 +53,7 @@ export class QueueService { @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, + @Inject('queue:scheduleNotePost') public ScheduleNotePostQueue: ScheduleNotePostQueue, ) { this.systemQueue.add('tickCharts', { }, { diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 64f7539031..5651b04ac2 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -36,6 +36,7 @@ export type RolePolicies = { ltlAvailable: boolean; btlAvailable: boolean; canPublicNote: boolean; + scheduleNoteMax: number; mentionLimit: number; canInvite: boolean; inviteLimit: number; @@ -72,6 +73,7 @@ export const DEFAULT_POLICIES: RolePolicies = { ltlAvailable: true, btlAvailable: false, canPublicNote: true, + scheduleNoteMax: 5, mentionLimit: 20, canInvite: false, inviteLimit: 0, @@ -377,6 +379,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { btlAvailable: calc('btlAvailable', vs => vs.some(v => v === true)), ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), + scheduleNoteMax: calc('scheduleNoteMax', vs => Math.max(...vs)), 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/di-symbols.ts b/packages/backend/src/di-symbols.ts index 5ea500ac77..296cc4815b 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -86,5 +86,6 @@ export const DI = { noteEditRepository: Symbol('noteEditRepository'), bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), reversiGamesRepository: Symbol('reversiGamesRepository'), + noteScheduleRepository: Symbol('noteScheduleRepository'), //#endregion }; diff --git a/packages/backend/src/models/NoteSchedule.ts b/packages/backend/src/models/NoteSchedule.ts new file mode 100644 index 0000000000..97ffe32ffa --- /dev/null +++ b/packages/backend/src/models/NoteSchedule.ts @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; +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'; + +type MinimumUser = { + id: MiUser['id']; + host: MiUser['host']; + username: MiUser['username']; + uri: MiUser['uri']; +}; + +export type MiScheduleNoteType={ + /** Date.toISOString() */ + createdAt: string; + visibility: 'public' | 'home' | 'followers' | 'specified'; + visibleUsers: MinimumUser[]; + channel?: MiChannel['id']; + poll: { + multiple: boolean; + choices: string[]; + /** Date.toISOString() */ + expiresAt: string | null + } | undefined; + renote?: MiNote['id']; + localOnly: boolean; + cw?: string | null; + reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null; + files: MiDriveFile['id'][]; + text?: string | null; + reply?: MiNote['id']; + apMentions?: MinimumUser[] | null; + apHashtags?: string[] | null; + apEmojis?: string[] | null; +} + +@Entity('note_schedule') +export class MiNoteSchedule { + @PrimaryColumn(id()) + public id: string; + + @Column('jsonb') + public note: MiScheduleNoteType; + + @Index() + @Column('varchar', { + length: 260, + }) + public userId: MiUser['id']; + + @Column('timestamp with time zone') + public scheduledAt: Date; +} diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index eb45b9a631..3a1158a42a 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -43,6 +43,7 @@ import { MiNote, MiNoteFavorite, MiNoteReaction, + MiNoteSchedule, MiNoteThreadMuting, MiNoteUnread, MiPage, @@ -509,6 +510,12 @@ const $reversiGamesRepository: Provider = { inject: [DI.db], }; +const $noteScheduleRepository: Provider = { + provide: DI.noteScheduleRepository, + useFactory: (db: DataSource) => db.getRepository(MiNoteSchedule).extend(miRepository as MiRepository<MiNoteSchedule>), + inject: [DI.db], +}; + @Module({ imports: [], providers: [ @@ -583,6 +590,7 @@ const $reversiGamesRepository: Provider = { $noteEditRepository, $bubbleGameRecordsRepository, $reversiGamesRepository, + $noteScheduleRepository, ], exports: [ $usersRepository, @@ -656,6 +664,7 @@ const $reversiGamesRepository: Provider = { $noteEditRepository, $bubbleGameRecordsRepository, $reversiGamesRepository, + $noteScheduleRepository, ], }) export class RepositoryModule { diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index ac2dd62aa2..9a4ebfc90f 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -81,6 +81,7 @@ import { MiUserListFavorite } from '@/models/UserListFavorite.js'; import { NoteEdit } from '@/models/NoteEdit.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; +import { MiNoteSchedule } from '@/models/NoteSchedule.js'; import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; export interface MiRepository<T extends ObjectLiteral> { @@ -160,6 +161,7 @@ export { MiNote, MiNoteFavorite, MiNoteReaction, + MiNoteSchedule, MiNoteThreadMuting, MiNoteUnread, MiPage, @@ -271,3 +273,4 @@ export type UserMemoRepository = Repository<MiUserMemo> & MiRepository<MiUserMem export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>; export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>; export type NoteEditRepository = Repository<NoteEdit> & MiRepository<NoteEdit>; +export type NoteScheduleRepository = Repository<MiNoteSchedule>; diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 19ea6263c9..ef0bb9f141 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -296,6 +296,10 @@ export const packedRolePoliciesSchema = { type: 'boolean', optional: false, nullable: false, }, + scheduleNoteMax: { + type: 'integer', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 2d66e6e445..c964c3ffee 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -79,6 +79,7 @@ import { MiUserMemo } from '@/models/UserMemo.js'; import { NoteEdit } from '@/models/NoteEdit.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'; @@ -158,6 +159,7 @@ export const entities = [ MiNote, MiNoteFavorite, MiNoteReaction, + MiNoteSchedule, MiNoteThreadMuting, MiNoteUnread, MiPage, diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 7c6675b15d..dd588e0115 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -42,6 +42,7 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js'; import { RelationshipProcessorService } from './processors/RelationshipProcessorService.js'; +import { ScheduleNotePostProcessorService } from './processors/ScheduleNotePostProcessorService.js'; @Module({ imports: [ @@ -85,6 +86,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor InboxProcessorService, AggregateRetentionProcessorService, QueueProcessorService, + ScheduleNotePostProcessorService, ], exports: [ QueueProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index f130314e74..4cc5446062 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -44,6 +44,7 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; +import { ScheduleNotePostProcessorService } from './processors/ScheduleNotePostProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; import { QUEUE, baseQueueOptions } from './const.js'; import { ImportNotesProcessorService } from './processors/ImportNotesProcessorService.js'; @@ -86,6 +87,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) @@ -126,6 +128,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService, private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService, private cleanProcessorService: CleanProcessorService, + private scheduleNotePostProcessorService: ScheduleNotePostProcessorService, ) { this.logger = this.queueLoggerService.logger; @@ -530,6 +533,15 @@ 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), + autorun: false, + }); + } + //#endregion } @bindThis @@ -544,6 +556,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker.run(), this.objectStorageQueueWorker.run(), this.endedPollNotificationQueueWorker.run(), + this.schedulerNotePostQueueWorker.run(), ]); } @@ -559,6 +572,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 67f689b618..fdf012f149 100644 --- a/packages/backend/src/queue/const.ts +++ b/packages/backend/src/queue/const.ts @@ -16,6 +16,7 @@ export const QUEUE = { OBJECT_STORAGE: 'objectStorage', USER_WEBHOOK_DELIVER: 'userWebhookDeliver', SYSTEM_WEBHOOK_DELIVER: 'systemWebhookDeliver', + SCHEDULE_NOTE_POST: 'scheduleNotePost', }; export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions { diff --git a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts new file mode 100644 index 0000000000..62d527953d --- /dev/null +++ b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts @@ -0,0 +1,94 @@ +/* + * 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 type { ChannelsRepository, DriveFilesRepository, MiDriveFile, NoteScheduleRepository, NotesRepository, 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'; + +@Injectable() +export class ScheduleNotePostProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.noteScheduleRepository) + private noteScheduleRepository: NoteScheduleRepository, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + private noteCreateService: NoteCreateService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('schedule-note-post'); + } + + @bindThis + public async process(job: Bull.Job<ScheduleNotePostJobData>): Promise<void> { + this.noteScheduleRepository.findOneBy({ id: job.data.scheduleNoteId }).then(async (data) => { + if (!data) { + this.logger.warn(`Schedule note ${job.data.scheduleNoteId} not found`); + } else { + const me = await this.usersRepository.findOneBy({ id: data.userId }); + const note = data.note; + + //idã®å½¢å¼ã§ã‚ューã«ç©ã‚“ã§ã‚ã£ãŸã®ã‚’DBã‹ã‚‰å–り寄ã›ã‚‹ + const reply = note.reply ? await this.notesRepository.findOneBy({ id: note.reply }) : undefined; + const renote = note.reply ? await this.notesRepository.findOneBy({ id: note.renote }) : undefined; + const channel = note.channel ? await this.channelsRepository.findOneBy({ id: note.channel, isArchived: false }) : undefined; + let files: MiDriveFile[] = []; + const fileIds = note.files ?? null; + if (fileIds != null && fileIds.length > 0 && me) { + 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 ( + !data.userId || + !me || + (note.reply && !reply) || + (note.renote && !renote) || + (note.channel && !channel) || + (note.files.length !== files.length) + ) { + //ã‚ューã«ç©ã‚“ã ã¨ãã¯æœ‰ã£ãŸç‰©ãŒæ¶ˆæ»…ã—ã¦ãŸã‚‰äºˆç´„投稿をã‚ャンセルã™ã‚‹ + this.logger.warn('cancel schedule note'); + await this.noteScheduleRepository.remove(data); + return; + } + await this.noteCreateService.create(me, { + ...note, + createdAt: new Date(note.createdAt), //typeORMã®jsonbã§ä½•æ•…ã‹stringã«ã•ã‚Œã‚‹ã‹ã‚‰æˆ»ã™ + files, + poll: note.poll ? { + choices: note.poll.choices, + multiple: note.poll.multiple, + expiresAt: note.poll.expiresAt ? new Date(note.poll.expiresAt) : null, + } : undefined, + reply, + renote, + channel, + }); + await this.noteScheduleRepository.remove(data); + } + }); + } +} diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index c0d246ebbc..9433392df5 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -155,3 +155,7 @@ export type UserWebhookDeliverJobData = { export type ThinUser = { id: MiUser['id']; }; + +export type ScheduleNotePostJobData = { + scheduleNoteId: MiNote['id']; +} diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 5bdd7cf650..c478bebdaf 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -311,6 +311,9 @@ import * as ep___notes_renotes from './endpoints/notes/renotes.js'; import * as ep___notes_replies from './endpoints/notes/replies.js'; import * as ep___notes_edit from './endpoints/notes/edit.js'; import * as ep___notes_versions from './endpoints/notes/versions.js'; +import * as ep___notes_schedule_create from './endpoints/notes/schedule/create.js'; +import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js'; +import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js'; import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js'; import * as ep___notes_search from './endpoints/notes/search.js'; import * as ep___notes_show from './endpoints/notes/show.js'; @@ -711,6 +714,9 @@ const $notes_reactions_delete: Provider = { provide: 'ep:notes/reactions/delete' const $notes_like: Provider = { provide: 'ep:notes/like', useClass: ep___notes_like.default }; const $notes_renotes: Provider = { provide: 'ep:notes/renotes', useClass: ep___notes_renotes.default }; const $notes_replies: Provider = { provide: 'ep:notes/replies', useClass: ep___notes_replies.default }; +const $notes_schedule_create: Provider = { provide: 'ep:notes/schedule/create', useClass: ep___notes_schedule_create.default }; +const $notes_schedule_delete: Provider = { provide: 'ep:notes/schedule/delete', useClass: ep___notes_schedule_delete.default }; +const $notes_schedule_list: Provider = { provide: 'ep:notes/schedule/list', useClass: ep___notes_schedule_list.default }; const $notes_searchByTag: Provider = { provide: 'ep:notes/search-by-tag', useClass: ep___notes_searchByTag.default }; const $notes_search: Provider = { provide: 'ep:notes/search', useClass: ep___notes_search.default }; const $notes_show: Provider = { provide: 'ep:notes/show', useClass: ep___notes_show.default }; @@ -1117,6 +1123,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_like, $notes_renotes, $notes_replies, + $notes_schedule_create, + $notes_schedule_delete, + $notes_schedule_list, $notes_searchByTag, $notes_search, $notes_show, @@ -1516,6 +1525,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_like, $notes_renotes, $notes_replies, + $notes_schedule_create, + $notes_schedule_delete, + $notes_schedule_list, $notes_searchByTag, $notes_search, $notes_show, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 7eb18fbfe2..269afbf14b 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -316,6 +316,9 @@ import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete import * as ep___notes_like from './endpoints/notes/like.js'; import * as ep___notes_renotes from './endpoints/notes/renotes.js'; import * as ep___notes_replies from './endpoints/notes/replies.js'; +import * as ep___notes_schedule_create from './endpoints/notes/schedule/create.js'; +import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js'; +import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js'; import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js'; import * as ep___notes_search from './endpoints/notes/search.js'; import * as ep___notes_show from './endpoints/notes/show.js'; @@ -716,6 +719,9 @@ const eps = [ ['notes/like', ep___notes_like], ['notes/renotes', ep___notes_renotes], ['notes/replies', ep___notes_replies], + ['notes/schedule/create', ep___notes_schedule_create], + ['notes/schedule/delete', ep___notes_schedule_delete], + ['notes/schedule/list', ep___notes_schedule_list], ['notes/search-by-tag', ep___notes_searchByTag], ['notes/search', ep___notes_search], ['notes/show', ep___notes_show], 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..e2bd38aac6 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'], @@ -55,6 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, + @Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue, ) { super(meta, paramDef, async (ps, me) => { const deliverJobCounts = await this.deliverQueue.getJobCounts(); diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts new file mode 100644 index 0000000000..ecdfa4bf2e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts @@ -0,0 +1,393 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { 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, + 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'; +import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { QueueService } from '@/core/QueueService.js'; +import { IdService } from '@/core/IdService.js'; +import { MiScheduleNoteType } from '@/models/NoteSchedule.js'; +import { RoleService } from '@/core/RoleService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + + prohibitMoved: true, + + limit: { + duration: ms('1hour'), + max: 300, + }, + + kind: 'write:notes-schedule', + + errors: { + scheduleNoteMax: { + message: 'Schedule note max.', + code: 'SCHEDULE_NOTE_MAX', + id: '168707c3-e7da-4031-989e-f42aa3a274b2', + }, + 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 }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, + disableRightClick: { type: 'boolean', default: false }, + 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 }, + + // 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'], + }, + event: { + type: 'object', + nullable: true, + properties: { + title: { type: 'string', minLength: 1, maxLength: 128, nullable: false }, + start: { type: 'integer', nullable: false }, + end: { type: 'integer', nullable: true }, + metadata: { type: 'object' }, + }, + }, + scheduleNote: { + type: 'object', + nullable: false, + properties: { + scheduledAt: { 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: ['scheduleNote'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + @Inject(DI.noteScheduleRepository) + private noteScheduleRepository: NoteScheduleRepository, + + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.channelsRepository) + private channelsRepository: ChannelsRepository, + + private queueService: QueueService, + private roleService: RoleService, + private idService: IdService, + ) { + super({ + ...meta, + }, paramDef, async (ps, me) => { + const scheduleNoteCount = await this.noteScheduleRepository.countBy({ userId: me.id }); + const scheduleNoteMax = (await this.roleService.getUserPolicies(me.id)).scheduleNoteMax; + if (scheduleNoteCount >= scheduleNoteMax) { + throw new ApiError(meta.errors.scheduleNoteMax); + } + 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); + } + } + + 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) { + let scheduleNote_scheduledAt = Date.now(); + if (typeof ps.scheduleNote.scheduledAt === 'number') { + scheduleNote_scheduledAt = ps.scheduleNote.scheduledAt; + } + if (typeof ps.poll.expiresAt === 'number') { + if (ps.poll.expiresAt < scheduleNote_scheduledAt) { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); + } + } else if (typeof ps.poll.expiredAfter === 'number') { + ps.poll.expiresAt = scheduleNote_scheduledAt + ps.poll.expiredAfter; + } + } + if (typeof ps.scheduleNote.scheduledAt === 'number') { + if (ps.scheduleNote.scheduledAt < Date.now()) { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredSchedule); + } + } else { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredSchedule); + } + const note:MiScheduleNoteType = { + createdAt: new Date(ps.scheduleNote.scheduledAt!).toISOString(), + files: files.map(f => f.id), + poll: ps.poll ? { + choices: ps.poll.choices, + multiple: ps.poll.multiple ?? false, + expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt).toISOString() : null, + } : undefined, + text: ps.text ?? undefined, + reply: reply?.id, + renote: renote?.id, + cw: ps.cw, + localOnly: false, + reactionAcceptance: ps.reactionAcceptance, + visibility: ps.visibility, + visibleUsers, + apMentions: ps.noExtractMentions ? [] : undefined, + apHashtags: ps.noExtractHashtags ? [] : undefined, + apEmojis: ps.noExtractEmojis ? [] : undefined, + event: ps.event ? { + start: new Date(ps.event.start!).toISOString(), + end: ps.event.end ? new Date(ps.event.end).toISOString() : null, + title: ps.event.title!, + metadata: ps.event.metadata ?? {}, + } : undefined, + disableRightClick: ps.disableRightClick, + }; + + if (ps.scheduleNote.scheduledAt) { + me.token = null; + const noteId = this.idService.gen(new Date().getTime()); + await this.noteScheduleRepository.insert({ + id: noteId, + note: note, + userId: me.id, + scheduledAt: new Date(ps.scheduleNote.scheduledAt), + }); + + const delay = new Date(ps.scheduleNote.scheduledAt).getTime() - Date.now(); + await this.queueService.ScheduleNotePostQueue.add(String(delay), { + scheduleNoteId: noteId, + }, { + delay, + removeOnComplete: true, + jobId: noteId, + }); + } + + return ''; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts b/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts new file mode 100644 index 0000000000..df406f99f0 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts @@ -0,0 +1,67 @@ +/* + * 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'; +import { ApiError } from '@/server/api/error.js'; +import { QueueService } from '@/core/QueueService.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + kind: 'write:notes-schedule', + + limit: { + duration: ms('1hour'), + max: 300, + }, + + errors: { + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: 'a58056ba-8ba1-4323-8ebf-e0b585bc244f', + }, + permissionDenied: { + message: 'Permission denied.', + code: 'PERMISSION_DENIED', + id: 'c0da2fed-8f61-4c47-a41d-431992607b5c', + httpStatusCode: 403, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + noteId: { type: 'string', format: 'misskey:id' }, + }, + required: ['noteId'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.noteScheduleRepository) + private noteScheduleRepository: NoteScheduleRepository, + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.noteScheduleRepository.findOneBy({ id: ps.noteId }); + if (note === null) { + throw new ApiError(meta.errors.noSuchNote); + } + if (note.userId !== me.id) { + throw new ApiError(meta.errors.permissionDenied); + } + await this.noteScheduleRepository.delete({ id: ps.noteId }); + await this.queueService.ScheduleNotePostQueue.remove(ps.noteId); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts new file mode 100644 index 0000000000..88da4f4043 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts @@ -0,0 +1,128 @@ +/* + * 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 { MiNote, 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'; +import { noteVisibilities } from '@/types.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + kind: 'read:notes-schedule', + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + properties: { + id: { type: 'string', format: 'misskey:id', optional: false, nullable: false }, + note: { + type: 'object', + optional: false, nullable: false, + properties: { + createdAt: { type: 'string', optional: false, nullable: false }, + text: { type: 'string', optional: true, 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', + }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, + isSchedule: { type: 'boolean', optional: false, nullable: false }, + }, + }, + userId: { type: 'string', optional: false, nullable: false }, + scheduledAt: { type: 'string', optional: false, nullable: false }, + }, + }, + }, + limit: { + duration: ms('1hour'), + max: 300, + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.noteScheduleRepository) + private noteScheduleRepository: NoteScheduleRepository, + + private userEntityService: UserEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.noteScheduleRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.userId = :userId', { userId: me.id }); + const scheduleNotes = await query.limit(ps.limit).getMany(); + const user = await this.userEntityService.pack(me, me); + const scheduleNotesPack: { + id: string; + note: { + text?: string; + cw?: string|null; + fileIds: string[]; + visibility: typeof noteVisibilities[number]; + visibleUsers: Packed<'UserLite'>[]; + reactionAcceptance: MiNote['reactionAcceptance']; + user: Packed<'User'>; + createdAt: string; + isSchedule: boolean; + }; + userId: string; + scheduledAt: string; + }[] = await Promise.all(scheduleNotes.map(async (item: MiNoteSchedule) => { + return { + ...item, + scheduledAt: item.scheduledAt.toISOString(), + note: { + ...item.note, + text: item.note.text ?? '', + user: user, + visibility: item.note.visibility ?? 'public', + reactionAcceptance: item.note.reactionAcceptance ?? null, + visibleUsers: item.note.visibleUsers ? await userEntityService.packMany(item.note.visibleUsers.map(u => u.id), me) : [], + fileIds: item.note.files ? item.note.files : [], + createdAt: item.scheduledAt.toISOString(), + isSchedule: true, + id: item.id, + }, + }; + })); + + return scheduleNotesPack; + }); + } +} diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index fbb8321730..aca98c4d37 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -33,6 +33,7 @@ import type { SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue, + ScheduleNotePostQueue, } from '@/core/QueueModule.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -124,6 +125,7 @@ export class ClientServerService { @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, + @Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue, ) { //this.createServer = this.createServer.bind(this); } @@ -254,6 +256,7 @@ export class ClientServerService { this.objectStorageQueue, this.userWebhookDeliverQueue, this.systemWebhookDeliverQueue, + this.scheduleNotePostQueue, ].map(q => new BullMQAdapter(q)), serverAdapter: bullBoardServerAdapter, }); diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts index 42cbf081e8..882f19c7fd 100644 --- a/packages/frontend-shared/js/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -140,6 +140,7 @@ export const ROLE_POLICIES = [ 'btlAvailable', 'canPublicNote', 'canImportNotes', + 'scheduleNoteMax', 'mentionLimit', 'canInvite', 'inviteLimit', diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index cd6fdf576c..2c69048ec5 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -50,7 +50,10 @@ import { popupMenu } from '@/os.js'; import { defaultStore } from '@/store.js'; const props = defineProps<{ - note: Misskey.entities.Note; + note: Misskey.entities.Note & { + isSchedule?: boolean + }; + scheduled?: boolean; }>(); const menuVersionsButton = shallowRef<HTMLElement>(); diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index 542e3e79ea..7d2bbb31d3 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 --> <template> -<div :class="$style.root"> +<div v-show="!isDeleted" :class="$style.root" :tabindex="!isDeleted ? '-1' : undefined"> <MkAvatar :class="$style.avatar" :user="note.user" link preview/> <div :class="$style.main"> <MkNoteHeader :class="$style.header" :note="note" :mini="true"/> @@ -15,6 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only </p> <div v-show="note.cw == null || showContent"> <MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note" :expandAllCws="props.expandAllCws"/> + <div v-if="note.isSchedule" style="margin-top: 10px;"> + <MkButton :class="$style.button" inline @click.stop.prevent="editScheduleNote()"><i class="ti ti-eraser"></i> {{ i18n.ts.deleteAndEdit }}</MkButton> + <MkButton :class="$style.button" inline danger @click.stop.prevent="deleteScheduleNote()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> </div> </div> </div> @@ -24,18 +28,60 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; +import * as os from '@/os.js'; import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkSubNoteContent from '@/components/MkSubNoteContent.vue'; import MkCwButton from '@/components/MkCwButton.vue'; import { defaultStore } from '@/store.js'; const props = defineProps<{ - note: Misskey.entities.Note; + note: Misskey.entities.Note & { + isSchedule? : boolean, + scheduledNoteId?: string + }; expandAllCws?: boolean; hideFiles?: boolean; }>(); let showContent = ref(defaultStore.state.uncollapseCW); +const isDeleted = ref(false); + +const emit = defineEmits<{ + (ev: 'editScheduleNote'): void; +}>(); + +async function deleteScheduleNote() { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.deleteConfirm, + okText: i18n.ts.delete, + cancelText: i18n.ts.cancel, + }); + if (canceled) return; + await os.apiWithDialog('notes/schedule/delete', { noteId: props.note.id }) + .then(() => { + isDeleted.value = true; + }); +} + +async function editScheduleNote() { + try { + await misskeyApi('notes/schedule/delete', { noteId: props.note.id }) + .then(() => { + isDeleted.value = true; + }); + } catch (err) { + console.error(err); + } + + await os.post({ + initialNote: props.note, + renote: props.note.renote, + reply: props.note.reply, + channel: props.note.channel, + }); + emit('editScheduleNote'); +} watch(() => props.expandAllCws, (expandAllCws) => { if (expandAllCws !== showContent.value) showContent.value = expandAllCws; @@ -50,6 +96,11 @@ watch(() => props.expandAllCws, (expandAllCws) => { font-size: 0.95em; } +.button{ + margin-right: var(--margin); + margin-bottom: var(--margin); +} + .avatar { flex-shrink: 0; display: block; diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 4a29b27ac4..443e9e7ee9 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -77,6 +77,7 @@ SPDX-License-Identifier: AGPL-3.0-only <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags"> <XPostFormAttaches v-model="files" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName" @replaceFile="replaceFile"/> <MkPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/> + <MkScheduleEditor v-if="scheduleNote" v-model="scheduleNote" @destroyed="scheduleNote = null"/> <MkNotePreview v-if="showPreview" :class="$style.preview" :text="text" :files="files" :poll="poll ?? undefined" :useCw="useCw" :cw="cw" :user="postAccount ?? $i"/> <div v-if="showingOptions" style="padding: 8px 16px;"> </div> @@ -90,6 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button> <button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button> <button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button> + <button v-tooltip="i18n.ts.otherSettings" :class="['_button', $style.footerButton]" @click="showOtherMenu"><i class="ti ti-dots"></i></button> </div> <div :class="$style.footerRight"> <button v-tooltip="i18n.ts.previewNoteText" class="_button" :class="[$style.footerButton, { [$style.previewButtonActive]: showPreview }]" @click="showPreview = !showPreview"><i class="ti ti-eye"></i></button> @@ -110,6 +112,7 @@ import * as Misskey from 'misskey-js'; import insertTextAtCursor from 'insert-text-at-cursor'; import { toASCII } from 'punycode/'; import { host, url } from '@@/js/config.js'; +import type { MenuItem } from '@/types/menu.js'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkNotePreview from '@/components/MkNotePreview.vue'; import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; @@ -133,6 +136,7 @@ import { miLocalStorage } from '@/local-storage.js'; import { claimAchievement } from '@/scripts/achievements.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js'; +import MkScheduleEditor from '@/components/MkScheduleEditor.vue'; const $i = signinRequired(); @@ -150,7 +154,9 @@ const props = withDefaults(defineProps<{ initialFiles?: Misskey.entities.DriveFile[]; initialLocalOnly?: boolean; initialVisibleUsers?: Misskey.entities.UserDetailed[]; - initialNote?: Misskey.entities.Note; + initialNote?: Misskey.entities.Note & { + isSchedule?: boolean, + }; instant?: boolean; fixed?: boolean; autofocus?: boolean; @@ -206,6 +212,9 @@ const recentHashtags = ref(JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]' const imeText = ref(''); const showingOptions = ref(false); const textAreaReadOnly = ref(false); +const scheduleNote = ref<{ + scheduledAt: number | null; +} | null>(null); const draftKey = computed((): string => { let key = props.channel ? `channel:${props.channel.id}` : ''; @@ -378,6 +387,7 @@ function watchForDraft() { watch(localOnly, () => saveDraft()); watch(quoteId, () => saveDraft()); watch(reactionAcceptance, () => saveDraft()); + watch(scheduleNote, () => saveDraft()); } function MFMWindow() { @@ -586,6 +596,7 @@ function clear() { files.value = []; poll.value = null; quoteId.value = null; + scheduleNote.value = null; } function onKeydown(ev: KeyboardEvent) { @@ -736,6 +747,7 @@ function saveDraft() { visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(x => x.id) : undefined, quoteId: quoteId.value, reactionAcceptance: reactionAcceptance.value, + scheduleNote: scheduleNote.value, }, }; @@ -843,6 +855,7 @@ async function post(ev?: MouseEvent) { visibleUserIds: visibility.value === 'specified' ? visibleUsers.value.map(u => u.id) : undefined, reactionAcceptance: reactionAcceptance.value, editId: props.editId ? props.editId : undefined, + scheduleNote: scheduleNote.value ?? undefined, }; if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') { @@ -879,7 +892,7 @@ async function post(ev?: MouseEvent) { } posting.value = true; - misskeyApi(postData.editId ? 'notes/edit' : 'notes/create', postData, token).then(() => { + misskeyApi(postData.editId ? 'notes/edit' : (postData.scheduleNote ? 'notes/schedule/create' : 'notes/create'), postData, token).then(() => { if (props.freezeAfterPosted) { posted.value = true; } else { @@ -901,6 +914,8 @@ async function post(ev?: MouseEvent) { claimAchievement('notes1'); } + poll.value = null; + const text = postData.text ?? ''; const lowerCase = text.toLowerCase(); if ((lowerCase.includes('love') || lowerCase.includes('â¤')) && lowerCase.includes('sharkey')) { @@ -1030,6 +1045,41 @@ function openAccountMenu(ev: MouseEvent) { }, ev); } +function toggleScheduleNote() { + if (scheduleNote.value) scheduleNote.value = null; + else { + scheduleNote.value = { + scheduledAt: null, + }; + } +} + +function showOtherMenu(ev: MouseEvent) { + const menuItems: MenuItem[] = []; + + if ($i.policies.scheduleNoteMax > 0) { + menuItems.push({ + type: 'button', + text: i18n.ts.schedulePost, + icon: 'ti ti-calendar-time', + action: toggleScheduleNote, + }, { + type: 'button', + text: i18n.ts.schedulePostList, + icon: 'ti ti-calendar-event', + action: () => { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSchedulePostListDialog.vue')), {}, { + closed: () => { + dispose(); + }, + }); + }, + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); +} + onMounted(() => { if (props.autofocus) { focus(); @@ -1099,6 +1149,11 @@ onMounted(() => { } quoteId.value = init.renote ? init.renote.id : null; reactionAcceptance.value = init.reactionAcceptance; + if (init.isSchedule) { + scheduleNote.value = { + scheduledAt: new Date(init.createdAt).getTime(), + }; + } } nextTick(() => watchForDraft()); diff --git a/packages/frontend/src/components/MkScheduleEditor.vue b/packages/frontend/src/components/MkScheduleEditor.vue new file mode 100644 index 0000000000..8f18f620ae --- /dev/null +++ b/packages/frontend/src/components/MkScheduleEditor.vue @@ -0,0 +1,69 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<div style="padding: 8px 16px;"> + <section> + <MkInput v-model="atDate" small type="date" class="input"> + <template #label>{{ i18n.ts._poll.deadlineDate }}</template> + </MkInput> + <MkInput v-model="atTime" small type="time" class="input"> + <template #label>{{ i18n.ts._poll.deadlineTime }}</template> + </MkInput> + </section> +</div> +</template> + +<script lang="ts" setup> +import { onMounted, ref, watch } from 'vue'; +import MkInput from '@/components/MkInput.vue'; +import { formatDateTimeString } from '@/scripts/format-time-string.js'; +import { addTime } from '@/scripts/time.js'; +import { i18n } from '@/i18n.js'; + +const props = defineProps<{ + modelValue: { + scheduledAt: number | null; + }; +}>(); + +const emit = defineEmits<{ + (ev: 'update:modelValue', v: { + scheduledAt: number | null; + }): void; +}>(); + +const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd')); +const atTime = ref('00:00'); + +if (props.modelValue.scheduledAt) { + const date = new Date(props.modelValue.scheduledAt); + atDate.value = formatDateTimeString(date, 'yyyy-MM-dd'); + atTime.value = formatDateTimeString(date, 'HH:mm'); +} + +function get() { + const calcAt = () => { + return new Date(`${ atDate.value } ${ atTime.value }`).getTime(); + }; + + return { + ...( + { scheduledAt: calcAt() } + ), + }; +} + +watch([ + atDate, + atTime, +], () => emit('update:modelValue', get()), { + deep: true, +}); + +onMounted(() => { + emit('update:modelValue', get()); +}); +</script> diff --git a/packages/frontend/src/components/MkSchedulePostListDialog.vue b/packages/frontend/src/components/MkSchedulePostListDialog.vue new file mode 100644 index 0000000000..cf793c7110 --- /dev/null +++ b/packages/frontend/src/components/MkSchedulePostListDialog.vue @@ -0,0 +1,60 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialogEl" + :withOkButton="false" + @click="cancel()" + @close="cancel()" +> + <template #header>{{ i18n.ts.schedulePostList }}</template> + <MkSpacer :marginMin="14" :marginMax="16"> + <MkPagination ref="paginationEl" :pagination="pagination"> + <template #empty> + <div class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.nothing }}</div> + </div> + </template> + + <template #default="{ items }"> + <div class="_gaps"> + <MkNoteSimple v-for="item in items" :key="item.id" :scheduled="true" :note="item.note" @editScheduleNote="listUpdate"/> + </div> + </template> + </MkPagination> + </MkSpacer> +</MkModalWindow> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +import type { Paging } from '@/components/MkPagination.vue'; +import MkModalWindow from '@/components/MkModalWindow.vue'; +import MkPagination from '@/components/MkPagination.vue'; +import MkNoteSimple from '@/components/MkNoteSimple.vue'; +import { i18n } from '@/i18n.js'; +import { infoImageUrl } from '@/instance.js'; + +const emit = defineEmits<{ + (ev: 'cancel'): void; +}>(); + +const dialogEl = ref(); +const cancel = () => { + emit('cancel'); + dialogEl.value.close(); +}; +const paginationEl = ref(); +const pagination: Paging = { + endpoint: 'notes/schedule/list', + limit: 10, +}; + +function listUpdate() { + paginationEl.value.reload(); +} +</script> diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index e73aa77a3c..deb629d534 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -733,3 +733,4 @@ export function checkExistence(fileData: ArrayBuffer): Promise<any> { }); }); }*/ + diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 1763db2323..5d896db98c 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -200,6 +200,25 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.scheduleNoteMax, 'scheduleNoteMax'])"> + <template #label>{{ i18n.ts._role._options.scheduleNoteMax }}</template> + <template #suffix> + <span v-if="role.policies.scheduleNoteMax.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>{{ role.policies.scheduleNoteMax.value }}</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.scheduleNoteMax)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.scheduleNoteMax.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkInput v-model="role.policies.scheduleNoteMax.value" :disabled="role.policies.scheduleNoteMax.useDefault" type="number" :readonly="readonly"> + </MkInput> + <MkRange v-model="role.policies.scheduleNoteMax.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])"> <template #label>{{ i18n.ts._role._options.mentionMax }}</template> <template #suffix> diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 00a25446ab..036f18fe0d 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -70,6 +70,13 @@ SPDX-License-Identifier: AGPL-3.0-only </MkSwitch> </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.scheduleNoteMax, 'scheduleNoteMax'])"> + <template #label>{{ i18n.ts._role._options.scheduleNoteMax }}</template> + <template #suffix>{{ policies.scheduleNoteMax }}</template> + <MkInput v-model="policies.scheduleNoteMax" type="number"> + </MkInput> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])"> <template #label>{{ i18n.ts._role._options.mentionMax }}</template> <template #suffix>{{ policies.mentionLimit }}</template> diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 5af1a4112f..a74a4521e7 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1684,6 +1684,10 @@ declare namespace entities { NotesRenotesResponse, NotesRepliesRequest, NotesRepliesResponse, + NotesScheduleCreateRequest, + NotesScheduleDeleteRequest, + NotesScheduleListRequest, + NotesScheduleListResponse, NotesSearchByTagRequest, NotesSearchByTagResponse, NotesSearchRequest, @@ -2807,6 +2811,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/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 3fa67b7990..4b0e8173f8 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -3385,6 +3385,39 @@ declare module '../api.js' { credential?: string | null, ): Promise<SwitchCaseResponseType<E, P>>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes-schedule* + */ + request<E extends 'notes/schedule/create', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes-schedule* + */ + request<E extends 'notes/schedule/delete', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:notes-schedule* + */ + request<E extends 'notes/schedule/list', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 609c9f99d8..5caddb602b 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -450,6 +450,10 @@ import type { NotesRenotesResponse, NotesRepliesRequest, NotesRepliesResponse, + NotesScheduleCreateRequest, + NotesScheduleDeleteRequest, + NotesScheduleListRequest, + NotesScheduleListResponse, NotesSearchByTagRequest, NotesSearchByTagResponse, NotesSearchRequest, @@ -900,6 +904,9 @@ export type Endpoints = { 'notes/like': { req: NotesLikeRequest; res: EmptyResponse }; 'notes/renotes': { req: NotesRenotesRequest; res: NotesRenotesResponse }; 'notes/replies': { req: NotesRepliesRequest; res: NotesRepliesResponse }; + 'notes/schedule/create': { req: NotesScheduleCreateRequest; res: EmptyResponse }; + 'notes/schedule/delete': { req: NotesScheduleDeleteRequest; res: EmptyResponse }; + 'notes/schedule/list': { req: NotesScheduleListRequest; res: NotesScheduleListResponse }; 'notes/search-by-tag': { req: NotesSearchByTagRequest; res: NotesSearchByTagResponse }; 'notes/search': { req: NotesSearchRequest; res: NotesSearchResponse }; 'notes/show': { req: NotesShowRequest; res: NotesShowResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 999dd4dd54..2da78f6a50 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -453,6 +453,10 @@ export type NotesRenotesRequest = operations['notes___renotes']['requestBody'][' export type NotesRenotesResponse = operations['notes___renotes']['responses']['200']['content']['application/json']; export type NotesRepliesRequest = operations['notes___replies']['requestBody']['content']['application/json']; export type NotesRepliesResponse = operations['notes___replies']['responses']['200']['content']['application/json']; +export type NotesScheduleCreateRequest = operations['notes___schedule___create']['requestBody']['content']['application/json']; +export type NotesScheduleDeleteRequest = operations['notes___schedule___delete']['requestBody']['content']['application/json']; +export type NotesScheduleListRequest = operations['notes___schedule___list']['requestBody']['content']['application/json']; +export type NotesScheduleListResponse = operations['notes___schedule___list']['responses']['200']['content']['application/json']; export type NotesSearchByTagRequest = operations['notes___search-by-tag']['requestBody']['content']['application/json']; export type NotesSearchByTagResponse = operations['notes___search-by-tag']['responses']['200']['content']['application/json']; export type NotesSearchRequest = operations['notes___search']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index ad30f47f2e..2e6320c5be 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -2935,6 +2935,33 @@ export type paths = { */ post: operations['notes___replies']; }; + '/notes/schedule/create': { + /** + * notes/schedule/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes-schedule* + */ + post: operations['notes___schedule___create']; + }; + '/notes/schedule/delete': { + /** + * notes/schedule/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes-schedule* + */ + post: operations['notes___schedule___delete']; + }; + '/notes/schedule/list': { + /** + * notes/schedule/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:notes-schedule* + */ + post: operations['notes___schedule___list']; + }; '/notes/search-by-tag': { /** * notes/search-by-tag @@ -5036,6 +5063,7 @@ export type components = { canImportFollowing: boolean; canImportMuting: boolean; canImportUserLists: boolean; + scheduleNoteMax: number; }; ReversiGameLite: { /** Format: id */ @@ -24424,6 +24452,247 @@ export type operations = { }; }; }; + /** + * notes/schedule/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes-schedule* + */ + notes___schedule___create: { + requestBody: { + content: { + 'application/json': { + /** + * @default public + * @enum {string} + */ + visibility?: 'public' | 'home' | 'followers' | 'specified'; + visibleUserIds?: string[]; + cw?: string | null; + /** + * @default null + * @enum {string|null} + */ + reactionAcceptance?: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote'; + /** @default false */ + disableRightClick?: boolean; + /** @default false */ + noExtractMentions?: boolean; + /** @default false */ + noExtractHashtags?: boolean; + /** @default false */ + noExtractEmojis?: boolean; + /** Format: misskey:id */ + replyId?: string | null; + /** Format: misskey:id */ + renoteId?: string | null; + text?: string | null; + fileIds?: string[]; + mediaIds?: string[]; + poll?: ({ + choices: string[]; + multiple?: boolean; + expiresAt?: number | null; + expiredAfter?: number | null; + }) | null; + event?: ({ + title?: string; + start?: number; + end?: number | null; + metadata?: Record<string, never>; + }) | null; + scheduleNote: { + scheduledAt?: number; + }; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/schedule/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:notes-schedule* + */ + notes___schedule___delete: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + noteId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + /** + * notes/schedule/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:notes-schedule* + */ + notes___schedule___list: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default 10 */ + limit?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': ({ + /** Format: misskey:id */ + id: string; + note: { + createdAt: string; + text?: 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'; + isSchedule: boolean; + }; + userId: string; + scheduledAt: string; + })[]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description To many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * notes/search-by-tag * @description No description provided. diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index c99b8f5570..d090a6b46f 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -42,6 +42,8 @@ export const permissions = [ 'read:mutes', 'write:mutes', 'write:notes', + 'read:notes-schedule', + 'write:notes-schedule', 'read:notifications', 'write:notifications', 'read:reactions', -- GitLab From 4f58b8de20625da577a2b7a8055d065bbddb94d1 Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Sun, 3 Nov 2024 03:39:19 +0100 Subject: [PATCH 02/18] fix: drive content not being loaded --- .../api/endpoints/notes/schedule/create.ts | 28 ++----------------- .../api/endpoints/notes/schedule/list.ts | 3 ++ .../frontend/src/components/MkMediaList.vue | 2 +- .../frontend/src/components/MkNoteSimple.vue | 2 ++ .../components/MkSchedulePostListDialog.vue | 3 ++ packages/misskey-js/etc/misskey-js.api.md | 2 +- packages/misskey-js/src/autogen/types.ts | 8 ------ 7 files changed, 13 insertions(+), 35 deletions(-) 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 ecdfa4bf2e..c22c29ae31 100644 --- a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts @@ -6,7 +6,7 @@ import ms from 'ms'; import { In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import { isPureRenote } from 'cherrypick-js/note.js'; +import { isPureRenote } from '@/misc/is-renote.js'; import type { MiUser } from '@/models/User.js'; import type { UsersRepository, @@ -19,7 +19,6 @@ import type { 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 { DI } from '@/di-symbols.js'; import { QueueService } from '@/core/QueueService.js'; @@ -129,7 +128,6 @@ export const paramDef = { } }, cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, - disableRightClick: { type: 'boolean', default: false }, noExtractMentions: { type: 'boolean', default: false }, noExtractHashtags: { type: 'boolean', default: false }, noExtractEmojis: { type: 'boolean', default: false }, @@ -141,7 +139,6 @@ export const paramDef = { text: { type: 'string', minLength: 1, - maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true, }, fileIds: { @@ -175,16 +172,6 @@ export const paramDef = { }, required: ['choices'], }, - event: { - type: 'object', - nullable: true, - properties: { - title: { type: 'string', minLength: 1, maxLength: 128, nullable: false }, - start: { type: 'integer', nullable: false }, - end: { type: 'integer', nullable: true }, - metadata: { type: 'object' }, - }, - }, scheduleNote: { type: 'object', nullable: false, @@ -227,11 +214,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private queueService: QueueService, private roleService: RoleService, - private idService: IdService, + private idService: IdService, ) { - super({ - ...meta, - }, paramDef, async (ps, me) => { + super(meta, paramDef, async (ps, me) => { const scheduleNoteCount = await this.noteScheduleRepository.countBy({ userId: me.id }); const scheduleNoteMax = (await this.roleService.getUserPolicies(me.id)).scheduleNoteMax; if (scheduleNoteCount >= scheduleNoteMax) { @@ -358,13 +343,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- apMentions: ps.noExtractMentions ? [] : undefined, apHashtags: ps.noExtractHashtags ? [] : undefined, apEmojis: ps.noExtractEmojis ? [] : undefined, - event: ps.event ? { - start: new Date(ps.event.start!).toISOString(), - end: ps.event.end ? new Date(ps.event.end).toISOString() : null, - title: ps.event.title!, - metadata: ps.event.metadata ?? {}, - } : undefined, - disableRightClick: ps.disableRightClick, }; if (ps.scheduleNote.scheduledAt) { 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 88da4f4043..4895733d4e 100644 --- a/packages/backend/src/server/api/endpoints/notes/schedule/list.ts +++ b/packages/backend/src/server/api/endpoints/notes/schedule/list.ts @@ -9,6 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import type { MiNote, MiNoteSchedule, NoteScheduleRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { QueryService } from '@/core/QueryService.js'; import { Packed } from '@/misc/json-schema.js'; import { noteVisibilities } from '@/types.js'; @@ -81,6 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private noteScheduleRepository: NoteScheduleRepository, private userEntityService: UserEntityService, + private driveFileEntityService: DriveFileEntityService, private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { @@ -115,6 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- reactionAcceptance: item.note.reactionAcceptance ?? null, visibleUsers: item.note.visibleUsers ? await userEntityService.packMany(item.note.visibleUsers.map(u => u.id), me) : [], fileIds: item.note.files ? item.note.files : [], + files: await this.driveFileEntityService.packManyByIds(item.note.files), createdAt: item.scheduledAt.toISOString(), isSchedule: true, id: item.id, diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index 5209489046..4ef929e81f 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -35,13 +35,13 @@ import * as Misskey from 'misskey-js'; import PhotoSwipeLightbox from 'photoswipe/lightbox'; import PhotoSwipe from 'photoswipe'; import 'photoswipe/style.css'; +import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES } from '@@/js/const.js'; import XBanner from '@/components/MkMediaBanner.vue'; import XImage from '@/components/MkMediaImage.vue'; import XVideo from '@/components/MkMediaVideo.vue'; import XModPlayer from '@/components/SkModPlayer.vue'; import XFlashPlayer from '@/components/SkFlashPlayer.vue'; import * as os from '@/os.js'; -import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES, FILE_TYPE_FLASH_CONTENT, FILE_EXT_FLASH_CONTENT } from '@@/js/const.js'; import { defaultStore } from '@/store.js'; import { focusParent } from '@/scripts/focus.js'; diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index 7d2bbb31d3..48bf53fab5 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -33,6 +33,8 @@ import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkSubNoteContent from '@/components/MkSubNoteContent.vue'; import MkCwButton from '@/components/MkCwButton.vue'; import { defaultStore } from '@/store.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { i18n } from '@/i18n.js'; const props = defineProps<{ note: Misskey.entities.Note & { diff --git a/packages/frontend/src/components/MkSchedulePostListDialog.vue b/packages/frontend/src/components/MkSchedulePostListDialog.vue index cf793c7110..8311981a75 100644 --- a/packages/frontend/src/components/MkSchedulePostListDialog.vue +++ b/packages/frontend/src/components/MkSchedulePostListDialog.vue @@ -52,8 +52,11 @@ const paginationEl = ref(); const pagination: Paging = { endpoint: 'notes/schedule/list', limit: 10, + offsetMode: true, }; +console.log(pagination); + function listUpdate() { paginationEl.value.reload(); } diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index a74a4521e7..ca7a374a67 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2953,7 +2953,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:channels", "write:channels", "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:approve-user", "write:admin:decline-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:silence-user", "write:admin:unsilence-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"]; +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", "write: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:channels", "write:channels", "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:approve-user", "write:admin:decline-user", "write:admin:nsfw-user", "write:admin:unnsfw-user", "write:admin:silence-user", "write:admin:unsilence-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"]; // @public (undocumented) type PingResponse = operations['ping']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 2e6320c5be..c8d7194405 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -24475,8 +24475,6 @@ export type operations = { */ reactionAcceptance?: null | 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote'; /** @default false */ - disableRightClick?: boolean; - /** @default false */ noExtractMentions?: boolean; /** @default false */ noExtractHashtags?: boolean; @@ -24495,12 +24493,6 @@ export type operations = { expiresAt?: number | null; expiredAfter?: number | null; }) | null; - event?: ({ - title?: string; - start?: number; - end?: number | null; - metadata?: Record<string, never>; - }) | null; scheduleNote: { scheduledAt?: number; }; -- GitLab From 1ee41d239c46f2f2ef64cc1fe77888ffd38259e2 Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Sun, 3 Nov 2024 03:43:09 +0100 Subject: [PATCH 03/18] chore: add new ti icon to replace --- packages/frontend/vite.replaceIcons.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/vite.replaceIcons.ts b/packages/frontend/vite.replaceIcons.ts index ae9a8f81d6..71005fab31 100644 --- a/packages/frontend/vite.replaceIcons.ts +++ b/packages/frontend/vite.replaceIcons.ts @@ -175,6 +175,7 @@ export function pluginReplaceIcons() { 'ti ti-cake': 'ph-cake ph-bold ph-lg', 'ti ti-calendar': 'ph-calendar ph-bold ph-lg', 'ti ti-calendar-time': 'ph-calendar ph-bold ph-lg', + 'ti ti-calendar-event': 'ph-calendar-star ph-bold ph-lg', 'ti ti-camera': 'ph-camera ph-bold ph-lg', 'ti ti-carousel-horizontal': 'ph-split-horizontal ph-bold ph-lg', 'ti ti-carousel-vertical': 'ph-split-vertical ph-bold ph-lg', -- GitLab From aeb10751ed9ee0ca8725183b20565b87e47d9616 Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Sun, 3 Nov 2024 03:55:44 +0100 Subject: [PATCH 04/18] chore: fix locales --- locales/en-US.yml | 2 -- locales/index.d.ts | 28 ++++++++++++++++++++-------- locales/ja-JP.yml | 2 -- locales/ko-KR.yml | 2 -- sharkey-locales/en-US.yml | 8 ++++++++ sharkey-locales/ja-JP.yml | 6 ++++++ 6 files changed, 34 insertions(+), 14 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index 38e9b03acb..6ea7fb4f8d 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -2073,8 +2073,6 @@ _permissions: "read:mutes": "View your list of muted users" "write:mutes": "Edit your list of muted users" "write:notes": "Compose or delete notes" - "read:notes-schedule": "View your list of scheduled notes" - "write:notes-schedule": "Compose or delete scheduled notes" "read:notifications": "View your notifications" "write:notifications": "Manage your notifications" "read:reactions": "View your reactions" diff --git a/locales/index.d.ts b/locales/index.d.ts index 92c09ffe12..e181b13f33 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -6947,6 +6947,10 @@ export interface Locale extends ILocale { * Can import notes */ "canImportNotes": string; + /** + * Maximum number of scheduled notes + */ + "scheduleNoteMax": string; }; "_condition": { /** @@ -8140,14 +8144,6 @@ export interface Locale extends ILocale { * ノートを作æˆãƒ»å‰Šé™¤ã™ã‚‹ */ "write:notes": string; - /** - * 予約投稿を見る - */ - "read:notes-schedule": string; - /** - * 予約投稿を作æˆãƒ»å‰Šé™¤ã™ã‚‹ - */ - "write:notes-schedule": string; /** * 通知を見る */ @@ -8424,6 +8420,14 @@ export interface Locale extends ILocale { * é•åã‚’å ±å‘Šã™ã‚‹ */ "write:report-abuse": string; + /** + * View your list of scheduled notes + */ + "read:notes-schedule": string; + /** + * Compose or delete scheduled notes + */ + "write:notes-schedule": string; }; "_auth": { /** @@ -11436,6 +11440,14 @@ export interface Locale extends ILocale { * Select a follow relationship... */ "selectFollowRelationship": string; + /** + * Schedule a note + */ + "schedulePost": string; + /** + * List of scheduled notes + */ + "schedulePostList": string; } declare const locales: { [lang: string]: Locale; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0d2229ac20..c448d4d50a 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2121,8 +2121,6 @@ _permissions: "read:mutes": "ミュートを見る" "write:mutes": "ミュートをæ“作ã™ã‚‹" "write:notes": "ノートを作æˆãƒ»å‰Šé™¤ã™ã‚‹" - "read:notes-schedule": "予約投稿を見る" - "write:notes-schedule": "予約投稿を作æˆãƒ»å‰Šé™¤ã™ã‚‹" "read:notifications": "通知を見る" "write:notifications": "通知をæ“作ã™ã‚‹" "read:reactions": "リアクションを見る" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 351d5a23ce..414202adab 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -2080,8 +2080,6 @@ _permissions: "read:mutes": "뮤트 여부를 확ì¸í•©ë‹ˆë‹¤" "write:mutes": "뮤트를 하거나 í•´ì œí•©ë‹ˆë‹¤" "write:notes": "노트를 작성하거나 ì‚ì œí•©ë‹ˆë‹¤" - "read:notes-schedule": "게시를 예약한 노트를 봅니다" - "write:notes-schedule": "노트 게시를 예약하거나 ì‚ì œí•©ë‹ˆë‹¤" "read:notifications": "ì•Œë¦¼ì„ í™•ì¸í•©ë‹ˆë‹¤" "write:notifications": "ì•Œë¦¼ì„ ëª¨ë‘ ì½ìŒ 처리합니다" "read:reactions": "ë¦¬ì•¡ì…˜ì„ í™•ì¸í•©ë‹ˆë‹¤" diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 827112facc..79c362cfd8 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -225,6 +225,7 @@ _role: btlAvailable: "Can view the bubble timeline" canImportNotes: "Can import notes" canUpdateBioMedia: "Allow users to edit their avatar or banner" + scheduleNoteMax: "Maximum number of scheduled notes" _condition: isLocked: "Private account" isExplorable: "Account is discoverable" @@ -414,3 +415,10 @@ _deck: following: "Following" selectFollowRelationship: "Select a follow relationship..." + +schedulePost: "Schedule a note" +schedulePostList: "List of scheduled notes" + +_permissions: + "read:notes-schedule": "View your list of scheduled notes" + "write:notes-schedule": "Compose or delete scheduled notes" diff --git a/sharkey-locales/ja-JP.yml b/sharkey-locales/ja-JP.yml index 22bd5235ca..46566c39ac 100644 --- a/sharkey-locales/ja-JP.yml +++ b/sharkey-locales/ja-JP.yml @@ -210,6 +210,7 @@ _role: btlAvailable: "ãƒãƒ–ルタイムラインã®é–²è¦§" canImportNotes: "ノートã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆãŒå¯èƒ½" canUpdateBioMedia: "アイコンã¨ãƒãƒŠãƒ¼ã®æ›´æ–°ã‚’許å¯" + scheduleNoteMax: "予約投稿ã®æœ€å¤§æ•°" _condition: isLocked: "éµã‚¢ã‚«ã‚¦ãƒ³ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼" isExplorable: "「アカウントを見ã¤ã‘ã‚„ã™ãã™ã‚‹ã€ãŒæœ‰åŠ¹ãªãƒ¦ãƒ¼ã‚¶ãƒ¼" @@ -384,3 +385,8 @@ _externalNavigationWarning: title: "外部サイトã«ç§»å‹•ã—ã¾ã™" description: "{host}を離れã¦å¤–部サイトã«ç§»å‹•ã—ã¾ã™" trustThisDomain: "ã“ã®ãƒ‡ãƒã‚¤ã‚¹ã§ä»Šå¾Œã“ã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã‚’ä¿¡é ¼ã™ã‚‹" +schedulePost: "予約投稿" +schedulePostList: "予約投稿一覧" +_permissions: + "read:notes-schedule": "予約投稿を見る" + "write:notes-schedule": "予約投稿を作æˆãƒ»å‰Šé™¤ã™ã‚‹" -- GitLab From f08d1ec38f846ad46bf95bd4d2d67ebaa143e042 Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Sun, 3 Nov 2024 04:04:17 +0100 Subject: [PATCH 05/18] chore: remove leftover log --- packages/frontend/src/components/MkSchedulePostListDialog.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/frontend/src/components/MkSchedulePostListDialog.vue b/packages/frontend/src/components/MkSchedulePostListDialog.vue index 8311981a75..cfae94951b 100644 --- a/packages/frontend/src/components/MkSchedulePostListDialog.vue +++ b/packages/frontend/src/components/MkSchedulePostListDialog.vue @@ -55,8 +55,6 @@ const pagination: Paging = { offsetMode: true, }; -console.log(pagination); - function listUpdate() { paginationEl.value.reload(); } -- GitLab From fc9d777dc3161b40c5c62bb65cb03e2c7d8f4380 Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Sun, 3 Nov 2024 17:59:50 +0100 Subject: [PATCH 06/18] upd: add notification for failures, add reasons for failure, apply suggestions --- locales/index.d.ts | 4 ++ .../entities/NotificationEntityService.ts | 5 +- packages/backend/src/models/NoteSchedule.ts | 2 - packages/backend/src/models/Notification.ts | 5 ++ .../src/models/json-schema/notification.ts | 14 +++++ .../ScheduleNotePostProcessorService.ts | 59 +++++++++++++++---- .../api/endpoints/notes/schedule/create.ts | 5 +- packages/backend/src/types.ts | 1 + packages/frontend-shared/js/const.ts | 1 + .../src/components/MkNotification.vue | 8 ++- .../frontend/src/components/MkPostForm.vue | 5 +- .../components/MkSchedulePostListDialog.vue | 1 + packages/misskey-js/etc/misskey-js.api.md | 2 +- packages/misskey-js/src/autogen/types.ts | 16 +++-- packages/misskey-js/src/consts.ts | 2 +- .../sw/src/scripts/create-notification.ts | 7 +++ sharkey-locales/en-US.yml | 1 + 17 files changed, 110 insertions(+), 28 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index e181b13f33..4cfc220731 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -9558,6 +9558,10 @@ export interface Locale extends ILocale { * Note got edited */ "edited": string; + /** + * Posting scheduled note failed + */ + "scheduledNoteFailed": string; }; "_deck": { /** diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index bbaf0cb7c8..27b8231854 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -20,7 +20,7 @@ import type { OnModuleInit } from '@nestjs/common'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; -const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited'] as (typeof groupedNotificationTypes[number])[]); +const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited', 'scheduledNoteFailed'] as (typeof groupedNotificationTypes[number])[]); @Injectable() export class NotificationEntityService implements OnModuleInit { @@ -169,6 +169,9 @@ export class NotificationEntityService implements OnModuleInit { exportedEntity: notification.exportedEntity, fileId: notification.fileId, } : {}), + ...(notification.type === 'scheduledNoteFailed' ? { + reason: notification.reason, + } : {}), ...(notification.type === 'app' ? { body: notification.customBody, header: notification.customHeader, diff --git a/packages/backend/src/models/NoteSchedule.ts b/packages/backend/src/models/NoteSchedule.ts index 97ffe32ffa..dde0af6ad7 100644 --- a/packages/backend/src/models/NoteSchedule.ts +++ b/packages/backend/src/models/NoteSchedule.ts @@ -18,8 +18,6 @@ type MinimumUser = { }; export type MiScheduleNoteType={ - /** Date.toISOString() */ - createdAt: string; visibility: 'public' | 'home' | 'followers' | 'specified'; visibleUsers: MinimumUser[]; channel?: MiChannel['id']; diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index c4f046c565..5003e02d96 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -122,6 +122,11 @@ export type MiNotification = { createdAt: string; notifierId: MiUser['id']; noteId: MiNote['id']; +} | { + type: 'scheduledNoteFailed'; + id: string; + createdAt: string; + reason: string; }; export type MiGroupedNotification = MiNotification | { diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 990e8957cf..69bd9531ec 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -369,6 +369,20 @@ export const packedNotificationSchema = { optional: false, nullable: false, }, }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['scheduledNoteFailed'], + }, + reason: { + type: 'string', + optional: false, nullable: false, + }, + }, }, { type: 'object', properties: { diff --git a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts index 62d527953d..59e23b865e 100644 --- a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts +++ b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts @@ -9,6 +9,7 @@ import { bindThis } from '@/decorators.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; import type { ChannelsRepository, DriveFilesRepository, MiDriveFile, NoteScheduleRepository, NotesRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; +import { NotificationService } from '@/core/NotificationService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { ScheduleNotePostJobData } from '../types.js'; @@ -32,6 +33,7 @@ export class ScheduleNotePostProcessorService { private noteCreateService: NoteCreateService, private queueLoggerService: QueueLoggerService, + private notificationService: NotificationService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('schedule-note-post'); } @@ -50,8 +52,9 @@ export class ScheduleNotePostProcessorService { const renote = note.reply ? await this.notesRepository.findOneBy({ id: note.renote }) : undefined; const channel = note.channel ? await this.channelsRepository.findOneBy({ id: note.channel, isArchived: false }) : undefined; let files: MiDriveFile[] = []; - const fileIds = note.files ?? null; - if (fileIds != null && fileIds.length > 0 && me) { + const fileIds = note.files; + + if (fileIds.length > 0 && me) { files = await this.driveFilesRepository.createQueryBuilder('file') .where('file.userId = :userId AND file.id IN (:...fileIds)', { userId: me.id, @@ -61,22 +64,52 @@ export class ScheduleNotePostProcessorService { .setParameters({ fileIds }) .getMany(); } - if ( - !data.userId || - !me || - (note.reply && !reply) || - (note.renote && !renote) || - (note.channel && !channel) || - (note.files.length !== files.length) - ) { - //ã‚ューã«ç©ã‚“ã ã¨ãã¯æœ‰ã£ãŸç‰©ãŒæ¶ˆæ»…ã—ã¦ãŸã‚‰äºˆç´„投稿をã‚ャンセルã™ã‚‹ - this.logger.warn('cancel schedule note'); + + if (!data.userId || !me) { + this.logger.warn('Schedule Note Failed Reason: User Not Found'); + await this.noteScheduleRepository.remove(data); + return; + } + + if (note.files.length !== files.length) { + this.logger.warn('Schedule Note Failed Reason: files are missing in the user\'s drive'); + this.notificationService.createNotification(me.id, 'scheduledNoteFailed', { + reason: 'Some attached files on your scheduled note no longer exist', + }); + await this.noteScheduleRepository.remove(data); + return; + } + + if (note.reply && !reply) { + this.logger.warn('Schedule Note Failed Reason: parent note to reply does not exist'); + this.notificationService.createNotification(me.id, 'scheduledNoteFailed', { + reason: 'Replied to note on your scheduled note no longer exists', + }); await this.noteScheduleRepository.remove(data); return; } + + if (note.renote && !renote) { + this.logger.warn('Schedule Note Failed Reason: attached quote note no longer exists'); + this.notificationService.createNotification(me.id, 'scheduledNoteFailed', { + reason: 'A quoted note from one of your scheduled notes no longer exists', + }); + await this.noteScheduleRepository.remove(data); + return; + } + + if (note.channel && !channel) { + this.logger.warn('Schedule Note Failed Reason: Channel does not exist'); + this.notificationService.createNotification(me.id, 'scheduledNoteFailed', { + reason: 'An attached channel on your scheduled note no longer exists', + }); + await this.noteScheduleRepository.remove(data); + return; + } + await this.noteCreateService.create(me, { ...note, - createdAt: new Date(note.createdAt), //typeORMã®jsonbã§ä½•æ•…ã‹stringã«ã•ã‚Œã‚‹ã‹ã‚‰æˆ»ã™ + createdAt: new Date(), files, poll: note.poll ? { choices: note.poll.choices, 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 c22c29ae31..b8ae3f44a3 100644 --- a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts @@ -292,7 +292,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- // Check blocking if (reply.userId !== me.id) { - const blockExist = await this.blockingsRepository.exist({ + const blockExist = await this.blockingsRepository.exists({ where: { blockerId: reply.userId, blockeeId: me.id, @@ -324,8 +324,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } else { throw new ApiError(meta.errors.cannotCreateAlreadyExpiredSchedule); } - const note:MiScheduleNoteType = { - createdAt: new Date(ps.scheduleNote.scheduledAt!).toISOString(), + const note: MiScheduleNoteType = { files: files.map(f => f.id), poll: ps.poll ? { choices: ps.poll.choices, diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 2aa4f279ea..7930129002 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -35,6 +35,7 @@ export const notificationTypes = [ 'roleAssigned', 'achievementEarned', 'exportCompleted', + 'scheduledNoteFailed', 'app', 'test', ] as const; diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts index 882f19c7fd..5bc75db908 100644 --- a/packages/frontend-shared/js/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -131,6 +131,7 @@ export const notificationTypes = [ 'test', 'app', 'edited', + 'scheduledNoteFailed', ] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 7bec9bdc65..3c4f56b537 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="$style.root"> <div :class="$style.head"> <MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/> - <MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> + <MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'scheduledNoteFailed'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> <div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div> @@ -29,6 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.t_exportCompleted]: notification.type === 'exportCompleted', [$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null, [$style.t_pollEnded]: notification.type === 'edited', + [$style.t_roleAssigned]: notification.type === 'scheduledNoteFailed', }]" > <!-- we re-use t_pollEnded for "edited" instead of making an identical style --> <i v-if="notification.type === 'follow'" class="ti ti-plus"></i> @@ -46,6 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-else class="ti ti-badges"></i> </template> <i v-else-if="notification.type === 'edited'" class="ph-pencil ph-bold ph-lg"></i> + <i v-else-if="notification.type === 'scheduledNoteFailed'" class="ti ti-calendar-event"></i> <!-- notification.reaction ㌠null ã«ãªã‚‹ã“ã¨ã¯ã¾ãšãªã„ãŒã€ã“ã“ã§optional chaining使ã†ã¨ä¸€éƒ¨ãƒ–ラウザã§åˆºã•ã‚‹ã®ã§å¿µã®ç‚º --> <MkReactionIcon v-else-if="notification.type === 'reaction'" @@ -70,6 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span> <span v-else-if="notification.type === 'app'">{{ notification.header }}</span> <span v-else-if="notification.type === 'edited'">{{ i18n.ts._notification.edited }}</span> + <span v-else-if="notification.type === 'scheduledNoteFailed'">{{ i18n.ts._notification.scheduledNoteFailed }}</span> <MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/> </header> <div> @@ -109,6 +112,9 @@ SPDX-License-Identifier: AGPL-3.0-only <MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`"> {{ i18n.ts.showFile }} </MkA> + <div v-else-if="notification.type === 'scheduledNoteFailed'" :class="$style.text"> + {{ notification.reason }} + </div> <template v-else-if="notification.type === 'follow'"> <span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span> </template> diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 443e9e7ee9..bbde7c65f9 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -1046,8 +1046,9 @@ function openAccountMenu(ev: MouseEvent) { } function toggleScheduleNote() { - if (scheduleNote.value) scheduleNote.value = null; - else { + if (scheduleNote.value) { + scheduleNote.value = null; + } else { scheduleNote.value = { scheduledAt: null, }; diff --git a/packages/frontend/src/components/MkSchedulePostListDialog.vue b/packages/frontend/src/components/MkSchedulePostListDialog.vue index cfae94951b..d0716ead79 100644 --- a/packages/frontend/src/components/MkSchedulePostListDialog.vue +++ b/packages/frontend/src/components/MkSchedulePostListDialog.vue @@ -48,6 +48,7 @@ const cancel = () => { emit('cancel'); dialogEl.value.close(); }; + const paginationEl = ref(); const pagination: Paging = { endpoint: 'notes/schedule/list', diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index ca7a374a67..880be518fa 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2890,7 +2890,7 @@ type Notification_2 = components['schemas']['Notification']; type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json']; // @public (undocumented) -export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned", "edited"]; +export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned", "edited", "scheduledNoteFailed"]; // @public (undocumented) export function nyaize(text: string): string; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index c8d7194405..6eb1819037 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4517,6 +4517,14 @@ export type components = { /** Format: id */ userId: string; note: components['schemas']['Note']; + } | { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** @enum {string} */ + type: 'scheduledNoteFailed'; + reason: string; } | { /** Format: id */ id: string; @@ -19984,8 +19992,8 @@ export type operations = { untilId?: string; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; }; }; }; @@ -20052,8 +20060,8 @@ export type operations = { untilId?: string; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; }; }; }; diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index d090a6b46f..34fc7c1a03 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -16,7 +16,7 @@ import type { UserLite, } from './autogen/models.js'; -export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'edited'] as const; +export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'edited', 'scheduledNoteFailed'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts index 9c56e338c7..8442552e3b 100644 --- a/packages/sw/src/scripts/create-notification.ts +++ b/packages/sw/src/scripts/create-notification.ts @@ -258,6 +258,13 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif data, }]; + case 'scheduledNoteFailed': + return [i18n.ts._notification.scheduledNoteFailed, { + body: data.body.reason, + badge: iconUrl('bell'), + data, + }]; + default: return null; } diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 79c362cfd8..582f4eda0a 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -277,6 +277,7 @@ _notification: youRenoted: "Boost from {name}" renotedBySomeUsers: "Boosted by {n} users" edited: "Note got edited" + scheduledNoteFailed: "Posting scheduled note failed" _types: renote: "Boosts" edited: "Edits" -- GitLab From 170093f2a2873f6fc74ddff2c47d546238e33716 Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Sun, 3 Nov 2024 17:01:46 +0000 Subject: [PATCH 07/18] chore: remove from note required type --- packages/backend/src/core/entities/NotificationEntityService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 27b8231854..447d95cc68 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -20,7 +20,7 @@ import type { OnModuleInit } from '@nestjs/common'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; -const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited', 'scheduledNoteFailed'] as (typeof groupedNotificationTypes[number])[]); +const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited'] as (typeof groupedNotificationTypes[number])[]); @Injectable() export class NotificationEntityService implements OnModuleInit { -- GitLab From 1d3c8253981fba2d431df2f5cc12dd78b21d37bc Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Mon, 4 Nov 2024 11:22:37 +0100 Subject: [PATCH 08/18] upd: add notification for when scheduled note gets posted --- locales/index.d.ts | 4 +++ .../entities/NotificationEntityService.ts | 2 +- packages/backend/src/models/Notification.ts | 5 ++++ .../src/models/json-schema/notification.ts | 25 +++++++++++++++++++ .../ScheduleNotePostProcessorService.ts | 5 +++- packages/backend/src/types.ts | 1 + packages/frontend-shared/js/const.ts | 1 + .../src/components/MkNotification.vue | 11 +++++++- packages/misskey-js/etc/misskey-js.api.md | 2 +- packages/misskey-js/src/autogen/types.ts | 19 +++++++++++--- packages/misskey-js/src/consts.ts | 2 +- .../sw/src/scripts/create-notification.ts | 8 ++++++ sharkey-locales/en-US.yml | 1 + 13 files changed, 77 insertions(+), 9 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index 4cfc220731..89541e6414 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -9562,6 +9562,10 @@ export interface Locale extends ILocale { * Posting scheduled note failed */ "scheduledNoteFailed": string; + /** + * Scheduled Note was posted + */ + "scheduledNotePosted": string; }; "_deck": { /** diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 447d95cc68..31a9809323 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -20,7 +20,7 @@ import type { OnModuleInit } from '@nestjs/common'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; -const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited'] as (typeof groupedNotificationTypes[number])[]); +const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited', 'scheduledNotePosted'] as (typeof groupedNotificationTypes[number])[]); @Injectable() export class NotificationEntityService implements OnModuleInit { diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 5003e02d96..53003a0a5a 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -127,6 +127,11 @@ export type MiNotification = { id: string; createdAt: string; reason: string; +} | { + type: 'scheduledNotePosted'; + id: string; + createdAt: string; + noteId: MiNote['id']; }; export type MiGroupedNotification = MiNotification | { diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 69bd9531ec..26498e3e9d 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -383,6 +383,31 @@ export const packedNotificationSchema = { optional: false, nullable: false, }, }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['scheduledNotePosted'], + }, + user: { + type: 'object', + ref: 'UserLite', + optional: false, nullable: false, + }, + userId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + note: { + type: 'object', + ref: 'Note', + optional: false, nullable: false, + }, + }, }, { type: 'object', properties: { diff --git a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts index 59e23b865e..f281b0ed7b 100644 --- a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts +++ b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts @@ -107,7 +107,7 @@ export class ScheduleNotePostProcessorService { return; } - await this.noteCreateService.create(me, { + const createdNote = await this.noteCreateService.create(me, { ...note, createdAt: new Date(), files, @@ -121,6 +121,9 @@ export class ScheduleNotePostProcessorService { channel, }); await this.noteScheduleRepository.remove(data); + this.notificationService.createNotification(me.id, 'scheduledNotePosted', { + noteId: createdNote.id, + }); } }); } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 7930129002..95f049f768 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -36,6 +36,7 @@ export const notificationTypes = [ 'achievementEarned', 'exportCompleted', 'scheduledNoteFailed', + 'scheduledNotePosted', 'app', 'test', ] as const; diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts index 5bc75db908..a6eebe8e15 100644 --- a/packages/frontend-shared/js/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -132,6 +132,7 @@ export const notificationTypes = [ 'app', 'edited', 'scheduledNoteFailed', + 'scheduledNotePosted', ] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 3c4f56b537..f713d46137 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root"> <div :class="$style.head"> - <MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/> + <MkAvatar v-if="['pollEnded', 'note', 'edited', 'scheduledNotePosted'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'scheduledNoteFailed'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> <div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div> @@ -30,6 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null, [$style.t_pollEnded]: notification.type === 'edited', [$style.t_roleAssigned]: notification.type === 'scheduledNoteFailed', + [$style.t_pollEnded]: notification.type === 'scheduledNotePosted', }]" > <!-- we re-use t_pollEnded for "edited" instead of making an identical style --> <i v-if="notification.type === 'follow'" class="ti ti-plus"></i> @@ -48,6 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <i v-else-if="notification.type === 'edited'" class="ph-pencil ph-bold ph-lg"></i> <i v-else-if="notification.type === 'scheduledNoteFailed'" class="ti ti-calendar-event"></i> + <i v-else-if="notification.type === 'scheduledNotePosted'" class="ti ti-calendar-event"></i> <!-- notification.reaction ㌠null ã«ãªã‚‹ã“ã¨ã¯ã¾ãšãªã„ãŒã€ã“ã“ã§optional chaining使ã†ã¨ä¸€éƒ¨ãƒ–ラウザã§åˆºã•ã‚‹ã®ã§å¿µã®ç‚º --> <MkReactionIcon v-else-if="notification.type === 'reaction'" @@ -73,6 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-else-if="notification.type === 'app'">{{ notification.header }}</span> <span v-else-if="notification.type === 'edited'">{{ i18n.ts._notification.edited }}</span> <span v-else-if="notification.type === 'scheduledNoteFailed'">{{ i18n.ts._notification.scheduledNoteFailed }}</span> + <span v-else-if="notification.type === 'scheduledNotePosted'">{{ i18n.ts._notification.scheduledNotePosted }}</span> <MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/> </header> <div> @@ -162,6 +165,12 @@ SPDX-License-Identifier: AGPL-3.0-only <Mfm :text="getNoteSummary(notification.note)" :isBlock="true" :plain="true" :nowrap="true" :author="notification.note.user"/> <i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i> </MkA> + + <MkA v-else-if="notification.type === 'scheduledNotePosted'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)"> + <i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i> + <Mfm :text="getNoteSummary(notification.note)" :isBlock="true" :plain="true" :nowrap="true" :author="notification.note.user"/> + <i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i> + </MkA> </div> </div> </div> diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 880be518fa..4a261028ed 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2890,7 +2890,7 @@ type Notification_2 = components['schemas']['Notification']; type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json']; // @public (undocumented) -export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned", "edited", "scheduledNoteFailed"]; +export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned", "edited", "scheduledNoteFailed", "scheduledNotePosted"]; // @public (undocumented) export function nyaize(text: string): string; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 6eb1819037..29c2e814d2 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4525,6 +4525,17 @@ export type components = { /** @enum {string} */ type: 'scheduledNoteFailed'; reason: string; + } | { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** @enum {string} */ + type: 'scheduledNotePosted'; + user: components['schemas']['UserLite']; + /** Format: id */ + userId: string; + note: components['schemas']['Note']; } | { /** Format: id */ id: string; @@ -19992,8 +20003,8 @@ export type operations = { untilId?: string; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'scheduledNotePosted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'scheduledNotePosted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; }; }; }; @@ -20060,8 +20071,8 @@ export type operations = { untilId?: string; /** @default true */ markAsRead?: boolean; - includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; - excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; + includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'scheduledNotePosted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; + excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'scheduledNoteFailed' | 'scheduledNotePosted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; }; }; }; diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts index 34fc7c1a03..6e32060fb7 100644 --- a/packages/misskey-js/src/consts.ts +++ b/packages/misskey-js/src/consts.ts @@ -16,7 +16,7 @@ import type { UserLite, } from './autogen/models.js'; -export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'edited', 'scheduledNoteFailed'] as const; +export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'edited', 'scheduledNoteFailed', 'scheduledNotePosted'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts index 8442552e3b..4fda784dba 100644 --- a/packages/sw/src/scripts/create-notification.ts +++ b/packages/sw/src/scripts/create-notification.ts @@ -265,6 +265,14 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif data, }]; + case 'scheduledNotePosted': + return [i18n.ts._notification.scheduledNotePosted, { + body: data.body.note.text ?? '', + icon: data.body.user.avatarUrl ?? undefined, + badge: iconUrl('bell'), + data, + }]; + default: return null; } diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 582f4eda0a..f15646a156 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -278,6 +278,7 @@ _notification: renotedBySomeUsers: "Boosted by {n} users" edited: "Note got edited" scheduledNoteFailed: "Posting scheduled note failed" + scheduledNotePosted: "Scheduled Note was posted" _types: renote: "Boosts" edited: "Edits" -- GitLab From 3efb99dd576a2e3492fea7bd15a77c0414289fff Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Mon, 4 Nov 2024 18:05:29 +0100 Subject: [PATCH 09/18] fix: missing MKButton import --- packages/frontend/src/components/MkNoteSimple.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index 48bf53fab5..6db95f1987 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -32,6 +32,7 @@ import * as os from '@/os.js'; import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkSubNoteContent from '@/components/MkSubNoteContent.vue'; import MkCwButton from '@/components/MkCwButton.vue'; +import MkButton from '@/components/MkButton.vue'; import { defaultStore } from '@/store.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -- GitLab From 7eb0996b60943cf4aeb7e753fec9cfac5d2322ed Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Mon, 4 Nov 2024 18:06:38 +0100 Subject: [PATCH 10/18] upd: use apiWithDialog instead of misskeyApi --- packages/frontend/src/components/MkNoteSimple.vue | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index 6db95f1987..e7a357d1b6 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -34,7 +34,6 @@ import MkSubNoteContent from '@/components/MkSubNoteContent.vue'; import MkCwButton from '@/components/MkCwButton.vue'; import MkButton from '@/components/MkButton.vue'; import { defaultStore } from '@/store.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; const props = defineProps<{ @@ -68,14 +67,10 @@ async function deleteScheduleNote() { } async function editScheduleNote() { - try { - await misskeyApi('notes/schedule/delete', { noteId: props.note.id }) - .then(() => { - isDeleted.value = true; - }); - } catch (err) { - console.error(err); - } + await os.apiWithDialog('notes/schedule/delete', { noteId: props.note.id }) + .then(() => { + isDeleted.value = true; + }); await os.post({ initialNote: props.note, -- GitLab From b10eb7ada9072190767cdb4ea4a016e2ee95eda8 Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Mon, 9 Dec 2024 05:10:52 +0100 Subject: [PATCH 11/18] fix missing imports --- packages/frontend/src/components/MkMediaList.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index 4ef929e81f..9952f511f3 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -35,7 +35,7 @@ import * as Misskey from 'misskey-js'; import PhotoSwipeLightbox from 'photoswipe/lightbox'; import PhotoSwipe from 'photoswipe'; import 'photoswipe/style.css'; -import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES } from '@@/js/const.js'; +import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES, FILE_TYPE_FLASH_CONTENT, FILE_EXT_FLASH_CONTENT } from '@@/js/const.js'; import XBanner from '@/components/MkMediaBanner.vue'; import XImage from '@/components/MkMediaImage.vue'; import XVideo from '@/components/MkMediaVideo.vue'; -- GitLab From 1d0fd4c40b7fca14fbea8375e329c113800d5770 Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Mon, 9 Dec 2024 05:11:22 +0100 Subject: [PATCH 12/18] update locales --- locales/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index 89541e6414..7ab1b7f545 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -11448,7 +11448,7 @@ export interface Locale extends ILocale { * Select a follow relationship... */ "selectFollowRelationship": string; - /** + /** * Schedule a note */ "schedulePost": string; -- GitLab From 152cc074831b784bb1e12267587184cea293a186 Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Mon, 9 Dec 2024 05:58:25 +0100 Subject: [PATCH 13/18] Apply suggestions --- .../queue/processors/ScheduleNotePostProcessorService.ts | 7 +++++++ .../src/server/api/endpoints/notes/schedule/create.ts | 2 +- packages/frontend/src/components/MkPostForm.vue | 4 +--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts index f281b0ed7b..ea43448ed0 100644 --- a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts +++ b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts @@ -10,6 +10,7 @@ import { NoteCreateService } from '@/core/NoteCreateService.js'; import type { ChannelsRepository, DriveFilesRepository, MiDriveFile, NoteScheduleRepository, NotesRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { NotificationService } from '@/core/NotificationService.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { ScheduleNotePostJobData } from '../types.js'; @@ -119,6 +120,12 @@ export class ScheduleNotePostProcessorService { reply, renote, channel, + }).catch(async (err: IdentifiableError) => { + this.notificationService.createNotification(me.id, 'scheduledNoteFailed', { + reason: err.message, + }); + await this.noteScheduleRepository.remove(data); + throw this.logger.error(`Schedule Note Failed Reason: ${err.message}`); }); await this.noteScheduleRepository.remove(data); this.notificationService.createNotification(me.id, 'scheduledNotePosted', { 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 b8ae3f44a3..7d20b6b82a 100644 --- a/packages/backend/src/server/api/endpoints/notes/schedule/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/schedule/create.ts @@ -360,7 +360,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- }, { delay, removeOnComplete: true, - jobId: noteId, + jobId: `schedNote:${noteId}`, }); } diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index bbde7c65f9..c7d5611847 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -821,7 +821,7 @@ async function post(ev?: MouseEvent) { const filesData = toRaw(files.value); const isMissingAltText = filesData.filter( - file => file.type.startsWith('image/') || file.type.startsWith('video/') || file.type.startsWith('audio/') + file => file.type.startsWith('image/') || file.type.startsWith('video/') || file.type.startsWith('audio/'), ).some(file => !file.comment); if (isMissingAltText) { @@ -914,8 +914,6 @@ async function post(ev?: MouseEvent) { claimAchievement('notes1'); } - poll.value = null; - const text = postData.text ?? ''; const lowerCase = text.toLowerCase(); if ((lowerCase.includes('love') || lowerCase.includes('â¤')) && lowerCase.includes('sharkey')) { -- GitLab From 116a14720285707f2b884ccf6d39e224b04ec390 Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Mon, 9 Dec 2024 05:00:20 +0000 Subject: [PATCH 14/18] remove comma added by vscode prettifer --- packages/frontend/src/components/MkPostForm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index c7d5611847..93328fe52f 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -821,7 +821,7 @@ async function post(ev?: MouseEvent) { const filesData = toRaw(files.value); const isMissingAltText = filesData.filter( - file => file.type.startsWith('image/') || file.type.startsWith('video/') || file.type.startsWith('audio/'), + file => file.type.startsWith('image/') || file.type.startsWith('video/') || file.type.startsWith('audio/') ).some(file => !file.comment); if (isMissingAltText) { -- GitLab From f02d0994132c5774f3adf0d4a993f4ecca549b77 Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Mon, 9 Dec 2024 06:10:32 +0100 Subject: [PATCH 15/18] fix deletion of scheduled note --- .../backend/src/server/api/endpoints/notes/schedule/delete.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts b/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts index df406f99f0..628fd89926 100644 --- a/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/schedule/delete.ts @@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.permissionDenied); } await this.noteScheduleRepository.delete({ id: ps.noteId }); - await this.queueService.ScheduleNotePostQueue.remove(ps.noteId); + await this.queueService.ScheduleNotePostQueue.remove(`schedNote:${ps.noteId}`); }); } } -- GitLab From dda922a030cdbd80f35aef38f2a67114a067c5c4 Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Mon, 9 Dec 2024 06:13:46 +0100 Subject: [PATCH 16/18] remove splatting --- packages/frontend/src/components/MkScheduleEditor.vue | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/frontend/src/components/MkScheduleEditor.vue b/packages/frontend/src/components/MkScheduleEditor.vue index 8f18f620ae..60a60bed28 100644 --- a/packages/frontend/src/components/MkScheduleEditor.vue +++ b/packages/frontend/src/components/MkScheduleEditor.vue @@ -49,11 +49,7 @@ function get() { return new Date(`${ atDate.value } ${ atTime.value }`).getTime(); }; - return { - ...( - { scheduledAt: calcAt() } - ), - }; + return { scheduledAt: calcAt() }; } watch([ -- GitLab From 234d14a892153f8249ced4e81d569418879f5f8c Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Mon, 9 Dec 2024 05:21:53 +0000 Subject: [PATCH 17/18] Revert import order update caused by saving --- packages/frontend/src/components/MkMediaList.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index 9952f511f3..5209489046 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -35,13 +35,13 @@ import * as Misskey from 'misskey-js'; import PhotoSwipeLightbox from 'photoswipe/lightbox'; import PhotoSwipe from 'photoswipe'; import 'photoswipe/style.css'; -import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES, FILE_TYPE_FLASH_CONTENT, FILE_EXT_FLASH_CONTENT } from '@@/js/const.js'; import XBanner from '@/components/MkMediaBanner.vue'; import XImage from '@/components/MkMediaImage.vue'; import XVideo from '@/components/MkMediaVideo.vue'; import XModPlayer from '@/components/SkModPlayer.vue'; import XFlashPlayer from '@/components/SkFlashPlayer.vue'; import * as os from '@/os.js'; +import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES, FILE_TYPE_FLASH_CONTENT, FILE_EXT_FLASH_CONTENT } from '@@/js/const.js'; import { defaultStore } from '@/store.js'; import { focusParent } from '@/scripts/focus.js'; -- GitLab From bf01dcd8fb770d88f8509bbcb63bdaef1aa97fa5 Mon Sep 17 00:00:00 2001 From: Marie <github@yuugi.dev> Date: Mon, 9 Dec 2024 18:58:57 +0100 Subject: [PATCH 18/18] Apply suggestions --- .../ScheduleNotePostProcessorService.ts | 65 ++++++++++--------- .../frontend/src/components/MkNoteSimple.vue | 2 +- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts index ea43448ed0..62e3d1072f 100644 --- a/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts +++ b/packages/backend/src/queue/processors/ScheduleNotePostProcessorService.ts @@ -11,6 +11,7 @@ import type { ChannelsRepository, DriveFilesRepository, MiDriveFile, NoteSchedul import { DI } from '@/di-symbols.js'; import { NotificationService } from '@/core/NotificationService.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import type { MiScheduleNoteType } from '@/models/NoteSchedule.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { ScheduleNotePostJobData } from '../types.js'; @@ -39,7 +40,36 @@ export class ScheduleNotePostProcessorService { this.logger = this.queueLoggerService.logger.createSubLogger('schedule-note-post'); } - @bindThis + @bindThis + private async isValidNoteSchedule(note: MiScheduleNoteType, id: string): Promise<boolean> { + const reply = note.reply ? await this.notesRepository.findOneBy({ id: note.reply }) : undefined; + const renote = note.reply ? await this.notesRepository.findOneBy({ id: note.renote }) : undefined; + const channel = note.channel ? await this.channelsRepository.findOneBy({ id: note.channel, isArchived: false }) : undefined; + if (note.reply && !reply) { + this.logger.warn('Schedule Note Failed Reason: parent note to reply does not exist'); + this.notificationService.createNotification(id, 'scheduledNoteFailed', { + reason: 'Replied to note on your scheduled note no longer exists', + }); + return false; + } + if (note.renote && !renote) { + this.logger.warn('Schedule Note Failed Reason: attached quote note no longer exists'); + this.notificationService.createNotification(id, 'scheduledNoteFailed', { + reason: 'A quoted note from one of your scheduled notes no longer exists', + }); + return false; + } + if (note.channel && !channel) { + this.logger.warn('Schedule Note Failed Reason: Channel does not exist'); + this.notificationService.createNotification(id, 'scheduledNoteFailed', { + reason: 'An attached channel on your scheduled note no longer exists', + }); + return false; + } + return true; + } + + @bindThis public async process(job: Bull.Job<ScheduleNotePostJobData>): Promise<void> { this.noteScheduleRepository.findOneBy({ id: job.data.scheduleNoteId }).then(async (data) => { if (!data) { @@ -47,11 +77,10 @@ export class ScheduleNotePostProcessorService { } else { const me = await this.usersRepository.findOneBy({ id: data.userId }); const note = data.note; - - //idã®å½¢å¼ã§ã‚ューã«ç©ã‚“ã§ã‚ã£ãŸã®ã‚’DBã‹ã‚‰å–り寄ã›ã‚‹ const reply = note.reply ? await this.notesRepository.findOneBy({ id: note.reply }) : undefined; const renote = note.reply ? await this.notesRepository.findOneBy({ id: note.renote }) : undefined; const channel = note.channel ? await this.channelsRepository.findOneBy({ id: note.channel, isArchived: false }) : undefined; + let files: MiDriveFile[] = []; const fileIds = note.files; @@ -72,37 +101,15 @@ export class ScheduleNotePostProcessorService { return; } - if (note.files.length !== files.length) { - this.logger.warn('Schedule Note Failed Reason: files are missing in the user\'s drive'); - this.notificationService.createNotification(me.id, 'scheduledNoteFailed', { - reason: 'Some attached files on your scheduled note no longer exist', - }); - await this.noteScheduleRepository.remove(data); - return; - } - - if (note.reply && !reply) { - this.logger.warn('Schedule Note Failed Reason: parent note to reply does not exist'); - this.notificationService.createNotification(me.id, 'scheduledNoteFailed', { - reason: 'Replied to note on your scheduled note no longer exists', - }); - await this.noteScheduleRepository.remove(data); - return; - } - - if (note.renote && !renote) { - this.logger.warn('Schedule Note Failed Reason: attached quote note no longer exists'); - this.notificationService.createNotification(me.id, 'scheduledNoteFailed', { - reason: 'A quoted note from one of your scheduled notes no longer exists', - }); + if (!await this.isValidNoteSchedule(note, me.id)) { await this.noteScheduleRepository.remove(data); return; } - if (note.channel && !channel) { - this.logger.warn('Schedule Note Failed Reason: Channel does not exist'); + if (note.files.length !== files.length) { + this.logger.warn('Schedule Note Failed Reason: files are missing in the user\'s drive'); this.notificationService.createNotification(me.id, 'scheduledNoteFailed', { - reason: 'An attached channel on your scheduled note no longer exists', + reason: 'Some attached files on your scheduled note no longer exist', }); await this.noteScheduleRepository.remove(data); return; diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index e7a357d1b6..924262d62e 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 --> <template> -<div v-show="!isDeleted" :class="$style.root" :tabindex="!isDeleted ? '-1' : undefined"> +<div v-if="!isDeleted" :class="$style.root"> <MkAvatar :class="$style.avatar" :user="note.user" link preview/> <div :class="$style.main"> <MkNoteHeader :class="$style.header" :note="note" :mini="true"/> -- GitLab