From 5b6146e348d3f589bbe0b948b31fc9f1ea54120b Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:14:47 +0900 Subject: [PATCH] =?UTF-8?q?page=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=88=E5=91=A8=E3=82=8A=E3=81=AE=E6=94=B9?= =?UTF-8?q?=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - featuredでページネーションが可能に - ページブロックのバリデーションを追加 - ページslugのバリデーションを追加 - ページブロックの容量制限を追加 - 未使用プロパティscriptとvariablesは変更が効かないように --- packages/backend/src/const.ts | 1 + packages/backend/src/core/PageService.ts | 40 +++++++++ packages/backend/src/models/Page.ts | 2 + .../backend/src/models/json-schema/page.ts | 22 +++++ .../src/server/api/endpoints/pages/create.ts | 25 ++++-- .../server/api/endpoints/pages/featured.ts | 27 +++--- .../src/server/api/endpoints/pages/update.ts | 22 +++-- packages/misskey-js/etc/misskey-js.api.md | 4 + packages/misskey-js/src/autogen/endpoint.ts | 3 +- packages/misskey-js/src/autogen/entities.ts | 1 + packages/misskey-js/src/autogen/types.ts | 84 +++++++++++++++++-- 11 files changed, 195 insertions(+), 36 deletions(-) create mode 100644 packages/backend/src/core/PageService.ts diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index e3a61861f4..c9fb7f9022 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -4,6 +4,7 @@ */ export const MAX_NOTE_TEXT_LENGTH = 3000; +export const MAX_PAGE_CONTENT_BYTES = 1024 * 1024 * 1.5; // 1.5MB export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days diff --git a/packages/backend/src/core/PageService.ts b/packages/backend/src/core/PageService.ts new file mode 100644 index 0000000000..62ab351efd --- /dev/null +++ b/packages/backend/src/core/PageService.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import { type PagesRepository } from '@/models/_.js'; + +/** + * ページ関係のService + */ +@Injectable() +export class PageService { + constructor( + @Inject(DI.pagesRepository) + private pagesRepository: PagesRepository, + ) { + } + + /** + * 人気のあるページ一覧を取得する. + */ + public async featured(opts?: { offset?: number, limit: number }) { + const builder = this.pagesRepository.createQueryBuilder('page') + .andWhere('page.likedCount > 0') + .andWhere('page.visibility = :visibility', { visibility: 'public' }) + .addOrderBy('page.likedCount', 'DESC') + .addOrderBy('page.updatedAt', 'DESC') + .addOrderBy('page.id', 'DESC'); + + if (opts?.offset) { + builder.skip(opts.offset); + } + + builder.take(opts?.limit ?? 10); + + return await builder.getMany(); + } +} diff --git a/packages/backend/src/models/Page.ts b/packages/backend/src/models/Page.ts index 1695bf570e..40a23acb95 100644 --- a/packages/backend/src/models/Page.ts +++ b/packages/backend/src/models/Page.ts @@ -118,3 +118,5 @@ export class MiPage { } } } + +export const pageNameSchema = { type: 'string', pattern: /^[a-zA-Z0-9_-]{1,256}$/.source } as const; diff --git a/packages/backend/src/models/json-schema/page.ts b/packages/backend/src/models/json-schema/page.ts index 748d6f1245..3ae15a157f 100644 --- a/packages/backend/src/models/json-schema/page.ts +++ b/packages/backend/src/models/json-schema/page.ts @@ -33,6 +33,27 @@ const textBlockSchema = { }, } as const; +const headingBlockSchema = { + type: 'object', + properties: { + ...blockBaseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['heading'], + }, + level: { + type: 'number', + optional: false, nullable: false, + }, + text: { + type: 'string', + optional: false, nullable: false, + }, + }, +} as const; + +/** @deprecated 要素を入れ子にする必要が(一旦)なくなったので非推奨。headingBlockを使用すること */ const sectionBlockSchema = { type: 'object', properties: { @@ -100,6 +121,7 @@ export const packedPageBlockSchema = { oneOf: [ textBlockSchema, sectionBlockSchema, + headingBlockSchema, imageBlockSchema, noteBlockSchema, ], diff --git a/packages/backend/src/server/api/endpoints/pages/create.ts b/packages/backend/src/server/api/endpoints/pages/create.ts index fa03b0b457..afdb3cd4a6 100644 --- a/packages/backend/src/server/api/endpoints/pages/create.ts +++ b/packages/backend/src/server/api/endpoints/pages/create.ts @@ -7,11 +7,13 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; import type { DriveFilesRepository, PagesRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; -import { MiPage } from '@/models/Page.js'; +import { MiPage, pageNameSchema } from '@/models/Page.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; +import { ApiError } from '@/server/api/error.js'; +import { MAX_PAGE_CONTENT_BYTES } from '@/const.js'; +import { packedPageBlockSchema } from '@/models/json-schema/page.js'; export const meta = { tags: ['pages'], @@ -44,6 +46,11 @@ export const meta = { code: 'NAME_ALREADY_EXISTS', id: '4650348e-301c-499a-83c9-6aa988c66bc1', }, + contentTooLarge: { + message: 'Content is too large.', + code: 'CONTENT_TOO_LARGE', + id: '2a93fcc9-4cd7-4885-9e5b-be56ed8f4d4f', + }, }, } as const; @@ -51,10 +58,10 @@ export const paramDef = { type: 'object', properties: { title: { type: 'string' }, - name: { type: 'string', minLength: 1 }, + name: { ...pageNameSchema, minLength: 1 }, summary: { type: 'string', nullable: true }, content: { type: 'array', items: { - type: 'object', additionalProperties: true, + ...packedPageBlockSchema, } }, variables: { type: 'array', items: { type: 'object', additionalProperties: true, @@ -65,7 +72,7 @@ export const paramDef = { alignCenter: { type: 'boolean', default: false }, hideTitleWhenPinned: { type: 'boolean', default: false }, }, - required: ['title', 'name', 'content', 'variables', 'script'], + required: ['title', 'name', 'content'], } as const; @Injectable() @@ -81,6 +88,10 @@ export default class extends Endpoint { // eslint- private idService: IdService, ) { super(meta, paramDef, async (ps, me) => { + if (new Blob([JSON.stringify(ps.content)]).size > MAX_PAGE_CONTENT_BYTES) { + throw new ApiError(meta.errors.contentTooLarge); + } + let eyeCatchingImage = null; if (ps.eyeCatchingImageId != null) { eyeCatchingImage = await this.driveFilesRepository.findOneBy({ @@ -109,8 +120,8 @@ export default class extends Endpoint { // eslint- name: ps.name, summary: ps.summary, content: ps.content, - variables: ps.variables, - script: ps.script, + //variables: ps.variables, もう使用されていない(動的ページ) + //script: ps.script, もう使用されていない(動的ページ) eyeCatchingImageId: eyeCatchingImage ? eyeCatchingImage.id : null, userId: me.id, visibility: 'public', diff --git a/packages/backend/src/server/api/endpoints/pages/featured.ts b/packages/backend/src/server/api/endpoints/pages/featured.ts index a47b69e56e..a9a6ea2750 100644 --- a/packages/backend/src/server/api/endpoints/pages/featured.ts +++ b/packages/backend/src/server/api/endpoints/pages/featured.ts @@ -3,11 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable } from '@nestjs/common'; -import type { PagesRepository } from '@/models/_.js'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import { PageService } from '@/core/PageService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js'; -import { DI } from '@/di-symbols.js'; export const meta = { tags: ['pages'], @@ -27,27 +26,25 @@ export const meta = { export const paramDef = { type: 'object', - properties: {}, + properties: { + offset: { type: 'integer', minimum: 0, default: 0 }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, required: [], } as const; @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.pagesRepository) - private pagesRepository: PagesRepository, - + private pageService: PageService, private pageEntityService: PageEntityService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.pagesRepository.createQueryBuilder('page') - .where('page.visibility = \'public\'') - .andWhere('page.likedCount > 0') - .orderBy('page.likedCount', 'DESC'); - - const pages = await query.limit(10).getMany(); - - return await this.pageEntityService.packMany(pages, me); + const result = await this.pageService.featured({ + offset: ps.offset, + limit: ps.limit, + }); + return await this.pageEntityService.packMany(result, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index f11bbbcb1a..393be2b32e 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -9,7 +9,10 @@ import { Inject, Injectable } from '@nestjs/common'; import type { PagesRepository, DriveFilesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { ApiError } from '../../error.js'; +import { ApiError } from '@/server/api/error.js'; +import { MAX_PAGE_CONTENT_BYTES } from '@/const.js'; +import { packedPageBlockSchema } from '@/models/json-schema/page.js'; +import { pageNameSchema } from '@/models/Page.js'; export const meta = { tags: ['pages'], @@ -48,6 +51,11 @@ export const meta = { code: 'NAME_ALREADY_EXISTS', id: '2298a392-d4a1-44c5-9ebb-ac1aeaa5a9ab', }, + contentTooLarge: { + message: 'Content is too large.', + code: 'CONTENT_TOO_LARGE', + id: '2a93fcc9-4cd7-4885-9e5b-be56ed8f4d4f', + }, }, } as const; @@ -56,10 +64,10 @@ export const paramDef = { properties: { pageId: { type: 'string', format: 'misskey:id' }, title: { type: 'string' }, - name: { type: 'string', minLength: 1 }, + name: { ...pageNameSchema, minLength: 1 }, summary: { type: 'string', nullable: true }, content: { type: 'array', items: { - type: 'object', additionalProperties: true, + ...packedPageBlockSchema, } }, variables: { type: 'array', items: { type: 'object', additionalProperties: true, @@ -91,6 +99,10 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.accessDenied); } + if (new Blob([JSON.stringify(ps.content)]).size > MAX_PAGE_CONTENT_BYTES) { + throw new ApiError(meta.errors.contentTooLarge); + } + if (ps.eyeCatchingImageId != null) { const eyeCatchingImage = await this.driveFilesRepository.findOneBy({ id: ps.eyeCatchingImageId, @@ -118,8 +130,8 @@ export default class extends Endpoint { // eslint- name: ps.name, summary: ps.summary === undefined ? page.summary : ps.summary, content: ps.content, - variables: ps.variables, - script: ps.script, + //variables: ps.variables, もう使用されていない(動的ページ) + //script: ps.script, もう使用されていない(動的ページ) alignCenter: ps.alignCenter, hideTitleWhenPinned: ps.hideTitleWhenPinned, font: ps.font, diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 061b533b72..ccf017d77a 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1684,6 +1684,7 @@ declare namespace entities { PagesCreateRequest, PagesCreateResponse, PagesDeleteRequest, + PagesFeaturedRequest, PagesFeaturedResponse, PagesLikeRequest, PagesShowRequest, @@ -2841,6 +2842,9 @@ type PagesCreateResponse = operations['pages___create']['responses']['200']['con // @public (undocumented) type PagesDeleteRequest = operations['pages___delete']['requestBody']['content']['application/json']; +// @public (undocumented) +type PagesFeaturedRequest = operations['pages___featured']['requestBody']['content']['application/json']; + // @public (undocumented) type PagesFeaturedResponse = operations['pages___featured']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 5e6bc0a99c..699fbefa29 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -459,6 +459,7 @@ import type { PagesCreateRequest, PagesCreateResponse, PagesDeleteRequest, + PagesFeaturedRequest, PagesFeaturedResponse, PagesLikeRequest, PagesShowRequest, @@ -888,7 +889,7 @@ export type Endpoints = { 'page-push': { req: PagePushRequest; res: EmptyResponse }; 'pages/create': { req: PagesCreateRequest; res: PagesCreateResponse }; 'pages/delete': { req: PagesDeleteRequest; res: EmptyResponse }; - 'pages/featured': { req: EmptyRequest; res: PagesFeaturedResponse }; + 'pages/featured': { req: PagesFeaturedRequest; res: PagesFeaturedResponse }; 'pages/like': { req: PagesLikeRequest; res: EmptyResponse }; 'pages/show': { req: PagesShowRequest; res: PagesShowResponse }; 'pages/unlike': { req: PagesUnlikeRequest; res: EmptyResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index f3ddf64481..d72481e38e 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -462,6 +462,7 @@ export type PagePushRequest = operations['page-push']['requestBody']['content'][ export type PagesCreateRequest = operations['pages___create']['requestBody']['content']['application/json']; export type PagesCreateResponse = operations['pages___create']['responses']['200']['content']['application/json']; export type PagesDeleteRequest = operations['pages___delete']['requestBody']['content']['application/json']; +export type PagesFeaturedRequest = operations['pages___featured']['requestBody']['content']['application/json']; export type PagesFeaturedResponse = operations['pages___featured']['responses']['200']['content']['application/json']; export type PagesLikeRequest = operations['pages___like']['requestBody']['content']['application/json']; export type PagesShowRequest = operations['pages___show']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index a5333d4f93..f23d7ee22b 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4545,6 +4545,12 @@ export type components = { type: 'section'; title: string; children: components['schemas']['PageBlock'][]; + }, { + id: string; + /** @enum {string} */ + type: 'heading'; + level: number; + text: string; }, { id: string; /** @enum {string} */ @@ -23426,13 +23432,39 @@ export type operations = { title: string; name: string; summary?: string | null; - content: { + content: (OneOf<[{ + id?: string; + /** @enum {string} */ + type?: 'text'; + text?: string; + }, { + id?: string; + /** @enum {string} */ + type?: 'section'; + title?: string; + children?: components['schemas']['PageBlock'][]; + }, { + id?: string; + /** @enum {string} */ + type?: 'heading'; + level?: number; + text?: string; + }, { + id?: string; + /** @enum {string} */ + type?: 'image'; + fileId?: string | null; + }, { + id?: string; + /** @enum {string} */ + type?: 'note'; + detailed?: boolean; + note?: string | null; + }]>)[]; + variables?: { [key: string]: unknown; }[]; - variables: { - [key: string]: unknown; - }[]; - script: string; + script?: string; /** Format: misskey:id */ eyeCatchingImageId?: string | null; /** @@ -23551,6 +23583,16 @@ export type operations = { * **Credential required**: *No* */ pages___featured: { + requestBody: { + content: { + 'application/json': { + /** @default 0 */ + offset?: number; + /** @default 10 */ + limit?: number; + }; + }; + }; responses: { /** @description OK (with results) */ 200: { @@ -23765,9 +23807,35 @@ export type operations = { title?: string; name?: string; summary?: string | null; - content?: { - [key: string]: unknown; - }[]; + content?: (OneOf<[{ + id?: string; + /** @enum {string} */ + type?: 'text'; + text?: string; + }, { + id?: string; + /** @enum {string} */ + type?: 'section'; + title?: string; + children?: components['schemas']['PageBlock'][]; + }, { + id?: string; + /** @enum {string} */ + type?: 'heading'; + level?: number; + text?: string; + }, { + id?: string; + /** @enum {string} */ + type?: 'image'; + fileId?: string | null; + }, { + id?: string; + /** @enum {string} */ + type?: 'note'; + detailed?: boolean; + note?: string | null; + }]>)[]; variables?: { [key: string]: unknown; }[];