wip
This commit is contained in:
		
							parent
							
								
									f7ebf14501
								
							
						
					
					
						commit
						b96651a478
					
				
					 14 changed files with 676 additions and 170 deletions
				
			
		|  | @ -5,3 +5,6 @@ files: | |||
|   - source: /src/docs/ja-JP/*.md | ||||
|     translation: /src/docs/%locale%/%original_file_name% | ||||
|     update_option: update_as_unapproved | ||||
|   - source: /src/api-docs/ja-JP/**/*.yml | ||||
|     translation: /src/api-docs/%locale%/**/%original_file_name% | ||||
|     update_option: update_as_unapproved | ||||
|  |  | |||
|  | @ -60,7 +60,14 @@ gulp.task('build:client:style', () => { | |||
| 		.pipe(gulp.dest('./built/server/web/')); | ||||
| }); | ||||
| 
 | ||||
| gulp.task('build:copy', gulp.parallel('build:copy:locales', 'build:copy:views', 'build:client:script', 'build:client:style', 'build:copy:fonts', () => | ||||
| gulp.task('copy:api-docs', () => | ||||
| 		gulp.src([ | ||||
| 			'./src/api-docs/**/*', | ||||
| 		]) | ||||
| 		.pipe(gulp.dest('./built/api-docs/')) | ||||
| ); | ||||
| 
 | ||||
| gulp.task('build:copy', gulp.parallel('build:copy:locales', 'copy:api-docs', 'build:copy:views', 'build:client:script', 'build:client:style', 'build:copy:fonts', () => | ||||
| 	gulp.src([ | ||||
| 		'./src/emojilist.json', | ||||
| 		'./src/server/web/views/**/*', | ||||
|  |  | |||
							
								
								
									
										10
									
								
								src/api-docs/ja-JP/meta.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/api-docs/ja-JP/meta.yml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| description: "インスタンスのメタ情報を取得します。" | ||||
| 
 | ||||
| params: | ||||
|   detail: "追加情報を含めるか否か" | ||||
| 
 | ||||
| res: | ||||
|   version: "Misskeyのバージョン" | ||||
|   announcements: "お知らせ" | ||||
|   announcements.title: "タイトル" | ||||
|   announcements.text: "本文" | ||||
							
								
								
									
										7
									
								
								src/api-docs/ja-JP/notes/create.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/api-docs/ja-JP/notes/create.yml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| description: "ノートを作成します。" | ||||
| 
 | ||||
| params: | ||||
|   visibility: "ノートの公開範囲" | ||||
| 
 | ||||
| res: | ||||
|   createdNote: "作成したノート" | ||||
							
								
								
									
										201
									
								
								src/client/pages/api-docs/endpoint.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								src/client/pages/api-docs/endpoint.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,201 @@ | |||
| <template> | ||||
| <div class="rfbvytqb" v-size="{ max: [500] }"> | ||||
| 	<div class="title">{{ endpoint }}</div> | ||||
| 	<div class="body" v-if="ep"> | ||||
| 		<div class="url _code">POST {{ apiUrl }}/{{ endpoint }}</div> | ||||
| 		<section class="description">{{ ep.spec.description }}</section> | ||||
| 		<MkA to="/api-console" :behavior="'window'">API console</MkA> | ||||
| 		<section class="params"> | ||||
| 			<h2>Params</h2> | ||||
| 			<XValue :value="ep.spec.requestBody.content['application/json'].schema" :schemas="ep.schemas"/> | ||||
| 		</section> | ||||
| 		<section class="res"> | ||||
| 			<h2>Response</h2> | ||||
| 			<section v-for="status in Object.keys(ep.spec.responses)" :key="status"> | ||||
| 				<h3>{{ status }}</h3> | ||||
| 				<XValue :value="ep.spec.responses[status].content['application/json'].schema" :schemas="ep.schemas"/> | ||||
| 			</section> | ||||
| 		</section> | ||||
| 		<section class="raw"> | ||||
| 			<h2>Raw spec info</h2> | ||||
| 			<details> | ||||
| 				<summary>Show</summary> | ||||
| 				<pre class="_code">{{ JSON.stringify(ep.spec, null, '\t') }}</pre> | ||||
| 			</details> | ||||
| 		</section> | ||||
| 	</div> | ||||
| 	<div class="footer"> | ||||
| 		<MkLink :url="`https://github.com/syuilo/misskey/blob/master/src/docs/${lang}/${doc}.md`" class="at">{{ $ts.docSource }}</MkLink> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons' | ||||
| import { url, lang, apiUrl } from '@/config'; | ||||
| import MkLink from '@/components/link.vue'; | ||||
| import XValue from './value.vue'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkLink, | ||||
| 		XValue, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		endpoint: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			INFO: computed(() => this.ep ? { | ||||
| 				title: this.endpoint, | ||||
| 				icon: faQuestionCircle, | ||||
| 			} : null), | ||||
| 			ep: null, | ||||
| 			lang, | ||||
| 			apiUrl, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		endpoint: { | ||||
| 			handler() { | ||||
| 				this.fetchDoc(); | ||||
| 			}, | ||||
| 			immediate: true, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		fetchDoc() { | ||||
| 			os.api('endpoint', { | ||||
| 				endpoint: this.endpoint, | ||||
| 				lang: lang | ||||
| 			}).then(ep => { | ||||
| 				this.ep = ep; | ||||
| 			}); | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .rfbvytqb { | ||||
| 	padding: 32px; | ||||
| 
 | ||||
| 	&.max-width_500px { | ||||
| 		padding: 16px; | ||||
| 	} | ||||
| 
 | ||||
| 	> .title { | ||||
| 		font-size: 1.5em; | ||||
| 		font-weight: bold; | ||||
| 		padding: 0 0 0.75em 0; | ||||
| 		margin: 0 0 1em 0; | ||||
| 		border-bottom: solid 2px var(--divider); | ||||
| 	} | ||||
| 
 | ||||
| 	> .body { | ||||
| 		> *:first-child { | ||||
| 			margin-top: 0; | ||||
| 		} | ||||
| 
 | ||||
| 		> *:last-child { | ||||
| 			margin-bottom: 0; | ||||
| 		} | ||||
| 
 | ||||
| 		> .url { | ||||
| 			padding: 6px 12px; | ||||
| 			border-radius: 4px; | ||||
| 			margin-bottom: 16px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .raw { | ||||
| 			> details { | ||||
| 				> pre { | ||||
| 					overflow: auto; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		::v-deep(a) { | ||||
| 			color: var(--link); | ||||
| 		} | ||||
| 
 | ||||
| 		::v-deep(blockquote) { | ||||
| 			display: block; | ||||
| 			margin: 8px; | ||||
| 			padding: 6px 0 6px 12px; | ||||
| 			color: var(--fg); | ||||
| 			border-left: solid 3px var(--fg); | ||||
| 			opacity: 0.7; | ||||
| 
 | ||||
| 			p { | ||||
| 				margin: 0; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		::v-deep(h2) { | ||||
| 			font-size: 1.25em; | ||||
| 			padding: 0 0 0.5em 0; | ||||
| 			margin: 1.5em 0 1em 0; | ||||
| 			border-bottom: solid 1px var(--divider); | ||||
| 		} | ||||
| 
 | ||||
| 		::v-deep(table) { | ||||
| 			width: 100%; | ||||
| 			max-width: 100%; | ||||
| 			overflow: auto; | ||||
| 		} | ||||
| 
 | ||||
| 		::v-deep(kbd.group) { | ||||
| 			display: inline-block; | ||||
| 			padding: 2px; | ||||
| 			border: 1px solid var(--divider); | ||||
| 			border-radius: 4px; | ||||
| 			box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); | ||||
| 		} | ||||
| 
 | ||||
| 		::v-deep(kbd.key) { | ||||
| 			display: inline-block; | ||||
| 			padding: 6px 8px; | ||||
| 			border: solid 1px var(--divider); | ||||
| 			border-radius: 4px; | ||||
| 			box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); | ||||
| 		} | ||||
| 
 | ||||
| 		::v-deep(code) { | ||||
| 			display: inline-block; | ||||
| 			font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; | ||||
| 			tab-size: 2; | ||||
| 			background: #272822; | ||||
| 			color: #f8f8f2; | ||||
| 			border-radius: 6px; | ||||
| 			padding: 4px 6px; | ||||
| 		} | ||||
| 
 | ||||
| 		::v-deep(pre) { | ||||
| 			background: #272822; | ||||
| 			color: #f8f8f2; | ||||
| 			border-radius: 6px; | ||||
| 			padding: 12px 16px; | ||||
| 
 | ||||
| 			> code { | ||||
| 				padding: 0; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .footer { | ||||
| 		padding: 1.5em 0 0 0; | ||||
| 		margin: 1.5em 0 0 0; | ||||
| 		border-top: solid 2px var(--divider); | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										39
									
								
								src/client/pages/api-docs/index.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/client/pages/api-docs/index.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<main class="_section"> | ||||
| 		<div class="_content"> | ||||
| 			<ul> | ||||
| 				<li v-for="endpoint in endpoints" :key="endpoint"> | ||||
| 					<MkA :to="`/api-docs/endpoints/${endpoint}`">{{ endpoint }}</MkA> | ||||
| 				</li> | ||||
| 			</ul> | ||||
| 		</div> | ||||
| 	</main> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons' | ||||
| import { url, lang } from '@/config'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	data() { | ||||
| 		return { | ||||
| 			INFO: { | ||||
| 				title: 'Misskey API', | ||||
| 				icon: faQuestionCircle | ||||
| 			}, | ||||
| 			endpoints: [], | ||||
| 			faQuestionCircle | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		os.api('endpoints').then(endpoints => { | ||||
| 			this.endpoints = endpoints; | ||||
| 		}); | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
							
								
								
									
										46
									
								
								src/client/pages/api-docs/value.array.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/client/pages/api-docs/value.array.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | |||
| <template> | ||||
| <div class=""> | ||||
| 	Array of | ||||
| 	<div class=""> | ||||
| 		<XValue class="kv _vMargin _shadow" :value="array" :schemas="schemas"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import XValue from './value.vue'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	name: 'XArray', | ||||
| 
 | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		XValue, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		array: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		schemas: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| 
 | ||||
| </style> | ||||
							
								
								
									
										59
									
								
								src/client/pages/api-docs/value.object.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/client/pages/api-docs/value.object.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,59 @@ | |||
| <template> | ||||
| <div class="jhpkzgfz"> | ||||
| 	<div class="kvs"> | ||||
| 		<XValue v-for="kv in kvs" :key="kv[0]" class="kv _vMargin _shadow" :name="kv[0]" :value="kv[1]" :schemas="schemas"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import XValue from './value.vue'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	name: 'XObject', | ||||
| 
 | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		XValue, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		obj: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		schemas: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			kvs: Object.entries(this.obj) | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .jhpkzgfz { | ||||
| 	> .kvs { | ||||
| 
 | ||||
| 		> .kv { | ||||
| 			::v-deep(.k) { | ||||
| 				font-weight: bold; | ||||
| 			} | ||||
| 
 | ||||
| 			::v-deep(.v) { | ||||
| 				padding: 16px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										106
									
								
								src/client/pages/api-docs/value.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/client/pages/api-docs/value.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,106 @@ | |||
| <template> | ||||
| <MkContainer :body-togglable="true" class="ezkosiua"> | ||||
| 	<template #header> | ||||
| 		<div class="header _monospace"> | ||||
| 			<span class="name">{{ name }}</span> | ||||
| 			<span class="type">{{ type }}</span> | ||||
| 		</div> | ||||
| 	</template> | ||||
| 	<div class="body"> | ||||
| 		<div class="description">{{ value.description }}</div> | ||||
| 
 | ||||
| 		<div v-if="value.$ref" class="ref"> | ||||
| 			<button class="_textButton" @click="resolveRef = true"> | ||||
| 				{{ value.$ref.replace('#/components/schemas/', '') }} | ||||
| 			</button> | ||||
| 			<div v-if="resolveRef"> | ||||
| 				<XValue :value="schemas[value.$ref.replace('#/components/schemas/', '')]" :schemas="schemas"/> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div v-else-if="value.type === 'object'"> | ||||
| 			<XObject :obj="value.properties || {}" :schemas="schemas"/> | ||||
| 		</div> | ||||
| 		<div v-else-if="value.type === 'array'"> | ||||
| 			<XArray :array="value.items" :schemas="schemas"/> | ||||
| 		</div> | ||||
| 		<div v-else-if="value.type === 'string'"> | ||||
| 		</div> | ||||
| 		<div v-else> | ||||
| 			unknown | ||||
| 		</div> | ||||
| 	</div> | ||||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Button from '@/components/ui/button.vue'; | ||||
| import { defineComponent, defineAsyncComponent } from 'vue'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| 
 | ||||
| function getType(value) { | ||||
| 	let t = value.type === 'array' ? `${getType(value.items)}[]` : value.type; | ||||
| 	if (value.nullable) t = `(${t} | null)`; | ||||
| 	return t; | ||||
| } | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	name: 'XValue', | ||||
| 
 | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 		XObject: defineAsyncComponent(() => import('./value.object.vue')), | ||||
| 		XArray: defineAsyncComponent(() => import('./value.array.vue')), | ||||
| 		Button, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		value: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		schemas: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		name: { | ||||
| 			type: String, | ||||
| 			required: false | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			resolveRef: false, | ||||
| 			type: getType(this.value) | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .ezkosiua { | ||||
| 	::v-deep(.header) { | ||||
| 		> .name { | ||||
| 			font-weight: bold; | ||||
| 			margin-right: 1em; | ||||
| 
 | ||||
| 			&:empty { | ||||
| 				display: none; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		> .type { | ||||
| 			border: solid 1px var(--divider); | ||||
| 			border-radius: 4px; | ||||
| 			padding: 3px 6px; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	::v-deep(.body) { | ||||
| 		padding: 16px; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -7,6 +7,11 @@ | |||
| 					<MkA :to="`/docs/${doc.path}`">{{ doc.title }}</MkA> | ||||
| 				</li> | ||||
| 			</ul> | ||||
| 			<ul> | ||||
| 				<li> | ||||
| 					<MkA :to="`/api-docs`">API reference</MkA> | ||||
| 				</li> | ||||
| 			</ul> | ||||
| 		</div> | ||||
| 	</main> | ||||
| </div> | ||||
|  |  | |||
|  | @ -28,8 +28,10 @@ export const router = createRouter({ | |||
| 		{ path: '/about-misskey', component: page('about-misskey') }, | ||||
| 		{ path: '/featured', component: page('featured') }, | ||||
| 		{ path: '/docs', component: page('docs') }, | ||||
| 		{ path: '/theme-editor', component: page('theme-editor') }, | ||||
| 		{ path: '/docs/:doc', component: page('doc'), props: route => ({ doc: route.params.doc }) }, | ||||
| 		{ path: '/api-docs', component: page('api-docs/index') }, | ||||
| 		{ path: '/api-docs/endpoints/:endpoint(.*)', component: page('api-docs/endpoint'), props: route => ({ endpoint: route.params.endpoint }) }, | ||||
| 		{ path: '/theme-editor', component: page('theme-editor') }, | ||||
| 		{ path: '/explore', component: page('explore') }, | ||||
| 		{ path: '/explore/tags/:tag', props: true, component: page('explore') }, | ||||
| 		{ path: '/search', component: page('search') }, | ||||
|  |  | |||
|  | @ -473,6 +473,7 @@ hr { | |||
| 	color: #ccc; | ||||
| 	font-size: 14px; | ||||
| 	line-height: 1.5; | ||||
| 	tab-size: 2; | ||||
| 	padding: 5px; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| import $ from 'cafy'; | ||||
| import define from '../define'; | ||||
| import endpoints from '../endpoints'; | ||||
| import { genOpenapiSpecForEndpoint } from '../openapi/gen-spec'; | ||||
| import { schemas } from '../openapi/schemas'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	requireCredential: false as const, | ||||
|  | @ -9,18 +11,28 @@ export const meta = { | |||
| 
 | ||||
| 	params: { | ||||
| 		endpoint: { | ||||
| 			// TODO: セキュリティリスクになりうるためバリデーションしたい
 | ||||
| 			validator: $.str, | ||||
| 		}, | ||||
| 		lang: { | ||||
| 			// TODO: セキュリティリスクになりうるためバリデーションしたい
 | ||||
| 			validator: $.str, | ||||
| 			default: 'ja-JP' | ||||
| 		} | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps) => { | ||||
| 	if (ps.endpoint.includes('.')) return null; | ||||
| 	if (ps.lang.includes('.')) return null; | ||||
| 	const ep = endpoints.find(x => x.name === ps.endpoint); | ||||
| 	if (ep == null) return null; | ||||
| 	return { | ||||
| 		params: Object.entries(ep.meta.params || {}).map(([k, v]) => ({ | ||||
| 			name: k, | ||||
| 			type: v.validator.name === 'ID' ? 'String' : v.validator.name | ||||
| 		})) | ||||
| 		})), | ||||
| 		schemas: schemas, | ||||
| 		spec: genOpenapiSpecForEndpoint(ep, ps.lang) | ||||
| 	}; | ||||
| }); | ||||
|  |  | |||
|  | @ -1,44 +1,14 @@ | |||
| import endpoints from '../endpoints'; | ||||
| import endpoints, { IEndpoint } from '../endpoints'; | ||||
| import { Context } from 'cafy'; | ||||
| import * as yaml from 'js-yaml'; | ||||
| import * as fs from 'fs'; | ||||
| import config from '../../../config'; | ||||
| import { errors as basicErrors } from './errors'; | ||||
| import { schemas, convertSchemaToOpenApiSchema } from './schemas'; | ||||
| import { getDescription } from './description'; | ||||
| 
 | ||||
| export function genOpenapiSpec(lang = 'ja-JP') { | ||||
| 	const spec = { | ||||
| 		openapi: '3.0.0', | ||||
| 
 | ||||
| 		info: { | ||||
| 			version: 'v1', | ||||
| 			title: 'Misskey API', | ||||
| 			description: getDescription(lang), | ||||
| 			'x-logo': { url: '/assets/api-doc.png' } | ||||
| 		}, | ||||
| 
 | ||||
| 		externalDocs: { | ||||
| 			description: 'Repository', | ||||
| 			url: 'https://github.com/syuilo/misskey' | ||||
| 		}, | ||||
| 
 | ||||
| 		servers: [{ | ||||
| 			url: config.apiUrl | ||||
| 		}], | ||||
| 
 | ||||
| 		paths: {} as any, | ||||
| 
 | ||||
| 		components: { | ||||
| 			schemas: schemas, | ||||
| 
 | ||||
| 			securitySchemes: { | ||||
| 				ApiKeyAuth: { | ||||
| 					type: 'apiKey', | ||||
| 					in: 'body', | ||||
| 					name: 'i' | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| export function genOpenapiSpecForEndpoint(endpoint: IEndpoint, lang = 'ja-JP') { | ||||
| 	const locale = yaml.safeLoad(fs.readFileSync(__dirname + `/../../../api-docs/${lang}/` + endpoint.name + '.yml', 'utf-8')); | ||||
| 
 | ||||
| 	function genProps(props: { [key: string]: Context; }) { | ||||
| 		const properties = {} as any; | ||||
|  | @ -79,157 +49,195 @@ export function genOpenapiSpec(lang = 'ja-JP') { | |||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { | ||||
| 		const porops = {} as any; | ||||
| 		const errors = {} as any; | ||||
| 	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.errors) { | ||||
| 		for (const e of Object.values(endpoint.meta.errors)) { | ||||
| 			errors[e.code] = { | ||||
| 				value: { | ||||
| 					error: e | ||||
| 				} | ||||
| 			}; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if (endpoint.meta.params) { | ||||
| 		for (const [k, v] of Object.entries(endpoint.meta.params)) { | ||||
| 			if (v.validator.data == null) v.validator.data = {}; | ||||
| 			v.validator.data.desc = locale.params[k]; | ||||
| 			if (v.deprecated) v.validator.data.deprecated = v.deprecated; | ||||
| 			if (v.default) v.validator.data.default = v.default; | ||||
| 			porops[k] = v.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 ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; | ||||
| 
 | ||||
| 	let desc = (locale.description || 'No description provided.') + '\n\n'; | ||||
| 	desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`; | ||||
| 	if (endpoint.meta.kind) { | ||||
| 		const kind = endpoint.meta.kind; | ||||
| 		desc += ` / **Permission**: *${kind}*`; | ||||
| 	} | ||||
| 
 | ||||
| 	const info = { | ||||
| 		operationId: endpoint.name, | ||||
| 		summary: endpoint.name, | ||||
| 		description: desc, | ||||
| 		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[0]] | ||||
| 		} : {}), | ||||
| 		...(endpoint.meta.requireCredential ? { | ||||
| 			security: [{ | ||||
| 				ApiKeyAuth: [] | ||||
| 			}] | ||||
| 		} : {}), | ||||
| 		requestBody: { | ||||
| 			required: true, | ||||
| 			content: { | ||||
| 				'application/json': { | ||||
| 					schema: { | ||||
| 						type: 'object', | ||||
| 						...(required.length > 0 ? { required } : {}), | ||||
| 						properties: endpoint.meta.params ? genProps(porops) : {} | ||||
| 					} | ||||
| 				}; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if (endpoint.meta.params) { | ||||
| 			for (const [k, v] of Object.entries(endpoint.meta.params)) { | ||||
| 				if (v.validator.data == null) v.validator.data = {}; | ||||
| 				if (v.desc) v.validator.data.desc = v.desc[lang]; | ||||
| 				if (v.deprecated) v.validator.data.deprecated = v.deprecated; | ||||
| 				if (v.default) v.validator.data.default = v.default; | ||||
| 				porops[k] = v.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 ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; | ||||
| 
 | ||||
| 		let desc = (endpoint.meta.desc ? endpoint.meta.desc[lang] : 'No description provided.') + '\n\n'; | ||||
| 		desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`; | ||||
| 		if (endpoint.meta.kind) { | ||||
| 			const kind = endpoint.meta.kind; | ||||
| 			desc += ` / **Permission**: *${kind}*`; | ||||
| 		} | ||||
| 
 | ||||
| 		const info = { | ||||
| 			operationId: endpoint.name, | ||||
| 			summary: endpoint.name, | ||||
| 			description: desc, | ||||
| 			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[0]] | ||||
| 			} : {}), | ||||
| 			...(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'] } | ||||
| 					} | ||||
| 				} | ||||
| 			}, | ||||
| 			responses: { | ||||
| 				...(endpoint.meta.res ? { | ||||
| 					'200': { | ||||
| 						description: 'OK (with results)', | ||||
| 						content: { | ||||
| 							'application/json': { | ||||
| 								schema: resSchema | ||||
| 							} | ||||
| 						} | ||||
| 			'401': { | ||||
| 				description: 'Authentication error', | ||||
| 				content: { | ||||
| 					'application/json': { | ||||
| 						schema: { | ||||
| 							$ref: '#/components/schemas/Error' | ||||
| 						}, | ||||
| 						examples: basicErrors['401'] | ||||
| 					} | ||||
| 				} : { | ||||
| 					'204': { | ||||
| 						description: 'OK (without any results)', | ||||
| 				} | ||||
| 			}, | ||||
| 			'403': { | ||||
| 				description: 'Forbiddon error', | ||||
| 				content: { | ||||
| 					'application/json': { | ||||
| 						schema: { | ||||
| 							$ref: '#/components/schemas/Error' | ||||
| 						}, | ||||
| 						examples: basicErrors['403'] | ||||
| 					} | ||||
| 				}), | ||||
| 				'400': { | ||||
| 					description: 'Client error', | ||||
| 				} | ||||
| 			}, | ||||
| 			'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: { ...errors, ...basicErrors['400'] } | ||||
| 							examples: basicErrors['429'] | ||||
| 						} | ||||
| 					} | ||||
| 				}, | ||||
| 				'401': { | ||||
| 					description: 'Authentication error', | ||||
| 					content: { | ||||
| 						'application/json': { | ||||
| 							schema: { | ||||
| 								$ref: '#/components/schemas/Error' | ||||
| 							}, | ||||
| 							examples: basicErrors['401'] | ||||
| 						} | ||||
| 				} | ||||
| 			} : {}), | ||||
| 			'500': { | ||||
| 				description: 'Internal server error', | ||||
| 				content: { | ||||
| 					'application/json': { | ||||
| 						schema: { | ||||
| 							$ref: '#/components/schemas/Error' | ||||
| 						}, | ||||
| 						examples: basicErrors['500'] | ||||
| 					} | ||||
| 				}, | ||||
| 				'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'] | ||||
| 						} | ||||
| 					} | ||||
| 				}, | ||||
| 			} | ||||
| 		}; | ||||
| 				} | ||||
| 			}, | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	return info; | ||||
| } | ||||
| 
 | ||||
| export function genOpenapiSpec(lang = 'ja-JP') { | ||||
| 	const spec = { | ||||
| 		openapi: '3.0.0', | ||||
| 
 | ||||
| 		info: { | ||||
| 			version: 'v1', | ||||
| 			title: 'Misskey API', | ||||
| 			description: getDescription(lang), | ||||
| 			'x-logo': { url: '/assets/api-doc.png' } | ||||
| 		}, | ||||
| 
 | ||||
| 		externalDocs: { | ||||
| 			description: 'Repository', | ||||
| 			url: 'https://github.com/syuilo/misskey' | ||||
| 		}, | ||||
| 
 | ||||
| 		servers: [{ | ||||
| 			url: config.apiUrl | ||||
| 		}], | ||||
| 
 | ||||
| 		paths: {} as any, | ||||
| 
 | ||||
| 		components: { | ||||
| 			schemas: schemas, | ||||
| 
 | ||||
| 			securitySchemes: { | ||||
| 				ApiKeyAuth: { | ||||
| 					type: 'apiKey', | ||||
| 					in: 'body', | ||||
| 					name: 'i' | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) { | ||||
| 		spec.paths['/' + endpoint.name] = { | ||||
| 			post: info | ||||
| 			post: genOpenapiSpecForEndpoint(endpoint, lang) | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue