Refator: separate files
This commit is contained in:
		
							parent
							
								
									0e0c35a701
								
							
						
					
					
						commit
						3446969121
					
				
					 6 changed files with 579 additions and 577 deletions
				
			
		|  | @ -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からアクセスできます。 | | ||||
| 
 | ||||
| 登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。 | ||||
| 
 | ||||
| > アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。</p> | ||||
| 
 | ||||
| #### 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にリクエストできます。 | ||||
| 
 | ||||
| アクセストークンの生成方法を擬似コードで表すと次のようになります: | ||||
| <pre><code>const i = sha256(userToken + secretKey);</code></pre> | ||||
| `;
 | ||||
| 
 | ||||
| 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; | ||||
| } | ||||
							
								
								
									
										58
									
								
								src/server/api/openapi/description.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/server/api/openapi/description.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,58 @@ | |||
| export const description = ` | ||||
| ## Usage | ||||
| APIはすべてPOSTでリクエスト/レスポンスともにJSON形式です。 | ||||
| 一部のAPIは認証情報(アクセストークン)が必要です。リクエストの際に\`i\`というパラメータでアクセストークンを添付してください。
 | ||||
| 
 | ||||
| ### アクセストークンを取得する | ||||
| #### 自分のアカウントのアクセストークンを取得する | ||||
| 「設定 > API」で、自分のアクセストークンを取得できます。 | ||||
| 
 | ||||
| > アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。 | ||||
| 
 | ||||
| ### アプリケーションとしてアクセストークンを取得する | ||||
| 直接ユーザーのアクセストークンをアプリケーションが扱うのは危険なので、 | ||||
| アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のアクセストークンをMisskeyに発行してもらいます。 | ||||
| 
 | ||||
| #### 1.アプリケーションを登録する | ||||
| まず、あなたのアプリケーションやWebサービス(以後、あなたのアプリと呼びます)をMisskeyに登録します。 | ||||
| [デベロッパーセンター](/dev)にアクセスし、「アプリ > アプリ作成」からアプリを作成してください。 | ||||
| フォームの記入欄の説明は以下の通りです: | ||||
| 
 | ||||
| | 名前 | 説明 | | ||||
| |---|---| | ||||
| | アプリケーション名 | あなたのアプリの名称。 | | ||||
| | アプリの概要 | あなたのアプリの簡単な説明や紹介。 | | ||||
| | コールバックURL | ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。あなたのアプリがWebサービスである場合に有用です。 | | ||||
| | 権限 | あなたのアプリが要求する権限。ここで要求した機能だけがAPIからアクセスできます。 | | ||||
| 
 | ||||
| 登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。 | ||||
| 
 | ||||
| > アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。</p> | ||||
| 
 | ||||
| #### 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にリクエストできます。 | ||||
| 
 | ||||
| アクセストークンの生成方法を擬似コードで表すと次のようになります: | ||||
| <pre><code>const i = sha256(userToken + secretKey);</code></pre> | ||||
| `;
 | ||||
							
								
								
									
										69
									
								
								src/server/api/openapi/errors.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/server/api/openapi/errors.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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', | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										255
									
								
								src/server/api/openapi/gen-spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								src/server/api/openapi/gen-spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
| } | ||||
							
								
								
									
										196
									
								
								src/server/api/openapi/schemas.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								src/server/api/openapi/schemas.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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'] | ||||
| 	} | ||||
| }; | ||||
|  | @ -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/`; | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue