From 2528508cff9d8c90abd33e46b15220a49a00e2e2 Mon Sep 17 00:00:00 2001
From: NoriDev <m1nthing2322@gmail.com>
Date: Thu, 31 Oct 2024 13:52:01 +0900
Subject: feat: 노트 게시를 예약할 수 있음 (yojo-art/cherrypick#483,
 [Type4ny-Project/Type4ny@271c872c](https://github.com/Type4ny-Project/Type4ny/commit/271c872c97f215ef5d8e0be62251dd422a52e5b1))
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/components/MkSchedulePostListDialog.vue    | 60 ++++++++++++++++++++++
 1 file changed, 60 insertions(+)
 create mode 100644 packages/frontend/src/components/MkSchedulePostListDialog.vue

(limited to 'packages/frontend/src/components/MkSchedulePostListDialog.vue')

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>
-- 
cgit v1.2.3-freya


From 4f58b8de20625da577a2b7a8055d065bbddb94d1 Mon Sep 17 00:00:00 2001
From: Marie <github@yuugi.dev>
Date: Sun, 3 Nov 2024 03:39:19 +0100
Subject: fix: drive content not being loaded

---
 .../server/api/endpoints/notes/schedule/create.ts  | 28 +++-------------------
 .../server/api/endpoints/notes/schedule/list.ts    |  3 +++
 packages/frontend/src/components/MkMediaList.vue   |  2 +-
 packages/frontend/src/components/MkNoteSimple.vue  |  2 ++
 .../src/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(-)

(limited to 'packages/frontend/src/components/MkSchedulePostListDialog.vue')

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;
           };
-- 
cgit v1.2.3-freya


From f08d1ec38f846ad46bf95bd4d2d67ebaa143e042 Mon Sep 17 00:00:00 2001
From: Marie <github@yuugi.dev>
Date: Sun, 3 Nov 2024 04:04:17 +0100
Subject: chore: remove leftover log

---
 packages/frontend/src/components/MkSchedulePostListDialog.vue | 2 --
 1 file changed, 2 deletions(-)

(limited to 'packages/frontend/src/components/MkSchedulePostListDialog.vue')

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();
 }
-- 
cgit v1.2.3-freya


From fc9d777dc3161b40c5c62bb65cb03e2c7d8f4380 Mon Sep 17 00:00:00 2001
From: Marie <github@yuugi.dev>
Date: Sun, 3 Nov 2024 17:59:50 +0100
Subject: upd: add notification for failures, add reasons for failure, apply
 suggestions

---
 locales/index.d.ts                                 |  4 ++
 .../src/core/entities/NotificationEntityService.ts |  5 +-
 packages/backend/src/models/NoteSchedule.ts        |  2 -
 packages/backend/src/models/Notification.ts        |  5 ++
 .../backend/src/models/json-schema/notification.ts | 14 +++++
 .../processors/ScheduleNotePostProcessorService.ts | 59 +++++++++++++++++-----
 .../server/api/endpoints/notes/schedule/create.ts  |  5 +-
 packages/backend/src/types.ts                      |  1 +
 packages/frontend-shared/js/const.ts               |  1 +
 .../frontend/src/components/MkNotification.vue     |  8 ++-
 packages/frontend/src/components/MkPostForm.vue    |  5 +-
 .../src/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 +-
 packages/sw/src/scripts/create-notification.ts     |  7 +++
 sharkey-locales/en-US.yml                          |  1 +
 17 files changed, 110 insertions(+), 28 deletions(-)

(limited to 'packages/frontend/src/components/MkSchedulePostListDialog.vue')

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"
-- 
cgit v1.2.3-freya

