513 lines
29 KiB
Python
513 lines
29 KiB
Python
#!/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()
|