(enhance) `notes/create` で予約投稿できるように

This commit is contained in:
kakkokari-gtyih 2023-11-09 23:31:16 +09:00
parent 1995400dac
commit 2bc15c09ed
9 changed files with 122 additions and 23 deletions

View File

@ -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 });

View File

@ -109,7 +109,7 @@ export type EndedPollNotificationJobData = {
};
export type ScheduleNotePostJobData = {
scheduleNoteId: MiNote['id'];
scheduledNoteId: MiNote['id'];
}
type MinimumUser = {

View File

@ -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),
};
}
});
}
}

View File

@ -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);
}
});
}
}

View File

@ -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,
},
};

View File

@ -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;
});

View File

@ -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 {

View File

@ -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

View File

@ -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[]; };