first init

This commit is contained in:
2026-08-14 00:13:56 +05:00
commit 7ae997f99e
14 changed files with 6310 additions and 0 deletions
+512
View File
@@ -0,0 +1,512 @@
#!/usr/bin/env python3
"""Generate the browser catalog and OpenAPI documents from one endpoint list."""
from __future__ import annotations
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PUBLIC = ROOT / "public"
OPENAPI = PUBLIC / "openapi"
def title(path: str, method: str) -> str:
names = {
"/register": "Регистрация аккаунта",
"/login": "Вход по паролю",
"/auth/email/start": "Запрос email-кода",
"/auth/email/verify": "Подтверждение email-кода",
"/.well-known/openid-configuration": "OIDC Discovery",
"/.well-known/oauth-authorization-server": "OAuth Server Metadata",
"/oauth/jwks.json": "Публичные ключи OIDC",
"/oauth/device_authorization": "Запуск Device Flow",
"/oauth/device": "Страница подтверждения Device Flow",
"/oauth/token": "Обмен device code на токены",
"/oauth/userinfo": "Профиль OIDC",
"/oauth/device/request": "Данные запроса входа",
"/oauth/device/decision": "Решение пользователя",
"/me": "Текущий пользователь",
"/account/delete": "Удаление аккаунта",
"/username": "Установка username",
"/name": "Изменение отображаемого имени",
"/profiles/description": "Описание профиля, бота или чата",
"/privacy": "Настройки приватности",
"/contacts": "Список контактов",
"/contacts/add": "Добавление контакта",
"/contacts/delete": "Удаление контакта",
"/groups": "Создание группы",
"/channels": "Создание канала",
"/chats/title": "Изменение названия чата",
"/channels/username": "Username канала",
"/channels/comments/settings": "Настройки комментариев канала",
"/channels/comments/send": "Комментарий к публикации",
"/channels/comments": "Комментарии к публикации",
"/chats/members/add": "Добавление участника",
"/chats/members/remove": "Удаление участника",
"/cloud-password": "Облачный пароль",
"/cloud-password/reset": "Сброс облачного пароля",
"/sessions": "Активные сессии",
"/sessions/revoke": "Завершение сессии",
"/sessions/revoke-others": "Завершение других сессий",
"/bots": "Создание бота",
"/bots/token/reset": "Перевыпуск bot token",
"/e2e/key": "Публичный E2E-ключ" if method == "GET" else "Публикация E2E-ключа",
"/e2e/backup": "Резервная копия E2E" if method == "GET" else "Сохранение E2E-копии",
"/e2e/reset": "Сброс E2E-ключей",
"/wallet": "Баланс Dastars",
"/wallet/send": "Перевод Dastars",
"/wallet/history": "История Dastars",
"/call": "Создание звонка",
"/voice-ticket": "Билет голосовой сессии",
"/voice/participants": "Участники голосовой сессии",
"/voice": "Голосовой WebSocket",
"/send": "Отправка текстового сообщения",
"/edit": "Редактирование сообщения",
"/callback": "Callback кнопки",
"/reactions": "Emoji-реакция",
"/reactions/paid": "Платная реакция Dastars",
"/read": "Отметка о прочтении",
"/delete": "Удаление сообщения",
"/favorite": "Избранное сообщение",
"/media/quote": "Расчет стоимости медиа",
"/messages/prepare": "Подготовка сообщения с медиа",
"/messages/commit": "Публикация сообщения с медиа",
"/messages/cancel": "Отмена сообщения с медиа",
"/forward": "Пересылка сообщения с медиа",
"/file/ticket": "Билет скачивания файла",
"/health/live": "Проверка процесса Messenger",
"/health/ready": "Готовность Messenger",
"/metrics": "Метрики Messenger",
"/nodes/status": "Статус сервисных узлов",
"/chats": "Список чатов",
"/chats/delete": "Удаление чата",
"/users/ban": "Блокировка пользователя",
"/users/unban": "Разблокировка пользователя",
"/history": "История сообщений",
"/updates": "Очередь обновлений",
"/updates/ack": "Подтверждение обновлений",
"/file/{id}": "Скачивание файла",
"/health": "Состояние Gateway",
"/v1/assets": "Доступные активы",
"/v1/invoices": "Список invoices" if method == "GET" else "Создание invoice",
"/v1/invoices/{invoice_id}": "Получение invoice",
"/v1/withdrawals": "Список withdrawals" if method == "GET" else "Создание withdrawal",
"/v1/withdrawals/{withdrawal_id}": "Получение withdrawal",
"/v1/balances": "Merchant-балансы",
"/v1/api-keys": "Список API-ключей",
"/admin/v1/status": "Операторский статус",
"/admin/v1/callbacks": "Инциденты callbacks",
"/admin/v1/consolidations": "Инциденты консолидации",
"/admin/v1/callbacks/{callback_id}/retry": "Повтор callback",
"/admin/v1/merchants": "Создание merchant",
"/admin/v1/fee-policies": "Глобальная fee policy",
"/admin/v1/merchants/{merchant_id}/api-keys": "Выпуск merchant API-ключа",
"/admin/v1/merchants/{merchant_id}/api-keys/{api_key_id}": "Отзыв merchant API-ключа",
"/admin/v1/merchants/{merchant_id}/policies": "Merchant policy",
"/admin/v1/withdrawals/{withdrawal_id}/resolve": "Решение manual review",
"/admin/v1/consolidations/{consolidation_id}/retry": "Повтор консолидации",
"/pay/{token}": "Hosted checkout",
"/pay/{token}/status": "Статус hosted checkout",
"/api/assets": "Активы для донатов",
"/api/auth/start": "Вход стримера через Messenger",
"/api/auth/poll/{flow}": "Статус входа стримера",
"/api/logout": "Выход из Donation Service",
"/api/me": "Панель стримера",
"/api/profile": "Настройки страницы и OBS",
"/api/sound": "Удаление звука" if method == "DELETE" else "Загрузка звука",
"/api/widget/rotate": "Ротация секретной OBS-ссылки",
"/api/widget/test": "Тестовый OBS alert",
"/api/widget/{token}/config": "Конфигурация OBS-виджета",
"/api/widget/{token}/events": "SSE-события OBS-виджета",
"/api/public/creators/{slug}": "Публичная страница стримера",
"/api/public/donations": "Создание доната",
"/api/public/donations/{token}/status": "Статус доната",
"/api/withdrawals": "Запрос вывода",
"/api/withdrawals/{flow}/poll": "Подтверждение вывода через Messenger",
"/api/admin/status": "Статус Donation Service",
"/api/admin/creators/{creator_id}/blocked": "Блокировка страницы стримера",
}
return names.get(path, f"{method} {path}")
MESSENGER = [
("POST", "/register", "Аккаунт", "public"),
("POST", "/login", "Аккаунт", "public"),
("POST", "/auth/email/start", "Аккаунт", "public"),
("POST", "/auth/email/verify", "Аккаунт", "public"),
("GET", "/.well-known/openid-configuration", "OAuth", "public"),
("GET", "/.well-known/oauth-authorization-server", "OAuth", "public"),
("GET", "/oauth/jwks.json", "OAuth", "public"),
("POST", "/oauth/device_authorization", "OAuth", "oauth_client"),
("GET", "/oauth/device", "OAuth", "public"),
("POST", "/oauth/token", "OAuth", "oauth_client"),
("GET", "/oauth/userinfo", "OAuth", "bearer"),
("GET", "/oauth/device/request", "OAuth", "bearer"),
("POST", "/oauth/device/decision", "OAuth", "bearer"),
("GET", "/me", "Аккаунт", "bearer"),
("POST", "/account/delete", "Аккаунт", "bearer"),
("POST", "/username", "Аккаунт", "bearer"),
("POST", "/name", "Аккаунт", "bearer"),
("POST", "/profiles/description", "Аккаунт", "bearer"),
("POST", "/privacy", "Аккаунт", "bearer"),
("GET", "/contacts", "Контакты", "bearer"),
("POST", "/contacts/add", "Контакты", "bearer"),
("POST", "/contacts/delete", "Контакты", "bearer"),
("POST", "/groups", "Чаты", "bearer"),
("POST", "/channels", "Чаты", "bearer"),
("POST", "/chats/title", "Чаты", "bearer"),
("POST", "/channels/username", "Чаты", "bearer"),
("POST", "/channels/comments/settings", "Чаты", "bearer"),
("POST", "/channels/comments/send", "Чаты", "bearer"),
("GET", "/channels/comments", "Чаты", "bearer"),
("POST", "/chats/members/add", "Чаты", "bearer"),
("POST", "/chats/members/remove", "Чаты", "bearer"),
("POST", "/cloud-password", "Безопасность", "bearer"),
("POST", "/cloud-password/reset", "Безопасность", "bearer"),
("GET", "/sessions", "Безопасность", "bearer"),
("POST", "/sessions/revoke", "Безопасность", "bearer"),
("POST", "/sessions/revoke-others", "Безопасность", "bearer"),
("POST", "/bots", "Боты", "bearer"),
("POST", "/bots/token/reset", "Боты", "bearer"),
("POST", "/e2e/key", "E2E", "bearer"),
("GET", "/e2e/key", "E2E", "bearer"),
("POST", "/e2e/backup", "E2E", "bearer"),
("GET", "/e2e/backup", "E2E", "bearer"),
("POST", "/e2e/reset", "E2E", "bearer"),
("GET", "/wallet", "Dastars", "bearer"),
("POST", "/wallet/send", "Dastars", "bearer"),
("GET", "/wallet/history", "Dastars", "bearer"),
("POST", "/call", "Звонки", "bearer"),
("POST", "/voice-ticket", "Звонки", "bearer"),
("GET", "/voice/participants", "Звонки", "bearer"),
("GET", "/voice", "Звонки", "bearer"),
("POST", "/send", "Сообщения", "bearer"),
("POST", "/edit", "Сообщения", "bearer"),
("POST", "/callback", "Боты", "bearer"),
("POST", "/reactions", "Реакции", "bearer"),
("POST", "/reactions/paid", "Реакции", "bearer"),
("POST", "/read", "Сообщения", "bearer"),
("POST", "/delete", "Сообщения", "bearer"),
("POST", "/favorite", "Сообщения", "bearer"),
("POST", "/media/quote", "Файлы", "bearer"),
("POST", "/messages/prepare", "Файлы", "bearer"),
("POST", "/messages/commit", "Файлы", "bearer"),
("POST", "/messages/cancel", "Файлы", "bearer"),
("POST", "/forward", "Файлы", "bearer"),
("GET", "/file/ticket", "Файлы", "bearer"),
("GET", "/nodes/status", "Сервис", "bearer"),
("GET", "/chats", "Чаты", "bearer"),
("POST", "/chats/delete", "Чаты", "bearer"),
("POST", "/users/ban", "Модерация", "bearer"),
("POST", "/users/unban", "Модерация", "bearer"),
("GET", "/history", "Сообщения", "bearer"),
("GET", "/updates", "Updates", "bearer"),
("POST", "/updates/ack", "Updates", "bearer"),
("GET", "/file/{id}", "Файлы", "bearer"),
("GET", "/health/live", "Сервис", "public"),
("GET", "/health/ready", "Сервис", "public"),
("GET", "/metrics", "Сервис", "public"),
]
GATEWAY = [
("GET", "/health", "Сервис", "public"),
("GET", "/v1/assets", "Сервис", "public"),
("GET", "/v1/invoices", "Invoices", "hmac"),
("POST", "/v1/invoices", "Invoices", "hmac"),
("GET", "/v1/invoices/{invoice_id}", "Invoices", "hmac"),
("GET", "/v1/withdrawals", "Withdrawals", "hmac"),
("POST", "/v1/withdrawals", "Withdrawals", "hmac"),
("GET", "/v1/withdrawals/{withdrawal_id}", "Withdrawals", "hmac"),
("GET", "/v1/balances", "Баланс", "hmac"),
("GET", "/v1/api-keys", "Ключи", "hmac"),
("GET", "/admin/v1/status", "Operator", "admin"),
("GET", "/admin/v1/callbacks", "Operator", "admin"),
("GET", "/admin/v1/consolidations", "Operator", "admin"),
("POST", "/admin/v1/callbacks/{callback_id}/retry", "Operator", "admin"),
("POST", "/admin/v1/merchants", "Operator", "admin"),
("POST", "/admin/v1/fee-policies", "Operator", "admin"),
("POST", "/admin/v1/merchants/{merchant_id}/api-keys", "Operator", "admin"),
("DELETE", "/admin/v1/merchants/{merchant_id}/api-keys/{api_key_id}", "Operator", "admin"),
("POST", "/admin/v1/merchants/{merchant_id}/policies", "Operator", "admin"),
("POST", "/admin/v1/withdrawals/{withdrawal_id}/resolve", "Operator", "admin"),
("POST", "/admin/v1/consolidations/{consolidation_id}/retry", "Operator", "admin"),
("GET", "/pay/{token}", "Checkout", "public"),
("GET", "/pay/{token}/status", "Checkout", "public"),
]
DONATIONS = [
("GET", "/health", "Сервис", "public"),
("GET", "/api/assets", "Сервис", "public"),
("POST", "/api/auth/start", "Авторизация", "public"),
("GET", "/api/auth/poll/{flow}", "Авторизация", "public"),
("POST", "/api/logout", "Авторизация", "session"),
("GET", "/api/me", "Стример", "session"),
("PATCH", "/api/profile", "Стример", "session"),
("PUT", "/api/sound", "OBS", "session"),
("DELETE", "/api/sound", "OBS", "session"),
("POST", "/api/widget/rotate", "OBS", "session"),
("POST", "/api/widget/test", "OBS", "session"),
("GET", "/api/widget/{token}/config", "OBS", "widget"),
("GET", "/api/widget/{token}/events", "OBS", "widget"),
("GET", "/api/public/creators/{slug}", "Донаты", "public"),
("POST", "/api/public/donations", "Донаты", "public"),
("GET", "/api/public/donations/{token}/status", "Донаты", "public"),
("POST", "/api/withdrawals", "Вывод", "session"),
("GET", "/api/withdrawals/{flow}/poll", "Вывод", "session"),
("GET", "/api/admin/status", "Оператор", "session"),
("PATCH", "/api/admin/creators/{creator_id}/blocked", "Оператор", "session"),
]
AUTH_LABELS = {
"public": "public",
"bearer": "Bearer",
"oauth_client": "OAuth client",
"hmac": "HMAC",
"admin": "Admin Bearer",
"session": "Secure session",
"widget": "Widget token",
}
BODIES = {
("messenger", "POST", "/register"): {"email": "you@example.com", "password": "correct horse battery staple"},
("messenger", "POST", "/login"): {"email": "you@example.com", "password": "correct horse battery staple"},
("messenger", "POST", "/auth/email/start"): {"email": "you@example.com"},
("messenger", "POST", "/auth/email/verify"): {"email": "you@example.com", "code": "123456"},
("messenger", "POST", "/send"): {"to": 42, "text": "Привет!", "client_message_id": "0190f6d4-example"},
("messenger", "POST", "/media/quote"): {"media": [{"client_id": "attachment-1", "name": "photo.jpg", "mime": "image/jpeg", "size": 524288}]},
("messenger", "POST", "/messages/prepare"): {"to": 42, "text": "Caption", "client_message_id": "0190f6d4-media", "media": [{"client_id": "attachment-1", "name": "photo.jpg", "mime": "image/jpeg", "size": 524288}]},
("messenger", "POST", "/messages/commit"): {"operation_id": "message-op-1-0190f6d4-media"},
("messenger", "POST", "/messages/cancel"): {"operation_id": "message-op-1-0190f6d4-media"},
("messenger", "POST", "/forward"): {"message_id": 1001, "to": 42, "client_message_id": "0190f6d4-forward"},
("messenger", "POST", "/edit"): {"chat_id": 42, "message_id": 1001, "text": "Исправленный текст"},
("messenger", "POST", "/reactions"): {"chat_id": 42, "message_id": 1001, "emoji": "🔥"},
("messenger", "POST", "/reactions/paid"): {"chat_id": 42, "message_id": 1001, "amount": 5},
("messenger", "POST", "/updates/ack"): {"update_id": 2048},
("messenger", "POST", "/bots"): {"username": "shop_helper_bot", "name": "Shop helper"},
("gateway", "POST", "/v1/invoices"): {
"merchant_order_id": "order-123", "asset": "USDT", "network": "tron",
"amount_atomic": "25000000", "expires_in_seconds": 1800,
"description": "Order 123", "success_url": "https://merchant.example/orders/123",
"metadata": {"customer": "42"},
},
("gateway", "POST", "/v1/withdrawals"): {
"asset": "USDT", "network": "tron", "amount_atomic": "10000000",
"address": "T...", "fee_mode": "additional", "metadata": {"payout": "p-123"},
},
("gateway", "POST", "/admin/v1/merchants"): {
"name": "Example shop", "callback_url": "https://merchant.example/payments",
"callback_hosts": ["merchant.example"],
},
("gateway", "POST", "/admin/v1/fee-policies"): {
"asset": "USDT", "network": "tron", "platform_fee_basis_points": 500,
"platform_fee_cap_atomic": "10000000",
},
("donations", "POST", "/api/auth/start"): {"email": "streamer@example.com"},
("donations", "PATCH", "/api/profile"): {
"display_name": "My channel", "slug": "my-channel", "accent_color": "#b9f541",
"duration_seconds": 8, "show_donor_name": True, "show_message": True,
},
("donations", "POST", "/api/public/donations"): {
"slug": "my-channel", "donor_name": "Anonymous", "message": "Great stream!",
"asset": "USDT", "network": "tron", "amount": "25.5", "cover_fee": True,
},
("donations", "POST", "/api/withdrawals"): {
"asset": "USDT", "network": "tron", "amount": "10",
"address": "TExampleRecipientAddress",
},
("donations", "PATCH", "/api/admin/creators/{creator_id}/blocked"): {"blocked": True},
}
DESCRIPTIONS = {
"/oauth/device_authorization": "Создает device_code, user_code и verification URI для универсального входа через Messenger.",
"/oauth/token": "Обменивает подтвержденный device_code на OIDC access/id token. Во время ожидания возвращает authorization_pending.",
"/send": "Отправляет сообщение без медиа. Для клиентской очереди используйте стабильный client_message_id.",
"/media/quote": "Рассчитывает единую стоимость новых вложений: сумма байтов округляется вверх до MiB один раз на сообщение.",
"/messages/prepare": "Атомарно резервирует оплату и выдает до 10 upload tickets. Текст и media[] опциональны по отдельности, но сообщение не может быть пустым.",
"/messages/commit": "Проверяет все загруженные вложения, одним платежом списывает DSR и публикует или редактирует одно сообщение.",
"/messages/cancel": "Удаляет незавершенные загрузки и отменяет резерв оплаты.",
"/updates": "Long polling событий клиента или бота. Bot updates долговечны и остаются до явного ACK.",
"/reactions/paid": "Переводит указанное количество Dastars автору сообщения; для бота — его владельцу. amount может быть больше 1.",
"/voice": "Переключает соединение на WebSocket после получения voice ticket.",
"/v1/invoices": "Создает invoice или возвращает историю с cursor pagination, в зависимости от HTTP-метода.",
"/v1/withdrawals": "Создает withdrawal или возвращает историю. Для token assets разрешен только fee_mode=additional.",
"/v1/assets": "Возвращает только пары asset/network, для которых wallet seed готов к работе.",
"/pay/{token}": "Публичная HTML-страница оплаты с QR, серверно-синхронизированным таймером и live status.",
"/pay/{token}/status": "JSON-состояние checkout для polling; включает серверное время и срок действия.",
"/api/public/donations": "Создает Gateway invoice с комиссией 5%. cover_fee=false удерживает комиссию из суммы; cover_fee=true точно увеличивает платеж так, чтобы стример получил введенную сумму после комиссии. Все расчеты выполняются в atomic без float.",
"/api/widget/{token}/events": "Долгоживущий SSE-поток для OBS Browser Source. Виджет сам ставит alerts в очередь.",
"/api/withdrawals": "Резервирует баланс и запускает новый Messenger Device Flow с описанием суммы и адреса.",
"/api/withdrawals/{flow}/poll": "После свайпа тем же Messenger-пользователем идемпотентно отправляет withdrawal в Gateway.",
}
def endpoint(service_id: str, row: tuple[str, str, str, str]) -> dict:
method, path, group, auth = row
default = f"{title(path, method)}. Возвращает JSON-ответ или стандартную JSON-ошибку."
item = {
"method": method,
"path": path,
"group": group,
"auth": auth,
"auth_label": AUTH_LABELS[auth],
"title": title(path, method),
"description": DESCRIPTIONS.get(path, default),
}
body = BODIES.get((service_id, method, path))
if body is not None:
item["body"] = body
return item
def openapi(service: dict) -> dict:
paths: dict[str, dict] = {}
for item in service["endpoints"]:
operation: dict = {
"tags": [item["group"]],
"summary": item["title"],
"description": item["description"],
"operationId": (
item["method"].lower() + "_" +
item["path"].strip("/").replace("/", "_").replace("{", "").replace("}", "")
).replace("-", "_").replace(".", "_") or "root",
"responses": {
"200": {"description": "Успешный ответ"},
"400": {"description": "Ошибка запроса"},
"401": {"description": "Ошибка авторизации"},
},
}
if item["auth"] == "bearer":
operation["security"] = [{"bearerAuth": []}]
elif item["auth"] == "admin":
operation["security"] = [{"adminBearer": []}]
elif item["auth"] == "hmac":
operation["security"] = [{"merchantHmac": []}]
operation["parameters"] = [
{"name": "X-Api-Timestamp", "in": "header", "required": True, "schema": {"type": "integer"}},
{"name": "X-Api-Nonce", "in": "header", "required": True, "schema": {"type": "string", "format": "uuid"}},
{"name": "X-Api-Signature", "in": "header", "required": True, "schema": {"type": "string"}},
]
elif item["auth"] == "oauth_client":
operation["security"] = [{"oauthClient": []}]
elif item["auth"] == "session":
operation["security"] = [{"sessionCookie": []}]
parameters = operation.setdefault("parameters", [])
for parameter in (part[1:-1] for part in item["path"].split("/") if part.startswith("{")):
parameters.append({
"name": parameter, "in": "path", "required": True,
"schema": {"type": "string"},
})
if not parameters:
operation.pop("parameters", None)
if item.get("body") is not None:
operation["requestBody"] = {
"required": True,
"content": {"application/json": {"schema": {"type": "object"}, "example": item["body"]}},
}
paths.setdefault(item["path"], {})[item["method"].lower()] = operation
schemes = {
"bearerAuth": {"type": "http", "scheme": "bearer"},
"oauthClient": {
"type": "http",
"scheme": "basic",
"description": (
"OAuth client_id/client_secret. Device Authorization Grant не имеет "
"отдельного стандартного flow в OpenAPI 3.1; discovery доступен в "
"/.well-known/oauth-authorization-server."
),
},
"merchantHmac": {"type": "apiKey", "in": "header", "name": "X-Api-Key",
"description": "Дополнительно обязательна HMAC-SHA256 подпись запроса."},
"adminBearer": {"type": "http", "scheme": "bearer", "description": "Operator admin token"},
"sessionCookie": {"type": "apiKey", "in": "cookie", "name": "ove_donations_session",
"description": "HttpOnly session; changing requests also require X-CSRF-Token."},
}
scheme_auth = {
"bearerAuth": "bearer",
"oauthClient": "oauth_client",
"merchantHmac": "hmac",
"adminBearer": "admin",
"sessionCookie": "session",
}
active_auth = {item["auth"] for item in service["endpoints"]}
return {
"openapi": "3.1.0",
"info": {
"title": f"OVE {service['name']} API",
"version": "1.0.0",
"description": service["description"],
},
"servers": [{"url": service["base_url"]}],
"paths": paths,
"components": {
"securitySchemes": {
key: value for key, value in schemes.items()
if scheme_auth[key] in active_auth
},
"schemas": {
"Error": {
"type": "object",
"properties": {"error": {"type": "string"}},
"required": ["error"],
}
},
},
}
def main() -> None:
services = [
{
"id": "messenger",
"name": "Messenger",
"base_url": "https://ms.ove.rs",
"description": "OVE Messenger user, bot, OAuth, files, calls and realtime updates API.",
"endpoints": [endpoint("messenger", row) for row in MESSENGER],
},
{
"id": "gateway",
"name": "Crypto Gateway",
"base_url": "https://cr.ove.rs",
"description": "OVE multi-merchant cryptocurrency payments, checkout and operator API.",
"endpoints": [endpoint("gateway", row) for row in GATEWAY],
},
{
"id": "donations",
"name": "Donations",
"base_url": "https://do.ove.rs",
"description": "OVE donation pages, Messenger login, Crypto Gateway payments and OBS alerts.",
"endpoints": [endpoint("donations", row) for row in DONATIONS],
},
]
manifest = {
"version": 1,
"generated_from": [
"micro-chat/micromsg/src/app_routes.rs",
"crypto-gateway/crates/gateway-api/src/main.rs",
"crypto-gateway/crates/gateway-api/src/web.rs",
"donations/src/main.rs",
],
"total_endpoints": sum(len(service["endpoints"]) for service in services),
"services": services,
}
OPENAPI.mkdir(parents=True, exist_ok=True)
(PUBLIC / "api-manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
for service in services:
(OPENAPI / f"{service['id']}.json").write_text(
json.dumps(openapi(service), ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
gateway_spec = OPENAPI / "gateway.json"
if not gateway_spec.exists():
(OPENAPI / "gateway.json").write_text(
(OPENAPI / "crypto-gateway.json").read_text(encoding="utf-8"), encoding="utf-8"
)
(OPENAPI / "crypto-gateway.json").unlink()
print(f"generated {manifest['total_endpoints']} endpoints")
if __name__ == "__main__":
main()