summaryrefslogtreecommitdiff
path: root/packages/backend/src/server
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-09-26 15:29:52 +0900
committerGitHub <noreply@github.com>2025-09-26 15:29:52 +0900
commitd1446d195abb52c560c7c97177d08103a175acf7 (patch)
tree355689adb542333f4c5abb186ab9819c29274612 /packages/backend/src/server
parentfix(frontend): ビルド成果物のファイル名にlocalesのhashを含め... (diff)
downloadmisskey-d1446d195abb52c560c7c97177d08103a175acf7.tar.gz
misskey-d1446d195abb52c560c7c97177d08103a175acf7.tar.bz2
misskey-d1446d195abb52c560c7c97177d08103a175acf7.zip
feat: scheduled post (#16577)
* Update NoteDraft.ts * Update NoteDraft.ts * wip * Update CHANGELOG.md * wip * Update PostScheduledNoteProcessorService.ts * Update PostScheduledNoteProcessorService.ts * Update Notification.ts * wip * Update NoteDraftService.ts * Update NoteDraftService.ts * Update NoteDraftService.ts * wip * Create 1758677617888-scheduled-post.js * Update index.d.ts * Update stats.ts * wip * wip * wip * wip * wip * Update MkNotification.vue * wip * wip * wip * Update NoteDraftService.ts * Update NoteDraftService.ts * wip * wip * Update NoteDraftEntityService.ts * wip * Update index.d.ts * Update MkPostForm.vue * wip * wip * wip * Update NoteCreateService.ts * wip * wip * wip * Update NoteDraftEntityService.ts * Update NoteCreateService.ts * Update NoteDraftService.ts * wip * Update NoteDraftService.ts * wip * wip * Update MkPostForm.vue * wip * Update MkPostForm.vue * Update os.ts * wip * Update MkNoteDraftsDialog.vue
Diffstat (limited to 'packages/backend/src/server')
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/stats.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/admin/show-user.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts207
-rw-r--r--packages/backend/src/server/api/endpoints/notes/drafts/create.ts41
-rw-r--r--packages/backend/src/server/api/endpoints/notes/drafts/list.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/notes/drafts/update.ts44
7 files changed, 111 insertions, 195 deletions
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
index d7f9e4eaa3..b69699c338 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts
@@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
+import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, PostScheduledNoteQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
export const meta = {
tags: ['admin'],
@@ -49,6 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
+ @Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index 1ba6853dbe..2fd7ab8ca2 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -103,6 +103,8 @@ export const meta = {
quote: { optional: true, ...notificationRecieveConfig },
reaction: { optional: true, ...notificationRecieveConfig },
pollEnded: { optional: true, ...notificationRecieveConfig },
+ scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
+ scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig },
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 082d97f5d4..5c7958fc1c 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -209,6 +209,8 @@ export const paramDef = {
quote: notificationRecieveConfig,
reaction: notificationRecieveConfig,
pollEnded: notificationRecieveConfig,
+ scheduledNotePosted: notificationRecieveConfig,
+ scheduledNotePostFailed: notificationRecieveConfig,
receiveFollowRequest: notificationRecieveConfig,
followRequestAccepted: notificationRecieveConfig,
roleAssigned: notificationRecieveConfig,
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 7caea8eedc..e48aa69d0f 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -6,17 +6,10 @@
import ms from 'ms';
import { In } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { MiUser } from '@/models/User.js';
-import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/_.js';
-import type { MiDriveFile } from '@/models/DriveFile.js';
-import type { MiNote } from '@/models/Note.js';
-import type { MiChannel } from '@/models/Channel.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
-import { DI } from '@/di-symbols.js';
-import { isQuote, isRenote } from '@/misc/is-renote.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { ApiError } from '../../error.js';
@@ -223,168 +216,28 @@ export const paramDef = {
@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.blockingsRepository)
- private blockingsRepository: BlockingsRepository,
-
- @Inject(DI.driveFilesRepository)
- private driveFilesRepository: DriveFilesRepository,
-
- @Inject(DI.channelsRepository)
- private channelsRepository: ChannelsRepository,
-
private noteEntityService: NoteEntityService,
private noteCreateService: NoteCreateService,
) {
super(meta, paramDef, async (ps, me) => {
- let visibleUsers: MiUser[] = [];
- if (ps.visibleUserIds) {
- visibleUsers = await this.usersRepository.findBy({
- id: In(ps.visibleUserIds),
- });
- }
-
- let files: MiDriveFile[] = [];
- const fileIds = ps.fileIds ?? ps.mediaIds ?? null;
- if (fileIds != null) {
- files = await this.driveFilesRepository.createQueryBuilder('file')
- .where('file.userId = :userId AND file.id IN (:...fileIds)', {
- userId: me.id,
- fileIds,
- })
- .orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
- .setParameters({ fileIds })
- .getMany();
-
- if (files.length !== fileIds.length) {
- throw new ApiError(meta.errors.noSuchFile);
- }
- }
-
- let renote: MiNote | null = null;
- if (ps.renoteId != null) {
- // Fetch renote to note
- renote = await this.notesRepository.findOne({
- where: { id: ps.renoteId },
- relations: ['user', 'renote', 'reply'],
- });
-
- if (renote == null) {
- throw new ApiError(meta.errors.noSuchRenoteTarget);
- } else if (isRenote(renote) && !isQuote(renote)) {
- throw new ApiError(meta.errors.cannotReRenote);
- }
-
- // Check blocking
- if (renote.userId !== me.id) {
- const blockExist = await this.blockingsRepository.exists({
- where: {
- blockerId: renote.userId,
- blockeeId: me.id,
- },
- });
- if (blockExist) {
- throw new ApiError(meta.errors.youHaveBeenBlocked);
- }
- }
-
- if (renote.visibility === 'followers' && renote.userId !== me.id) {
- // 他人のfollowers noteはreject
- throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
- } else if (renote.visibility === 'specified') {
- // specified / direct noteはreject
- throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
- }
-
- if (renote.channelId && renote.channelId !== ps.channelId) {
- // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
- // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
- const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId });
- if (renoteChannel == null) {
- // リノートしたいノートが書き込まれているチャンネルが無い
- throw new ApiError(meta.errors.noSuchChannel);
- } else if (!renoteChannel.allowRenoteToExternal) {
- // リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合
- throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel);
- }
- }
- }
-
- let reply: MiNote | null = null;
- if (ps.replyId != null) {
- // Fetch reply
- reply = await this.notesRepository.findOne({
- where: { id: ps.replyId },
- relations: ['user'],
- });
-
- if (reply == null) {
- throw new ApiError(meta.errors.noSuchReplyTarget);
- } else if (isRenote(reply) && !isQuote(reply)) {
- throw new ApiError(meta.errors.cannotReplyToPureRenote);
- } else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
- throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
- } else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
- throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
- }
-
- // Check blocking
- if (reply.userId !== me.id) {
- const blockExist = await this.blockingsRepository.exists({
- where: {
- blockerId: reply.userId,
- blockeeId: me.id,
- },
- });
- if (blockExist) {
- throw new ApiError(meta.errors.youHaveBeenBlocked);
- }
- }
- }
-
- if (ps.poll) {
- if (typeof ps.poll.expiresAt === 'number') {
- if (ps.poll.expiresAt < Date.now()) {
- throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
- }
- } else if (typeof ps.poll.expiredAfter === 'number') {
- ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter;
- }
- }
-
- let channel: MiChannel | null = null;
- if (ps.channelId != null) {
- channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false });
-
- if (channel == null) {
- throw new ApiError(meta.errors.noSuchChannel);
- }
- }
-
- // 投稿を作成
try {
- const note = await this.noteCreateService.create(me, {
+ const note = await this.noteCreateService.fetchAndCreate(me, {
createdAt: new Date(),
- files: files,
+ fileIds: ps.fileIds ?? ps.mediaIds ?? [],
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false,
- expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
- } : undefined,
- text: ps.text ?? undefined,
- reply,
- renote,
- cw: ps.cw,
+ expiresAt: ps.poll.expiredAfter ? new Date(Date.now() + ps.poll.expiredAfter) : ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
+ } : null,
+ text: ps.text ?? null,
+ replyId: ps.replyId ?? null,
+ renoteId: ps.renoteId ?? null,
+ cw: ps.cw ?? null,
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
- visibleUsers,
- channel,
+ visibleUserIds: ps.visibleUserIds ?? [],
+ channelId: ps.channelId ?? null,
apMentions: ps.noExtractMentions ? [] : undefined,
apHashtags: ps.noExtractHashtags ? [] : undefined,
apEmojis: ps.noExtractEmojis ? [] : undefined,
@@ -393,16 +246,46 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return {
createdNote: await this.noteEntityService.pack(note, me),
};
- } catch (e) {
+ } catch (err) {
// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
- if (e instanceof IdentifiableError) {
- if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
+ if (err instanceof IdentifiableError) {
+ if (err.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
throw new ApiError(meta.errors.containsProhibitedWords);
- } else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
+ } else if (err.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
throw new ApiError(meta.errors.containsTooManyMentions);
+ } else if (err.id === '801c046c-5bf5-4234-ad2b-e78fc20a2ac7') {
+ throw new ApiError(meta.errors.noSuchFile);
+ } else if (err.id === '53983c56-e163-45a6-942f-4ddc485d4290') {
+ throw new ApiError(meta.errors.noSuchRenoteTarget);
+ } else if (err.id === 'bde24c37-121f-4e7d-980d-cec52f599f02') {
+ throw new ApiError(meta.errors.cannotReRenote);
+ } else if (err.id === '2b4fe776-4414-4a2d-ae39-f3418b8fd4d3') {
+ throw new ApiError(meta.errors.youHaveBeenBlocked);
+ } else if (err.id === '90b9d6f0-893a-4fef-b0f1-e9a33989f71a') {
+ throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
+ } else if (err.id === '48d7a997-da5c-4716-b3c3-92db3f37bf7d') {
+ throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
+ } else if (err.id === 'b060f9a6-8909-4080-9e0b-94d9fa6f6a77') {
+ throw new ApiError(meta.errors.noSuchChannel);
+ } else if (err.id === '7e435f4a-780d-4cfc-a15a-42519bd6fb67') {
+ throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel);
+ } else if (err.id === '60142edb-1519-408e-926d-4f108d27bee0') {
+ throw new ApiError(meta.errors.noSuchReplyTarget);
+ } else if (err.id === 'f089e4e2-c0e7-4f60-8a23-e5a6bf786b36') {
+ throw new ApiError(meta.errors.cannotReplyToPureRenote);
+ } else if (err.id === '11cd37b3-a411-4f77-8633-c580ce6a8dce') {
+ throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
+ } else if (err.id === 'ced780a1-2012-4caf-bc7e-a95a291294cb') {
+ throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
+ } else if (err.id === 'b0df6025-f2e8-44b4-a26a-17ad99104612') {
+ throw new ApiError(meta.errors.youHaveBeenBlocked);
+ } else if (err.id === '0c11c11e-0c8d-48e7-822c-76ccef660068') {
+ throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
+ } else if (err.id === 'bfa3905b-25f5-4894-b430-da331a490e4b') {
+ throw new ApiError(meta.errors.noSuchChannel);
}
}
- throw e;
+ throw err;
}
});
}
diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts
index 1c28ec22d0..8f2fbf9197 100644
--- a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts
@@ -124,6 +124,12 @@ export const meta = {
id: '9ee33bbe-fde3-4c71-9b51-e50492c6b9c8',
},
+ tooManyScheduledNotes: {
+ message: 'You cannot create scheduled notes any more.',
+ code: 'TOO_MANY_SCHEDULED_NOTES',
+ id: '22ae69eb-09e3-4541-a850-773cfa45e693',
+ },
+
cannotRenoteToExternal: {
message: 'Cannot Renote to External.',
code: 'CANNOT_RENOTE_TO_EXTERNAL',
@@ -162,7 +168,7 @@ export const paramDef = {
fileIds: {
type: 'array',
uniqueItems: true,
- minItems: 1,
+ minItems: 0,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
@@ -183,8 +189,10 @@ export const paramDef = {
},
required: ['choices'],
},
+ scheduledAt: { type: 'integer', nullable: true },
+ isActuallyScheduled: { type: 'boolean', default: false },
},
- required: [],
+ required: ['visibility', 'visibleUserIds', 'cw', 'hashtag', 'localOnly', 'reactionAcceptance', 'replyId', 'renoteId', 'channelId', 'text', 'fileIds', 'poll', 'scheduledAt', 'isActuallyScheduled'],
} as const;
@Injectable()
@@ -196,22 +204,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
const draft = await this.noteDraftService.create(me, {
fileIds: ps.fileIds,
- poll: ps.poll ? {
- choices: ps.poll.choices,
- multiple: ps.poll.multiple ?? false,
- expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
- expiredAfter: ps.poll.expiredAfter ?? null,
- } : undefined,
- text: ps.text ?? null,
- replyId: ps.replyId ?? undefined,
- renoteId: ps.renoteId ?? undefined,
- cw: ps.cw ?? null,
- ...(ps.hashtag ? { hashtag: ps.hashtag } : {}),
+ pollChoices: ps.poll?.choices ?? [],
+ pollMultiple: ps.poll?.multiple ?? false,
+ pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null,
+ pollExpiredAfter: ps.poll?.expiredAfter ?? null,
+ hasPoll: ps.poll != null,
+ text: ps.text,
+ replyId: ps.replyId,
+ renoteId: ps.renoteId,
+ cw: ps.cw,
+ hashtag: ps.hashtag,
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
- visibleUserIds: ps.visibleUserIds ?? [],
- channelId: ps.channelId ?? undefined,
+ visibleUserIds: ps.visibleUserIds,
+ channelId: ps.channelId,
+ scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
+ isActuallyScheduled: ps.isActuallyScheduled,
}).catch((err) => {
if (err instanceof IdentifiableError) {
switch (err.id) {
@@ -241,6 +250,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
case '215dbc76-336c-4d2a-9605-95766ba7dab0':
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
+ case 'c3275f19-4558-4c59-83e1-4f684b5fab66':
+ throw new ApiError(meta.errors.tooManyScheduledNotes);
default:
throw err;
}
diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/list.ts b/packages/backend/src/server/api/endpoints/notes/drafts/list.ts
index f24f9b8fb2..0774f09228 100644
--- a/packages/backend/src/server/api/endpoints/notes/drafts/list.ts
+++ b/packages/backend/src/server/api/endpoints/notes/drafts/list.ts
@@ -41,6 +41,7 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
+ scheduled: { type: 'boolean', nullable: true },
},
required: [],
} as const;
@@ -58,6 +59,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const query = this.queryService.makePaginationQuery<MiNoteDraft>(this.noteDraftsRepository.createQueryBuilder('drafts'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('drafts.userId = :meId', { meId: me.id });
+ if (ps.scheduled === true) {
+ query.andWhere('drafts.isActuallyScheduled = true');
+ } else if (ps.scheduled === false) {
+ query.andWhere('drafts.isActuallyScheduled = false');
+ }
+
const drafts = await query
.limit(ps.limit)
.getMany();
diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts
index ee221fb765..9a2e2ca415 100644
--- a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts
+++ b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts
@@ -159,6 +159,12 @@ export const meta = {
code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
id: '215dbc76-336c-4d2a-9605-95766ba7dab0',
},
+
+ tooManyScheduledNotes: {
+ message: 'You cannot create scheduled notes any more.',
+ code: 'TOO_MANY_SCHEDULED_NOTES',
+ id: '02f5df79-08ae-4a33-8524-f1503c8f6212',
+ },
},
limit: {
@@ -171,14 +177,14 @@ export const paramDef = {
type: 'object',
properties: {
draftId: { type: 'string', nullable: false, format: 'misskey:id' },
- visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' },
+ visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'] },
visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id',
} },
cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 },
hashtag: { type: 'string', nullable: true, maxLength: 200 },
- localOnly: { type: 'boolean', default: false },
- reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
+ localOnly: { type: 'boolean' },
+ reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'] },
replyId: { type: 'string', format: 'misskey:id', nullable: true },
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
channelId: { type: 'string', format: 'misskey:id', nullable: true },
@@ -194,7 +200,7 @@ export const paramDef = {
fileIds: {
type: 'array',
uniqueItems: true,
- minItems: 1,
+ minItems: 0,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
@@ -215,6 +221,8 @@ export const paramDef = {
},
required: ['choices'],
},
+ scheduledAt: { type: 'integer', nullable: true },
+ isActuallyScheduled: { type: 'boolean' },
},
required: ['draftId'],
} as const;
@@ -228,22 +236,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
const draft = await this.noteDraftService.update(me, ps.draftId, {
fileIds: ps.fileIds,
- poll: ps.poll ? {
- choices: ps.poll.choices,
- multiple: ps.poll.multiple ?? false,
- expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
- expiredAfter: ps.poll.expiredAfter ?? null,
- } : undefined,
- text: ps.text ?? null,
- replyId: ps.replyId ?? undefined,
- renoteId: ps.renoteId ?? undefined,
- cw: ps.cw ?? null,
- ...(ps.hashtag ? { hashtag: ps.hashtag } : {}),
+ pollChoices: ps.poll?.choices,
+ pollMultiple: ps.poll?.multiple,
+ pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null,
+ pollExpiredAfter: ps.poll?.expiredAfter,
+ text: ps.text,
+ replyId: ps.replyId,
+ renoteId: ps.renoteId,
+ cw: ps.cw,
+ hashtag: ps.hashtag,
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
- visibleUserIds: ps.visibleUserIds ?? [],
- channelId: ps.channelId ?? undefined,
+ visibleUserIds: ps.visibleUserIds,
+ channelId: ps.channelId,
+ scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
+ isActuallyScheduled: ps.isActuallyScheduled,
}).catch((err) => {
if (err instanceof IdentifiableError) {
switch (err.id) {
@@ -285,6 +293,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.containsProhibitedWords);
case '4de0363a-3046-481b-9b0f-feff3e211025':
throw new ApiError(meta.errors.containsTooManyMentions);
+ case 'bacdf856-5c51-4159-b88a-804fa5103be5':
+ throw new ApiError(meta.errors.tooManyScheduledNotes);
default:
throw err;
}