From 344696912179edcb13623df5f7c9e8d8cd438031 Mon Sep 17 00:00:00 2001
From: syuilo
const i = sha256(userToken + secretKey);
+`;
diff --git a/src/server/api/openapi/errors.ts b/src/server/api/openapi/errors.ts
new file mode 100644
index 0000000000..43bcc323ba
--- /dev/null
+++ b/src/server/api/openapi/errors.ts
@@ -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',
+ }
+ }
+ }
+ }
+};
diff --git a/src/server/api/openapi/gen-spec.ts b/src/server/api/openapi/gen-spec.ts
new file mode 100644
index 0000000000..ad46eb20a4
--- /dev/null
+++ b/src/server/api/openapi/gen-spec.ts
@@ -0,0 +1,255 @@
+import endpoints from '../endpoints';
+import { Context } from 'cafy';
+import config from '../../../config';
+import { errors as basicErrors } from './errors';
+import { schemas } from './schemas';
+import { description } from './description';
+
+export function genOpenapiSpec(lang = 'ja-JP') {
+ const spec = {
+ openapi: '3.0.0',
+
+ info: {
+ version: 'v1',
+ title: 'Misskey API',
+ description: '**Misskey is a decentralized microblogging platform.**\n\n' + description,
+ 'x-logo': { url: '/assets/api-doc.png' }
+ },
+
+ externalDocs: {
+ description: 'Repository',
+ url: 'https://github.com/syuilo/misskey'
+ },
+
+ servers: [{
+ url: config.api_url
+ }],
+
+ paths: {} as any,
+
+ components: {
+ schemas: schemas,
+
+ securitySchemes: {
+ ApiKeyAuth: {
+ type: 'apiKey',
+ in: 'body',
+ name: 'i'
+ }
+ }
+ }
+ };
+
+ function genProps(props: { [key: string]: Context & { desc: any, default: any }; }) {
+ const properties = {} as any;
+
+ const kvs = Object.entries(props);
+
+ for (const kv of kvs) {
+ properties[kv[0]] = genProp(kv[1], kv[1].desc, kv[1].default);
+ }
+
+ return properties;
+ }
+
+ function genProp(param: Context, desc?: string, _default?: any): 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: desc,
+ default: _default,
+ ...(_default ? { default: _default } : {}),
+ type: param.name === 'ID' ? 'string' : param.name.toLowerCase(),
+ ...(param.name === 'ID' ? { example: 'xxxxxxxxxxxxxxxxxxxxxxxx', 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 kv of Object.entries(endpoint.meta.params)) {
+ if (kv[1].desc) (kv[1].validator as any).desc = kv[1].desc[lang];
+ if (kv[1].default) (kv[1].validator as any).default = kv[1].default;
+ porops[kv[0]] = kv[1].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 ? renderType(endpoint.meta.res) : {};
+
+ function renderType(x: any) {
+ const res = {} as any;
+
+ if (['User', 'Note', 'DriveFile'].includes(x.type)) {
+ res['$ref'] = `#/components/schemas/${x.type}`;
+ } else if (x.type === 'object') {
+ res['type'] = 'object';
+ if (x.props) {
+ const props = {} as any;
+ for (const kv of Object.entries(x.props)) {
+ props[kv[0]] = renderType(kv[1]);
+ }
+ res['properties'] = props;
+ }
+ } else if (x.type === 'array') {
+ res['type'] = 'array';
+ if (x.items) {
+ res['items'] = renderType(x.items);
+ }
+ } else {
+ res['type'] = x.type;
+ }
+
+ return res;
+ }
+
+ const info = {
+ operationId: endpoint.name,
+ summary: endpoint.name,
+ description: endpoint.meta.desc ? endpoint.meta.desc[lang] : 'No description provided.',
+ 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
+ } : {}),
+ ...(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: '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']
+ }
+ }
+ },
+ }
+ };
+
+ spec.paths['/' + endpoint.name] = {
+ post: info
+ };
+ }
+
+ return spec;
+}
diff --git a/src/server/api/openapi/schemas.ts b/src/server/api/openapi/schemas.ts
new file mode 100644
index 0000000000..3de9e42e0c
--- /dev/null
+++ b/src/server/api/openapi/schemas.ts
@@ -0,0 +1,196 @@
+
+export const schemas = {
+ Error: {
+ type: 'object',
+ properties: {
+ error: {
+ type: 'object',
+ description: 'An error object.',
+ properties: {
+ code: {
+ type: 'string',
+ description: 'An error code.',
+ },
+ 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']
+ },
+
+ User: {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'id',
+ description: 'The unique identifier for this User.'
+ },
+ username: {
+ type: 'string',
+ description: 'The screen name, handle, or alias that this user identifies themselves with.',
+ example: 'ai'
+ },
+ name: {
+ type: 'string',
+ nullable: true,
+ description: 'The name of the user, as they’ve defined it.',
+ example: '藍'
+ },
+ host: {
+ type: 'string',
+ nullable: true,
+ example: 'misskey.example.com'
+ },
+ description: {
+ type: 'string',
+ nullable: true,
+ description: 'The user-defined UTF-8 string describing their account.',
+ example: 'Hi masters, I am Ai!'
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ description: 'The date that the user account was created on Misskey.'
+ },
+ followersCount: {
+ type: 'number',
+ description: 'The number of followers this account currently has.'
+ },
+ followingCount: {
+ type: 'number',
+ description: 'The number of users this account is following.'
+ },
+ notesCount: {
+ type: 'number',
+ description: 'The number of Notes (including renotes) issued by the user.'
+ },
+ isBot: {
+ type: 'boolean',
+ description: 'Whether this account is a bot.'
+ },
+ isCat: {
+ type: 'boolean',
+ description: 'Whether this account is a cat.'
+ },
+ isAdmin: {
+ type: 'boolean',
+ description: 'Whether this account is the admin.'
+ },
+ isVerified: {
+ type: 'boolean'
+ },
+ isLocked: {
+ type: 'boolean'
+ },
+ },
+ required: ['id', 'name', 'username', 'createdAt']
+ },
+
+ Note: {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'id',
+ description: 'The unique identifier for this Note.'
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ description: 'The date that the Note was created on Misskey.'
+ },
+ text: {
+ type: 'string'
+ },
+ cw: {
+ type: 'string'
+ },
+ userId: {
+ type: 'string',
+ format: 'id',
+ },
+ user: {
+ $ref: '#/components/schemas/User'
+ },
+ replyId: {
+ type: 'string',
+ format: 'id',
+ },
+ renoteId: {
+ type: 'string',
+ format: 'id',
+ },
+ reply: {
+ $ref: '#/components/schemas/Note'
+ },
+ renote: {
+ $ref: '#/components/schemas/Note'
+ },
+ viaMobile: {
+ type: 'boolean'
+ },
+ visibility: {
+ type: 'string'
+ },
+ },
+ required: ['id', 'userId', 'createdAt']
+ },
+
+ DriveFile: {
+ type: 'object',
+ properties: {
+ id: {
+ type: 'string',
+ format: 'id',
+ description: 'The unique identifier for this Drive file.'
+ },
+ createdAt: {
+ type: 'string',
+ format: 'date-time',
+ description: 'The date that the Drive file was created on Misskey.'
+ },
+ name: {
+ type: 'string',
+ description: 'The file name with extension.',
+ example: 'lenna.jpg'
+ },
+ type: {
+ type: 'string',
+ description: 'The MIME type of this Drive file.',
+ example: 'image/jpeg'
+ },
+ md5: {
+ type: 'string',
+ format: 'md5',
+ description: 'The MD5 hash of this Drive file.',
+ example: '15eca7fba0480996e2245f5185bf39f2'
+ },
+ datasize: {
+ type: 'number',
+ description: 'The size of this Drive file. (bytes)',
+ example: 51469
+ },
+ folderId: {
+ type: 'string',
+ format: 'id',
+ nullable: true,
+ description: 'The parent folder ID of this Drive file.',
+ },
+ isSensitive: {
+ type: 'boolean',
+ description: 'Whether this Drive file is sensitive.',
+ },
+ },
+ required: ['id', 'createdAt', 'name', 'type', 'datasize', 'md5']
+ }
+};
--
cgit v1.2.3-freya