diff --git a/crowdin.yml b/crowdin.yml
index 160b9184d..4c050b1a9 100644
--- a/crowdin.yml
+++ b/crowdin.yml
@@ -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
diff --git a/gulpfile.ts b/gulpfile.ts
index bdc20089c..f123ccd74 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -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/**/*',
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 992f6c842..ed53dfc4f 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1590,3 +1590,6 @@ _deck:
list: "リスト"
mentions: "あなた宛て"
direct: "ダイレクト"
+
+_apiDoc:
+ accessTokenRequired: "アクセストークンが必要です"
diff --git a/src/api-docs/ja-JP/meta.yml b/src/api-docs/ja-JP/meta.yml
new file mode 100644
index 000000000..ce680b820
--- /dev/null
+++ b/src/api-docs/ja-JP/meta.yml
@@ -0,0 +1,10 @@
+description: "インスタンスのメタ情報を取得します。"
+
+params:
+ detail: "追加情報を含めるか否か"
+
+res:
+ version: "Misskeyのバージョン"
+ announcements: "お知らせ"
+ announcements.title: "タイトル"
+ announcements.text: "本文"
diff --git a/src/api-docs/ja-JP/notes/create.yml b/src/api-docs/ja-JP/notes/create.yml
new file mode 100644
index 000000000..add6ff5e3
--- /dev/null
+++ b/src/api-docs/ja-JP/notes/create.yml
@@ -0,0 +1,7 @@
+description: "ノートを作成します。"
+
+params:
+ visibility: "ノートの公開範囲"
+
+res:
+ createdNote: "作成したノート"
diff --git a/src/client/pages/api-console.vue b/src/client/pages/api-console.vue
index dd5cacaee..1d2012b5d 100644
--- a/src/client/pages/api-console.vue
+++ b/src/client/pages/api-console.vue
@@ -58,7 +58,7 @@ export default defineComponent({
created() {
os.api('endpoints').then(endpoints => {
- this.endpoints = endpoints;
+ this.endpoints = endpoints.map(x => x.name);
});
},
diff --git a/src/client/pages/api-docs/endpoint.vue b/src/client/pages/api-docs/endpoint.vue
new file mode 100644
index 000000000..4eabedcb9
--- /dev/null
+++ b/src/client/pages/api-docs/endpoint.vue
@@ -0,0 +1,201 @@
+
+
+
{{ endpoint }}
+
+
POST {{ apiUrl }}/{{ endpoint }}
+
{{ ep.spec.description }}
+
API console
+
+
+
+ Raw spec info
+
+ Show
+ {{ JSON.stringify(ep.spec, null, '\t') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/client/pages/api-docs/index.vue b/src/client/pages/api-docs/index.vue
new file mode 100644
index 000000000..f47c8fc97
--- /dev/null
+++ b/src/client/pages/api-docs/index.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+ -
+ {{ endpoint.name }}
+
+
+
+
+
+
+
+
diff --git a/src/client/pages/api-docs/value.array.vue b/src/client/pages/api-docs/value.array.vue
new file mode 100644
index 000000000..9ffd0e88b
--- /dev/null
+++ b/src/client/pages/api-docs/value.array.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
diff --git a/src/client/pages/api-docs/value.object.vue b/src/client/pages/api-docs/value.object.vue
new file mode 100644
index 000000000..8f1c8824f
--- /dev/null
+++ b/src/client/pages/api-docs/value.object.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
diff --git a/src/client/pages/api-docs/value.vue b/src/client/pages/api-docs/value.vue
new file mode 100644
index 000000000..d7f7c087a
--- /dev/null
+++ b/src/client/pages/api-docs/value.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
{{ value.description }}
+
+
+
+
+
+
+
+
+
+
+
+ unknown
+
+
+
+
+
+
+
+
diff --git a/src/client/pages/docs.vue b/src/client/pages/docs.vue
index 59d23efcb..05d1f407b 100644
--- a/src/client/pages/docs.vue
+++ b/src/client/pages/docs.vue
@@ -7,6 +7,11 @@
{{ doc.title }}
+
diff --git a/src/client/router.ts b/src/client/router.ts
index 5f2a65745..db0870f3e 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -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') },
diff --git a/src/client/style.scss b/src/client/style.scss
index f4da7cafe..f81a2b4e1 100644
--- a/src/client/style.scss
+++ b/src/client/style.scss
@@ -474,6 +474,7 @@ hr {
color: #ccc;
font-size: 14px;
line-height: 1.5;
+ tab-size: 2;
padding: 5px;
}
diff --git a/src/server/api/endpoints/endpoint.ts b/src/server/api/endpoints/endpoint.ts
index 1a04d8bee..faa76b687 100644
--- a/src/server/api/endpoints/endpoint.ts
+++ b/src/server/api/endpoints/endpoint.ts
@@ -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)
};
});
diff --git a/src/server/api/endpoints/endpoints.ts b/src/server/api/endpoints/endpoints.ts
index 3ab14389a..e42d73935 100644
--- a/src/server/api/endpoints/endpoints.ts
+++ b/src/server/api/endpoints/endpoints.ts
@@ -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,
+ }));
});
diff --git a/src/server/api/openapi/gen-spec.ts b/src/server/api/openapi/gen-spec.ts
index 78e481037..87cddedac 100644
--- a/src/server/api/openapi/gen-spec.ts
+++ b/src/server/api/openapi/gen-spec.ts
@@ -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)
};
}