commit 7ae997f99e730be3a7c42573959283e8246ce6f9 Author: bitheaven Date: Fri Aug 14 00:13:56 2026 +0500 first init diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3bd2bf7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM nginx:1.27-alpine + +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY public/ /usr/share/nginx/html/ + +EXPOSE 80 + +HEALTHCHECK --interval=15s --timeout=3s --start-period=3s --retries=3 \ + CMD wget -q -O /dev/null http://127.0.0.1/healthz || exit 1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..55ceb84 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# OVE API documentation + +Static public API reference served at `https://docs.ove.rs`. + +Regenerate the machine-readable catalog and OpenAPI documents: + +```sh +python3 scripts/generate.py +``` + +Verify that every public route in the Messenger and Crypto Gateway source is +documented: + +```sh +python3 scripts/verify.py +``` + +The verifier intentionally excludes Messenger `/internal/*` routes and the +Crypto Gateway browser console. Hosted checkout endpoints remain part of the +public reference. diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..78775d6 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,26 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location = /healthz { + access_log off; + default_type text/plain; + return 200 "ok\n"; + } + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(?:css|js|json|txt)$ { + expires 5m; + add_header Cache-Control "public, max-age=300"; + try_files $uri =404; + } + + add_header X-Content-Type-Options nosniff always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + add_header X-Frame-Options DENY always; +} diff --git a/public/api-manifest.json b/public/api-manifest.json new file mode 100644 index 0000000..79c95cc --- /dev/null +++ b/public/api-manifest.json @@ -0,0 +1,1247 @@ +{ + "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": 119, + "services": [ + { + "id": "messenger", + "name": "Messenger", + "base_url": "https://ms.ove.rs", + "description": "OVE Messenger user, bot, OAuth, files, calls and realtime updates API.", + "endpoints": [ + { + "method": "POST", + "path": "/register", + "group": "Аккаунт", + "auth": "public", + "auth_label": "public", + "title": "Регистрация аккаунта", + "description": "Регистрация аккаунта. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "body": { + "email": "you@example.com", + "password": "correct horse battery staple" + } + }, + { + "method": "POST", + "path": "/login", + "group": "Аккаунт", + "auth": "public", + "auth_label": "public", + "title": "Вход по паролю", + "description": "Вход по паролю. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "body": { + "email": "you@example.com", + "password": "correct horse battery staple" + } + }, + { + "method": "POST", + "path": "/auth/email/start", + "group": "Аккаунт", + "auth": "public", + "auth_label": "public", + "title": "Запрос email-кода", + "description": "Запрос email-кода. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "body": { + "email": "you@example.com" + } + }, + { + "method": "POST", + "path": "/auth/email/verify", + "group": "Аккаунт", + "auth": "public", + "auth_label": "public", + "title": "Подтверждение email-кода", + "description": "Подтверждение email-кода. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "body": { + "email": "you@example.com", + "code": "123456" + } + }, + { + "method": "GET", + "path": "/.well-known/openid-configuration", + "group": "OAuth", + "auth": "public", + "auth_label": "public", + "title": "OIDC Discovery", + "description": "OIDC Discovery. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/.well-known/oauth-authorization-server", + "group": "OAuth", + "auth": "public", + "auth_label": "public", + "title": "OAuth Server Metadata", + "description": "OAuth Server Metadata. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/oauth/jwks.json", + "group": "OAuth", + "auth": "public", + "auth_label": "public", + "title": "Публичные ключи OIDC", + "description": "Публичные ключи OIDC. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/oauth/device_authorization", + "group": "OAuth", + "auth": "oauth_client", + "auth_label": "OAuth client", + "title": "Запуск Device Flow", + "description": "Создает device_code, user_code и verification URI для универсального входа через Messenger." + }, + { + "method": "GET", + "path": "/oauth/device", + "group": "OAuth", + "auth": "public", + "auth_label": "public", + "title": "Страница подтверждения Device Flow", + "description": "Страница подтверждения Device Flow. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/oauth/token", + "group": "OAuth", + "auth": "oauth_client", + "auth_label": "OAuth client", + "title": "Обмен device code на токены", + "description": "Обменивает подтвержденный device_code на OIDC access/id token. Во время ожидания возвращает authorization_pending." + }, + { + "method": "GET", + "path": "/oauth/userinfo", + "group": "OAuth", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Профиль OIDC", + "description": "Профиль OIDC. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/oauth/device/request", + "group": "OAuth", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Данные запроса входа", + "description": "Данные запроса входа. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/oauth/device/decision", + "group": "OAuth", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Решение пользователя", + "description": "Решение пользователя. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/me", + "group": "Аккаунт", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Текущий пользователь", + "description": "Текущий пользователь. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/account/delete", + "group": "Аккаунт", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Удаление аккаунта", + "description": "Удаление аккаунта. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/username", + "group": "Аккаунт", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Установка username", + "description": "Установка username. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/name", + "group": "Аккаунт", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Изменение отображаемого имени", + "description": "Изменение отображаемого имени. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/profiles/description", + "group": "Аккаунт", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Описание профиля, бота или чата", + "description": "Описание профиля, бота или чата. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/privacy", + "group": "Аккаунт", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Настройки приватности", + "description": "Настройки приватности. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/contacts", + "group": "Контакты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Список контактов", + "description": "Список контактов. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/contacts/add", + "group": "Контакты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Добавление контакта", + "description": "Добавление контакта. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/contacts/delete", + "group": "Контакты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Удаление контакта", + "description": "Удаление контакта. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/groups", + "group": "Чаты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Создание группы", + "description": "Создание группы. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/channels", + "group": "Чаты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Создание канала", + "description": "Создание канала. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/chats/title", + "group": "Чаты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Изменение названия чата", + "description": "Изменение названия чата. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/channels/username", + "group": "Чаты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Username канала", + "description": "Username канала. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/channels/comments/settings", + "group": "Чаты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Настройки комментариев канала", + "description": "Настройки комментариев канала. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/channels/comments/send", + "group": "Чаты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Комментарий к публикации", + "description": "Комментарий к публикации. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/channels/comments", + "group": "Чаты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Комментарии к публикации", + "description": "Комментарии к публикации. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/chats/members/add", + "group": "Чаты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Добавление участника", + "description": "Добавление участника. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/chats/members/remove", + "group": "Чаты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Удаление участника", + "description": "Удаление участника. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/cloud-password", + "group": "Безопасность", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Облачный пароль", + "description": "Облачный пароль. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/cloud-password/reset", + "group": "Безопасность", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Сброс облачного пароля", + "description": "Сброс облачного пароля. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/sessions", + "group": "Безопасность", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Активные сессии", + "description": "Активные сессии. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/sessions/revoke", + "group": "Безопасность", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Завершение сессии", + "description": "Завершение сессии. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/sessions/revoke-others", + "group": "Безопасность", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Завершение других сессий", + "description": "Завершение других сессий. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/bots", + "group": "Боты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Создание бота", + "description": "Создание бота. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "body": { + "username": "shop_helper_bot", + "name": "Shop helper" + } + }, + { + "method": "POST", + "path": "/bots/token/reset", + "group": "Боты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Перевыпуск bot token", + "description": "Перевыпуск bot token. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/e2e/key", + "group": "E2E", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Публикация E2E-ключа", + "description": "Публикация E2E-ключа. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/e2e/key", + "group": "E2E", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Публичный E2E-ключ", + "description": "Публичный E2E-ключ. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/e2e/backup", + "group": "E2E", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Сохранение E2E-копии", + "description": "Сохранение E2E-копии. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/e2e/backup", + "group": "E2E", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Резервная копия E2E", + "description": "Резервная копия E2E. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/e2e/reset", + "group": "E2E", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Сброс E2E-ключей", + "description": "Сброс E2E-ключей. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/wallet", + "group": "Dastars", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Баланс Dastars", + "description": "Баланс Dastars. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/wallet/send", + "group": "Dastars", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Перевод Dastars", + "description": "Перевод Dastars. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/wallet/history", + "group": "Dastars", + "auth": "bearer", + "auth_label": "Bearer", + "title": "История Dastars", + "description": "История Dastars. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/call", + "group": "Звонки", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Создание звонка", + "description": "Создание звонка. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/voice-ticket", + "group": "Звонки", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Билет голосовой сессии", + "description": "Билет голосовой сессии. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/voice/participants", + "group": "Звонки", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Участники голосовой сессии", + "description": "Участники голосовой сессии. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/voice", + "group": "Звонки", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Голосовой WebSocket", + "description": "Переключает соединение на WebSocket после получения voice ticket." + }, + { + "method": "POST", + "path": "/send", + "group": "Сообщения", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Отправка текстового сообщения", + "description": "Отправляет сообщение без медиа. Для клиентской очереди используйте стабильный client_message_id.", + "body": { + "to": 42, + "text": "Привет!", + "client_message_id": "0190f6d4-example" + } + }, + { + "method": "POST", + "path": "/edit", + "group": "Сообщения", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Редактирование сообщения", + "description": "Редактирование сообщения. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "body": { + "chat_id": 42, + "message_id": 1001, + "text": "Исправленный текст" + } + }, + { + "method": "POST", + "path": "/callback", + "group": "Боты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Callback кнопки", + "description": "Callback кнопки. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/reactions", + "group": "Реакции", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Emoji-реакция", + "description": "Emoji-реакция. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "body": { + "chat_id": 42, + "message_id": 1001, + "emoji": "🔥" + } + }, + { + "method": "POST", + "path": "/reactions/paid", + "group": "Реакции", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Платная реакция Dastars", + "description": "Переводит указанное количество Dastars автору сообщения; для бота — его владельцу. amount может быть больше 1.", + "body": { + "chat_id": 42, + "message_id": 1001, + "amount": 5 + } + }, + { + "method": "POST", + "path": "/read", + "group": "Сообщения", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Отметка о прочтении", + "description": "Отметка о прочтении. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/delete", + "group": "Сообщения", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Удаление сообщения", + "description": "Удаление сообщения. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/favorite", + "group": "Сообщения", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Избранное сообщение", + "description": "Избранное сообщение. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/media/quote", + "group": "Файлы", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Расчет стоимости медиа", + "description": "Рассчитывает единую стоимость новых вложений: сумма байтов округляется вверх до MiB один раз на сообщение.", + "body": { + "media": [ + { + "client_id": "attachment-1", + "name": "photo.jpg", + "mime": "image/jpeg", + "size": 524288 + } + ] + } + }, + { + "method": "POST", + "path": "/messages/prepare", + "group": "Файлы", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Подготовка сообщения с медиа", + "description": "Атомарно резервирует оплату и выдает до 10 upload tickets. Текст и media[] опциональны по отдельности, но сообщение не может быть пустым.", + "body": { + "to": 42, + "text": "Caption", + "client_message_id": "0190f6d4-media", + "media": [ + { + "client_id": "attachment-1", + "name": "photo.jpg", + "mime": "image/jpeg", + "size": 524288 + } + ] + } + }, + { + "method": "POST", + "path": "/messages/commit", + "group": "Файлы", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Публикация сообщения с медиа", + "description": "Проверяет все загруженные вложения, одним платежом списывает DSR и публикует или редактирует одно сообщение.", + "body": { + "operation_id": "message-op-1-0190f6d4-media" + } + }, + { + "method": "POST", + "path": "/messages/cancel", + "group": "Файлы", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Отмена сообщения с медиа", + "description": "Удаляет незавершенные загрузки и отменяет резерв оплаты.", + "body": { + "operation_id": "message-op-1-0190f6d4-media" + } + }, + { + "method": "POST", + "path": "/forward", + "group": "Файлы", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Пересылка сообщения с медиа", + "description": "Пересылка сообщения с медиа. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "body": { + "message_id": 1001, + "to": 42, + "client_message_id": "0190f6d4-forward" + } + }, + { + "method": "GET", + "path": "/file/ticket", + "group": "Файлы", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Билет скачивания файла", + "description": "Билет скачивания файла. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/nodes/status", + "group": "Сервис", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Статус сервисных узлов", + "description": "Статус сервисных узлов. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/chats", + "group": "Чаты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Список чатов", + "description": "Список чатов. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/chats/delete", + "group": "Чаты", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Удаление чата", + "description": "Удаление чата. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/users/ban", + "group": "Модерация", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Блокировка пользователя", + "description": "Блокировка пользователя. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/users/unban", + "group": "Модерация", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Разблокировка пользователя", + "description": "Разблокировка пользователя. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/history", + "group": "Сообщения", + "auth": "bearer", + "auth_label": "Bearer", + "title": "История сообщений", + "description": "История сообщений. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/updates", + "group": "Updates", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Очередь обновлений", + "description": "Long polling событий клиента или бота. Bot updates долговечны и остаются до явного ACK." + }, + { + "method": "POST", + "path": "/updates/ack", + "group": "Updates", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Подтверждение обновлений", + "description": "Подтверждение обновлений. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "body": { + "update_id": 2048 + } + }, + { + "method": "GET", + "path": "/file/{id}", + "group": "Файлы", + "auth": "bearer", + "auth_label": "Bearer", + "title": "Скачивание файла", + "description": "Скачивание файла. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/health/live", + "group": "Сервис", + "auth": "public", + "auth_label": "public", + "title": "Проверка процесса Messenger", + "description": "Проверка процесса Messenger. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/health/ready", + "group": "Сервис", + "auth": "public", + "auth_label": "public", + "title": "Готовность Messenger", + "description": "Готовность Messenger. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/metrics", + "group": "Сервис", + "auth": "public", + "auth_label": "public", + "title": "Метрики Messenger", + "description": "Метрики Messenger. Возвращает JSON-ответ или стандартную JSON-ошибку." + } + ] + }, + { + "id": "gateway", + "name": "Crypto Gateway", + "base_url": "https://cr.ove.rs", + "description": "OVE multi-merchant cryptocurrency payments, checkout and operator API.", + "endpoints": [ + { + "method": "GET", + "path": "/health", + "group": "Сервис", + "auth": "public", + "auth_label": "public", + "title": "Состояние Gateway", + "description": "Состояние Gateway. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/v1/assets", + "group": "Сервис", + "auth": "public", + "auth_label": "public", + "title": "Доступные активы", + "description": "Возвращает только пары asset/network, для которых wallet seed готов к работе." + }, + { + "method": "GET", + "path": "/v1/invoices", + "group": "Invoices", + "auth": "hmac", + "auth_label": "HMAC", + "title": "Список invoices", + "description": "Создает invoice или возвращает историю с cursor pagination, в зависимости от HTTP-метода." + }, + { + "method": "POST", + "path": "/v1/invoices", + "group": "Invoices", + "auth": "hmac", + "auth_label": "HMAC", + "title": "Создание invoice", + "description": "Создает invoice или возвращает историю с cursor pagination, в зависимости от HTTP-метода.", + "body": { + "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" + } + } + }, + { + "method": "GET", + "path": "/v1/invoices/{invoice_id}", + "group": "Invoices", + "auth": "hmac", + "auth_label": "HMAC", + "title": "Получение invoice", + "description": "Получение invoice. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/v1/withdrawals", + "group": "Withdrawals", + "auth": "hmac", + "auth_label": "HMAC", + "title": "Список withdrawals", + "description": "Создает withdrawal или возвращает историю. Для token assets разрешен только fee_mode=additional." + }, + { + "method": "POST", + "path": "/v1/withdrawals", + "group": "Withdrawals", + "auth": "hmac", + "auth_label": "HMAC", + "title": "Создание withdrawal", + "description": "Создает withdrawal или возвращает историю. Для token assets разрешен только fee_mode=additional.", + "body": { + "asset": "USDT", + "network": "tron", + "amount_atomic": "10000000", + "address": "T...", + "fee_mode": "additional", + "metadata": { + "payout": "p-123" + } + } + }, + { + "method": "GET", + "path": "/v1/withdrawals/{withdrawal_id}", + "group": "Withdrawals", + "auth": "hmac", + "auth_label": "HMAC", + "title": "Получение withdrawal", + "description": "Получение withdrawal. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/v1/balances", + "group": "Баланс", + "auth": "hmac", + "auth_label": "HMAC", + "title": "Merchant-балансы", + "description": "Merchant-балансы. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/v1/api-keys", + "group": "Ключи", + "auth": "hmac", + "auth_label": "HMAC", + "title": "Список API-ключей", + "description": "Список API-ключей. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/admin/v1/status", + "group": "Operator", + "auth": "admin", + "auth_label": "Admin Bearer", + "title": "Операторский статус", + "description": "Операторский статус. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/admin/v1/callbacks", + "group": "Operator", + "auth": "admin", + "auth_label": "Admin Bearer", + "title": "Инциденты callbacks", + "description": "Инциденты callbacks. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/admin/v1/consolidations", + "group": "Operator", + "auth": "admin", + "auth_label": "Admin Bearer", + "title": "Инциденты консолидации", + "description": "Инциденты консолидации. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/admin/v1/callbacks/{callback_id}/retry", + "group": "Operator", + "auth": "admin", + "auth_label": "Admin Bearer", + "title": "Повтор callback", + "description": "Повтор callback. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/admin/v1/merchants", + "group": "Operator", + "auth": "admin", + "auth_label": "Admin Bearer", + "title": "Создание merchant", + "description": "Создание merchant. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "body": { + "name": "Example shop", + "callback_url": "https://merchant.example/payments", + "callback_hosts": [ + "merchant.example" + ] + } + }, + { + "method": "POST", + "path": "/admin/v1/fee-policies", + "group": "Operator", + "auth": "admin", + "auth_label": "Admin Bearer", + "title": "Глобальная fee policy", + "description": "Глобальная fee policy. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "body": { + "asset": "USDT", + "network": "tron", + "platform_fee_basis_points": 500, + "platform_fee_cap_atomic": "10000000" + } + }, + { + "method": "POST", + "path": "/admin/v1/merchants/{merchant_id}/api-keys", + "group": "Operator", + "auth": "admin", + "auth_label": "Admin Bearer", + "title": "Выпуск merchant API-ключа", + "description": "Выпуск merchant API-ключа. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "DELETE", + "path": "/admin/v1/merchants/{merchant_id}/api-keys/{api_key_id}", + "group": "Operator", + "auth": "admin", + "auth_label": "Admin Bearer", + "title": "Отзыв merchant API-ключа", + "description": "Отзыв merchant API-ключа. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/admin/v1/merchants/{merchant_id}/policies", + "group": "Operator", + "auth": "admin", + "auth_label": "Admin Bearer", + "title": "Merchant policy", + "description": "Merchant policy. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/admin/v1/withdrawals/{withdrawal_id}/resolve", + "group": "Operator", + "auth": "admin", + "auth_label": "Admin Bearer", + "title": "Решение manual review", + "description": "Решение manual review. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/admin/v1/consolidations/{consolidation_id}/retry", + "group": "Operator", + "auth": "admin", + "auth_label": "Admin Bearer", + "title": "Повтор консолидации", + "description": "Повтор консолидации. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/pay/{token}", + "group": "Checkout", + "auth": "public", + "auth_label": "public", + "title": "Hosted checkout", + "description": "Публичная HTML-страница оплаты с QR, серверно-синхронизированным таймером и live status." + }, + { + "method": "GET", + "path": "/pay/{token}/status", + "group": "Checkout", + "auth": "public", + "auth_label": "public", + "title": "Статус hosted checkout", + "description": "JSON-состояние checkout для polling; включает серверное время и срок действия." + } + ] + }, + { + "id": "donations", + "name": "Donations", + "base_url": "https://do.ove.rs", + "description": "OVE donation pages, Messenger login, Crypto Gateway payments and OBS alerts.", + "endpoints": [ + { + "method": "GET", + "path": "/health", + "group": "Сервис", + "auth": "public", + "auth_label": "public", + "title": "Состояние Gateway", + "description": "Состояние Gateway. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/api/assets", + "group": "Сервис", + "auth": "public", + "auth_label": "public", + "title": "Активы для донатов", + "description": "Активы для донатов. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/api/auth/start", + "group": "Авторизация", + "auth": "public", + "auth_label": "public", + "title": "Вход стримера через Messenger", + "description": "Вход стримера через Messenger. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "body": { + "email": "streamer@example.com" + } + }, + { + "method": "GET", + "path": "/api/auth/poll/{flow}", + "group": "Авторизация", + "auth": "public", + "auth_label": "public", + "title": "Статус входа стримера", + "description": "Статус входа стримера. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/api/logout", + "group": "Авторизация", + "auth": "session", + "auth_label": "Secure session", + "title": "Выход из Donation Service", + "description": "Выход из Donation Service. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/api/me", + "group": "Стример", + "auth": "session", + "auth_label": "Secure session", + "title": "Панель стримера", + "description": "Панель стримера. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "PATCH", + "path": "/api/profile", + "group": "Стример", + "auth": "session", + "auth_label": "Secure session", + "title": "Настройки страницы и OBS", + "description": "Настройки страницы и OBS. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "body": { + "display_name": "My channel", + "slug": "my-channel", + "accent_color": "#b9f541", + "duration_seconds": 8, + "show_donor_name": true, + "show_message": true + } + }, + { + "method": "PUT", + "path": "/api/sound", + "group": "OBS", + "auth": "session", + "auth_label": "Secure session", + "title": "Загрузка звука", + "description": "Загрузка звука. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "DELETE", + "path": "/api/sound", + "group": "OBS", + "auth": "session", + "auth_label": "Secure session", + "title": "Удаление звука", + "description": "Удаление звука. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/api/widget/rotate", + "group": "OBS", + "auth": "session", + "auth_label": "Secure session", + "title": "Ротация секретной OBS-ссылки", + "description": "Ротация секретной OBS-ссылки. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/api/widget/test", + "group": "OBS", + "auth": "session", + "auth_label": "Secure session", + "title": "Тестовый OBS alert", + "description": "Тестовый OBS alert. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/api/widget/{token}/config", + "group": "OBS", + "auth": "widget", + "auth_label": "Widget token", + "title": "Конфигурация OBS-виджета", + "description": "Конфигурация OBS-виджета. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "GET", + "path": "/api/widget/{token}/events", + "group": "OBS", + "auth": "widget", + "auth_label": "Widget token", + "title": "SSE-события OBS-виджета", + "description": "Долгоживущий SSE-поток для OBS Browser Source. Виджет сам ставит alerts в очередь." + }, + { + "method": "GET", + "path": "/api/public/creators/{slug}", + "group": "Донаты", + "auth": "public", + "auth_label": "public", + "title": "Публичная страница стримера", + "description": "Публичная страница стримера. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/api/public/donations", + "group": "Донаты", + "auth": "public", + "auth_label": "public", + "title": "Создание доната", + "description": "Создает Gateway invoice с комиссией 5%. cover_fee=false удерживает комиссию из суммы; cover_fee=true точно увеличивает платеж так, чтобы стример получил введенную сумму после комиссии. Все расчеты выполняются в atomic без float.", + "body": { + "slug": "my-channel", + "donor_name": "Anonymous", + "message": "Great stream!", + "asset": "USDT", + "network": "tron", + "amount": "25.5", + "cover_fee": true + } + }, + { + "method": "GET", + "path": "/api/public/donations/{token}/status", + "group": "Донаты", + "auth": "public", + "auth_label": "public", + "title": "Статус доната", + "description": "Статус доната. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "POST", + "path": "/api/withdrawals", + "group": "Вывод", + "auth": "session", + "auth_label": "Secure session", + "title": "Запрос вывода", + "description": "Резервирует баланс и запускает новый Messenger Device Flow с описанием суммы и адреса.", + "body": { + "asset": "USDT", + "network": "tron", + "amount": "10", + "address": "TExampleRecipientAddress" + } + }, + { + "method": "GET", + "path": "/api/withdrawals/{flow}/poll", + "group": "Вывод", + "auth": "session", + "auth_label": "Secure session", + "title": "Подтверждение вывода через Messenger", + "description": "После свайпа тем же Messenger-пользователем идемпотентно отправляет withdrawal в Gateway." + }, + { + "method": "GET", + "path": "/api/admin/status", + "group": "Оператор", + "auth": "session", + "auth_label": "Secure session", + "title": "Статус Donation Service", + "description": "Статус Donation Service. Возвращает JSON-ответ или стандартную JSON-ошибку." + }, + { + "method": "PATCH", + "path": "/api/admin/creators/{creator_id}/blocked", + "group": "Оператор", + "auth": "session", + "auth_label": "Secure session", + "title": "Блокировка страницы стримера", + "description": "Блокировка страницы стримера. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "body": { + "blocked": true + } + } + ] + } + ] +} diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..28f88ec --- /dev/null +++ b/public/app.js @@ -0,0 +1,142 @@ +(() => { + "use strict"; + + const list = document.querySelector("#endpoint-list"); + const template = document.querySelector("#endpoint-template"); + const search = document.querySelector("#search"); + const filters = document.querySelector("#service-filters"); + const count = document.querySelector("#endpoint-count"); + const empty = document.querySelector("#empty"); + const sidebar = document.querySelector("#sidebar"); + const menu = document.querySelector("#menu"); + let manifest; + let activeService = "all"; + + const escapeHtml = (value) => String(value) + .replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); + + function curlFor(service, endpoint) { + const url = `${service.base_url}${endpoint.path.replaceAll(/{([^}]+)}/g, "<$1>")}`; + const lines = [`curl -X ${endpoint.method} '${url}'`]; + if (endpoint.auth === "bearer" || endpoint.auth === "admin") { + lines.push(" -H 'Authorization: Bearer $TOKEN'"); + } else if (endpoint.auth === "hmac") { + lines.push(" -H 'X-Api-Key: $API_KEY'", + " -H 'X-Api-Timestamp: $TIMESTAMP'", + " -H 'X-Api-Nonce: $UUID'", + " -H 'X-Api-Signature: $SIGNATURE'"); + if (endpoint.method !== "GET") lines.push(" -H 'Idempotency-Key: $UUID'"); + } + if (endpoint.body) { + lines.push(" -H 'Content-Type: application/json'", + ` -d '${JSON.stringify(endpoint.body)}'`); + } + return lines.join(" \\\n"); + } + + function endpointId(service, endpoint) { + return `${service.id}-${endpoint.method.toLowerCase()}-${endpoint.path + .replace(/[{}]/g, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "")}`; + } + + function render() { + const query = search.value.trim().toLowerCase(); + list.replaceChildren(); + let shown = 0; + manifest.services.forEach((service) => { + service.endpoints.forEach((endpoint) => { + const haystack = `${service.name} ${endpoint.method} ${endpoint.path} ${endpoint.title} ${endpoint.description} ${endpoint.group}`.toLowerCase(); + if ((activeService !== "all" && activeService !== service.id) || !haystack.includes(query)) return; + shown += 1; + const node = template.content.firstElementChild.cloneNode(true); + node.id = endpointId(service, endpoint); + node.dataset.service = service.id; + const summary = node.querySelector(".endpoint-summary"); + const method = node.querySelector(".method"); + method.textContent = endpoint.method; + method.classList.add(endpoint.method); + node.querySelector(".path").textContent = endpoint.path; + node.querySelector(".endpoint-title").textContent = endpoint.title; + node.querySelector(".auth-badge").textContent = endpoint.auth_label; + node.querySelector(".endpoint-description").textContent = endpoint.description; + node.querySelector(".endpoint-meta").innerHTML = + `${escapeHtml(service.base_url)}${escapeHtml(endpoint.group)}`; + const curl = curlFor(service, endpoint); + node.querySelector(".endpoint-example").innerHTML = + `
cURL
${escapeHtml(curl)}
`; + summary.addEventListener("click", () => { + const open = node.classList.toggle("open"); + summary.setAttribute("aria-expanded", String(open)); + if (open) history.replaceState(null, "", `#${node.id}`); + }); + node.querySelector(".copy").addEventListener("click", (event) => copyText(event.currentTarget, curl)); + list.append(node); + }); + }); + count.textContent = `${shown} / ${manifest.total_endpoints}`; + empty.hidden = shown !== 0; + } + + function copyText(button, text) { + navigator.clipboard.writeText(text).then(() => { + const old = button.textContent; + button.textContent = "Скопировано"; + window.setTimeout(() => { button.textContent = old; }, 1300); + }); + } + + function makeFilters() { + const choices = [["all", "Все"], ...manifest.services.map((service) => [service.id, service.name])]; + choices.forEach(([id, label]) => { + const button = document.createElement("button"); + button.className = `filter${id === "all" ? " active" : ""}`; + button.textContent = label; + button.addEventListener("click", () => { + activeService = id; + filters.querySelectorAll(".filter").forEach((item) => item.classList.toggle("active", item === button)); + render(); + }); + filters.append(button); + }); + } + + document.querySelectorAll("[data-copy-target]").forEach((button) => { + button.addEventListener("click", () => copyText(button, document.querySelector(`#${button.dataset.copyTarget}`).innerText)); + }); + search.addEventListener("input", render); + document.addEventListener("keydown", (event) => { + if (event.key === "/" && document.activeElement !== search) { + event.preventDefault(); + search.focus(); + } + if (event.key === "Escape") sidebar.classList.remove("open"); + }); + menu.addEventListener("click", () => { + const open = sidebar.classList.toggle("open"); + menu.setAttribute("aria-expanded", String(open)); + }); + sidebar.querySelectorAll("a").forEach((link) => link.addEventListener("click", () => sidebar.classList.remove("open"))); + + fetch("/api-manifest.json") + .then((response) => { + if (!response.ok) throw new Error(`manifest ${response.status}`); + return response.json(); + }) + .then((data) => { + manifest = data; + makeFilters(); + render(); + if (location.hash.startsWith("#messenger-") || location.hash.startsWith("#gateway-")) { + const target = document.querySelector(location.hash); + if (target) { + target.classList.add("open"); + target.querySelector(".endpoint-summary").setAttribute("aria-expanded", "true"); + target.scrollIntoView(); + } + } + }) + .catch(() => { + empty.hidden = false; + empty.textContent = "Не удалось загрузить API catalog."; + }); +})(); diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..a79ba3f --- /dev/null +++ b/public/index.html @@ -0,0 +1,209 @@ + + + + + + + OVE API Reference + + + + + + + +
+
+ + + OpenAPI +
+ +
+
PUBLIC API · OVE.RS
+

