diff --git a/src/server/api/gen-openapi-spec.ts b/src/server/api/gen-openapi-spec.ts deleted file mode 100644 index 5c460d0dae..0000000000 --- a/src/server/api/gen-openapi-spec.ts +++ /dev/null @@ -1,576 +0,0 @@ -import endpoints from './endpoints'; -import { Context } from 'cafy'; -import config from '../../config'; - -const basicErrors = { - '400': { - 'INVALID_PARAM': { - value: { - error: { - message: 'Invalid param.', - code: 'INVALID_PARAM', - id: '3d81ceae-475f-4600-b2a8-2bc116157532', - } - } - } - }, - '401': { - 'CREDENTIAL_REQUIRED': { - value: { - error: { - message: 'Credential required.', - code: 'CREDENTIAL_REQUIRED', - id: '1384574d-a912-4b81-8601-c7b1c4085df1', - } - } - } - }, - '403': { - 'AUTHENTICATION_FAILED': { - value: { - error: { - message: 'Authentication failed. Please ensure your token is correct.', - code: 'AUTHENTICATION_FAILED', - id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14', - } - } - } - }, - '418': { - 'I_AM_AI': { - value: { - error: { - message: 'You sent a request to Ai-chan, Misskey\'s showgirl, instead of the server.', - code: 'I_AM_AI', - id: '60c46cd1-f23a-46b1-bebe-5d2b73951a84', - } - } - } - }, - '429': { - 'RATE_LIMIT_EXCEEDED': { - value: { - error: { - message: 'Rate limit exceeded. Please try again later.', - code: 'RATE_LIMIT_EXCEEDED', - id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef', - } - } - } - }, - '500': { - 'INTERNAL_ERROR': { - value: { - error: { - message: 'Internal error occurred. Please contact us if the error persists.', - code: 'INTERNAL_ERROR', - id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac', - } - } - } - } -}; - -const schemas = { - Error: { - type: 'object', - properties: { - error: { - type: 'object', - description: 'An error object.', - properties: { - code: { - type: 'string', - description: 'An error code.', - }, - message: { - type: 'string', - description: 'An error message.', - }, - id: { - type: 'string', - format: 'uuid', - description: 'An error ID. This ID is static.', - } - }, - required: ['code', 'id', 'message'] - }, - }, - required: ['error'] - }, - - User: { - type: 'object', - properties: { - id: { - type: 'string', - format: 'id', - description: 'The unique identifier for this User.' - }, - username: { - type: 'string', - description: 'The screen name, handle, or alias that this user identifies themselves with.', - example: 'ai' - }, - name: { - type: 'string', - nullable: true, - description: 'The name of the user, as they’ve defined it.', - example: '藍' - }, - host: { - type: 'string', - nullable: true, - example: 'misskey.example.com' - }, - description: { - type: 'string', - nullable: true, - description: 'The user-defined UTF-8 string describing their account.', - example: 'Hi masters, I am Ai!' - }, - createdAt: { - type: 'string', - format: 'date-time', - description: 'The date that the user account was created on Misskey.' - }, - followersCount: { - type: 'number', - description: 'The number of followers this account currently has.' - }, - followingCount: { - type: 'number', - description: 'The number of users this account is following.' - }, - notesCount: { - type: 'number', - description: 'The number of Notes (including renotes) issued by the user.' - }, - isBot: { - type: 'boolean', - description: 'Whether this account is a bot.' - }, - isCat: { - type: 'boolean', - description: 'Whether this account is a cat.' - }, - isAdmin: { - type: 'boolean', - description: 'Whether this account is the admin.' - }, - isVerified: { - type: 'boolean' - }, - isLocked: { - type: 'boolean' - }, - }, - required: ['id', 'name', 'username', 'createdAt'] - }, - - Note: { - type: 'object', - properties: { - id: { - type: 'string', - format: 'id', - description: 'The unique identifier for this Note.' - }, - createdAt: { - type: 'string', - format: 'date-time', - description: 'The date that the Note was created on Misskey.' - }, - text: { - type: 'string' - }, - cw: { - type: 'string' - }, - userId: { - type: 'string', - format: 'id', - }, - user: { - $ref: '#/components/schemas/User' - }, - replyId: { - type: 'string', - format: 'id', - }, - renoteId: { - type: 'string', - format: 'id', - }, - reply: { - $ref: '#/components/schemas/Note' - }, - renote: { - $ref: '#/components/schemas/Note' - }, - viaMobile: { - type: 'boolean' - }, - visibility: { - type: 'string' - }, - }, - required: ['id', 'userId', 'createdAt'] - }, - - DriveFile: { - type: 'object', - properties: { - id: { - type: 'string', - format: 'id', - description: 'The unique identifier for this Drive file.' - }, - createdAt: { - type: 'string', - format: 'date-time', - description: 'The date that the Drive file was created on Misskey.' - }, - name: { - type: 'string', - description: 'The file name with extension.', - example: 'lenna.jpg' - }, - type: { - type: 'string', - description: 'The MIME type of this Drive file.', - example: 'image/jpeg' - }, - md5: { - type: 'string', - format: 'md5', - description: 'The MD5 hash of this Drive file.', - example: '15eca7fba0480996e2245f5185bf39f2' - }, - datasize: { - type: 'number', - description: 'The size of this Drive file. (bytes)', - example: 51469 - }, - folderId: { - type: 'string', - format: 'id', - nullable: true, - description: 'The parent folder ID of this Drive file.', - }, - isSensitive: { - type: 'boolean', - description: 'Whether this Drive file is sensitive.', - }, - }, - required: ['id', 'createdAt', 'name', 'type', 'datasize', 'md5'] - } -}; - -const desc = ` -## Usage -APIはすべてPOSTでリクエスト/レスポンスともにJSON形式です。 -一部のAPIは認証情報(アクセストークン)が必要です。リクエストの際に\`i\`というパラメータでアクセストークンを添付してください。 - -### アクセストークンを取得する -#### 自分のアカウントのアクセストークンを取得する -「設定 > API」で、自分のアクセストークンを取得できます。 - -> アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。 - -### アプリケーションとしてアクセストークンを取得する -直接ユーザーのアクセストークンをアプリケーションが扱うのは危険なので、 -アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のアクセストークンをMisskeyに発行してもらいます。 - -#### 1.アプリケーションを登録する -まず、あなたのアプリケーションやWebサービス(以後、あなたのアプリと呼びます)をMisskeyに登録します。 -[デベロッパーセンター](/dev)にアクセスし、「アプリ > アプリ作成」からアプリを作成してください。 -フォームの記入欄の説明は以下の通りです: - -| 名前 | 説明 | -|---|---| -| アプリケーション名 | あなたのアプリの名称。 | -| アプリの概要 | あなたのアプリの簡単な説明や紹介。 | -| コールバックURL | ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。あなたのアプリがWebサービスである場合に有用です。 | -| 権限 | あなたのアプリが要求する権限。ここで要求した機能だけがAPIからアクセスできます。 | - -登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。 - -> アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。
- -#### 2.ユーザーに認証させる -アプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。 - -認証セッションを開始するには、%API_URL%/auth/session/generate へパラメータに appSecret としてシークレットキーを含めたリクエストを送信します。 -リクエスト形式はJSONで、メソッドはPOSTです。 -レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。 - -あなたのアプリがコールバックURLを設定している場合、 -ユーザーがあなたのアプリの連携を許可すると設定しているコールバックURLに token という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。 - -あなたのアプリがコールバックURLを設定していない場合、ユーザーがあなたのアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。 - -#### 3.ユーザートークンを取得する -ユーザーが連携を許可したら、%API_URL%/auth/session/userkey へ次のパラメータを含むリクエストを送信します: - -| 名前 | 型 | 説明 | -|---|---|---| -| appSecret | string | アプリのシークレットキー | -| token | string | セッションのトークン | - -上手くいけば、認証したユーザーのユーザートークンがレスポンスとして取得できます。おめでとうございます! - -ユーザートークンが取得できたら、「ユーザーのユーザートークン+あなたのアプリのシークレットキーをsha256したもの」をアクセストークンとして、APIにリクエストできます。 - -アクセストークンの生成方法を擬似コードで表すと次のようになります: -const i = sha256(userToken + secretKey);
-`;
-
-export function genOpenapiSpec(lang = 'ja-JP') {
- const spec = {
- openapi: '3.0.0',
-
- info: {
- version: 'v1',
- title: 'Misskey API',
- description: '**Misskey is a decentralized microblogging platform.**\n\n' + desc,
- 'x-logo': { url: '/assets/api-doc.png' }
- },
-
- externalDocs: {
- description: 'Repository',
- url: 'https://github.com/syuilo/misskey'
- },
-
- servers: [{
- url: config.api_url
- }],
-
- paths: {} as any,
-
- components: {
- schemas: schemas,
-
- securitySchemes: {
- ApiKeyAuth: {
- type: 'apiKey',
- in: 'body',
- name: 'i'
- }
- }
- }
- };
-
- function genProps(props: { [key: string]: Context & { desc: any, default: any }; }) {
- const properties = {} as any;
-
- const kvs = Object.entries(props);
-
- for (const kv of kvs) {
- properties[kv[0]] = genProp(kv[1], kv[1].desc, kv[1].default);
- }
-
- return properties;
- }
-
- function genProp(param: Context, desc?: string, _default?: any): any {
- const required = param.name === 'Object' ? (param as any).props ? Object.entries((param as any).props).filter(([k, v]: any) => !v.isOptional).map(([k, v]) => k) : [] : [];
- return {
- description: desc,
- default: _default,
- ...(_default ? { default: _default } : {}),
- type: param.name === 'ID' ? 'string' : param.name.toLowerCase(),
- ...(param.name === 'ID' ? { example: 'xxxxxxxxxxxxxxxxxxxxxxxx', format: 'id' } : {}),
- nullable: param.isNullable,
- ...(param.name === 'String' ? {
- ...((param as any).enum ? { enum: (param as any).enum } : {}),
- ...((param as any).minLength ? { minLength: (param as any).minLength } : {}),
- ...((param as any).maxLength ? { maxLength: (param as any).maxLength } : {}),
- } : {}),
- ...(param.name === 'Number' ? {
- ...((param as any).minimum ? { minimum: (param as any).minimum } : {}),
- ...((param as any).maximum ? { maximum: (param as any).maximum } : {}),
- } : {}),
- ...(param.name === 'Object' ? {
- ...(required.length > 0 ? { required } : {}),
- properties: (param as any).props ? genProps((param as any).props) : {}
- } : {}),
- ...(param.name === 'Array' ? {
- items: (param as any).ctx ? genProp((param as any).ctx) : {}
- } : {})
- };
- }
-
- for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
- const porops = {} as any;
- const errors = {} as any;
-
- if (endpoint.meta.errors) {
- for (const e of Object.values(endpoint.meta.errors)) {
- errors[e.code] = {
- value: {
- error: e
- }
- };
- }
- }
-
- if (endpoint.meta.params) {
- for (const kv of Object.entries(endpoint.meta.params)) {
- if (kv[1].desc) (kv[1].validator as any).desc = kv[1].desc[lang];
- if (kv[1].default) (kv[1].validator as any).default = kv[1].default;
- porops[kv[0]] = kv[1].validator;
- }
- }
-
- const required = endpoint.meta.params ? Object.entries(endpoint.meta.params).filter(([k, v]) => !v.validator.isOptional).map(([k, v]) => k) : [];
-
- const resSchema = endpoint.meta.res ? renderType(endpoint.meta.res) : {};
-
- function renderType(x: any) {
- const res = {} as any;
-
- if (['User', 'Note', 'DriveFile'].includes(x.type)) {
- res['$ref'] = `#/components/schemas/${x.type}`;
- } else if (x.type === 'object') {
- res['type'] = 'object';
- if (x.props) {
- const props = {} as any;
- for (const kv of Object.entries(x.props)) {
- props[kv[0]] = renderType(kv[1]);
- }
- res['properties'] = props;
- }
- } else if (x.type === 'array') {
- res['type'] = 'array';
- if (x.items) {
- res['items'] = renderType(x.items);
- }
- } else {
- res['type'] = x.type;
- }
-
- return res;
- }
-
- const info = {
- operationId: endpoint.name,
- summary: endpoint.name,
- description: endpoint.meta.desc ? endpoint.meta.desc[lang] : 'No description provided.',
- externalDocs: {
- description: 'Source code',
- url: `https://github.com/syuilo/misskey/blob/develop/src/server/api/endpoints/${endpoint.name}.ts`
- },
- ...(endpoint.meta.tags ? {
- tags: endpoint.meta.tags
- } : {}),
- ...(endpoint.meta.requireCredential ? {
- security: [{
- ApiKeyAuth: []
- }]
- } : {}),
- requestBody: {
- required: true,
- content: {
- 'application/json': {
- schema: {
- type: 'object',
- ...(required.length > 0 ? { required } : {}),
- properties: endpoint.meta.params ? genProps(porops) : {}
- }
- }
- }
- },
- responses: {
- ...(endpoint.meta.res ? {
- '200': {
- description: 'OK (with results)',
- content: {
- 'application/json': {
- schema: resSchema
- }
- }
- }
- } : {
- '204': {
- description: 'OK (without any results)',
- }
- }),
- '400': {
- description: 'Client error',
- content: {
- 'application/json': {
- schema: {
- $ref: '#/components/schemas/Error'
- },
- examples: { ...errors, ...basicErrors['400'] }
- }
- }
- },
- '401': {
- description: 'Authentication error',
- content: {
- 'application/json': {
- schema: {
- $ref: '#/components/schemas/Error'
- },
- examples: basicErrors['401']
- }
- }
- },
- '403': {
- description: 'Forbiddon error',
- content: {
- 'application/json': {
- schema: {
- $ref: '#/components/schemas/Error'
- },
- examples: basicErrors['403']
- }
- }
- },
- '418': {
- description: 'I\'m Ai',
- content: {
- 'application/json': {
- schema: {
- $ref: '#/components/schemas/Error'
- },
- examples: basicErrors['418']
- }
- }
- },
- ...(endpoint.meta.limit ? {
- '429': {
- description: 'To many requests',
- content: {
- 'application/json': {
- schema: {
- $ref: '#/components/schemas/Error'
- },
- examples: basicErrors['429']
- }
- }
- }
- } : {}),
- '500': {
- description: 'Internal server error',
- content: {
- 'application/json': {
- schema: {
- $ref: '#/components/schemas/Error'
- },
- examples: basicErrors['500']
- }
- }
- },
- }
- };
-
- spec.paths['/' + endpoint.name] = {
- post: info
- };
- }
-
- return spec;
-}
diff --git a/src/server/api/openapi/description.ts b/src/server/api/openapi/description.ts
new file mode 100644
index 0000000000..e32edf7b22
--- /dev/null
+++ b/src/server/api/openapi/description.ts
@@ -0,0 +1,58 @@
+export const description = `
+## Usage
+APIはすべてPOSTでリクエスト/レスポンスともにJSON形式です。
+一部のAPIは認証情報(アクセストークン)が必要です。リクエストの際に\`i\`というパラメータでアクセストークンを添付してください。
+
+### アクセストークンを取得する
+#### 自分のアカウントのアクセストークンを取得する
+「設定 > API」で、自分のアクセストークンを取得できます。
+
+> アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。
+
+### アプリケーションとしてアクセストークンを取得する
+直接ユーザーのアクセストークンをアプリケーションが扱うのは危険なので、
+アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のアクセストークンをMisskeyに発行してもらいます。
+
+#### 1.アプリケーションを登録する
+まず、あなたのアプリケーションやWebサービス(以後、あなたのアプリと呼びます)をMisskeyに登録します。
+[デベロッパーセンター](/dev)にアクセスし、「アプリ > アプリ作成」からアプリを作成してください。
+フォームの記入欄の説明は以下の通りです:
+
+| 名前 | 説明 |
+|---|---|
+| アプリケーション名 | あなたのアプリの名称。 |
+| アプリの概要 | あなたのアプリの簡単な説明や紹介。 |
+| コールバックURL | ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。あなたのアプリがWebサービスである場合に有用です。 |
+| 権限 | あなたのアプリが要求する権限。ここで要求した機能だけがAPIからアクセスできます。 |
+
+登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。
+
+> アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。
+
+#### 2.ユーザーに認証させる
+アプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。
+
+認証セッションを開始するには、%API_URL%/auth/session/generate へパラメータに appSecret としてシークレットキーを含めたリクエストを送信します。
+リクエスト形式はJSONで、メソッドはPOSTです。
+レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。
+
+あなたのアプリがコールバックURLを設定している場合、
+ユーザーがあなたのアプリの連携を許可すると設定しているコールバックURLに token という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。
+
+あなたのアプリがコールバックURLを設定していない場合、ユーザーがあなたのアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。
+
+#### 3.ユーザートークンを取得する
+ユーザーが連携を許可したら、%API_URL%/auth/session/userkey へ次のパラメータを含むリクエストを送信します:
+
+| 名前 | 型 | 説明 |
+|---|---|---|
+| appSecret | string | アプリのシークレットキー |
+| token | string | セッションのトークン |
+
+上手くいけば、認証したユーザーのユーザートークンがレスポンスとして取得できます。おめでとうございます!
+
+ユーザートークンが取得できたら、「ユーザーのユーザートークン+あなたのアプリのシークレットキーをsha256したもの」をアクセストークンとして、APIにリクエストできます。
+
+アクセストークンの生成方法を擬似コードで表すと次のようになります:
+const i = sha256(userToken + secretKey);
+`;
diff --git a/src/server/api/openapi/errors.ts b/src/server/api/openapi/errors.ts
new file mode 100644
index 0000000000..43bcc323ba
--- /dev/null
+++ b/src/server/api/openapi/errors.ts
@@ -0,0 +1,69 @@
+
+export const errors = {
+ '400': {
+ 'INVALID_PARAM': {
+ value: {
+ error: {
+ message: 'Invalid param.',
+ code: 'INVALID_PARAM',
+ id: '3d81ceae-475f-4600-b2a8-2bc116157532',
+ }
+ }
+ }
+ },
+ '401': {
+ 'CREDENTIAL_REQUIRED': {
+ value: {
+ error: {
+ message: 'Credential required.',
+ code: 'CREDENTIAL_REQUIRED',
+ id: '1384574d-a912-4b81-8601-c7b1c4085df1',
+ }
+ }
+ }
+ },
+ '403': {
+ 'AUTHENTICATION_FAILED': {
+ value: {
+ error: {
+ message: 'Authentication failed. Please ensure your token is correct.',
+ code: 'AUTHENTICATION_FAILED',
+ id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
+ }
+ }
+ }
+ },
+ '418': {
+ 'I_AM_AI': {
+ value: {
+ error: {
+ message: 'You sent a request to Ai-chan, Misskey\'s showgirl, instead of the server.',
+ code: 'I_AM_AI',
+ id: '60c46cd1-f23a-46b1-bebe-5d2b73951a84',
+ }
+ }
+ }
+ },
+ '429': {
+ 'RATE_LIMIT_EXCEEDED': {
+ value: {
+ error: {
+ message: 'Rate limit exceeded. Please try again later.',
+ code: 'RATE_LIMIT_EXCEEDED',
+ id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
+ }
+ }
+ }
+ },
+ '500': {
+ 'INTERNAL_ERROR': {
+ value: {
+ error: {
+ message: 'Internal error occurred. Please contact us if the error persists.',
+ code: 'INTERNAL_ERROR',
+ id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac',
+ }
+ }
+ }
+ }
+};
diff --git a/src/server/api/openapi/gen-spec.ts b/src/server/api/openapi/gen-spec.ts
new file mode 100644
index 0000000000..ad46eb20a4
--- /dev/null
+++ b/src/server/api/openapi/gen-spec.ts
@@ -0,0 +1,255 @@
+import endpoints from '../endpoints';
+import { Context } from 'cafy';
+import config from '../../../config';
+import { errors as basicErrors } from './errors';
+import { schemas } from './schemas';
+import { description } from './description';
+
+export function genOpenapiSpec(lang = 'ja-JP') {
+ const spec = {
+ openapi: '3.0.0',
+
+ info: {
+ version: 'v1',
+ title: 'Misskey API',
+ description: '**Misskey is a decentralized microblogging platform.**\n\n' + description,
+ 'x-logo': { url: '/assets/api-doc.png' }
+ },
+
+ externalDocs: {
+ description: 'Repository',
+ url: 'https://github.com/syuilo/misskey'
+ },
+
+ servers: [{
+ url: config.api_url
+ }],
+
+ paths: {} as any,
+
+ components: {
+ schemas: schemas,
+
+ securitySchemes: {
+ ApiKeyAuth: {
+ type: 'apiKey',
+ in: 'body',
+ name: 'i'
+ }
+ }
+ }
+ };
+
+ function genProps(props: { [key: string]: Context & { desc: any, default: any }; }) {
+ const properties = {} as any;
+
+ const kvs = Object.entries(props);
+
+ for (const kv of kvs) {
+ properties[kv[0]] = genProp(kv[1], kv[1].desc, kv[1].default);
+ }
+
+ return properties;
+ }
+
+ function genProp(param: Context, desc?: string, _default?: any): any {
+ const required = param.name === 'Object' ? (param as any).props ? Object.entries((param as any).props).filter(([k, v]: any) => !v.isOptional).map(([k, v]) => k) : [] : [];
+ return {
+ description: desc,
+ default: _default,
+ ...(_default ? { default: _default } : {}),
+ type: param.name === 'ID' ? 'string' : param.name.toLowerCase(),
+ ...(param.name === 'ID' ? { example: 'xxxxxxxxxxxxxxxxxxxxxxxx', format: 'id' } : {}),
+ nullable: param.isNullable,
+ ...(param.name === 'String' ? {
+ ...((param as any).enum ? { enum: (param as any).enum } : {}),
+ ...((param as any).minLength ? { minLength: (param as any).minLength } : {}),
+ ...((param as any).maxLength ? { maxLength: (param as any).maxLength } : {}),
+ } : {}),
+ ...(param.name === 'Number' ? {
+ ...((param as any).minimum ? { minimum: (param as any).minimum } : {}),
+ ...((param as any).maximum ? { maximum: (param as any).maximum } : {}),
+ } : {}),
+ ...(param.name === 'Object' ? {
+ ...(required.length > 0 ? { required } : {}),
+ properties: (param as any).props ? genProps((param as any).props) : {}
+ } : {}),
+ ...(param.name === 'Array' ? {
+ items: (param as any).ctx ? genProp((param as any).ctx) : {}
+ } : {})
+ };
+ }
+
+ for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
+ const porops = {} as any;
+ const errors = {} as any;
+
+ if (endpoint.meta.errors) {
+ for (const e of Object.values(endpoint.meta.errors)) {
+ errors[e.code] = {
+ value: {
+ error: e
+ }
+ };
+ }
+ }
+
+ if (endpoint.meta.params) {
+ for (const kv of Object.entries(endpoint.meta.params)) {
+ if (kv[1].desc) (kv[1].validator as any).desc = kv[1].desc[lang];
+ if (kv[1].default) (kv[1].validator as any).default = kv[1].default;
+ porops[kv[0]] = kv[1].validator;
+ }
+ }
+
+ const required = endpoint.meta.params ? Object.entries(endpoint.meta.params).filter(([k, v]) => !v.validator.isOptional).map(([k, v]) => k) : [];
+
+ const resSchema = endpoint.meta.res ? renderType(endpoint.meta.res) : {};
+
+ function renderType(x: any) {
+ const res = {} as any;
+
+ if (['User', 'Note', 'DriveFile'].includes(x.type)) {
+ res['$ref'] = `#/components/schemas/${x.type}`;
+ } else if (x.type === 'object') {
+ res['type'] = 'object';
+ if (x.props) {
+ const props = {} as any;
+ for (const kv of Object.entries(x.props)) {
+ props[kv[0]] = renderType(kv[1]);
+ }
+ res['properties'] = props;
+ }
+ } else if (x.type === 'array') {
+ res['type'] = 'array';
+ if (x.items) {
+ res['items'] = renderType(x.items);
+ }
+ } else {
+ res['type'] = x.type;
+ }
+
+ return res;
+ }
+
+ const info = {
+ operationId: endpoint.name,
+ summary: endpoint.name,
+ description: endpoint.meta.desc ? endpoint.meta.desc[lang] : 'No description provided.',
+ externalDocs: {
+ description: 'Source code',
+ url: `https://github.com/syuilo/misskey/blob/develop/src/server/api/endpoints/${endpoint.name}.ts`
+ },
+ ...(endpoint.meta.tags ? {
+ tags: endpoint.meta.tags
+ } : {}),
+ ...(endpoint.meta.requireCredential ? {
+ security: [{
+ ApiKeyAuth: []
+ }]
+ } : {}),
+ requestBody: {
+ required: true,
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ ...(required.length > 0 ? { required } : {}),
+ properties: endpoint.meta.params ? genProps(porops) : {}
+ }
+ }
+ }
+ },
+ responses: {
+ ...(endpoint.meta.res ? {
+ '200': {
+ description: 'OK (with results)',
+ content: {
+ 'application/json': {
+ schema: resSchema
+ }
+ }
+ }
+ } : {
+ '204': {
+ description: 'OK (without any results)',
+ }
+ }),
+ '400': {
+ description: 'Client error',
+ content: {
+ 'application/json': {
+ schema: {
+ $ref: '#/components/schemas/Error'
+ },
+ examples: { ...errors, ...basicErrors['400'] }
+ }
+ }
+ },
+ '401': {
+ description: 'Authentication error',
+ content: {
+ 'application/json': {
+ schema: {
+ $ref: '#/components/schemas/Error'
+ },
+ examples: basicErrors['401']
+ }
+ }
+ },
+ '403': {
+ description: 'Forbiddon error',
+ content: {
+ 'application/json': {
+ schema: {
+ $ref: '#/components/schemas/Error'
+ },
+ examples: basicErrors['403']
+ }
+ }
+ },
+ '418': {
+ description: 'I\'m Ai',
+ content: {
+ 'application/json': {
+ schema: {
+ $ref: '#/components/schemas/Error'
+ },
+ examples: basicErrors['418']
+ }
+ }
+ },
+ ...(endpoint.meta.limit ? {
+ '429': {
+ description: 'To many requests',
+ content: {
+ 'application/json': {
+ schema: {
+ $ref: '#/components/schemas/Error'
+ },
+ examples: basicErrors['429']
+ }
+ }
+ }
+ } : {}),
+ '500': {
+ description: 'Internal server error',
+ content: {
+ 'application/json': {
+ schema: {
+ $ref: '#/components/schemas/Error'
+ },
+ examples: basicErrors['500']
+ }
+ }
+ },
+ }
+ };
+
+ spec.paths['/' + endpoint.name] = {
+ post: info
+ };
+ }
+
+ return spec;
+}
diff --git a/src/server/api/openapi/schemas.ts b/src/server/api/openapi/schemas.ts
new file mode 100644
index 0000000000..3de9e42e0c
--- /dev/null
+++ b/src/server/api/openapi/schemas.ts
@@ -0,0 +1,196 @@
+
+export const schemas = {
+ Error: {
+ type: 'object',
+ properties: {
+ error: {
+ type: 'object',
+ description: 'An error object.',
+ properties: {
+ code: {
+ type: 'string',
+ description: 'An error code.',
+ },
+ message: {
+ type: 'string',
+ description: 'An error message.',
+ },
+ id: {
+ type: 'string',
+ format: 'uuid',
+ description: 'An error ID. This ID is static.',
+ }
+ },
+ required: ['code', 'id', 'message']
+ },
+ },
+ required: ['error']
+ },
+
+ User: {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'id',
+ description: 'The unique identifier for this User.'
+ },
+ username: {
+ type: 'string',
+ description: 'The screen name, handle, or alias that this user identifies themselves with.',
+ example: 'ai'
+ },
+ name: {
+ type: 'string',
+ nullable: true,
+ description: 'The name of the user, as they’ve defined it.',
+ example: '藍'
+ },
+ host: {
+ type: 'string',
+ nullable: true,
+ example: 'misskey.example.com'
+ },
+ description: {
+ type: 'string',
+ nullable: true,
+ description: 'The user-defined UTF-8 string describing their account.',
+ example: 'Hi masters, I am Ai!'
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ description: 'The date that the user account was created on Misskey.'
+ },
+ followersCount: {
+ type: 'number',
+ description: 'The number of followers this account currently has.'
+ },
+ followingCount: {
+ type: 'number',
+ description: 'The number of users this account is following.'
+ },
+ notesCount: {
+ type: 'number',
+ description: 'The number of Notes (including renotes) issued by the user.'
+ },
+ isBot: {
+ type: 'boolean',
+ description: 'Whether this account is a bot.'
+ },
+ isCat: {
+ type: 'boolean',
+ description: 'Whether this account is a cat.'
+ },
+ isAdmin: {
+ type: 'boolean',
+ description: 'Whether this account is the admin.'
+ },
+ isVerified: {
+ type: 'boolean'
+ },
+ isLocked: {
+ type: 'boolean'
+ },
+ },
+ required: ['id', 'name', 'username', 'createdAt']
+ },
+
+ Note: {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'id',
+ description: 'The unique identifier for this Note.'
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ description: 'The date that the Note was created on Misskey.'
+ },
+ text: {
+ type: 'string'
+ },
+ cw: {
+ type: 'string'
+ },
+ userId: {
+ type: 'string',
+ format: 'id',
+ },
+ user: {
+ $ref: '#/components/schemas/User'
+ },
+ replyId: {
+ type: 'string',
+ format: 'id',
+ },
+ renoteId: {
+ type: 'string',
+ format: 'id',
+ },
+ reply: {
+ $ref: '#/components/schemas/Note'
+ },
+ renote: {
+ $ref: '#/components/schemas/Note'
+ },
+ viaMobile: {
+ type: 'boolean'
+ },
+ visibility: {
+ type: 'string'
+ },
+ },
+ required: ['id', 'userId', 'createdAt']
+ },
+
+ DriveFile: {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'id',
+ description: 'The unique identifier for this Drive file.'
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ description: 'The date that the Drive file was created on Misskey.'
+ },
+ name: {
+ type: 'string',
+ description: 'The file name with extension.',
+ example: 'lenna.jpg'
+ },
+ type: {
+ type: 'string',
+ description: 'The MIME type of this Drive file.',
+ example: 'image/jpeg'
+ },
+ md5: {
+ type: 'string',
+ format: 'md5',
+ description: 'The MD5 hash of this Drive file.',
+ example: '15eca7fba0480996e2245f5185bf39f2'
+ },
+ datasize: {
+ type: 'number',
+ description: 'The size of this Drive file. (bytes)',
+ example: 51469
+ },
+ folderId: {
+ type: 'string',
+ format: 'id',
+ nullable: true,
+ description: 'The parent folder ID of this Drive file.',
+ },
+ isSensitive: {
+ type: 'boolean',
+ description: 'Whether this Drive file is sensitive.',
+ },
+ },
+ required: ['id', 'createdAt', 'name', 'type', 'datasize', 'md5']
+ }
+};
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 0270dde612..61878d880b 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -21,7 +21,7 @@ import getNoteSummary from '../../misc/get-note-summary';
import fetchMeta from '../../misc/fetch-meta';
import Emoji from '../../models/emoji';
import * as pkg from '../../../package.json';
-import { genOpenapiSpec } from '../api/gen-openapi-spec';
+import { genOpenapiSpec } from '../api/openapi/gen-spec';
const client = `${__dirname}/../../client/`;