(enhance) `notes/create` で予約投稿できるように
This commit is contained in:
		
							parent
							
								
									1995400dac
								
							
						
					
					
						commit
						2bc15c09ed
					
				|  | @ -32,9 +32,9 @@ export class ScheduleNotePostProcessorService { | |||
| 
 | ||||
|     @bindThis | ||||
| 	public async process(job: Bull.Job<ScheduleNotePostJobData>): Promise<void> { | ||||
| 		this.scheduledNotesRepository.findOneBy({ id: job.data.scheduleNoteId }).then(async (data) => { | ||||
| 		this.scheduledNotesRepository.findOneBy({ id: job.data.scheduledNoteId }).then(async (data) => { | ||||
| 			if (!data) { | ||||
| 				this.logger.warn(`Schedule note ${job.data.scheduleNoteId} not found`); | ||||
| 				this.logger.warn(`Schedule note ${job.data.scheduledNoteId} not found`); | ||||
| 			} else { | ||||
| 				data.note.createdAt = new Date(); | ||||
| 				const me = await this.usersRepository.findOneByOrFail({ id: data.userId }); | ||||
|  |  | |||
|  | @ -109,7 +109,7 @@ export type EndedPollNotificationJobData = { | |||
| }; | ||||
| 
 | ||||
| export type ScheduleNotePostJobData = { | ||||
|     scheduleNoteId: MiNote['id']; | ||||
|   scheduledNoteId: MiNote['id']; | ||||
| } | ||||
| 
 | ||||
| type MinimumUser = { | ||||
|  |  | |||
|  | @ -7,7 +7,8 @@ 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 { UsersRepository, NotesRepository, ScheduledNotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/_.js'; | ||||
| import type { MiNoteCreateOption } from '@/types.js'; | ||||
| import type { MiDriveFile } from '@/models/DriveFile.js'; | ||||
| import type { MiNote } from '@/models/Note.js'; | ||||
| import type { MiChannel } from '@/models/Channel.js'; | ||||
|  | @ -15,6 +16,8 @@ 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 { QueueService } from '@/core/QueueService.js'; | ||||
| import { IdService } from '@/core/IdService.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import { isPureRenote } from '@/misc/is-pure-renote.js'; | ||||
| import { ApiError } from '../../error.js'; | ||||
|  | @ -39,9 +42,17 @@ export const meta = { | |||
| 		properties: { | ||||
| 			createdNote: { | ||||
| 				type: 'object', | ||||
| 				optional: false, nullable: false, | ||||
| 				optional: false, nullable: true, | ||||
| 				ref: 'Note', | ||||
| 			}, | ||||
| 			scheduledNoteId: { | ||||
| 				type: 'string', | ||||
| 				optional: true, nullable: true, | ||||
| 			}, | ||||
| 			scheduledNote: { | ||||
| 				type: 'object', | ||||
| 				optional: true, nullable: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -105,6 +116,22 @@ export const meta = { | |||
| 			code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL', | ||||
| 			id: '33510210-8452-094c-6227-4a6c05d99f00', | ||||
| 		}, | ||||
| 
 | ||||
| 		cannotCreateAlreadyExpiredSchedule: { | ||||
| 			message: 'Schedule is already expired.', | ||||
| 			code: 'CANNOT_CREATE_ALREADY_EXPIRED_SCHEDULE', | ||||
| 			id: '8a9bfb90-fc7e-4878-a3e8-d97faaf5fb07', | ||||
| 		}, | ||||
| 		specifyScheduleDate: { | ||||
| 			message: 'Please specify schedule date.', | ||||
| 			code: 'PLEASE_SPECIFY_SCHEDULE_DATE', | ||||
| 			id: 'c93a6ad6-f7e2-4156-a0c2-3d03529e5e0f', | ||||
| 		}, | ||||
| 		noSuchSchedule: { | ||||
| 			message: 'No such schedule.', | ||||
| 			code: 'NO_SUCH_SCHEDULE', | ||||
| 			id: '44dee229-8da1-4a61-856d-e3a4bbc12032', | ||||
| 		}, | ||||
| 	}, | ||||
| } as const; | ||||
| 
 | ||||
|  | @ -164,6 +191,13 @@ export const paramDef = { | |||
| 			}, | ||||
| 			required: ['choices'], | ||||
| 		}, | ||||
| 		schedule: { | ||||
| 			type: 'object', | ||||
| 			nullable: true, | ||||
| 			properties: { | ||||
| 				expiresAt: { type: 'integer', nullable: false }, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	// (re)note with text, files and poll are optional
 | ||||
| 	anyOf: [ | ||||
|  | @ -172,6 +206,7 @@ export const paramDef = { | |||
| 		{ required: ['fileIds'] }, | ||||
| 		{ required: ['mediaIds'] }, | ||||
| 		{ required: ['poll'] }, | ||||
| 		{ required: ['schedule'] }, | ||||
| 	], | ||||
| } as const; | ||||
| 
 | ||||
|  | @ -184,6 +219,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 		@Inject(DI.notesRepository) | ||||
| 		private notesRepository: NotesRepository, | ||||
| 
 | ||||
| 		@Inject(DI.scheduledNotesRepository) | ||||
| 		private scheduledNotesRepository: ScheduledNotesRepository, | ||||
| 
 | ||||
| 		@Inject(DI.blockingsRepository) | ||||
| 		private blockingsRepository: BlockingsRepository, | ||||
| 
 | ||||
|  | @ -195,6 +233,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 
 | ||||
| 		private noteEntityService: NoteEntityService, | ||||
| 		private noteCreateService: NoteCreateService, | ||||
| 
 | ||||
| 		private queueService: QueueService, | ||||
|     private idService: IdService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			let visibleUsers: MiUser[] = []; | ||||
|  | @ -311,8 +352,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			// 投稿を作成
 | ||||
| 			const note = await this.noteCreateService.create(me, { | ||||
| 			const note: MiNoteCreateOption = { | ||||
| 				createdAt: new Date(), | ||||
| 				files: files, | ||||
| 				poll: ps.poll ? { | ||||
|  | @ -332,11 +372,45 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 				apMentions: ps.noExtractMentions ? [] : undefined, | ||||
| 				apHashtags: ps.noExtractHashtags ? [] : undefined, | ||||
| 				apEmojis: ps.noExtractEmojis ? [] : undefined, | ||||
| 			}); | ||||
| 
 | ||||
| 			return { | ||||
| 				createdNote: await this.noteEntityService.pack(note, me), | ||||
| 			}; | ||||
| 
 | ||||
| 			if (ps.schedule) { | ||||
| 				if (!ps.schedule.expiresAt) { | ||||
| 					throw new ApiError(meta.errors.specifyScheduleDate); | ||||
| 				} | ||||
| 
 | ||||
| 				me.token = null; | ||||
| 				const scheduledNoteId = this.idService.gen(new Date().getTime()); | ||||
| 				await this.scheduledNotesRepository.insert({ | ||||
| 					id: scheduledNoteId, | ||||
| 					note: note, | ||||
| 					userId: me.id, | ||||
| 					expiresAt: new Date(ps.schedule.expiresAt), | ||||
| 				}); | ||||
| 
 | ||||
| 				const delay = new Date(ps.schedule.expiresAt).getTime() - Date.now(); | ||||
| 				await this.queueService.ScheduleNotePostQueue.add(String(delay), { | ||||
| 					scheduledNoteId, | ||||
| 				}, { | ||||
| 					jobId: scheduledNoteId, | ||||
| 					delay, | ||||
| 					removeOnComplete: true, | ||||
| 				}); | ||||
| 
 | ||||
| 				return { | ||||
| 					scheduledNoteId, | ||||
| 					scheduledNote: note, | ||||
| 
 | ||||
| 					// ↓互換性のため(微妙)
 | ||||
| 					createdNote: null, | ||||
| 				}; | ||||
| 			} else { | ||||
| 				// 投稿を作成
 | ||||
| 				const createdNoteRaw = await this.noteCreateService.create(me, note); | ||||
| 				return { | ||||
| 					createdNote: await this.noteEntityService.pack(createdNoteRaw, me), | ||||
| 				}; | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ | |||
| import ms from 'ms'; | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import type { ScheduledNotesRepository } from '@/models/_.js'; | ||||
| import { QueueService } from '@/core/QueueService.js'; | ||||
| import { Endpoint } from '@/server/api/endpoint-base.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| 
 | ||||
|  | @ -31,9 +32,9 @@ export const meta = { | |||
| export const paramDef = { | ||||
| 	type: 'object', | ||||
| 	properties: { | ||||
| 		noteId: { type: 'string', format: 'misskey:id' }, | ||||
| 		scheduledNoteId: { type: 'string', format: 'misskey:id' }, | ||||
| 	}, | ||||
| 	required: ['noteId'], | ||||
| 	required: ['scheduledNoteId'], | ||||
| } as const; | ||||
| 
 | ||||
| @Injectable() | ||||
|  | @ -41,9 +42,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 	constructor( | ||||
| 		@Inject(DI.scheduledNotesRepository) | ||||
| 		private scheduledNotesRepository: ScheduledNotesRepository, | ||||
| 
 | ||||
| 		private queueService: QueueService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			await this.scheduledNotesRepository.delete({ id: ps.noteId }); | ||||
| 			await this.scheduledNotesRepository.delete({ id: ps.scheduledNoteId }); | ||||
| 			if (ps.scheduledNoteId) { | ||||
| 				await this.queueService.ScheduleNotePostQueue.remove(ps.scheduledNoteId); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -93,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 						user: user, | ||||
| 						createdAt: new Date(item.expiresAt), | ||||
| 						isSchedule: true, | ||||
| 						// ↓TODO: NoteのIDに予約投稿IDを入れたくない(本来別ものなため)
 | ||||
| 						id: item.id, | ||||
| 					}, | ||||
| 				}; | ||||
|  |  | |||
|  | @ -40,7 +40,9 @@ const props = defineProps<{ | |||
| }>(); | ||||
| 
 | ||||
| async function deleteScheduleNote() { | ||||
| 	await os.apiWithDialog('notes/schedule/delete', { noteId: props.note.id }) | ||||
| 	if (!props.note.isSchedule) return; | ||||
| 	// スケジュールつきノートの場合は、ノートIDのフィールドに予約投稿ID(scheduledNoteId)が入るので注意!!!! | ||||
| 	await os.apiWithDialog('notes/schedule/delete', { scheduledNoteId: props.note.id }) | ||||
| 		.then(() => { | ||||
| 			isDeleted.value = true; | ||||
| 		}); | ||||
|  |  | |||
|  | @ -751,7 +751,7 @@ async function post(ev?: MouseEvent) { | |||
| 		renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined, | ||||
| 		channelId: props.channel ? props.channel.id : undefined, | ||||
| 		schedule, | ||||
| 		poll: poll, | ||||
| 		poll, | ||||
| 		cw: useCw ? cw ?? '' : null, | ||||
| 		localOnly: localOnly, | ||||
| 		visibility: visibility, | ||||
|  | @ -783,11 +783,11 @@ async function post(ev?: MouseEvent) { | |||
| 
 | ||||
| 	if (postAccount) { | ||||
| 		const storedAccounts = await getAccounts(); | ||||
| 		token = storedAccounts.find(x => x.id === postAccount.id)?.token; | ||||
| 		token = storedAccounts.find(x => x.id === postAccount?.id)?.token; | ||||
| 	} | ||||
| 
 | ||||
| 	posting = true; | ||||
| 	os.api(postData.schedule ? 'notes/schedule/create' : 'notes/create', postData, token).then(() => { | ||||
| 	os.api('notes/create', postData, token).then(() => { | ||||
| 		if (props.freezeAfterPosted) { | ||||
| 			posted = true; | ||||
| 		} else { | ||||
|  |  | |||
|  | @ -1748,6 +1748,9 @@ export type Endpoints = { | |||
|                 expiresAt?: null | number; | ||||
|                 expiredAfter?: null | number; | ||||
|             }; | ||||
|             schedule?: null | { | ||||
|                 expiresAt?: null | number; | ||||
|             }; | ||||
|         }; | ||||
|         res: { | ||||
|             createdNote: Note; | ||||
|  | @ -1771,13 +1774,18 @@ export type Endpoints = { | |||
|     }; | ||||
|     'notes/schedule/delete': { | ||||
|         req: { | ||||
|             noteId: Note['id']; | ||||
|             scheduledNoteId: Note['id']; | ||||
|         }; | ||||
|         res: null; | ||||
|     }; | ||||
|     'notes/schedule/list': { | ||||
|         req: TODO; | ||||
|         res: Note[]; | ||||
|         res: { | ||||
|             id: Note['id']; | ||||
|             userId: User['id']; | ||||
|             expiresAt: number; | ||||
|             note: Note; | ||||
|         }[]; | ||||
|     }; | ||||
|     'notes/favorites/create': { | ||||
|         req: { | ||||
|  | @ -3055,7 +3063,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u | |||
| // | ||||
| // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts | ||||
| // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts | ||||
| // src/api.types.ts:635:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts | ||||
| // src/api.types.ts:643:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts | ||||
| // src/entities.ts:116:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts | ||||
| // src/entities.ts:627:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts | ||||
| // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts | ||||
|  |  | |||
|  | @ -507,11 +507,19 @@ export type Endpoints = { | |||
| 			expiresAt?: null | number; | ||||
| 			expiredAfter?: null | number; | ||||
| 		}; | ||||
| 		schedule?: null | { | ||||
| 			expiresAt?: null | number; | ||||
| 		}; | ||||
| 	}; res: { createdNote: Note }; }; | ||||
| 	'notes/delete': { req: { noteId: Note['id']; }; res: null; }; | ||||
| 	'notes/schedule/create': { req: Partial<Note> & { schedule: { expiresAt: number; } }; res: { createdNote: Note }; }; | ||||
| 	'notes/schedule/delete': { req: { noteId: Note['id']; }; res: null; }; | ||||
| 	'notes/schedule/list': { req: TODO; res: Note[]; }; | ||||
| 	'notes/schedule/delete': { req: { scheduledNoteId: Note['id']; }; res: null; }; | ||||
| 	'notes/schedule/list': { req: TODO; res: { | ||||
| 		id: Note['id']; | ||||
| 		userId: User['id']; | ||||
| 		expiresAt: number; | ||||
| 		note: Note; | ||||
| 	}[]; }; | ||||
| 	'notes/favorites/create': { req: { noteId: Note['id']; }; res: null; }; | ||||
| 	'notes/favorites/delete': { req: { noteId: Note['id']; }; res: null; }; | ||||
| 	'notes/featured': { req: TODO; res: Note[]; }; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue