summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api
diff options
context:
space:
mode:
Diffstat (limited to 'packages/backend/src/server/api')
-rw-r--r--packages/backend/src/server/api/ApiServerService.ts2
-rw-r--r--packages/backend/src/server/api/openapi/OpenApiServerService.ts31
-rw-r--r--packages/backend/src/server/api/openapi/gen-spec.ts193
3 files changed, 225 insertions, 1 deletions
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index 115d60986c..b806ad5ca3 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -167,7 +167,7 @@ export class ApiServerService {
// Make sure any unknown path under /api returns HTTP 404 Not Found,
// because otherwise ClientServerService will return the base client HTML
// page with HTTP 200.
- fastify.get('*', (request, reply) => {
+ fastify.get('/*', (request, reply) => {
reply.code(404);
// Mock ApiCallService.send's error handling
reply.send({
diff --git a/packages/backend/src/server/api/openapi/OpenApiServerService.ts b/packages/backend/src/server/api/openapi/OpenApiServerService.ts
new file mode 100644
index 0000000000..e804ba276c
--- /dev/null
+++ b/packages/backend/src/server/api/openapi/OpenApiServerService.ts
@@ -0,0 +1,31 @@
+import { fileURLToPath } from 'node:url';
+import { Inject, Injectable } from '@nestjs/common';
+import type { Config } from '@/config.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { genOpenapiSpec } from './gen-spec.js';
+import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
+
+const staticAssets = fileURLToPath(new URL('../../../../assets/', import.meta.url));
+
+@Injectable()
+export class OpenApiServerService {
+ constructor(
+ @Inject(DI.config)
+ private config: Config,
+ ) {
+ }
+
+ @bindThis
+ public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
+ fastify.get('/api-doc', async (_request, reply) => {
+ reply.header('Cache-Control', 'public, max-age=86400');
+ return await reply.sendFile('/redoc.html', staticAssets);
+ });
+ fastify.get('/api.json', (_request, reply) => {
+ reply.header('Cache-Control', 'public, max-age=600');
+ reply.send(genOpenapiSpec(this.config));
+ });
+ done();
+ }
+}
diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts
new file mode 100644
index 0000000000..fa62480c02
--- /dev/null
+++ b/packages/backend/src/server/api/openapi/gen-spec.ts
@@ -0,0 +1,193 @@
+import type { Config } from '@/config.js';
+import endpoints from '../endpoints.js';
+import { errors as basicErrors } from './errors.js';
+import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
+
+export function genOpenapiSpec(config: Config) {
+ const spec = {
+ openapi: '3.0.0',
+
+ info: {
+ version: config.version,
+ 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',
+ },
+ },
+ },
+ };
+
+ for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
+ const errors = {} as any;
+
+ if (endpoint.meta.errors) {
+ for (const e of Object.values(endpoint.meta.errors)) {
+ errors[e.code] = {
+ value: {
+ error: e,
+ },
+ };
+ }
+ }
+
+ const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {};
+
+ let desc = (endpoint.meta.description ? endpoint.meta.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 requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
+ const schema = { ...endpoint.params };
+
+ if (endpoint.meta.requireFile) {
+ schema.properties = {
+ ...schema.properties,
+ file: {
+ type: 'string',
+ format: 'binary',
+ description: 'The file contents.',
+ },
+ };
+ schema.required = [...schema.required ?? [], 'file'];
+ }
+
+ const info = {
+ operationId: endpoint.name,
+ summary: endpoint.name,
+ description: desc,
+ externalDocs: {
+ description: 'Source code',
+ url: `https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`,
+ },
+ ...(endpoint.meta.tags ? {
+ tags: [endpoint.meta.tags[0]],
+ } : {}),
+ ...(endpoint.meta.requireCredential ? {
+ security: [{
+ ApiKeyAuth: [],
+ }],
+ } : {}),
+ requestBody: {
+ required: true,
+ content: {
+ [requestType]: {
+ schema,
+ },
+ },
+ },
+ 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;
+}