Files
TeleUploader/src/routes/swagger.ts
T

279 lines
9.0 KiB
TypeScript

import { config } from '../env';
const errorSchema = (example: string) => ({
type: 'object',
properties: {
error: { type: 'string', example },
},
});
const jsonContent = (schema: object) => ({
'application/json': { schema },
});
export const handleSwaggerJson = async (): Promise<Response> => {
const spec = {
openapi: '3.0.0',
info: {
title: 'TeleUploader API',
version: '1.0.0',
description: 'Telegram-backed file uploader API with redirect-based downloads.',
},
servers: [
{
url: '/',
description: 'Current environment',
},
],
paths: {
'/health': {
get: {
summary: 'Health Check',
description: 'Checks database connectivity status.',
responses: {
'200': {
description: 'Database is healthy',
content: jsonContent({
type: 'object',
properties: {
status: { type: 'string', example: 'ok' },
},
}),
},
'500': {
description: 'Database or server is unhealthy',
content: jsonContent({
type: 'object',
properties: {
status: { type: 'string', example: 'error' },
error: { type: 'string', example: 'DB Connection Failed' },
},
}),
},
},
},
},
'/api/upload': {
post: {
summary: 'Upload File',
description: 'Uploads a file to Telegram storage via multipart/form-data or JSON base64.',
requestBody: {
required: true,
content: {
'multipart/form-data': {
schema: {
type: 'object',
required: ['file'],
properties: {
file: {
type: 'string',
format: 'binary',
description: 'File binary payload.',
},
fileName: {
type: 'string',
description: 'Optional file name override.',
},
},
},
},
'application/json': {
schema: {
type: 'object',
required: ['file'],
properties: {
file: {
type: 'string',
description: 'Base64 encoded file content.',
},
fileName: {
type: 'string',
default: 'file',
description: 'Optional file name.',
},
},
},
},
},
},
responses: {
'200': {
description: 'Successful upload metadata.',
content: jsonContent({
type: 'object',
properties: {
public_id: { type: 'string', example: 'xYz123' },
telegram_file_id: { type: 'string', example: 'BQACAgQAAxkBA...' },
telegram_file_unique_id: { type: 'string', example: 'AgAD8w...' },
storage_chat_id: { type: 'integer', example: -1001234567890 },
storage_message_id: { type: 'integer', example: 42 },
file_name: { type: 'string', example: 'document.pdf' },
mime_type: { type: 'string', example: 'application/pdf' },
size_bytes: { type: 'integer', example: 1048576 },
file_type: { type: 'string', example: 'document' },
uploader_id: { type: 'integer', example: 0 },
created_at: {
type: 'string',
format: 'date-time',
example: '2026-05-18T10:00:00.000Z',
},
download_url: {
type: 'string',
example: `${config.baseUrl}/f/xYz123`,
},
},
}),
},
'400': {
description: 'Bad request.',
content: jsonContent(errorSchema('No file provided')),
},
'500': {
description: 'Internal server error.',
content: jsonContent(errorSchema('Upload failed')),
},
},
},
},
'/f/{public_id}': {
get: {
summary: 'Redirect to Telegram File URL',
description:
'Gets a fresh Telegram download URL and redirects with 302. Rate-limited by IP.',
parameters: [
{
name: 'public_id',
in: 'path',
required: true,
description: 'Permanent public file ID.',
schema: { type: 'string' },
},
],
responses: {
'302': {
description: 'Redirect to Telegram CDN URL.',
headers: {
Location: {
schema: {
type: 'string',
example: 'https://api.telegram.org/file/botTOKEN/documents/file_0.pdf',
},
},
},
},
'404': {
description: 'File not found.',
content: jsonContent(errorSchema('File not found')),
},
'429': {
description: 'Rate limit exceeded.',
content: jsonContent(errorSchema('Rate limit exceeded')),
},
'500': {
description: 'Internal server error.',
content: jsonContent(errorSchema('Server error')),
},
},
},
},
'/file/{public_id}/info': {
get: {
summary: 'Get File Info',
description: 'Gets saved file metadata by public ID.',
parameters: [
{
name: 'public_id',
in: 'path',
required: true,
description: 'Permanent public file ID.',
schema: { type: 'string' },
},
],
responses: {
'200': {
description: 'File metadata.',
content: jsonContent({
type: 'object',
properties: {
public_id: { type: 'string', example: 'xYz123' },
file_name: { type: 'string', example: 'document.pdf' },
mime_type: { type: 'string', example: 'application/pdf' },
size_bytes: { type: 'integer', example: 1048576 },
file_type: { type: 'string', example: 'document' },
uploader_id: { type: 'integer', example: 0 },
created_at: {
type: 'string',
format: 'date-time',
example: '2026-05-18T10:00:00.000Z',
},
},
}),
},
'400': {
description: 'Missing public ID.',
content: jsonContent(errorSchema('Missing file id')),
},
'404': {
description: 'File not found.',
content: jsonContent(errorSchema('File not found')),
},
'500': {
description: 'Internal server error.',
content: jsonContent(errorSchema('Server error')),
},
},
},
},
},
};
return Response.json(spec, {
status: 200,
headers: {
'access-control-allow-origin': '*',
},
});
};
export const handleSwaggerHtml = async (): Promise<Response> => {
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>TeleUploader API Documentation</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui.css">
<style>
html { box-sizing: border-box; overflow-y: scroll; }
*, *::before, *::after { box-sizing: inherit; }
body { margin: 0; background: #fafafa; }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui-bundle.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.17.14/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {
window.ui = SwaggerUIBundle({
url: '/swagger.json',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
layout: 'BaseLayout'
});
};
</script>
</body>
</html>`;
return new Response(html, {
status: 200,
headers: {
'content-type': 'text/html; charset=utf-8',
'access-control-allow-origin': '*',
'x-content-type-options': 'nosniff',
},
});
};