Compare commits
7 Commits
stage
...
refine-api
Author | SHA1 | Date |
---|---|---|
syuilo | 6db7aef322 | |
syuilo | 0039f5774d | |
syuilo | ec6c93b082 | |
syuilo | 0ff714d11e | |
syuilo | 10c1e8161f | |
syuilo | e5107f47cf | |
syuilo | b96651a478 |
|
@ -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/**/*',
|
||||
|
|
|
@ -1590,3 +1590,6 @@ _deck:
|
|||
list: "リスト"
|
||||
mentions: "あなた宛て"
|
||||
direct: "ダイレクト"
|
||||
|
||||
_apiDoc:
|
||||
accessTokenRequired: "アクセストークンが必要です"
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
description: "インスタンスのメタ情報を取得します。"
|
||||
|
||||
params:
|
||||
detail: "追加情報を含めるか否か"
|
||||
|
||||
res:
|
||||
version: "Misskeyのバージョン"
|
||||
announcements: "お知らせ"
|
||||
announcements.title: "タイトル"
|
||||
announcements.text: "本文"
|
|
@ -0,0 +1,7 @@
|
|||
description: "ノートを作成します。"
|
||||
|
||||
params:
|
||||
visibility: "ノートの公開範囲"
|
||||
|
||||
res:
|
||||
createdNote: "作成したノート"
|
|
@ -58,7 +58,7 @@ export default defineComponent({
|
|||
|
||||
created() {
|
||||
os.api('endpoints').then(endpoints => {
|
||||
this.endpoints = endpoints;
|
||||
this.endpoints = endpoints.map(x => x.name);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<MkA :to="`/api-docs`">API reference</MkA>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
@ -31,6 +31,9 @@ export const router = createRouter({
|
|||
{ path: '/theme-editor', component: page('theme-editor') },
|
||||
{ path: '/advanced-theme-editor', component: page('advanced-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') },
|
||||
|
|
|
@ -474,6 +474,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)
|
||||
};
|
||||
});
|
||||
|
|
|
@ -11,5 +11,8 @@ export const meta = {
|
|||
};
|
||||
|
||||
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 * 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',
|
||||
export function genOpenapiSpecForEndpoint(endpoint: IEndpoint, lang = 'ja-JP') {
|
||||
let locale;
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
locale = yaml.safeLoad(fs.readFileSync(__dirname + `/../../../api-docs/${lang}/` + endpoint.name + '.yml', 'utf-8'));
|
||||
} catch (e) {
|
||||
locale = {
|
||||
params: {}
|
||||
};
|
||||
}
|
||||
|
||||
function genProps(props: { [key: string]: Context; }) {
|
||||
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 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…
Reference in New Issue