mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-03 23:14:13 +00:00 
			
		
		
		
	enhance(backend): restore OpenAPI endpoints (#10281)
* enhance(backend): restore OpenAPI endpoints * Update CHANGELOG.md * version * set max-age * update redoc * follow redoc documentation --------- Co-authored-by: tamaina <tamaina@hotmail.co.jp>
This commit is contained in:
		
							parent
							
								
									caf646fcb0
								
							
						
					
					
						commit
						e0b7633a7a
					
				
					 10 changed files with 270 additions and 29 deletions
				
			
		| 
						 | 
				
			
			@ -17,9 +17,11 @@ You should also include the user name that made the change.
 | 
			
		|||
- ノートごとに絵文字リアクションを受け取るか設定できるように
 | 
			
		||||
- enhance(client): DM作成時にメンションも含むように
 | 
			
		||||
- enhance(client): フォロー申請のボタンのデザインを改善
 | 
			
		||||
- enhance(backend): OpenAPIエンドポイントを復旧
 | 
			
		||||
 | 
			
		||||
### Bugfixes
 | 
			
		||||
- ロールで広告を無効にするとadmin/adsでプレビューがでてこない問題を修正
 | 
			
		||||
- /api-consoleページにアクセスすると404が出る問題を修正
 | 
			
		||||
 | 
			
		||||
## 13.9.2 (2023/03/06)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,6 @@
 | 
			
		|||
	</head>
 | 
			
		||||
	<body>
 | 
			
		||||
		<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
 | 
			
		||||
		<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.50/bundles/redoc.standalone.js" integrity="sha256-WJbngBWN9vp6vkEuzeoSj5tE5saW9Hfj6/SinkzhL2s=" crossorigin="anonymous"></script>
 | 
			
		||||
		<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
 | 
			
		||||
	</body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,6 @@ import * as fs from 'node:fs';
 | 
			
		|||
import { fileURLToPath } from 'node:url';
 | 
			
		||||
import { dirname } from 'node:path';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import fastifyStatic from '@fastify/static';
 | 
			
		||||
import rename from 'rename';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -60,11 +59,6 @@ export class FileServerService {
 | 
			
		|||
			done();
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		fastify.register(fastifyStatic, {
 | 
			
		||||
			root: _dirname,
 | 
			
		||||
			serve: false,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		fastify.get('/files/app-default.jpg', (request, reply) => {
 | 
			
		||||
			const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
 | 
			
		||||
			reply.header('Content-Type', 'image/jpeg');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,7 @@ import { LocalTimelineChannelService } from './api/stream/channels/local-timelin
 | 
			
		|||
import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
 | 
			
		||||
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
 | 
			
		||||
import { UserListChannelService } from './api/stream/channels/user-list.js';
 | 
			
		||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
	imports: [
 | 
			
		||||
| 
						 | 
				
			
			@ -72,6 +73,7 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
 | 
			
		|||
		QueueStatsChannelService,
 | 
			
		||||
		ServerStatsChannelService,
 | 
			
		||||
		UserListChannelService,
 | 
			
		||||
		OpenApiServerService,
 | 
			
		||||
	],
 | 
			
		||||
	exports: [
 | 
			
		||||
		ServerService,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,9 @@
 | 
			
		|||
import cluster from 'node:cluster';
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
import { fileURLToPath } from 'node:url';
 | 
			
		||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
 | 
			
		||||
import Fastify, { FastifyInstance } from 'fastify';
 | 
			
		||||
import fastifyStatic from '@fastify/static';
 | 
			
		||||
import { IsNull } from 'typeorm';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +23,9 @@ import { StreamingApiServerService } from './api/StreamingApiServerService.js';
 | 
			
		|||
import { WellKnownServerService } from './WellKnownServerService.js';
 | 
			
		||||
import { FileServerService } from './FileServerService.js';
 | 
			
		||||
import { ClientServerService } from './web/ClientServerService.js';
 | 
			
		||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
 | 
			
		||||
 | 
			
		||||
const _dirname = fileURLToPath(new URL('.', import.meta.url));
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ServerService implements OnApplicationShutdown {
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +47,7 @@ export class ServerService implements OnApplicationShutdown {
 | 
			
		|||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private apiServerService: ApiServerService,
 | 
			
		||||
		private openApiServerService: OpenApiServerService,
 | 
			
		||||
		private streamingApiServerService: StreamingApiServerService,
 | 
			
		||||
		private activityPubServerService: ActivityPubServerService,
 | 
			
		||||
		private wellKnownServerService: WellKnownServerService,
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +77,15 @@ export class ServerService implements OnApplicationShutdown {
 | 
			
		|||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Register non-serving static server so that the child services can use reply.sendFile.
 | 
			
		||||
		// `root` here is just a placeholder and each call must use its own `rootPath`.
 | 
			
		||||
		fastify.register(fastifyStatic, {
 | 
			
		||||
			root: _dirname,
 | 
			
		||||
			serve: false,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		fastify.register(this.apiServerService.createServer, { prefix: '/api' });
 | 
			
		||||
		fastify.register(this.openApiServerService.createServer);
 | 
			
		||||
		fastify.register(this.fileServerService.createServer);
 | 
			
		||||
		fastify.register(this.activityPubServerService.createServer);
 | 
			
		||||
		fastify.register(this.nodeinfoServerService.createServer);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										193
									
								
								packages/backend/src/server/api/openapi/gen-spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								packages/backend/src/server/api/openapi/gen-spec.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -194,11 +194,6 @@ export class ClientServerService {
 | 
			
		|||
 | 
			
		||||
		//#region static assets
 | 
			
		||||
 | 
			
		||||
		fastify.register(fastifyStatic, {
 | 
			
		||||
			root: _dirname,
 | 
			
		||||
			serve: false,
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		fastify.register(fastifyStatic, {
 | 
			
		||||
			root: staticAssets,
 | 
			
		||||
			prefix: '/static-assets/',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ const UNSPECIFIED = '*/*';
 | 
			
		|||
// Response Content-Type
 | 
			
		||||
const AP = 'application/activity+json; charset=utf-8';
 | 
			
		||||
const HTML = 'text/html; charset=utf-8';
 | 
			
		||||
const JSON_UTF8 = 'application/json; charset=utf-8';
 | 
			
		||||
 | 
			
		||||
describe('Fetch resource', () => {
 | 
			
		||||
	let p: INestApplicationContext;
 | 
			
		||||
| 
						 | 
				
			
			@ -52,14 +53,17 @@ describe('Fetch resource', () => {
 | 
			
		|||
			assert.strictEqual(res.type, HTML);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('GET api-doc (廃止)', async () => {
 | 
			
		||||
		test('GET api-doc', async () => {
 | 
			
		||||
			const res = await simpleGet('/api-doc');
 | 
			
		||||
			assert.strictEqual(res.status, 404);
 | 
			
		||||
			assert.strictEqual(res.status, 200);
 | 
			
		||||
			// fastify-static gives charset=UTF-8 instead of utf-8 and that's okay
 | 
			
		||||
			assert.strictEqual(res.type?.toLowerCase(), HTML);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('GET api.json (廃止)', async () => {
 | 
			
		||||
		test('GET api.json', async () => {
 | 
			
		||||
			const res = await simpleGet('/api.json');
 | 
			
		||||
			assert.strictEqual(res.status, 404);
 | 
			
		||||
			assert.strictEqual(res.status, 200);
 | 
			
		||||
			assert.strictEqual(res.type, JSON_UTF8);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('GET api/foo (存在しない)', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -68,6 +72,12 @@ describe('Fetch resource', () => {
 | 
			
		|||
			assert.strictEqual(res.body.error.code, 'UNKNOWN_API_ENDPOINT');
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('GET api-console (client page)', async () => {
 | 
			
		||||
			const res = await simpleGet('/api-console');
 | 
			
		||||
			assert.strictEqual(res.status, 200);
 | 
			
		||||
			assert.strictEqual(res.type, HTML);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('GET favicon.ico', async () => {
 | 
			
		||||
			const res = await simpleGet('/favicon.ico');
 | 
			
		||||
			assert.strictEqual(res.status, 200);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue