From 80d5796d174c2211be7e74c8dbfa4f67a4890442 Mon Sep 17 00:00:00 2001 From: GrapeApple0 <84321396+GrapeApple0@users.noreply.github.com> Date: Fri, 14 Jun 2024 04:36:19 +0000 Subject: [PATCH] =?UTF-8?q?wip:=20=E3=82=B9=E3=83=88=E3=83=AA=E3=83=BC?= =?UTF-8?q?=E3=83=A0/api=E9=96=A2=E9=80=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/core/GlobalEventService.ts | 3 +- packages/backend/src/core/RoleService.ts | 3 + .../src/core/entities/NoteEntityService.ts | 1 + packages/backend/src/models/Note.ts | 2 +- .../backend/src/models/json-schema/role.ts | 4 + .../src/server/api/endpoints/notes/update.ts | 142 ++++++++++++++++++ 6 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/notes/update.ts diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index a70743bed2..200936dab3 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -118,8 +118,7 @@ export interface NoteEventTypes { deletedAt: Date; }; updated: { - cw: string | null; - text: string; + note: MiNote; }; reacted: { reaction: string; diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index e2ebecb99f..f2cf660708 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -35,6 +35,7 @@ export type RolePolicies = { gtlAvailable: boolean; ltlAvailable: boolean; canPublicNote: boolean; + canEditNote: boolean; mentionLimit: number; canInvite: boolean; inviteLimit: number; @@ -63,6 +64,7 @@ export const DEFAULT_POLICIES: RolePolicies = { gtlAvailable: true, ltlAvailable: true, canPublicNote: true, + canEditNote: false, mentionLimit: 20, canInvite: false, inviteLimit: 0, @@ -364,6 +366,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)), ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), + canEditNote: calc('canEditNote', vs => vs.some(v => v === true)), mentionLimit: calc('mentionLimit', vs => Math.max(...vs)), canInvite: calc('canInvite', vs => vs.some(v => v === true)), inviteLimit: calc('inviteLimit', vs => Math.max(...vs)), diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 2ce72c50b8..e09d96b15a 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -325,6 +325,7 @@ export class NoteEntityService implements OnModuleInit { const packed: Packed<'Note'> = await awaitAll({ id: note.id, createdAt: this.idService.parse(note.id).date.toISOString(), + updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined, userId: note.userId, user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me), text: text, diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index f3ad343884..98b869206e 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -19,7 +19,7 @@ export class MiNote { nullable: true, comment: 'The updated date of the Note.', }) - public updatedAt: Date; + public updatedAt: Date | null; @Index() @Column({ diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index d9987a70c3..a2e801ef36 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -180,6 +180,10 @@ export const packedRolePoliciesSchema = { type: 'boolean', optional: false, nullable: false, }, + canEditNote: { + type: 'boolean', + optional: false, nullable: false, + }, mentionLimit: { type: 'integer', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts new file mode 100644 index 0000000000..6bf626dcfa --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/update.ts @@ -0,0 +1,142 @@ +/* + * 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 { In } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { DriveFilesRepository, UsersRepository } from '@/models/_.js'; +import { NoteEditService } from '@/core/NoteEditService.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { RoleService } from '@/core/RoleService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + requireRolePolicy: 'canEditNote', + + kind: 'write:notes', + + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('2min'), + }, + + errors: { + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: 'a6584e14-6e01-4ad3-b566-851e7bf0d474', + }, + accessDenied: { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: 'fe8d7103-0ea8-4ec3-814d-f8b401dc69e9', + }, + containsProhibitedWords: { + message: 'Cannot post because it contains prohibited words.', + code: 'CONTAINS_PROHIBITED_WORDS', + id: 'aa6e01d3-a85c-669d-758a-76aab43af334', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + noteId: { type: 'string', format: 'misskey:id' }, + text: { + type: 'string', + minLength: 1, + maxLength: MAX_NOTE_TEXT_LENGTH, + nullable: false, + }, + cw: { + type: 'string', + nullable: true, + maxLength: 100, + }, + fileIds: { + 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'], + }, + }, + required: ['noteId', 'text', 'cw'], +} as const; + +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private getterService: GetterService, + private globalEventService: GlobalEventService, + private noteEditService: NoteEditService, + private roleService: RoleService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + // この操作を行うのが投稿者とは限らない(例えばモデレーター)ため + if (!await this.roleService.isModerator(me) && (note.userId !== me.id) && (await this.roleService.getUserPolicies(me.id)).canEditNote !== true) { + throw new ApiError(meta.errors.accessDenied); + } + + try { + const targetNote = await this.noteEditService.edit(await this.usersRepository.findOneByOrFail({ id: note.userId }), note.id, { + text: ps.text, + cw: ps.cw, + files: ps.fileIds ? await this.driveFilesRepository.findBy({ id: In(ps.fileIds) }) : undefined, + poll: ps.poll ? { + choices: ps.poll.choices, + multiple: ps.poll.multiple ?? false, + expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, + } : undefined, + }, undefined, me); + this.globalEventService.publishNoteStream(note.id, 'updated', { + note: targetNote, + }); + } catch (e) { + if (e instanceof NoteEditService.ContainsProhibitedWordsError) { + throw new ApiError(meta.errors.containsProhibitedWords); + } + throw e; + } + }); + } +}