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 |   - source: /src/docs/ja-JP/*.md | ||||||
|     translation: /src/docs/%locale%/%original_file_name% |     translation: /src/docs/%locale%/%original_file_name% | ||||||
|     update_option: update_as_unapproved |     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/')); | 		.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([ | 	gulp.src([ | ||||||
| 		'./src/emojilist.json', | 		'./src/emojilist.json', | ||||||
| 		'./src/server/web/views/**/*', | 		'./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> | 					<MkA :to="`/docs/${doc.path}`">{{ doc.title }}</MkA> | ||||||
| 				</li> | 				</li> | ||||||
| 			</ul> | 			</ul> | ||||||
|  | 			<ul> | ||||||
|  | 				<li> | ||||||
|  | 					<MkA :to="`/api-docs`">API reference</MkA> | ||||||
|  | 				</li> | ||||||
|  | 			</ul> | ||||||
| 		</div> | 		</div> | ||||||
| 	</main> | 	</main> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | @ -28,8 +28,10 @@ export const router = createRouter({ | ||||||
| 		{ path: '/about-misskey', component: page('about-misskey') }, | 		{ path: '/about-misskey', component: page('about-misskey') }, | ||||||
| 		{ path: '/featured', component: page('featured') }, | 		{ path: '/featured', component: page('featured') }, | ||||||
| 		{ path: '/docs', component: page('docs') }, | 		{ 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: '/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', component: page('explore') }, | ||||||
| 		{ path: '/explore/tags/:tag', props: true, component: page('explore') }, | 		{ path: '/explore/tags/:tag', props: true, component: page('explore') }, | ||||||
| 		{ path: '/search', component: page('search') }, | 		{ path: '/search', component: page('search') }, | ||||||
|  |  | ||||||
|  | @ -473,6 +473,7 @@ hr { | ||||||
| 	color: #ccc; | 	color: #ccc; | ||||||
| 	font-size: 14px; | 	font-size: 14px; | ||||||
| 	line-height: 1.5; | 	line-height: 1.5; | ||||||
|  | 	tab-size: 2; | ||||||
| 	padding: 5px; | 	padding: 5px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| import $ from 'cafy'; | import $ from 'cafy'; | ||||||
| import define from '../define'; | import define from '../define'; | ||||||
| import endpoints from '../endpoints'; | import endpoints from '../endpoints'; | ||||||
|  | import { genOpenapiSpecForEndpoint } from '../openapi/gen-spec'; | ||||||
|  | import { schemas } from '../openapi/schemas'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	requireCredential: false as const, | 	requireCredential: false as const, | ||||||
|  | @ -9,18 +11,28 @@ export const meta = { | ||||||
| 
 | 
 | ||||||
| 	params: { | 	params: { | ||||||
| 		endpoint: { | 		endpoint: { | ||||||
|  | 			// TODO: セキュリティリスクになりうるためバリデーションしたい
 | ||||||
| 			validator: $.str, | 			validator: $.str, | ||||||
|  | 		}, | ||||||
|  | 		lang: { | ||||||
|  | 			// TODO: セキュリティリスクになりうるためバリデーションしたい
 | ||||||
|  | 			validator: $.str, | ||||||
|  | 			default: 'ja-JP' | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default define(meta, async (ps) => { | 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); | 	const ep = endpoints.find(x => x.name === ps.endpoint); | ||||||
| 	if (ep == null) return null; | 	if (ep == null) return null; | ||||||
| 	return { | 	return { | ||||||
| 		params: Object.entries(ep.meta.params || {}).map(([k, v]) => ({ | 		params: Object.entries(ep.meta.params || {}).map(([k, v]) => ({ | ||||||
| 			name: k, | 			name: k, | ||||||
| 			type: v.validator.name === 'ID' ? 'String' : v.validator.name | 			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 { Context } from 'cafy'; | ||||||
|  | import * as yaml from 'js-yaml'; | ||||||
|  | import * as fs from 'fs'; | ||||||
| import config from '../../../config'; | import config from '../../../config'; | ||||||
| import { errors as basicErrors } from './errors'; | import { errors as basicErrors } from './errors'; | ||||||
| import { schemas, convertSchemaToOpenApiSchema } from './schemas'; | import { schemas, convertSchemaToOpenApiSchema } from './schemas'; | ||||||
| import { getDescription } from './description'; | import { getDescription } from './description'; | ||||||
| 
 | 
 | ||||||
| export function genOpenapiSpec(lang = 'ja-JP') { | export function genOpenapiSpecForEndpoint(endpoint: IEndpoint, lang = 'ja-JP') { | ||||||
| 	const spec = { | 	const locale = yaml.safeLoad(fs.readFileSync(__dirname + `/../../../api-docs/${lang}/` + endpoint.name + '.yml', 'utf-8')); | ||||||
| 		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' |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}; |  | ||||||
| 
 | 
 | ||||||
| 	function genProps(props: { [key: string]: Context; }) { | 	function genProps(props: { [key: string]: Context; }) { | ||||||
| 		const properties = {} as any; | 		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 porops = {} as any; | 	const errors = {} as any; | ||||||
| 		const errors = {} as any; |  | ||||||
| 
 | 
 | ||||||
| 		if (endpoint.meta.errors) { | 	if (endpoint.meta.errors) { | ||||||
| 			for (const e of Object.values(endpoint.meta.errors)) { | 		for (const e of Object.values(endpoint.meta.errors)) { | ||||||
| 				errors[e.code] = { | 			errors[e.code] = { | ||||||
| 					value: { | 				value: { | ||||||
| 						error: e | 					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) : {} | ||||||
| 					} | 					} | ||||||
| 				}; | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		}, | ||||||
| 
 | 		responses: { | ||||||
| 		if (endpoint.meta.params) { | 			...(endpoint.meta.res ? { | ||||||
| 			for (const [k, v] of Object.entries(endpoint.meta.params)) { | 				'200': { | ||||||
| 				if (v.validator.data == null) v.validator.data = {}; | 					description: 'OK (with results)', | ||||||
| 				if (v.desc) v.validator.data.desc = v.desc[lang]; | 					content: { | ||||||
| 				if (v.deprecated) v.validator.data.deprecated = v.deprecated; | 						'application/json': { | ||||||
| 				if (v.default) v.validator.data.default = v.default; | 							schema: resSchema | ||||||
| 				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) : {} |  | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
|  | 			} : { | ||||||
|  | 				'204': { | ||||||
|  | 					description: 'OK (without any results)', | ||||||
|  | 				} | ||||||
|  | 			}), | ||||||
|  | 			'400': { | ||||||
|  | 				description: 'Client error', | ||||||
|  | 				content: { | ||||||
|  | 					'application/json': { | ||||||
|  | 						schema: { | ||||||
|  | 							$ref: '#/components/schemas/Error' | ||||||
|  | 						}, | ||||||
|  | 						examples: { ...errors, ...basicErrors['400'] } | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
| 			}, | 			}, | ||||||
| 			responses: { | 			'401': { | ||||||
| 				...(endpoint.meta.res ? { | 				description: 'Authentication error', | ||||||
| 					'200': { | 				content: { | ||||||
| 						description: 'OK (with results)', | 					'application/json': { | ||||||
| 						content: { | 						schema: { | ||||||
| 							'application/json': { | 							$ref: '#/components/schemas/Error' | ||||||
| 								schema: resSchema | 						}, | ||||||
| 							} | 						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: { | 					content: { | ||||||
| 						'application/json': { | 						'application/json': { | ||||||
| 							schema: { | 							schema: { | ||||||
| 								$ref: '#/components/schemas/Error' | 								$ref: '#/components/schemas/Error' | ||||||
| 							}, | 							}, | ||||||
| 							examples: { ...errors, ...basicErrors['400'] } | 							examples: basicErrors['429'] | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
| 				}, | 				} | ||||||
| 				'401': { | 			} : {}), | ||||||
| 					description: 'Authentication error', | 			'500': { | ||||||
| 					content: { | 				description: 'Internal server error', | ||||||
| 						'application/json': { | 				content: { | ||||||
| 							schema: { | 					'application/json': { | ||||||
| 								$ref: '#/components/schemas/Error' | 						schema: { | ||||||
| 							}, | 							$ref: '#/components/schemas/Error' | ||||||
| 							examples: basicErrors['401'] | 						}, | ||||||
| 						} | 						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] = { | 		spec.paths['/' + endpoint.name] = { | ||||||
| 			post: info | 			post: genOpenapiSpecForEndpoint(endpoint, lang) | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue