69
packages/backend/src/server/api/openapi/errors.ts
Normal file
69
packages/backend/src/server/api/openapi/errors.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
|
||||
export const errors = {
|
||||
'400': {
|
||||
'INVALID_PARAM': {
|
||||
value: {
|
||||
error: {
|
||||
message: 'Invalid param.',
|
||||
code: 'INVALID_PARAM',
|
||||
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'401': {
|
||||
'CREDENTIAL_REQUIRED': {
|
||||
value: {
|
||||
error: {
|
||||
message: 'Credential required.',
|
||||
code: 'CREDENTIAL_REQUIRED',
|
||||
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'403': {
|
||||
'AUTHENTICATION_FAILED': {
|
||||
value: {
|
||||
error: {
|
||||
message: 'Authentication failed. Please ensure your token is correct.',
|
||||
code: 'AUTHENTICATION_FAILED',
|
||||
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'418': {
|
||||
'I_AM_AI': {
|
||||
value: {
|
||||
error: {
|
||||
message: 'You sent a request to Ai-chan, Misskey\'s showgirl, instead of the server.',
|
||||
code: 'I_AM_AI',
|
||||
id: '60c46cd1-f23a-46b1-bebe-5d2b73951a84',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'429': {
|
||||
'RATE_LIMIT_EXCEEDED': {
|
||||
value: {
|
||||
error: {
|
||||
message: 'Rate limit exceeded. Please try again later.',
|
||||
code: 'RATE_LIMIT_EXCEEDED',
|
||||
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'500': {
|
||||
'INTERNAL_ERROR': {
|
||||
value: {
|
||||
error: {
|
||||
message: 'Internal error occurred. Please contact us if the error persists.',
|
||||
code: 'INTERNAL_ERROR',
|
||||
id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
235
packages/backend/src/server/api/openapi/gen-spec.ts
Normal file
235
packages/backend/src/server/api/openapi/gen-spec.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import endpoints from '../endpoints';
|
||||
import { Context } from 'cafy';
|
||||
import config from '@/config/index';
|
||||
import { errors as basicErrors } from './errors';
|
||||
import { schemas, convertSchemaToOpenApiSchema } from './schemas';
|
||||
|
||||
export function genOpenapiSpec(lang = 'ja-JP') {
|
||||
const spec = {
|
||||
openapi: '3.0.0',
|
||||
|
||||
info: {
|
||||
version: 'v1',
|
||||
title: 'Misskey API',
|
||||
'x-logo': { url: '/static-assets/api-doc.png' }
|
||||
},
|
||||
|
||||
externalDocs: {
|
||||
description: 'Repository',
|
||||
url: 'https://github.com/misskey-dev/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; }) {
|
||||
const properties = {} as any;
|
||||
|
||||
for (const [k, v] of Object.entries(props)) {
|
||||
properties[k] = genProp(v);
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
function genProp(param: Context): any {
|
||||
const required = param.name === 'Object' ? (param as any).props ? Object.entries((param as any).props).filter(([k, v]: any) => !v.isOptional).map(([k, v]) => k) : [] : [];
|
||||
return {
|
||||
description: (param.data || {}).desc,
|
||||
default: (param.data || {}).default,
|
||||
deprecated: (param.data || {}).deprecated,
|
||||
...((param.data || {}).default ? { default: (param.data || {}).default } : {}),
|
||||
type: param.name === 'ID' ? 'string' : param.name.toLowerCase(),
|
||||
...(param.name === 'ID' ? { example: 'xxxxxxxxxx', format: 'id' } : {}),
|
||||
nullable: param.isNullable,
|
||||
...(param.name === 'String' ? {
|
||||
...((param as any).enum ? { enum: (param as any).enum } : {}),
|
||||
...((param as any).minLength ? { minLength: (param as any).minLength } : {}),
|
||||
...((param as any).maxLength ? { maxLength: (param as any).maxLength } : {}),
|
||||
} : {}),
|
||||
...(param.name === 'Number' ? {
|
||||
...((param as any).minimum ? { minimum: (param as any).minimum } : {}),
|
||||
...((param as any).maximum ? { maximum: (param as any).maximum } : {}),
|
||||
} : {}),
|
||||
...(param.name === 'Object' ? {
|
||||
...(required.length > 0 ? { required } : {}),
|
||||
properties: (param as any).props ? genProps((param as any).props) : {}
|
||||
} : {}),
|
||||
...(param.name === 'Array' ? {
|
||||
items: (param as any).ctx ? genProp((param as any).ctx) : {}
|
||||
} : {})
|
||||
};
|
||||
}
|
||||
|
||||
for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
|
||||
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.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/misskey-dev/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'] }
|
||||
}
|
||||
}
|
||||
},
|
||||
'401': {
|
||||
description: 'Authentication error',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error'
|
||||
},
|
||||
examples: basicErrors['401']
|
||||
}
|
||||
}
|
||||
},
|
||||
'403': {
|
||||
description: 'Forbidden 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']
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
spec.paths['/' + endpoint.name] = {
|
||||
post: info
|
||||
};
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
56
packages/backend/src/server/api/openapi/schemas.ts
Normal file
56
packages/backend/src/server/api/openapi/schemas.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { refs, Schema } from '@/misc/schema';
|
||||
|
||||
export function convertSchemaToOpenApiSchema(schema: Schema) {
|
||||
const res: any = schema;
|
||||
|
||||
if (schema.type === 'object' && schema.properties) {
|
||||
res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
|
||||
|
||||
for (const k of Object.keys(schema.properties)) {
|
||||
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.type === 'array' && schema.items) {
|
||||
res.items = convertSchemaToOpenApiSchema(schema.items);
|
||||
}
|
||||
|
||||
if (schema.ref) {
|
||||
res.$ref = `#/components/schemas/${schema.ref}`;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export const schemas = {
|
||||
Error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
error: {
|
||||
type: 'object',
|
||||
description: 'An error object.',
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
description: 'An error code. Unique within the endpoint.',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'An error message.',
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
description: 'An error ID. This ID is static.',
|
||||
}
|
||||
},
|
||||
required: ['code', 'id', 'message']
|
||||
},
|
||||
},
|
||||
required: ['error']
|
||||
},
|
||||
|
||||
...Object.fromEntries(
|
||||
Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema)])
|
||||
),
|
||||
};
|
Reference in New Issue
Block a user