Один вход.
Все сервисы OVE.

+

Документация публичных HTTP API Messenger, Crypto Gateway и Donation Service: авторизация, сообщения, боты, платежи, OBS alerts, webhooks и операторские методы.

+ +
+
M
Messengerhttps://ms.ove.rs
REST · OAuth · WS
+
C
Crypto Gatewayhttps://cr.ove.rs
REST · HMAC
+
D
Donationshttps://do.ove.rs
REST · SSE · OBS
+
+
+ +
+ +

Первый запрос

+

Все ответы API — JSON, если endpoint явно не обозначен как HTML, WebSocket или файл. Для Messenger получите токен через email-код, затем передавайте его в заголовке Bearer.

+
+
Email login
+
curl -X POST https://ms.ove.rs/auth/email/start \
+  -H 'Content-Type: application/json' \
+  -d '{"email":"you@example.com"}'
+
+curl -X POST https://ms.ove.rs/auth/email/verify \
+  -H 'Content-Type: application/json' \
+  -d '{"email":"you@example.com","code":"123456"}'
+
+
HTTPS в тестовом полигоне. Сертификаты выпущены локальным CA OVE. Добавьте CA в доверенные на устройстве; не отключайте проверку TLS в production-клиенте.
+
+ +
+ +

Три схемы авторизации

+
+
01

Messenger Bearer

Пользовательский или bot token передается как Authorization: Bearer <token>.

+
02

Gateway HMAC

Каждый merchant-запрос подписывается ключом, timestamp и одноразовым UUID nonce.

+
03

Operator Bearer

/admin/v1/* принимает отдельный секрет GATEWAY_ADMIN_TOKEN.

+
+

Каноническая строка Gateway

+
timestamp + "\n" +
+nonce + "\n" +
+HTTP_METHOD + "\n" +
+path_and_query + "\n" +
+hex(sha256(raw_request_body))
+

Результат подпишите HMAC-SHA256 и отправьте lowercase hex в X-Api-Signature. Также обязательны X-Api-Key, X-Api-Timestamp, X-Api-Nonce; для изменяющих запросов — Idempotency-Key.

+
+ +
+ +

Клиенты, сообщения и real-time

+

HTTP API покрывает аккаунты, чаты, E2E-ключи, файлы, голосовые сессии, Dastars и очередь обновлений. Native Android транспорт MST5 работает на ms.ove.rs:8080: после защищённого handshake он передаёт CBOR-команды и мультиплексированные ответы внутри зашифрованного потока.

+
+
Updateslong polling + ACK
+
VoiceWebSocket ticket
+
E2Eне зависит от username
+
Filesmultipart upload
+
+
+ +
+ +

Bot API

+

Создайте бота через проверенного botfather или POST /bots. Bot token использует ту же Bearer-схему. Обновления хранятся до подтверждения через POST /updates/ack.

+
+

Кнопки

url, callback и pay_dsr; не более 12 кнопок, текст до 64 символов.

+

Реакции

Бот получает callback для обычных и платных реакций. Пакет платной реакции может содержать amount > 1.

+
+
+ +
+ +

OAuth 2.0 Device Flow + OIDC

+

Сервис запрашивает device code у Messenger, показывает QR или user code, а пользователь подтверждает вход в Android-клиенте. Токен обменивается только после решения пользователя.

+
1 · device_authorization2 · QR / code3 · approve4 · token
+
+ +
+ +

Crypto Gateway

+

Создавайте invoice и withdrawal, сверяйте баланс и принимайте подписанные callbacks. Денежные величины всегда передаются строками в атомарных единицах — без float.

+
+
AssetNetworkDecimals
+
BTCbitcoin8
+
LTClitecoin8
+
TONton9
+
TRXtron6
+
USDTton / tron6
+
+
Hosted checkout. URL из ответа invoice открывается как /pay/{token}; JSON-статус доступен в /pay/{token}/status. Таймер оплаты синхронизирован с серверным временем.
+
+ +
+ +

Подпись callbacks

+
signature = hex(HMAC-SHA256(
+  webhook_secret,
+  timestamp + "." + event_id + "." + raw_body
+))
+

Проверяйте X-Gateway-Timestamp, X-Gateway-Event-Id и X-Gateway-Signature. Доставка at-least-once повторяется до 24 часов — дедуплицируйте события по event_id.

+
+ +
+ +

Страница доната и OBS Browser Source

+

Стример входит через Messenger, получает публичный адрес do.ove.rs/u/{slug} и секретную ссылку виджета. Доноры авторизоваться не обязаны: Donation Service создаёт invoice в Crypto Gateway и показывает его hosted checkout.

+
1 · public donation2 · Gateway checkout3 · signed webhook4 · OBS SSE alert
+
Вывод средств. Каждый запрос резервирует доступный баланс и отдельно подтверждается свайпом в Messenger. В окне подтверждения показаны актив, сумма, сеть и сокращённый адрес.
+
+ +
+ +

Все endpoints

+
+
+ +
+
+ +
+ +
+ +

Ошибки и повторные запросы

+
+
400Неверные поля или состояние
+
401Нет или неверна авторизация
+
403Недостаточно прав
+
404Объект не найден
+
409Конфликт / повтор
+
429Превышен лимит
+
5xxВременная ошибка сервиса
+
+

Для безопасного повтора merchant POST используйте тот же Idempotency-Key. Не повторяйте запрос с новым ключом, пока результат предыдущего неизвестен.

+
+ +
+ OVE API · public contract + Internal wallet gRPC и системные bot nodes не публикуются. +
+
+ + + + diff --git a/public/llms.txt b/public/llms.txt new file mode 100644 index 0000000..477427f --- /dev/null +++ b/public/llms.txt @@ -0,0 +1,16 @@ +# OVE public API + +Canonical human documentation: https://docs.ove.rs/ +Messenger OpenAPI: https://docs.ove.rs/openapi/messenger.json +Crypto Gateway OpenAPI: https://docs.ove.rs/openapi/gateway.json +Donation Service OpenAPI: https://docs.ove.rs/openapi/donations.json + +Public services: +- Messenger: https://ms.ove.rs +- Crypto Gateway: https://cr.ove.rs +- Donation Service: https://do.ove.rs + +Messenger uses Bearer tokens for users and bots. Crypto Gateway merchant API +uses request-specific HMAC-SHA256 signatures. Gateway operator API uses a +Bearer admin token. Amounts in Crypto Gateway are decimal strings in atomic +units. diff --git a/public/openapi/donations.json b/public/openapi/donations.json new file mode 100644 index 0000000..a6cab38 --- /dev/null +++ b/public/openapi/donations.json @@ -0,0 +1,660 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OVE Donations API", + "version": "1.0.0", + "description": "OVE donation pages, Messenger login, Crypto Gateway payments and OBS alerts." + }, + "servers": [ + { + "url": "https://do.ove.rs" + } + ], + "paths": { + "/health": { + "get": { + "tags": [ + "Сервис" + ], + "summary": "Состояние Gateway", + "description": "Состояние Gateway. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_health", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + } + } + }, + "/api/assets": { + "get": { + "tags": [ + "Сервис" + ], + "summary": "Активы для донатов", + "description": "Активы для донатов. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_api_assets", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + } + } + }, + "/api/auth/start": { + "post": { + "tags": [ + "Авторизация" + ], + "summary": "Вход стримера через Messenger", + "description": "Вход стримера через Messenger. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_api_auth_start", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "email": "streamer@example.com" + } + } + } + } + } + }, + "/api/auth/poll/{flow}": { + "get": { + "tags": [ + "Авторизация" + ], + "summary": "Статус входа стримера", + "description": "Статус входа стримера. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_api_auth_poll_flow", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "parameters": [ + { + "name": "flow", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/logout": { + "post": { + "tags": [ + "Авторизация" + ], + "summary": "Выход из Donation Service", + "description": "Выход из Donation Service. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_api_logout", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "sessionCookie": [] + } + ] + } + }, + "/api/me": { + "get": { + "tags": [ + "Стример" + ], + "summary": "Панель стримера", + "description": "Панель стримера. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_api_me", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "sessionCookie": [] + } + ] + } + }, + "/api/profile": { + "patch": { + "tags": [ + "Стример" + ], + "summary": "Настройки страницы и OBS", + "description": "Настройки страницы и OBS. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "patch_api_profile", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "sessionCookie": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "display_name": "My channel", + "slug": "my-channel", + "accent_color": "#b9f541", + "duration_seconds": 8, + "show_donor_name": true, + "show_message": true + } + } + } + } + } + }, + "/api/sound": { + "put": { + "tags": [ + "OBS" + ], + "summary": "Загрузка звука", + "description": "Загрузка звука. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "put_api_sound", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "sessionCookie": [] + } + ] + }, + "delete": { + "tags": [ + "OBS" + ], + "summary": "Удаление звука", + "description": "Удаление звука. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "delete_api_sound", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "sessionCookie": [] + } + ] + } + }, + "/api/widget/rotate": { + "post": { + "tags": [ + "OBS" + ], + "summary": "Ротация секретной OBS-ссылки", + "description": "Ротация секретной OBS-ссылки. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_api_widget_rotate", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "sessionCookie": [] + } + ] + } + }, + "/api/widget/test": { + "post": { + "tags": [ + "OBS" + ], + "summary": "Тестовый OBS alert", + "description": "Тестовый OBS alert. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_api_widget_test", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "sessionCookie": [] + } + ] + } + }, + "/api/widget/{token}/config": { + "get": { + "tags": [ + "OBS" + ], + "summary": "Конфигурация OBS-виджета", + "description": "Конфигурация OBS-виджета. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_api_widget_token_config", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/widget/{token}/events": { + "get": { + "tags": [ + "OBS" + ], + "summary": "SSE-события OBS-виджета", + "description": "Долгоживущий SSE-поток для OBS Browser Source. Виджет сам ставит alerts в очередь.", + "operationId": "get_api_widget_token_events", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/public/creators/{slug}": { + "get": { + "tags": [ + "Донаты" + ], + "summary": "Публичная страница стримера", + "description": "Публичная страница стримера. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_api_public_creators_slug", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/public/donations": { + "post": { + "tags": [ + "Донаты" + ], + "summary": "Создание доната", + "description": "Создает Gateway invoice с комиссией 5%. cover_fee=false удерживает комиссию из суммы; cover_fee=true точно увеличивает платеж так, чтобы стример получил введенную сумму после комиссии. Все расчеты выполняются в atomic без float.", + "operationId": "post_api_public_donations", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "slug": "my-channel", + "donor_name": "Anonymous", + "message": "Great stream!", + "asset": "USDT", + "network": "tron", + "amount": "25.5", + "cover_fee": true + } + } + } + } + } + }, + "/api/public/donations/{token}/status": { + "get": { + "tags": [ + "Донаты" + ], + "summary": "Статус доната", + "description": "Статус доната. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_api_public_donations_token_status", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/withdrawals": { + "post": { + "tags": [ + "Вывод" + ], + "summary": "Запрос вывода", + "description": "Резервирует баланс и запускает новый Messenger Device Flow с описанием суммы и адреса.", + "operationId": "post_api_withdrawals", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "sessionCookie": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "asset": "USDT", + "network": "tron", + "amount": "10", + "address": "TExampleRecipientAddress" + } + } + } + } + } + }, + "/api/withdrawals/{flow}/poll": { + "get": { + "tags": [ + "Вывод" + ], + "summary": "Подтверждение вывода через Messenger", + "description": "После свайпа тем же Messenger-пользователем идемпотентно отправляет withdrawal в Gateway.", + "operationId": "get_api_withdrawals_flow_poll", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "sessionCookie": [] + } + ], + "parameters": [ + { + "name": "flow", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/admin/status": { + "get": { + "tags": [ + "Оператор" + ], + "summary": "Статус Donation Service", + "description": "Статус Donation Service. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_api_admin_status", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "sessionCookie": [] + } + ] + } + }, + "/api/admin/creators/{creator_id}/blocked": { + "patch": { + "tags": [ + "Оператор" + ], + "summary": "Блокировка страницы стримера", + "description": "Блокировка страницы стримера. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "patch_api_admin_creators_creator_id_blocked", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "sessionCookie": [] + } + ], + "parameters": [ + { + "name": "creator_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "blocked": true + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "sessionCookie": { + "type": "apiKey", + "in": "cookie", + "name": "ove_donations_session", + "description": "HttpOnly session; changing requests also require X-CSRF-Token." + } + }, + "schemas": { + "Error": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } +} diff --git a/public/openapi/gateway.json b/public/openapi/gateway.json new file mode 100644 index 0000000..282bca2 --- /dev/null +++ b/public/openapi/gateway.json @@ -0,0 +1,1012 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OVE Crypto Gateway API", + "version": "1.0.0", + "description": "OVE multi-merchant cryptocurrency payments, checkout and operator API." + }, + "servers": [ + { + "url": "https://cr.ove.rs" + } + ], + "paths": { + "/health": { + "get": { + "tags": [ + "Сервис" + ], + "summary": "Состояние Gateway", + "description": "Состояние Gateway. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_health", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + } + } + }, + "/v1/assets": { + "get": { + "tags": [ + "Сервис" + ], + "summary": "Доступные активы", + "description": "Возвращает только пары asset/network, для которых wallet seed готов к работе.", + "operationId": "get_v1_assets", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + } + } + }, + "/v1/invoices": { + "get": { + "tags": [ + "Invoices" + ], + "summary": "Список invoices", + "description": "Создает invoice или возвращает историю с cursor pagination, в зависимости от HTTP-метода.", + "operationId": "get_v1_invoices", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "merchantHmac": [] + } + ], + "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" + } + } + ] + }, + "post": { + "tags": [ + "Invoices" + ], + "summary": "Создание invoice", + "description": "Создает invoice или возвращает историю с cursor pagination, в зависимости от HTTP-метода.", + "operationId": "post_v1_invoices", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "merchantHmac": [] + } + ], + "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" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "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" + } + } + } + } + } + } + }, + "/v1/invoices/{invoice_id}": { + "get": { + "tags": [ + "Invoices" + ], + "summary": "Получение invoice", + "description": "Получение invoice. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_v1_invoices_invoice_id", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "merchantHmac": [] + } + ], + "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" + } + }, + { + "name": "invoice_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/v1/withdrawals": { + "get": { + "tags": [ + "Withdrawals" + ], + "summary": "Список withdrawals", + "description": "Создает withdrawal или возвращает историю. Для token assets разрешен только fee_mode=additional.", + "operationId": "get_v1_withdrawals", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "merchantHmac": [] + } + ], + "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" + } + } + ] + }, + "post": { + "tags": [ + "Withdrawals" + ], + "summary": "Создание withdrawal", + "description": "Создает withdrawal или возвращает историю. Для token assets разрешен только fee_mode=additional.", + "operationId": "post_v1_withdrawals", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "merchantHmac": [] + } + ], + "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" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "asset": "USDT", + "network": "tron", + "amount_atomic": "10000000", + "address": "T...", + "fee_mode": "additional", + "metadata": { + "payout": "p-123" + } + } + } + } + } + } + }, + "/v1/withdrawals/{withdrawal_id}": { + "get": { + "tags": [ + "Withdrawals" + ], + "summary": "Получение withdrawal", + "description": "Получение withdrawal. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_v1_withdrawals_withdrawal_id", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "merchantHmac": [] + } + ], + "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" + } + }, + { + "name": "withdrawal_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/v1/balances": { + "get": { + "tags": [ + "Баланс" + ], + "summary": "Merchant-балансы", + "description": "Merchant-балансы. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_v1_balances", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "merchantHmac": [] + } + ], + "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" + } + } + ] + } + }, + "/v1/api-keys": { + "get": { + "tags": [ + "Ключи" + ], + "summary": "Список API-ключей", + "description": "Список API-ключей. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_v1_api_keys", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "merchantHmac": [] + } + ], + "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" + } + } + ] + } + }, + "/admin/v1/status": { + "get": { + "tags": [ + "Operator" + ], + "summary": "Операторский статус", + "description": "Операторский статус. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_admin_v1_status", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "adminBearer": [] + } + ] + } + }, + "/admin/v1/callbacks": { + "get": { + "tags": [ + "Operator" + ], + "summary": "Инциденты callbacks", + "description": "Инциденты callbacks. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_admin_v1_callbacks", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "adminBearer": [] + } + ] + } + }, + "/admin/v1/consolidations": { + "get": { + "tags": [ + "Operator" + ], + "summary": "Инциденты консолидации", + "description": "Инциденты консолидации. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_admin_v1_consolidations", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "adminBearer": [] + } + ] + } + }, + "/admin/v1/callbacks/{callback_id}/retry": { + "post": { + "tags": [ + "Operator" + ], + "summary": "Повтор callback", + "description": "Повтор callback. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_admin_v1_callbacks_callback_id_retry", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "callback_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/admin/v1/merchants": { + "post": { + "tags": [ + "Operator" + ], + "summary": "Создание merchant", + "description": "Создание merchant. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_admin_v1_merchants", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "adminBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "name": "Example shop", + "callback_url": "https://merchant.example/payments", + "callback_hosts": [ + "merchant.example" + ] + } + } + } + } + } + }, + "/admin/v1/fee-policies": { + "post": { + "tags": [ + "Operator" + ], + "summary": "Глобальная fee policy", + "description": "Глобальная fee policy. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_admin_v1_fee_policies", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "adminBearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "asset": "USDT", + "network": "tron", + "platform_fee_basis_points": 500, + "platform_fee_cap_atomic": "10000000" + } + } + } + } + } + }, + "/admin/v1/merchants/{merchant_id}/api-keys": { + "post": { + "tags": [ + "Operator" + ], + "summary": "Выпуск merchant API-ключа", + "description": "Выпуск merchant API-ключа. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_admin_v1_merchants_merchant_id_api_keys", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "merchant_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/admin/v1/merchants/{merchant_id}/api-keys/{api_key_id}": { + "delete": { + "tags": [ + "Operator" + ], + "summary": "Отзыв merchant API-ключа", + "description": "Отзыв merchant API-ключа. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "delete_admin_v1_merchants_merchant_id_api_keys_api_key_id", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "merchant_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "api_key_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/admin/v1/merchants/{merchant_id}/policies": { + "post": { + "tags": [ + "Operator" + ], + "summary": "Merchant policy", + "description": "Merchant policy. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_admin_v1_merchants_merchant_id_policies", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "merchant_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/admin/v1/withdrawals/{withdrawal_id}/resolve": { + "post": { + "tags": [ + "Operator" + ], + "summary": "Решение manual review", + "description": "Решение manual review. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_admin_v1_withdrawals_withdrawal_id_resolve", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "withdrawal_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/admin/v1/consolidations/{consolidation_id}/retry": { + "post": { + "tags": [ + "Operator" + ], + "summary": "Повтор консолидации", + "description": "Повтор консолидации. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_admin_v1_consolidations_consolidation_id_retry", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "adminBearer": [] + } + ], + "parameters": [ + { + "name": "consolidation_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/pay/{token}": { + "get": { + "tags": [ + "Checkout" + ], + "summary": "Hosted checkout", + "description": "Публичная HTML-страница оплаты с QR, серверно-синхронизированным таймером и live status.", + "operationId": "get_pay_token", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/pay/{token}/status": { + "get": { + "tags": [ + "Checkout" + ], + "summary": "Статус hosted checkout", + "description": "JSON-состояние checkout для polling; включает серверное время и срок действия.", + "operationId": "get_pay_token_status", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "parameters": [ + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + } + }, + "components": { + "securitySchemes": { + "merchantHmac": { + "type": "apiKey", + "in": "header", + "name": "X-Api-Key", + "description": "Дополнительно обязательна HMAC-SHA256 подпись запроса." + }, + "adminBearer": { + "type": "http", + "scheme": "bearer", + "description": "Operator admin token" + } + }, + "schemas": { + "Error": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } +} diff --git a/public/openapi/messenger.json b/public/openapi/messenger.json new file mode 100644 index 0000000..7ca552d --- /dev/null +++ b/public/openapi/messenger.json @@ -0,0 +1,2194 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OVE Messenger API", + "version": "1.0.0", + "description": "OVE Messenger user, bot, OAuth, files, calls and realtime updates API." + }, + "servers": [ + { + "url": "https://ms.ove.rs" + } + ], + "paths": { + "/register": { + "post": { + "tags": [ + "Аккаунт" + ], + "summary": "Регистрация аккаунта", + "description": "Регистрация аккаунта. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_register", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "email": "you@example.com", + "password": "correct horse battery staple" + } + } + } + } + } + }, + "/login": { + "post": { + "tags": [ + "Аккаунт" + ], + "summary": "Вход по паролю", + "description": "Вход по паролю. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_login", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "email": "you@example.com", + "password": "correct horse battery staple" + } + } + } + } + } + }, + "/auth/email/start": { + "post": { + "tags": [ + "Аккаунт" + ], + "summary": "Запрос email-кода", + "description": "Запрос email-кода. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_auth_email_start", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "email": "you@example.com" + } + } + } + } + } + }, + "/auth/email/verify": { + "post": { + "tags": [ + "Аккаунт" + ], + "summary": "Подтверждение email-кода", + "description": "Подтверждение email-кода. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_auth_email_verify", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "email": "you@example.com", + "code": "123456" + } + } + } + } + } + }, + "/.well-known/openid-configuration": { + "get": { + "tags": [ + "OAuth" + ], + "summary": "OIDC Discovery", + "description": "OIDC Discovery. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get__well_known_openid_configuration", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + } + } + }, + "/.well-known/oauth-authorization-server": { + "get": { + "tags": [ + "OAuth" + ], + "summary": "OAuth Server Metadata", + "description": "OAuth Server Metadata. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get__well_known_oauth_authorization_server", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + } + } + }, + "/oauth/jwks.json": { + "get": { + "tags": [ + "OAuth" + ], + "summary": "Публичные ключи OIDC", + "description": "Публичные ключи OIDC. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_oauth_jwks_json", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + } + } + }, + "/oauth/device_authorization": { + "post": { + "tags": [ + "OAuth" + ], + "summary": "Запуск Device Flow", + "description": "Создает device_code, user_code и verification URI для универсального входа через Messenger.", + "operationId": "post_oauth_device_authorization", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "oauthClient": [] + } + ] + } + }, + "/oauth/device": { + "get": { + "tags": [ + "OAuth" + ], + "summary": "Страница подтверждения Device Flow", + "description": "Страница подтверждения Device Flow. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_oauth_device", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + } + } + }, + "/oauth/token": { + "post": { + "tags": [ + "OAuth" + ], + "summary": "Обмен device code на токены", + "description": "Обменивает подтвержденный device_code на OIDC access/id token. Во время ожидания возвращает authorization_pending.", + "operationId": "post_oauth_token", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "oauthClient": [] + } + ] + } + }, + "/oauth/userinfo": { + "get": { + "tags": [ + "OAuth" + ], + "summary": "Профиль OIDC", + "description": "Профиль OIDC. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_oauth_userinfo", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/oauth/device/request": { + "get": { + "tags": [ + "OAuth" + ], + "summary": "Данные запроса входа", + "description": "Данные запроса входа. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_oauth_device_request", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/oauth/device/decision": { + "post": { + "tags": [ + "OAuth" + ], + "summary": "Решение пользователя", + "description": "Решение пользователя. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_oauth_device_decision", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/me": { + "get": { + "tags": [ + "Аккаунт" + ], + "summary": "Текущий пользователь", + "description": "Текущий пользователь. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_me", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/account/delete": { + "post": { + "tags": [ + "Аккаунт" + ], + "summary": "Удаление аккаунта", + "description": "Удаление аккаунта. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_account_delete", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/username": { + "post": { + "tags": [ + "Аккаунт" + ], + "summary": "Установка username", + "description": "Установка username. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_username", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/name": { + "post": { + "tags": [ + "Аккаунт" + ], + "summary": "Изменение отображаемого имени", + "description": "Изменение отображаемого имени. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_name", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/profiles/description": { + "post": { + "tags": [ + "Аккаунт" + ], + "summary": "Описание профиля, бота или чата", + "description": "Описание профиля, бота или чата. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_profiles_description", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/privacy": { + "post": { + "tags": [ + "Аккаунт" + ], + "summary": "Настройки приватности", + "description": "Настройки приватности. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_privacy", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/contacts": { + "get": { + "tags": [ + "Контакты" + ], + "summary": "Список контактов", + "description": "Список контактов. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_contacts", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/contacts/add": { + "post": { + "tags": [ + "Контакты" + ], + "summary": "Добавление контакта", + "description": "Добавление контакта. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_contacts_add", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/contacts/delete": { + "post": { + "tags": [ + "Контакты" + ], + "summary": "Удаление контакта", + "description": "Удаление контакта. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_contacts_delete", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/groups": { + "post": { + "tags": [ + "Чаты" + ], + "summary": "Создание группы", + "description": "Создание группы. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_groups", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/channels": { + "post": { + "tags": [ + "Чаты" + ], + "summary": "Создание канала", + "description": "Создание канала. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_channels", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/chats/title": { + "post": { + "tags": [ + "Чаты" + ], + "summary": "Изменение названия чата", + "description": "Изменение названия чата. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_chats_title", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/channels/username": { + "post": { + "tags": [ + "Чаты" + ], + "summary": "Username канала", + "description": "Username канала. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_channels_username", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/channels/comments/settings": { + "post": { + "tags": [ + "Чаты" + ], + "summary": "Настройки комментариев канала", + "description": "Настройки комментариев канала. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_channels_comments_settings", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/channels/comments/send": { + "post": { + "tags": [ + "Чаты" + ], + "summary": "Комментарий к публикации", + "description": "Комментарий к публикации. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_channels_comments_send", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/channels/comments": { + "get": { + "tags": [ + "Чаты" + ], + "summary": "Комментарии к публикации", + "description": "Комментарии к публикации. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_channels_comments", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/chats/members/add": { + "post": { + "tags": [ + "Чаты" + ], + "summary": "Добавление участника", + "description": "Добавление участника. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_chats_members_add", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/chats/members/remove": { + "post": { + "tags": [ + "Чаты" + ], + "summary": "Удаление участника", + "description": "Удаление участника. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_chats_members_remove", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/cloud-password": { + "post": { + "tags": [ + "Безопасность" + ], + "summary": "Облачный пароль", + "description": "Облачный пароль. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_cloud_password", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/cloud-password/reset": { + "post": { + "tags": [ + "Безопасность" + ], + "summary": "Сброс облачного пароля", + "description": "Сброс облачного пароля. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_cloud_password_reset", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/sessions": { + "get": { + "tags": [ + "Безопасность" + ], + "summary": "Активные сессии", + "description": "Активные сессии. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_sessions", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/sessions/revoke": { + "post": { + "tags": [ + "Безопасность" + ], + "summary": "Завершение сессии", + "description": "Завершение сессии. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_sessions_revoke", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/sessions/revoke-others": { + "post": { + "tags": [ + "Безопасность" + ], + "summary": "Завершение других сессий", + "description": "Завершение других сессий. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_sessions_revoke_others", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/bots": { + "post": { + "tags": [ + "Боты" + ], + "summary": "Создание бота", + "description": "Создание бота. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_bots", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "username": "shop_helper_bot", + "name": "Shop helper" + } + } + } + } + } + }, + "/bots/token/reset": { + "post": { + "tags": [ + "Боты" + ], + "summary": "Перевыпуск bot token", + "description": "Перевыпуск bot token. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_bots_token_reset", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/e2e/key": { + "post": { + "tags": [ + "E2E" + ], + "summary": "Публикация E2E-ключа", + "description": "Публикация E2E-ключа. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_e2e_key", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "get": { + "tags": [ + "E2E" + ], + "summary": "Публичный E2E-ключ", + "description": "Публичный E2E-ключ. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_e2e_key", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/e2e/backup": { + "post": { + "tags": [ + "E2E" + ], + "summary": "Сохранение E2E-копии", + "description": "Сохранение E2E-копии. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_e2e_backup", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "get": { + "tags": [ + "E2E" + ], + "summary": "Резервная копия E2E", + "description": "Резервная копия E2E. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_e2e_backup", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/e2e/reset": { + "post": { + "tags": [ + "E2E" + ], + "summary": "Сброс E2E-ключей", + "description": "Сброс E2E-ключей. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_e2e_reset", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/wallet": { + "get": { + "tags": [ + "Dastars" + ], + "summary": "Баланс Dastars", + "description": "Баланс Dastars. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_wallet", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/wallet/send": { + "post": { + "tags": [ + "Dastars" + ], + "summary": "Перевод Dastars", + "description": "Перевод Dastars. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_wallet_send", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/wallet/history": { + "get": { + "tags": [ + "Dastars" + ], + "summary": "История Dastars", + "description": "История Dastars. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_wallet_history", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/call": { + "post": { + "tags": [ + "Звонки" + ], + "summary": "Создание звонка", + "description": "Создание звонка. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_call", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/voice-ticket": { + "post": { + "tags": [ + "Звонки" + ], + "summary": "Билет голосовой сессии", + "description": "Билет голосовой сессии. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_voice_ticket", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/voice/participants": { + "get": { + "tags": [ + "Звонки" + ], + "summary": "Участники голосовой сессии", + "description": "Участники голосовой сессии. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_voice_participants", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/voice": { + "get": { + "tags": [ + "Звонки" + ], + "summary": "Голосовой WebSocket", + "description": "Переключает соединение на WebSocket после получения voice ticket.", + "operationId": "get_voice", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/send": { + "post": { + "tags": [ + "Сообщения" + ], + "summary": "Отправка текстового сообщения", + "description": "Отправляет сообщение без медиа. Для клиентской очереди используйте стабильный client_message_id.", + "operationId": "post_send", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "to": 42, + "text": "Привет!", + "client_message_id": "0190f6d4-example" + } + } + } + } + } + }, + "/edit": { + "post": { + "tags": [ + "Сообщения" + ], + "summary": "Редактирование сообщения", + "description": "Редактирование сообщения. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_edit", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "chat_id": 42, + "message_id": 1001, + "text": "Исправленный текст" + } + } + } + } + } + }, + "/callback": { + "post": { + "tags": [ + "Боты" + ], + "summary": "Callback кнопки", + "description": "Callback кнопки. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_callback", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/reactions": { + "post": { + "tags": [ + "Реакции" + ], + "summary": "Emoji-реакция", + "description": "Emoji-реакция. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_reactions", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "chat_id": 42, + "message_id": 1001, + "emoji": "🔥" + } + } + } + } + } + }, + "/reactions/paid": { + "post": { + "tags": [ + "Реакции" + ], + "summary": "Платная реакция Dastars", + "description": "Переводит указанное количество Dastars автору сообщения; для бота — его владельцу. amount может быть больше 1.", + "operationId": "post_reactions_paid", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "chat_id": 42, + "message_id": 1001, + "amount": 5 + } + } + } + } + } + }, + "/read": { + "post": { + "tags": [ + "Сообщения" + ], + "summary": "Отметка о прочтении", + "description": "Отметка о прочтении. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_read", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/delete": { + "post": { + "tags": [ + "Сообщения" + ], + "summary": "Удаление сообщения", + "description": "Удаление сообщения. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_delete", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/favorite": { + "post": { + "tags": [ + "Сообщения" + ], + "summary": "Избранное сообщение", + "description": "Избранное сообщение. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_favorite", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/media/quote": { + "post": { + "tags": [ + "Файлы" + ], + "summary": "Расчет стоимости медиа", + "description": "Рассчитывает единую стоимость новых вложений: сумма байтов округляется вверх до MiB один раз на сообщение.", + "operationId": "post_media_quote", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "media": [ + { + "client_id": "attachment-1", + "name": "photo.jpg", + "mime": "image/jpeg", + "size": 524288 + } + ] + } + } + } + } + } + }, + "/messages/prepare": { + "post": { + "tags": [ + "Файлы" + ], + "summary": "Подготовка сообщения с медиа", + "description": "Атомарно резервирует оплату и выдает до 10 upload tickets. Текст и media[] опциональны по отдельности, но сообщение не может быть пустым.", + "operationId": "post_messages_prepare", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "to": 42, + "text": "Caption", + "client_message_id": "0190f6d4-media", + "media": [ + { + "client_id": "attachment-1", + "name": "photo.jpg", + "mime": "image/jpeg", + "size": 524288 + } + ] + } + } + } + } + } + }, + "/messages/commit": { + "post": { + "tags": [ + "Файлы" + ], + "summary": "Публикация сообщения с медиа", + "description": "Проверяет все загруженные вложения, одним платежом списывает DSR и публикует или редактирует одно сообщение.", + "operationId": "post_messages_commit", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "operation_id": "message-op-1-0190f6d4-media" + } + } + } + } + } + }, + "/messages/cancel": { + "post": { + "tags": [ + "Файлы" + ], + "summary": "Отмена сообщения с медиа", + "description": "Удаляет незавершенные загрузки и отменяет резерв оплаты.", + "operationId": "post_messages_cancel", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "operation_id": "message-op-1-0190f6d4-media" + } + } + } + } + } + }, + "/forward": { + "post": { + "tags": [ + "Файлы" + ], + "summary": "Пересылка сообщения с медиа", + "description": "Пересылка сообщения с медиа. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_forward", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "message_id": 1001, + "to": 42, + "client_message_id": "0190f6d4-forward" + } + } + } + } + } + }, + "/file/ticket": { + "get": { + "tags": [ + "Файлы" + ], + "summary": "Билет скачивания файла", + "description": "Билет скачивания файла. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_file_ticket", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/nodes/status": { + "get": { + "tags": [ + "Сервис" + ], + "summary": "Статус сервисных узлов", + "description": "Статус сервисных узлов. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_nodes_status", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/chats": { + "get": { + "tags": [ + "Чаты" + ], + "summary": "Список чатов", + "description": "Список чатов. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_chats", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/chats/delete": { + "post": { + "tags": [ + "Чаты" + ], + "summary": "Удаление чата", + "description": "Удаление чата. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_chats_delete", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/users/ban": { + "post": { + "tags": [ + "Модерация" + ], + "summary": "Блокировка пользователя", + "description": "Блокировка пользователя. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_users_ban", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/users/unban": { + "post": { + "tags": [ + "Модерация" + ], + "summary": "Разблокировка пользователя", + "description": "Разблокировка пользователя. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_users_unban", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/history": { + "get": { + "tags": [ + "Сообщения" + ], + "summary": "История сообщений", + "description": "История сообщений. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_history", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/updates": { + "get": { + "tags": [ + "Updates" + ], + "summary": "Очередь обновлений", + "description": "Long polling событий клиента или бота. Bot updates долговечны и остаются до явного ACK.", + "operationId": "get_updates", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/updates/ack": { + "post": { + "tags": [ + "Updates" + ], + "summary": "Подтверждение обновлений", + "description": "Подтверждение обновлений. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "post_updates_ack", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "update_id": 2048 + } + } + } + } + } + }, + "/file/{id}": { + "get": { + "tags": [ + "Файлы" + ], + "summary": "Скачивание файла", + "description": "Скачивание файла. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_file_id", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/health/live": { + "get": { + "tags": [ + "Сервис" + ], + "summary": "Проверка процесса Messenger", + "description": "Проверка процесса Messenger. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_health_live", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + } + } + }, + "/health/ready": { + "get": { + "tags": [ + "Сервис" + ], + "summary": "Готовность Messenger", + "description": "Готовность Messenger. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_health_ready", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + } + } + }, + "/metrics": { + "get": { + "tags": [ + "Сервис" + ], + "summary": "Метрики Messenger", + "description": "Метрики Messenger. Возвращает JSON-ответ или стандартную JSON-ошибку.", + "operationId": "get_metrics", + "responses": { + "200": { + "description": "Успешный ответ" + }, + "400": { + "description": "Ошибка запроса" + }, + "401": { + "description": "Ошибка авторизации" + } + } + } + } + }, + "components": { + "securitySchemes": { + "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." + } + }, + "schemas": { + "Error": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..c2a49f4 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..7113bff --- /dev/null +++ b/public/styles.css @@ -0,0 +1,137 @@ +:root { + color-scheme: dark; + --bg: #0b0d10; + --panel: #111419; + --panel-2: #171b21; + --line: #262c34; + --muted: #929ba8; + --text: #f3f5f7; + --lime: #b9f541; + --cyan: #61ddff; + --violet: #a78bfa; + --orange: #ffb86b; + --sidebar: 260px; +} +* { box-sizing: border-box; } +html { scroll-behavior: smooth; scroll-padding-top: 88px; } +body { + margin: 0; + background: var(--bg); + color: var(--text); + font: 15px/1.65 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} +body::before { + content: ""; position: fixed; inset: 0; pointer-events: none; + background: radial-gradient(circle at 80% 5%, rgba(97,221,255,.07), transparent 28%), + radial-gradient(circle at 30% 40%, rgba(185,245,65,.035), transparent 25%); +} +.noise { position: fixed; inset: 0; opacity: .025; pointer-events: none; z-index: 10; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); +} +a { color: inherit; } +code, pre { font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; } +code { font-size: .9em; } +.sidebar { + position: fixed; inset: 0 auto 0 0; z-index: 20; width: var(--sidebar); + display: flex; flex-direction: column; padding: 26px 20px; + background: rgba(12,14,17,.96); border-right: 1px solid var(--line); backdrop-filter: blur(18px); +} +.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; margin-bottom: 38px; } +.brand-mark { width: 38px; height: 38px; display: grid; place-items: center; border-radius: 11px; + background: var(--lime); color: #0a0d08; font-weight: 900; font-size: 20px; box-shadow: 0 0 30px rgba(185,245,65,.15); } +.brand strong { display: block; letter-spacing: .08em; } +.brand small { display: block; color: var(--muted); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; } +nav { display: grid; gap: 3px; } +nav a { padding: 8px 11px; border-radius: 8px; color: #aab2bd; text-decoration: none; font-size: 13px; } +nav a:hover, nav a.active { color: var(--text); background: var(--panel-2); } +.sidebar-foot { margin-top: auto; display: grid; gap: 8px; color: var(--muted); font-size: 11px; } +.sidebar-foot a { text-decoration: none; color: #c5cbd3; } +.status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--lime); display: inline-block; box-shadow: 0 0 10px var(--lime); } +main { margin-left: var(--sidebar); min-width: 0; } +.topbar { height: 66px; position: sticky; top: 0; z-index: 15; display: flex; align-items: center; gap: 18px; + padding: 0 max(32px, calc((100vw - var(--sidebar) - 980px) / 2)); + border-bottom: 1px solid rgba(38,44,52,.75); background: rgba(11,13,16,.82); backdrop-filter: blur(20px); } +.menu { display: none; background: none; color: white; border: 0; font-size: 20px; } +.search { flex: 1; max-width: 620px; height: 38px; display: flex; align-items: center; gap: 9px; padding: 0 11px; + border: 1px solid var(--line); border-radius: 9px; background: #111419; color: var(--muted); } +.search:focus-within { border-color: #53606e; } +.search input { width: 100%; border: 0; outline: 0; color: var(--text); background: transparent; font: inherit; font-size: 13px; } +kbd { border: 1px solid #343b45; border-radius: 5px; padding: 0 6px; font: 11px/19px inherit; color: var(--muted); } +.spec-link { margin-left: auto; color: var(--lime); text-decoration: none; font-size: 12px; letter-spacing: .05em; text-transform: uppercase; } +.hero, .doc-section, footer { width: min(980px, calc(100% - 64px)); margin-inline: auto; } +.hero { min-height: 690px; display: flex; flex-direction: column; justify-content: center; padding: 100px 0 72px; } +.eyebrow, .section-label { color: var(--lime); font: 700 11px/1.3 "SFMono-Regular", Consolas, monospace; letter-spacing: .14em; text-transform: uppercase; } +h1 { margin: 24px 0 20px; font-size: clamp(50px, 7vw, 88px); line-height: .98; letter-spacing: -.065em; font-weight: 720; } +h1 em { color: var(--muted); font-style: normal; } +.lead { max-width: 670px; margin: 0; color: #b6bec8; font-size: 19px; line-height: 1.7; } +.hero-actions { display: flex; gap: 12px; margin: 32px 0 66px; } +.button { padding: 11px 17px; border-radius: 9px; text-decoration: none; font-weight: 650; font-size: 13px; } +.button.primary { background: var(--lime); color: #10140a; } +.button.secondary { border: 1px solid var(--line); background: var(--panel); } +.service-strip { display: grid; grid-template-columns: 1fr 1fr; border: 1px solid var(--line); border-radius: 14px; overflow: hidden; } +.service-strip article { display: flex; align-items: center; gap: 13px; padding: 18px; background: rgba(17,20,25,.7); } +.service-strip article + article { border-left: 1px solid var(--line); } +.service-icon { width: 36px; height: 36px; display: grid; place-items: center; border-radius: 9px; font-weight: 800; color: #0b0d10; } +.service-icon.messenger { background: var(--cyan); }.service-icon.gateway { background: var(--violet); } +.service-strip b, .service-strip code { display: block; } +.service-strip code { color: var(--muted); font-size: 11px; } +.service-strip i { margin-left: auto; color: var(--muted); font: normal 10px/1.2 monospace; } +.doc-section { padding: 100px 0; border-top: 1px solid var(--line); } +h2 { margin: 14px 0 20px; font-size: clamp(32px, 4vw, 49px); line-height: 1.1; letter-spacing: -.045em; } +h3 { margin: 28px 0 10px; font-size: 17px; } +p { color: #b2bac5; max-width: 760px; } +pre { position: relative; overflow-x: auto; margin: 22px 0; padding: 22px; border: 1px solid var(--line); border-radius: 12px; + background: #0d1014; color: #dce4ec; font-size: 12px; line-height: 1.75; } +.code-card { border: 1px solid var(--line); border-radius: 13px; overflow: hidden; margin-top: 28px; background: #0d1014; } +.code-card pre { border: 0; border-radius: 0; margin: 0; } +.code-head { display: flex; justify-content: space-between; align-items: center; padding: 9px 13px; border-bottom: 1px solid var(--line); color: var(--muted); font-size: 11px; } +.copy { border: 0; color: var(--lime); background: none; cursor: pointer; font: inherit; } +.callout { margin-top: 24px; padding: 18px 20px; border-left: 3px solid var(--lime); background: rgba(185,245,65,.055); color: #abb4bf; } +.callout strong { color: var(--text); } +.auth-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 32px; } +.auth-grid article { min-height: 190px; padding: 20px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); } +.auth-grid article > span { color: var(--lime); font: 11px monospace; }.auth-grid h3 { margin-top: 32px; }.auth-grid p { font-size: 13px; } +.facts { display: grid; grid-template-columns: repeat(4, 1fr); margin-top: 34px; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; } +.facts div { padding: 18px; }.facts div + div { border-left: 1px solid var(--line); }.facts b,.facts span { display:block; }.facts span { color:var(--muted);font-size:11px; } +.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }.two-col > div { padding: 0 20px 12px; border-left: 1px solid var(--line); } +.flow { display: flex; align-items: center; gap: 12px; margin-top: 30px; overflow-x: auto; padding-bottom: 8px; } +.flow span { flex: 0 0 auto; padding: 12px 15px; border: 1px solid var(--line); border-radius: 9px; background: var(--panel); font: 12px monospace; }.flow b { color: var(--lime); } +.asset-table { max-width: 670px; margin-top: 28px; border: 1px solid var(--line); border-radius: 12px; overflow: hidden; } +.asset-table > div { display: grid; grid-template-columns: 1fr 2fr 1fr; padding: 10px 15px; border-top: 1px solid var(--line); } +.asset-table > div:first-child { border: 0; }.asset-table .thead { color: var(--muted); background: var(--panel); font-size: 11px; text-transform: uppercase; } +.asset-table code { color: var(--lime); } +.endpoint-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 15px; margin: 28px 0 14px; } +.filters { display: flex; gap: 7px; flex-wrap: wrap; } +.filter { border: 1px solid var(--line); border-radius: 99px; background: var(--panel); color: #b7bec7; padding: 6px 11px; cursor: pointer; font: 11px inherit; } +.filter.active { background: var(--text); color: var(--bg); border-color: var(--text); } +#endpoint-count { color: var(--muted); font: 11px monospace; white-space: nowrap; } +.endpoint-list { display: grid; gap: 7px; } +.endpoint { border: 1px solid var(--line); border-radius: 11px; background: rgba(17,20,25,.74); overflow: hidden; scroll-margin-top: 80px; } +.endpoint-summary { width: 100%; min-height: 57px; display: grid; grid-template-columns: 54px minmax(175px, 1.25fr) 1fr auto 20px; align-items: center; gap: 12px; + border: 0; padding: 9px 14px; background: none; color: var(--text); text-align: left; cursor: pointer; } +.method { width: 48px; padding: 3px 0; border-radius: 5px; text-align:center; font: 800 10px monospace; } +.method.GET { color: var(--cyan); background: rgba(97,221,255,.09); }.method.POST { color: var(--lime); background: rgba(185,245,65,.09); }.method.DELETE { color: #ff718a; background: rgba(255,113,138,.09); } +.path { overflow-wrap: anywhere; color: #dce2e9; font-size: 12px; } +.endpoint-title { color: var(--muted); font-size: 12px; } +.auth-badge { border: 1px solid #343b45; border-radius: 99px; padding: 2px 8px; color: var(--muted); font: 9px monospace; text-transform: uppercase; } +.chevron { color: var(--muted); transition: transform .2s; }.endpoint.open .chevron { transform: rotate(180deg); } +.endpoint-body { display: none; padding: 0 18px 18px 80px; border-top: 1px solid var(--line); }.endpoint.open .endpoint-body { display: block; } +.endpoint-meta { display: flex; gap: 8px; flex-wrap: wrap; margin: 14px 0; }.endpoint-meta code { padding: 4px 8px; background:#0d1014; border-radius:5px;color:#aeb7c2; } +.endpoint-example pre { margin-bottom: 0; }.empty { padding: 50px; text-align:center; color:var(--muted); } +.error-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; overflow:hidden; margin: 25px 0; border:1px solid var(--line); border-radius:12px; background:var(--line); } +.error-grid div { display:flex; gap:15px; padding:12px 15px; background:var(--panel); }.error-grid code{color:var(--orange)}.error-grid span{color:var(--muted)} +footer { display: flex; justify-content: space-between; gap: 20px; padding: 40px 0 60px; border-top: 1px solid var(--line); color: var(--muted); font-size: 11px; } +@media (max-width: 900px) { + :root { --sidebar: 0px; } + .sidebar { width: 260px; transform: translateX(-100%); transition: transform .25s; box-shadow: 20px 0 50px rgba(0,0,0,.4); } + .sidebar.open { transform: translateX(0); } + .menu { display:block; }.topbar{padding:0 20px}.hero,.doc-section,footer{width:min(100% - 38px,760px)} + .auth-grid{grid-template-columns:1fr}.facts{grid-template-columns:1fr 1fr}.facts div:nth-child(3){border-left:0;border-top:1px solid var(--line)}.facts div:nth-child(4){border-top:1px solid var(--line)} +} +@media (max-width: 620px) { + .hero{min-height:auto;padding-top:75px}.service-strip{grid-template-columns:1fr}.service-strip article+article{border-left:0;border-top:1px solid var(--line)} + .two-col{grid-template-columns:1fr}.endpoint-summary{grid-template-columns:48px 1fr 18px}.endpoint-title,.auth-badge{display:none}.endpoint-body{padding-left:18px} + .facts{grid-template-columns:1fr}.facts div+div{border-left:0;border-top:1px solid var(--line)}.error-grid{grid-template-columns:1fr}.spec-link,kbd{display:none} + footer{flex-direction:column}h1{font-size:48px}.lead{font-size:16px} +} +@media (prefers-reduced-motion: reduce) { html{scroll-behavior:auto}*{transition:none!important} } diff --git a/scripts/generate.py b/scripts/generate.py new file mode 100644 index 0000000..538a920 --- /dev/null +++ b/scripts/generate.py @@ -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() diff --git a/scripts/verify.py b/scripts/verify.py new file mode 100644 index 0000000..29cb839 --- /dev/null +++ b/scripts/verify.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Fail when a public service route is missing from docs or OpenAPI.""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +DOCS = Path(__file__).resolve().parents[1] +REPO = DOCS.parent +PUBLIC = DOCS / "public" + + +def messenger_routes() -> set[tuple[str, str]]: + source = (REPO / "micro-chat/micromsg/src/app_routes.rs").read_text(encoding="utf-8") + routes = set(re.findall(r'\("([A-Z]+)", "([^"]+)"\)\s*=>', source)) + routes = {(method, path) for method, path in routes if not path.startswith("/internal/")} + if 'path.starts_with("/file/")' in source: + routes.add(("GET", "/file/{id}")) + return routes + + +def gateway_routes() -> set[tuple[str, str]]: + source = (REPO / "crypto-gateway/crates/gateway-api/src/main.rs").read_text(encoding="utf-8") + routes: set[tuple[str, str]] = set() + cursor = 0 + while (start := source.find(".route(", cursor)) != -1: + depth = 0 + end = start + len(".route(") + while end < len(source): + if source[end] == "(": + depth += 1 + elif source[end] == ")": + if depth == 0: + break + depth -= 1 + end += 1 + block = source[start:end + 1] + match = re.search(r'\.route\(\s*"([^"]+)"\s*,(.*)\)$', block, re.S) + cursor = end + 1 + if not match: + continue + path, handlers = match.groups() + if path.startswith("/v1/") or path.startswith("/admin/v1/") or path == "/health": + for method in re.findall(r'\b(get|post|delete)\s*\(', handlers): + routes.add((method.upper(), path)) + web = (REPO / "crypto-gateway/crates/gateway-api/src/web.rs").read_text(encoding="utf-8") + for path in ("/pay/{token}", "/pay/{token}/status"): + if f'.route("{path}", get(' not in web: + raise AssertionError(f"hosted checkout route disappeared: {path}") + routes.add(("GET", path)) + return routes + + +def donations_routes() -> set[tuple[str, str]]: + source = (REPO / "donations/src/main.rs").read_text(encoding="utf-8") + routes: set[tuple[str, str]] = set() + cursor = 0 + while (start := source.find(".route(", cursor)) != -1: + depth = 0 + end = start + len(".route(") + while end < len(source): + if source[end] == "(": + depth += 1 + elif source[end] == ")": + if depth == 0: + break + depth -= 1 + end += 1 + block = source[start:end + 1] + match = re.search(r'\.route\(\s*"([^"]+)"\s*,(.*)\)$', block, re.S) + cursor = end + 1 + if not match: + continue + path, handlers = match.groups() + if path == "/health" or path.startswith("/api/"): + for method in re.findall(r'\b(get|post|delete|put|patch)\s*\(', handlers): + routes.add((method.upper(), path)) + for method in re.findall(r'\.(delete|put|patch)\s*\(', handlers): + routes.add((method.upper(), path)) + return routes + + +def main() -> int: + manifest = json.loads((PUBLIC / "api-manifest.json").read_text(encoding="utf-8")) + documented = { + service["id"]: {(item["method"], item["path"]) for item in service["endpoints"]} + for service in manifest["services"] + } + source = { + "messenger": messenger_routes(), + "gateway": gateway_routes(), + "donations": donations_routes(), + } + failed = False + for service_id, actual in source.items(): + missing = actual - documented[service_id] + stale = documented[service_id] - actual + if missing: + print(f"{service_id}: undocumented routes: {sorted(missing)}", file=sys.stderr) + failed = True + if stale: + print(f"{service_id}: stale routes: {sorted(stale)}", file=sys.stderr) + failed = True + spec = json.loads((PUBLIC / "openapi" / f"{service_id}.json").read_text(encoding="utf-8")) + in_spec = {(method.upper(), path) for path, operations in spec["paths"].items() + for method in operations if method.upper() in {"GET", "POST", "DELETE", "PUT", "PATCH"}} + if in_spec != documented[service_id]: + print(f"{service_id}: OpenAPI mismatch", file=sys.stderr) + failed = True + counted = sum(len(routes) for routes in documented.values()) + if counted != manifest["total_endpoints"]: + print(f"manifest total is {manifest['total_endpoints']}, counted {counted}", file=sys.stderr) + failed = True + if failed: + return 1 + print(f"verified {counted} public endpoints across {len(source)} services") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())