(enhance) `notes/create` で予約投稿できるように
This commit is contained in:
parent
1995400dac
commit
2bc15c09ed
|
@ -32,9 +32,9 @@ export class ScheduleNotePostProcessorService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<ScheduleNotePostJobData>): Promise<void> {
|
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) {
|
if (!data) {
|
||||||
this.logger.warn(`Schedule note ${job.data.scheduleNoteId} not found`);
|
this.logger.warn(`Schedule note ${job.data.scheduledNoteId} not found`);
|
||||||
} else {
|
} else {
|
||||||
data.note.createdAt = new Date();
|
data.note.createdAt = new Date();
|
||||||
const me = await this.usersRepository.findOneByOrFail({ id: data.userId });
|
const me = await this.usersRepository.findOneByOrFail({ id: data.userId });
|
||||||
|
|
|
@ -109,7 +109,7 @@ export type EndedPollNotificationJobData = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ScheduleNotePostJobData = {
|
export type ScheduleNotePostJobData = {
|
||||||
scheduleNoteId: MiNote['id'];
|
scheduledNoteId: MiNote['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
type MinimumUser = {
|
type MinimumUser = {
|
||||||
|
|
|
@ -7,7 +7,8 @@ import ms from 'ms';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { MiUser } from '@/models/User.js';
|
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 { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import type { MiChannel } from '@/models/Channel.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 { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { NoteCreateService } from '@/core/NoteCreateService.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 { DI } from '@/di-symbols.js';
|
||||||
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
@ -39,9 +42,17 @@ export const meta = {
|
||||||
properties: {
|
properties: {
|
||||||
createdNote: {
|
createdNote: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: true,
|
||||||
ref: 'Note',
|
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',
|
code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL',
|
||||||
id: '33510210-8452-094c-6227-4a6c05d99f00',
|
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;
|
} as const;
|
||||||
|
|
||||||
|
@ -164,6 +191,13 @@ export const paramDef = {
|
||||||
},
|
},
|
||||||
required: ['choices'],
|
required: ['choices'],
|
||||||
},
|
},
|
||||||
|
schedule: {
|
||||||
|
type: 'object',
|
||||||
|
nullable: true,
|
||||||
|
properties: {
|
||||||
|
expiresAt: { type: 'integer', nullable: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// (re)note with text, files and poll are optional
|
// (re)note with text, files and poll are optional
|
||||||
anyOf: [
|
anyOf: [
|
||||||
|
@ -172,6 +206,7 @@ export const paramDef = {
|
||||||
{ required: ['fileIds'] },
|
{ required: ['fileIds'] },
|
||||||
{ required: ['mediaIds'] },
|
{ required: ['mediaIds'] },
|
||||||
{ required: ['poll'] },
|
{ required: ['poll'] },
|
||||||
|
{ required: ['schedule'] },
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -184,6 +219,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.scheduledNotesRepository)
|
||||||
|
private scheduledNotesRepository: ScheduledNotesRepository,
|
||||||
|
|
||||||
@Inject(DI.blockingsRepository)
|
@Inject(DI.blockingsRepository)
|
||||||
private blockingsRepository: BlockingsRepository,
|
private blockingsRepository: BlockingsRepository,
|
||||||
|
|
||||||
|
@ -195,6 +233,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private noteCreateService: NoteCreateService,
|
private noteCreateService: NoteCreateService,
|
||||||
|
|
||||||
|
private queueService: QueueService,
|
||||||
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
let visibleUsers: MiUser[] = [];
|
let visibleUsers: MiUser[] = [];
|
||||||
|
@ -311,8 +352,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 投稿を作成
|
const note: MiNoteCreateOption = {
|
||||||
const note = await this.noteCreateService.create(me, {
|
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
files: files,
|
files: files,
|
||||||
poll: ps.poll ? {
|
poll: ps.poll ? {
|
||||||
|
@ -332,11 +372,45 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
apMentions: ps.noExtractMentions ? [] : undefined,
|
apMentions: ps.noExtractMentions ? [] : undefined,
|
||||||
apHashtags: ps.noExtractHashtags ? [] : undefined,
|
apHashtags: ps.noExtractHashtags ? [] : undefined,
|
||||||
apEmojis: ps.noExtractEmojis ? [] : 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 ms from 'ms';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { ScheduledNotesRepository } from '@/models/_.js';
|
import type { ScheduledNotesRepository } from '@/models/_.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
||||||
|
@ -31,9 +32,9 @@ export const meta = {
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
noteId: { type: 'string', format: 'misskey:id' },
|
scheduledNoteId: { type: 'string', format: 'misskey:id' },
|
||||||
},
|
},
|
||||||
required: ['noteId'],
|
required: ['scheduledNoteId'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -41,9 +42,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.scheduledNotesRepository)
|
@Inject(DI.scheduledNotesRepository)
|
||||||
private scheduledNotesRepository: ScheduledNotesRepository,
|
private scheduledNotesRepository: ScheduledNotesRepository,
|
||||||
|
|
||||||
|
private queueService: QueueService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
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,
|
user: user,
|
||||||
createdAt: new Date(item.expiresAt),
|
createdAt: new Date(item.expiresAt),
|
||||||
isSchedule: true,
|
isSchedule: true,
|
||||||
|
// ↓TODO: NoteのIDに予約投稿IDを入れたくない(本来別ものなため)
|
||||||
id: item.id,
|
id: item.id,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -40,7 +40,9 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
async function deleteScheduleNote() {
|
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(() => {
|
.then(() => {
|
||||||
isDeleted.value = true;
|
isDeleted.value = true;
|
||||||
});
|
});
|
||||||
|
|
|
@ -751,7 +751,7 @@ async function post(ev?: MouseEvent) {
|
||||||
renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
|
renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
|
||||||
channelId: props.channel ? props.channel.id : undefined,
|
channelId: props.channel ? props.channel.id : undefined,
|
||||||
schedule,
|
schedule,
|
||||||
poll: poll,
|
poll,
|
||||||
cw: useCw ? cw ?? '' : null,
|
cw: useCw ? cw ?? '' : null,
|
||||||
localOnly: localOnly,
|
localOnly: localOnly,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
|
@ -783,11 +783,11 @@ async function post(ev?: MouseEvent) {
|
||||||
|
|
||||||
if (postAccount) {
|
if (postAccount) {
|
||||||
const storedAccounts = await getAccounts();
|
const storedAccounts = await getAccounts();
|
||||||
token = storedAccounts.find(x => x.id === postAccount.id)?.token;
|
token = storedAccounts.find(x => x.id === postAccount?.id)?.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
posting = true;
|
posting = true;
|
||||||
os.api(postData.schedule ? 'notes/schedule/create' : 'notes/create', postData, token).then(() => {
|
os.api('notes/create', postData, token).then(() => {
|
||||||
if (props.freezeAfterPosted) {
|
if (props.freezeAfterPosted) {
|
||||||
posted = true;
|
posted = true;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1748,6 +1748,9 @@ export type Endpoints = {
|
||||||
expiresAt?: null | number;
|
expiresAt?: null | number;
|
||||||
expiredAfter?: null | number;
|
expiredAfter?: null | number;
|
||||||
};
|
};
|
||||||
|
schedule?: null | {
|
||||||
|
expiresAt?: null | number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
res: {
|
res: {
|
||||||
createdNote: Note;
|
createdNote: Note;
|
||||||
|
@ -1771,13 +1774,18 @@ export type Endpoints = {
|
||||||
};
|
};
|
||||||
'notes/schedule/delete': {
|
'notes/schedule/delete': {
|
||||||
req: {
|
req: {
|
||||||
noteId: Note['id'];
|
scheduledNoteId: Note['id'];
|
||||||
};
|
};
|
||||||
res: null;
|
res: null;
|
||||||
};
|
};
|
||||||
'notes/schedule/list': {
|
'notes/schedule/list': {
|
||||||
req: TODO;
|
req: TODO;
|
||||||
res: Note[];
|
res: {
|
||||||
|
id: Note['id'];
|
||||||
|
userId: User['id'];
|
||||||
|
expiresAt: number;
|
||||||
|
note: Note;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
'notes/favorites/create': {
|
'notes/favorites/create': {
|
||||||
req: {
|
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: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: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: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/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
|
// 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;
|
expiresAt?: null | number;
|
||||||
expiredAfter?: null | number;
|
expiredAfter?: null | number;
|
||||||
};
|
};
|
||||||
|
schedule?: null | {
|
||||||
|
expiresAt?: null | number;
|
||||||
|
};
|
||||||
}; res: { createdNote: Note }; };
|
}; res: { createdNote: Note }; };
|
||||||
'notes/delete': { req: { noteId: Note['id']; }; res: null; };
|
'notes/delete': { req: { noteId: Note['id']; }; res: null; };
|
||||||
'notes/schedule/create': { req: Partial<Note> & { schedule: { expiresAt: number; } }; res: { createdNote: Note }; };
|
'notes/schedule/create': { req: Partial<Note> & { schedule: { expiresAt: number; } }; res: { createdNote: Note }; };
|
||||||
'notes/schedule/delete': { req: { noteId: Note['id']; }; res: null; };
|
'notes/schedule/delete': { req: { scheduledNoteId: Note['id']; }; res: null; };
|
||||||
'notes/schedule/list': { req: TODO; res: Note[]; };
|
'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/create': { req: { noteId: Note['id']; }; res: null; };
|
||||||
'notes/favorites/delete': { req: { noteId: Note['id']; }; res: null; };
|
'notes/favorites/delete': { req: { noteId: Note['id']; }; res: null; };
|
||||||
'notes/featured': { req: TODO; res: Note[]; };
|
'notes/featured': { req: TODO; res: Note[]; };
|
||||||
|
|
Loading…
Reference in New Issue