Merge remote-tracking branch 'misskey-mattyatea/schedule-note' into develop

# Conflicts:
#	locales/index.d.ts
#	locales/ja-JP.yml
#	packages/backend/src/core/RoleService.ts
#	packages/frontend/src/components/MkNoteHeader.vue
#	packages/frontend/src/components/MkPostForm.vue
#	packages/frontend/src/const.ts
#	packages/frontend/src/navbar.ts
#	packages/frontend/src/pages/admin/roles.editor.vue
#	packages/frontend/src/pages/admin/roles.vue
This commit is contained in:
mattyatea 2023-12-02 00:50:04 +09:00
commit b7f9ad1944
39 changed files with 900 additions and 74 deletions

View file

@ -269,6 +269,8 @@ import * as ep___notes_children from './endpoints/notes/children.js';
import * as ep___notes_clips from './endpoints/notes/clips.js';
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
import * as ep___notes_create from './endpoints/notes/create.js';
import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js';
import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js';
import * as ep___notes_delete from './endpoints/notes/delete.js';
import * as ep___notes_update from './endpoints/notes/update.js';
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
@ -636,6 +638,8 @@ const $notes_children: Provider = { provide: 'ep:notes/children', useClass: ep__
const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes_clips.default };
const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default };
const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default };
const $notes_schedule_delete: Provider = { provide: 'ep:notes/schedule/delete', useClass: ep___notes_schedule_delete.default };
const $notes_schedule_list: Provider = { provide: 'ep:notes/schedule/list', useClass: ep___notes_schedule_list.default };
const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default };
const $notes_update: Provider = { provide: 'ep:notes/update', useClass: ep___notes_update.default };
const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default };
@ -1006,6 +1010,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_clips,
$notes_conversation,
$notes_create,
$notes_schedule_delete,
$notes_schedule_list,
$notes_delete,
$notes_update,
$notes_favorites_create,
@ -1370,6 +1376,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_clips,
$notes_conversation,
$notes_create,
$notes_schedule_delete,
$notes_schedule_list,
$notes_delete,
$notes_update,
$notes_favorites_create,

View file

@ -269,6 +269,8 @@ import * as ep___notes_children from './endpoints/notes/children.js';
import * as ep___notes_clips from './endpoints/notes/clips.js';
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
import * as ep___notes_create from './endpoints/notes/create.js';
import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js';
import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js';
import * as ep___notes_delete from './endpoints/notes/delete.js';
import * as ep___notes_update from './endpoints/notes/update.js';
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
@ -633,6 +635,8 @@ const eps = [
['notes/clips', ep___notes_clips],
['notes/conversation', ep___notes_conversation],
['notes/create', ep___notes_create],
['notes/schedule/delete', ep___notes_schedule_delete],
['notes/schedule/list', ep___notes_schedule_list],
['notes/delete', ep___notes_delete],
['notes/update', ep___notes_update],
['notes/favorites/create', ep___notes_favorites_create],

View file

@ -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, WebhookDeliverQueue } from '@/core/QueueModule.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue, ScheduleNotePostQueue } from '@/core/QueueModule.js';
export const meta = {
tags: ['admin'],
@ -48,6 +48,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,

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,9 @@ 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 { RoleService } from '@/core/RoleService.js';
import { DI } from '@/di-symbols.js';
import { isPureRenote } from '@/misc/is-pure-renote.js';
import { ApiError } from '../../error.js';
@ -39,9 +43,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,
},
},
},
@ -111,6 +123,28 @@ 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',
},
rolePermissionDenied: {
message: 'You are not assigned to a required role.',
code: 'ROLE_PERMISSION_DENIED',
kind: 'permission',
id: '7f86f06f-7e15-4057-8561-f4b6d4ac755a',
},
},
} as const;
@ -170,6 +204,13 @@ export const paramDef = {
},
required: ['choices'],
},
schedule: {
type: 'object',
nullable: true,
properties: {
scheduledAt: { type: 'string', nullable: false },
},
},
},
// (re)note with text, files and poll are optional
anyOf: [
@ -178,6 +219,7 @@ export const paramDef = {
{ required: ['fileIds'] },
{ required: ['mediaIds'] },
{ required: ['poll'] },
{ required: ['schedule'] },
],
} as const;
@ -190,6 +232,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,
@ -201,6 +246,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private noteEntityService: NoteEntityService,
private noteCreateService: NoteCreateService,
private roleService: RoleService,
private queueService: QueueService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
let visibleUsers: MiUser[] = [];
@ -319,8 +368,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 ? {
@ -340,11 +388,51 @@ 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) {
// 予約投稿
const canCreateScheduledNote = (await this.roleService.getUserPolicies(me.id)).canScheduleNote;
if (!canCreateScheduledNote) {
throw new ApiError(meta.errors.rolePermissionDenied);
}
if (!ps.schedule.scheduledAt) {
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,
scheduledAt: new Date(ps.schedule.scheduledAt),
});
const delay = new Date(ps.schedule.scheduledAt).getTime() - Date.now();
await this.queueService.ScheduleNotePostQueue.add(delay.toString(), {
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

@ -0,0 +1,55 @@
/*
* 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 { ScheduledNotesRepository } from '@/models/_.js';
import { QueueService } from '@/core/QueueService.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes'],
requireCredential: true,
limit: {
duration: ms('1hour'),
max: 300,
},
errors: {
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: '490be23f-8c1f-4796-819f-94cb4f9d1630',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
scheduledNoteId: { type: 'string', format: 'misskey:id' },
},
required: ['scheduledNoteId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.scheduledNotesRepository)
private scheduledNotesRepository: ScheduledNotesRepository,
private queueService: QueueService,
) {
super(meta, paramDef, async (ps, me) => {
await this.scheduledNotesRepository.delete({ id: ps.scheduledNoteId });
if (ps.scheduledNoteId) {
await this.queueService.ScheduleNotePostQueue.remove(ps.scheduledNoteId);
}
});
}
}

View file

@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import type { ScheduledNotesRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { QueryService } from '@/core/QueryService.js';
import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['notes'],
requireCredential: true,
requireRolePolicy: 'canScheduleNote',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
},
},
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<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.scheduledNotesRepository)
private scheduledNotesRepository: ScheduledNotesRepository,
private idService: IdService,
private userEntityService: UserEntityService,
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.scheduledNotesRepository.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 = scheduleNotes.map((item) => {
return {
...item,
scheduledAt: new Date(item.scheduledAt).toISOString(),
note: {
...item.note,
user: user,
createdAt: new Date(item.scheduledAt).toISOString(),
isSchedule: true,
id: null,
scheduledNoteId: item.id,
},
};
});
return scheduleNotesPack;
});
}
}

View file

@ -24,7 +24,16 @@ import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js';
import { MetaService } from '@/core/MetaService.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js';
import type {
DbQueue,
DeliverQueue,
EndedPollNotificationQueue,
InboxQueue,
ObjectStorageQueue,
ScheduleNotePostQueue,
SystemQueue,
WebhookDeliverQueue,
} from '@/core/QueueModule.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
@ -98,6 +107,7 @@ export class ClientServerService {
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@ -215,6 +225,7 @@ export class ClientServerService {
queues: [
this.systemQueue,
this.endedPollNotificationQueue,
this.scheduleNotePostQueue,
this.deliverQueue,
this.inboxQueue,
this.dbQueue,