Skip to content

Commit

Permalink
fix(backend): 虚無ノートを投稿できる問題の修正と api.json の OpenAPI Specification 3.…
Browse files Browse the repository at this point in the history
…1.0 への対応 (#12969)

* fix(backend): `text: null`だけのノートは投稿できないように

* add test

* Update CHANGELOG.md

* chore: bump OpenAPI Specification from 3.0.0 to 3.1.0

* chore: テストがすでにコメントで記述されていたのでそっちを使うことにする

* fix test

* fix(backend): prohibit posting whitespace-only notes

* Update CHANGELOG.md

* fix(backend): `renoteId`または`fileIds`(`mediaIds`)または`poll`が`null`でない場合に、`text  が空白文字のみで構成されたリクエストになることを許可して、結果は`text: null`を返すように

* test(backend): 引用renoteで空白文字のみで構成されたtextにするとレスポンスが`text: null`になることをチェックするテストを追加

* fix(frontend): `text`が`null`であって`renoteId`と`replyId`が`null`でないようなノートは引用リノートとして表示するように

* fix(misskey-js): OpenAPI 3.1に対応

* fix(misskey-js): 型生成をOpenAPI Specification 3.1.0に対応

* fix(ci): `validate-api.json`をOpenAPI Specification 3.1.0に対応

* fix(ci): スキーマ書き換えの際のミスを修正

* Revert "fix(frontend): `text`が`null`であって`renoteId`と`replyId`が`null`でないようなノートは引用リノートとして表示するように"

This reverts commit a9ca553.

* fix(misskey-js): `build-misskey-js-with-types`時は`api.json`のGETをスキップするように

* Revert "fix(misskey-js): `build-misskey-js-with-types`時は`api.json`のGETをスキップするように"

This reverts commit 8654589.

* fix(misskey-js): `openapi-parser`で`validate`のかわりに`parse`を用いるように

* Update CHANGELOG.md
  • Loading branch information
zyoshoka authored Jan 13, 2024
1 parent 79370aa commit d792f4f
Show file tree
Hide file tree
Showing 19 changed files with 259 additions and 146 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/validate-api-json.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install swagger-cli
run: npm i -g swagger-cli
- name: Install Redocly CLI
run: npm i -g @redocly/cli
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
Expand All @@ -44,4 +44,4 @@ jobs:
- name: Build and generate
run: pnpm build && pnpm --filter backend generate-api-json
- name: Validation
run: swagger-cli validate ./packages/backend/built/api.json
run: npx @redocly/cli lint --extends=minimal ./packages/backend/built/api.json
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916)
- Enhance: クリップをエクスポートできるように
- Enhance: `api.json`のOpenAPI Specificationを3.1.0に更新
- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正
- Fix: `notes/create`で、`text`が空白文字のみで構成されているか`null`であって、かつ`text`だけであるリクエストに対するレスポンスが400になるように変更
- Fix: `notes/create`で、`text`が空白文字のみで構成されていてかつリノート、ファイルまたは投票を含んでいるリクエストに対するレスポンスの`text``""`から`null`になるように変更

## 2023.12.2

Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/core/NoteCreateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,9 @@ export class NoteCreateService implements OnApplicationShutdown {
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
}
data.text = data.text.trim();
if (data.text === '') {
data.text = null;
}
} else {
data.text = null;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/server/api/endpoints/drive/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' },
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) },
sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size'] },
sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size', null] },
},
required: [],
} as const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const paramDef = {
'-firstRetrievedAt',
'+latestRequestReceivedAt',
'-latestRequestReceivedAt',
null,
],
},
},
Expand Down
33 changes: 17 additions & 16 deletions packages/backend/src/server/api/endpoints/i/2fa/register-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,13 @@ export const meta = {
items: {
type: 'string',
enum: [
"ble",
"cable",
"hybrid",
"internal",
"nfc",
"smart-card",
"usb",
'ble',
'cable',
'hybrid',
'internal',
'nfc',
'smart-card',
'usb',
],
},
},
Expand All @@ -123,8 +123,8 @@ export const meta = {
authenticatorAttachment: {
type: 'string',
enum: [
"cross-platform",
"platform",
'cross-platform',
'platform',
],
},
requireResidentKey: {
Expand All @@ -133,9 +133,9 @@ export const meta = {
userVerification: {
type: 'string',
enum: [
"discouraged",
"preferred",
"required",
'discouraged',
'preferred',
'required',
],
},
},
Expand All @@ -144,10 +144,11 @@ export const meta = {
type: 'string',
nullable: true,
enum: [
"direct",
"enterprise",
"indirect",
"none",
'direct',
'enterprise',
'indirect',
'none',
null,
],
},
extensions: {
Expand Down
14 changes: 9 additions & 5 deletions packages/backend/src/server/api/endpoints/notes/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,10 @@ describe('api:notes/create', () => {
.toBe(VALID);
});

// TODO
//test('null post', () => {
// expect(v({ text: null }))
// .toBe(INVALID);
//});
test('null post', () => {
expect(v({ text: null }))
.toBe(INVALID);
});

test('0 characters post', () => {
expect(v({ text: '' }))
Expand All @@ -49,6 +48,11 @@ describe('api:notes/create', () => {
expect(v({ text: await tooLong }))
.toBe(INVALID);
});

test('whitespace-only post', () => {
expect(v({ text: ' ' }))
.toBe(INVALID);
});
});

describe('cw', () => {
Expand Down
34 changes: 27 additions & 7 deletions packages/backend/src/server/api/endpoints/notes/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,33 @@ export const paramDef = {
},
},
// (re)note with text, files and poll are optional
anyOf: [
{ required: ['text'] },
{ required: ['renoteId'] },
{ required: ['fileIds'] },
{ required: ['mediaIds'] },
{ required: ['poll'] },
],
if: {
properties: {
renoteId: {
type: 'null',
},
fileIds: {
type: 'null',
},
mediaIds: {
type: 'null',
},
poll: {
type: 'null',
},
},
},
then: {
properties: {
text: {
type: 'string',
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
pattern: '[^\\s]+',
},
},
required: ['text'],
},
} as const;

@Injectable()
Expand Down
10 changes: 6 additions & 4 deletions packages/backend/src/server/api/openapi/gen-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';

export function genOpenapiSpec(config: Config) {
const spec = {
openapi: '3.0.0',
openapi: '3.1.0',

info: {
version: config.version,
Expand Down Expand Up @@ -56,7 +56,7 @@ export function genOpenapiSpec(config: Config) {
}
}

const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {};
const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res, 'res') : {};

let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n';

Expand All @@ -71,7 +71,7 @@ export function genOpenapiSpec(config: Config) {
}

const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
const schema = { ...endpoint.params };
const schema = { ...convertSchemaToOpenApiSchema(endpoint.params, 'param') };

if (endpoint.meta.requireFile) {
schema.properties = {
Expand Down Expand Up @@ -210,7 +210,9 @@ export function genOpenapiSpec(config: Config) {
};

spec.paths['/' + endpoint.name] = {
...(endpoint.meta.allowGet ? { get: info } : {}),
...(endpoint.meta.allowGet ? {
get: info,
} : {}),
post: info,
};
}
Expand Down
37 changes: 24 additions & 13 deletions packages/backend/src/server/api/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,35 @@
import type { Schema } from '@/misc/json-schema.js';
import { refs } from '@/misc/json-schema.js';

export function convertSchemaToOpenApiSchema(schema: Schema) {
// optional, refはスキーマ定義に含まれないので分離しておく
export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res') {
// optional, nullable, refはスキーマ定義に含まれないので分離しておく
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { optional, ref, ...res }: any = schema;
const { optional, nullable, ref, ...res }: any = schema;

if (schema.type === 'object' && schema.properties) {
const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
if (required.length > 0) {
if (type === 'res') {
const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
if (required.length > 0) {
// 空配列は許可されない
res.required = required;
res.required = required;
}
}

for (const k of Object.keys(schema.properties)) {
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]);
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k], type);
}
}

if (schema.type === 'array' && schema.items) {
res.items = convertSchemaToOpenApiSchema(schema.items);
res.items = convertSchemaToOpenApiSchema(schema.items, type);
}

if (schema.anyOf) res.anyOf = schema.anyOf.map(convertSchemaToOpenApiSchema);
if (schema.oneOf) res.oneOf = schema.oneOf.map(convertSchemaToOpenApiSchema);
if (schema.allOf) res.allOf = schema.allOf.map(convertSchemaToOpenApiSchema);
for (const o of ['anyOf', 'oneOf', 'allOf'] as const) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (o in schema) res[o] = schema[o]!.map(schema => convertSchemaToOpenApiSchema(schema, type));
}

if (schema.ref) {
if (type === 'res' && schema.ref) {
const $ref = `#/components/schemas/${schema.ref}`;
if (schema.nullable || schema.optional) {
res.allOf = [{ $ref }];
Expand All @@ -40,6 +43,14 @@ export function convertSchemaToOpenApiSchema(schema: Schema) {
}
}

if (schema.nullable) {
if (Array.isArray(schema.type) && !schema.type.includes('null')) {
res.type.push('null');
} else if (typeof schema.type === 'string') {
res.type = [res.type, 'null'];
}
}

return res;
}

