Compare commits
7 commits
develop
...
refine-api
Author | SHA1 | Date | |
---|---|---|---|
|
6db7aef322 | ||
|
0039f5774d | ||
|
ec6c93b082 | ||
|
0ff714d11e | ||
|
10c1e8161f | ||
|
e5107f47cf | ||
|
b96651a478 |
17 changed files with 728 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/**/*',
|
||||||
|
|
|
@ -1590,3 +1590,6 @@ _deck:
|
||||||
list: "リスト"
|
list: "リスト"
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
direct: "ダイレクト"
|
direct: "ダイレクト"
|
||||||
|
|
||||||
|
_apiDoc:
|
||||||
|
accessTokenRequired: "アクセストークンが必要です"
|
||||||
|
|
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: "作成したノート"
|
|
@ -58,7 +58,7 @@ export default defineComponent({
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
os.api('endpoints').then(endpoints => {
|
os.api('endpoints').then(endpoints => {
|
||||||
this.endpoints = endpoints;
|
this.endpoints = endpoints.map(x => x.name);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
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 v-if="ep.spec.responses[status].content" :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>
|
67
src/client/pages/api-docs/index.vue
Normal file
67
src/client/pages/api-docs/index.vue
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
<template>
|
||||||
|
<div class="smzyuecx">
|
||||||
|
<div class="tags">
|
||||||
|
<button v-for="tag in tags" :key="tag" class="tag _button">
|
||||||
|
{{ tag }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li v-for="endpoint in endpoints" :key="endpoint.name">
|
||||||
|
<MkA :to="`/api-docs/endpoints/${endpoint.name}`">{{ endpoint.name }}</MkA>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</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: [],
|
||||||
|
tags: [],
|
||||||
|
faQuestionCircle
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
os.api('endpoints').then(endpoints => {
|
||||||
|
this.endpoints = endpoints;
|
||||||
|
|
||||||
|
const tags = new Set();
|
||||||
|
for (const endpoint of this.endpoints) {
|
||||||
|
if (endpoint.tags) {
|
||||||
|
for (const tag of endpoint.tags) {
|
||||||
|
tags.add(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tags = Array.from(tags);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.smzyuecx {
|
||||||
|
> .tags {
|
||||||
|
> .tag {
|
||||||
|
display: inline-block;
|
||||||
|
border: solid 1px var(--divider);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
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="" :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>
|
78
src/client/pages/api-docs/value.object.vue
Normal file
78
src/client/pages/api-docs/value.object.vue
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<div class="jhpkzgfz">
|
||||||
|
<div class="empty" v-if="kvs.length === 0">
|
||||||
|
No fields
|
||||||
|
</div>
|
||||||
|
<div class="kvs" v-else>
|
||||||
|
<div class="kv" v-for="kv in kvs" :key="kv[0]">
|
||||||
|
<div class="k _monospace">{{ kv[0] }}</div>
|
||||||
|
<XValue class="v" :value="kv[1]" :schemas="schemas"/>
|
||||||
|
</div>
|
||||||
|
</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 {
|
||||||
|
border: solid 1px var(--divider);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
> .kvs {
|
||||||
|
> .kv {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: solid 1px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .k {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 1em;
|
||||||
|
min-width: 8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .v {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
96
src/client/pages/api-docs/value.vue
Normal file
96
src/client/pages/api-docs/value.vue
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
<template>
|
||||||
|
<div class="ezkosiua">
|
||||||
|
<div class="header _monospace">
|
||||||
|
<span v-if="value.$ref" class="ref">
|
||||||
|
<button class="_textButton" @click="resolveRef = true">
|
||||||
|
{{ value.$ref.replace('#/components/schemas/', '') }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span class="type">{{ type }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="description">{{ value.description }}</div>
|
||||||
|
|
||||||
|
<div v-if="value.$ref" class="ref">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
resolveRef: false,
|
||||||
|
type: getType(this.value)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.ezkosiua {
|
||||||
|
> .header {
|
||||||
|
> .ref {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .type {
|
||||||
|
border: solid 1px var(--divider);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .body {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
|
|
@ -31,6 +31,9 @@ export const router = createRouter({
|
||||||
{ path: '/theme-editor', component: page('theme-editor') },
|
{ path: '/theme-editor', component: page('theme-editor') },
|
||||||
{ path: '/advanced-theme-editor', component: page('advanced-theme-editor') },
|
{ path: '/advanced-theme-editor', component: page('advanced-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') },
|
||||||
|
|
|
@ -474,6 +474,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)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,5 +11,8 @@ export const meta = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default define(meta, async () => {
|
export default define(meta, async () => {
|
||||||
return endpoints.map(x => x.name);
|
return endpoints.map(x => ({
|
||||||
|
name: x.name,
|
||||||
|
tags: x.meta.tags,
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,44 +1,22 @@
|
||||||
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 = {
|
let locale;
|
||||||
openapi: '3.0.0',
|
|
||||||
|
|
||||||
info: {
|
try {
|
||||||
version: 'v1',
|
locale = yaml.safeLoad(fs.readFileSync(__dirname + `/../../../api-docs/${lang}/` + endpoint.name + '.yml', 'utf-8'));
|
||||||
title: 'Misskey API',
|
} catch (e) {
|
||||||
description: getDescription(lang),
|
locale = {
|
||||||
'x-logo': { url: '/assets/api-doc.png' }
|
params: {}
|
||||||
},
|
};
|
||||||
|
}
|
||||||
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 +57,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…
Reference in a new issue