Files
2026-08-14 00:13:56 +05:00

513 lines
29 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()