Expand Down Expand Up @@ -72,6 +83,6 @@ export const schemas = {
},

...Object.fromEntries(
Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema)]),
Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema, 'res')]),
),
};
13 changes: 13 additions & 0 deletions packages/backend/test/e2e/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,19 @@ describe('Note', () => {
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
});

test('引用renoteで空白文字のみで構成されたtextにするとレスポンスがtext: nullになる', async () => {
const bobPost = await post(bob, {
text: 'test',
});
const res = await api('/notes/create', {
text: ' ',
renoteId: bobPost.id,
}, alice);

assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.createdNote.text, null);
});

test('visibility: followersでrenoteできる', async () => {
const createRes = await api('/notes/create', {
text: 'test',
Expand Down
4 changes: 2 additions & 2 deletions packages/misskey-js/generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
"generate": "tsx src/generator.ts && eslint ./built/**/* --ext .ts --fix"
},
"devDependencies": {
"@apidevtools/swagger-parser": "10.1.0",
"@misskey-dev/eslint-plugin": "^1.0.0",
"@readme/openapi-parser": "2.5.0",
"@types/node": "20.9.1",
"@typescript-eslint/eslint-plugin": "6.11.0",
"@typescript-eslint/parser": "6.11.0",
"eslint": "8.53.0",
"openapi-types": "12.1.3",
"openapi-typescript": "6.7.1",
"openapi-typescript": "6.7.3",
"ts-case-convert": "2.0.2",
"tsx": "4.4.0",
"typescript": "5.3.3"
Expand Down
Loading

0 comments on commit d792f4f

Please sign in to comment.