From 2528508cff9d8c90abd33e46b15220a49a00e2e2 Mon Sep 17 00:00:00 2001 From: NoriDev 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/server/api/endpoints/admin/queue/stats.ts | 3 +- .../server/api/endpoints/notes/schedule/create.ts | 393 +++++++++++++++++++++ .../server/api/endpoints/notes/schedule/delete.ts | 67 ++++ .../server/api/endpoints/notes/schedule/list.ts | 128 +++++++ 4 files changed, 590 insertions(+), 1 deletion(-) 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 (limited to 'packages/backend/src/server/api/endpoints') 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 { // 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 { // 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 { // 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 { // 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; + }); + } +} -- cgit v1.2.3-freya From 4f58b8de20625da577a2b7a8055d065bbddb94d1 Mon Sep 17 00:00:00 2001 From: Marie 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/backend/src/server/api/endpoints') 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 { // 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 { // 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 { // 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 { // 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; - }) | null; scheduleNote: { scheduledAt?: number; }; -- cgit v1.2.3-freya From fc9d777dc3161b40c5c62bb65cb03e2c7d8f4380 Mon Sep 17 00:00:00 2001 From: Marie 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/backend/src/server/api/endpoints') 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 { // 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 { // 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
- +
@@ -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', }]" > @@ -46,6 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only + {{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }} {{ notification.header }} {{ i18n.ts._notification.edited }} + {{ i18n.ts._notification.scheduledNoteFailed }}
@@ -109,6 +112,9 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.showFile }} +
+ {{ notification.reason }} +
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 From 152cc074831b784bb1e12267587184cea293a186 Mon Sep 17 00:00:00 2001 From: Marie Date: Mon, 9 Dec 2024 05:58:25 +0100 Subject: Apply suggestions --- .../src/queue/processors/ScheduleNotePostProcessorService.ts | 7 +++++++ packages/backend/src/server/api/endpoints/notes/schedule/create.ts | 2 +- packages/frontend/src/components/MkPostForm.vue | 4 +--- 3 files changed, 9 insertions(+), 4 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') 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 { // 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')) { -- cgit v1.2.3-freya From f02d0994132c5774f3adf0d4a993f4ecca549b77 Mon Sep 17 00:00:00 2001 From: Marie Date: Mon, 9 Dec 2024 06:10:32 +0100 Subject: fix deletion of scheduled note --- packages/backend/src/server/api/endpoints/notes/schedule/delete.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/backend/src/server/api/endpoints') 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 { // 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}`); }); } } -- cgit v1.2.3-freya