first init

This commit is contained in:
2026-08-14 00:13:56 +05:00
commit 7ae997f99e
14 changed files with 6310 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+142
View File
@@ -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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
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 =
`<code>${escapeHtml(service.base_url)}</code><code>${escapeHtml(endpoint.group)}</code>`;
const curl = curlFor(service, endpoint);
node.querySelector(".endpoint-example").innerHTML =
`<div class="code-card"><div class="code-head"><span>cURL</span><button class="copy">Копировать</button></div><pre><code>${escapeHtml(curl)}</code></pre></div>`;
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.";
});
})();
+209
View File
@@ -0,0 +1,209 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Публичная документация API сервисов OVE">
<title>OVE API Reference</title>
<link rel="stylesheet" href="/styles.css">
<script src="/app.js" defer></script>
</head>
<body>
<div class="noise" aria-hidden="true"></div>
<aside class="sidebar" id="sidebar">
<a class="brand" href="#top" aria-label="OVE API">
<span class="brand-mark">O</span>
<span><strong>OVE</strong><small>API reference</small></span>
</a>
<nav aria-label="Основная навигация">
<a href="#overview">Обзор</a>
<a href="#quickstart">Быстрый старт</a>
<a href="#auth">Авторизация</a>
<a href="#messenger">Messenger API</a>
<a href="#bots">Bot API</a>
<a href="#oauth">OAuth / OIDC</a>
<a href="#gateway">Crypto Gateway</a>
<a href="#donations">Donations + OBS</a>
<a href="#webhooks">Webhooks</a>
<a href="#errors">Ошибки</a>
</nav>
<div class="sidebar-foot">
<span class="status-dot"></span> Production polygon
<a href="/openapi/messenger.json" download>Messenger OpenAPI ↗</a>
<a href="/openapi/gateway.json" download>Gateway OpenAPI ↗</a>
<a href="/openapi/donations.json" download>Donations OpenAPI ↗</a>
</div>
</aside>
<main id="top">
<header class="topbar">
<button class="menu" id="menu" aria-label="Открыть меню" aria-expanded="false"></button>
<label class="search">
<span></span>
<input id="search" type="search" placeholder="Поиск endpoint, пути или метода…" autocomplete="off">
<kbd>/</kbd>
</label>
<a class="spec-link" href="/openapi/gateway.json">OpenAPI</a>
</header>
<section class="hero" id="overview">
<div class="eyebrow">PUBLIC API · OVE.RS</div>
<h1>Один вход.<br><em>Все сервисы OVE.</em></h1>
<p class="lead">Документация публичных HTTP API Messenger, Crypto Gateway и Donation Service: авторизация, сообщения, боты, платежи, OBS alerts, webhooks и операторские методы.</p>
<div class="hero-actions">
<a class="button primary" href="#quickstart">Начать интеграцию</a>
<a class="button secondary" href="#endpoints">Все endpoints</a>
</div>
<div class="service-strip">
<article><span class="service-icon messenger">M</span><div><b>Messenger</b><code>https://ms.ove.rs</code></div><i>REST · OAuth · WS</i></article>
<article><span class="service-icon gateway">C</span><div><b>Crypto Gateway</b><code>https://cr.ove.rs</code></div><i>REST · HMAC</i></article>
<article><span class="service-icon donations">D</span><div><b>Donations</b><code>https://do.ove.rs</code></div><i>REST · SSE · OBS</i></article>
</div>
</section>
<section class="doc-section" id="quickstart">
<div class="section-label">01 — Quick start</div>
<h2>Первый запрос</h2>
<p>Все ответы API — JSON, если endpoint явно не обозначен как HTML, WebSocket или файл. Для Messenger получите токен через email-код, затем передавайте его в заголовке Bearer.</p>
<div class="code-card">
<div class="code-head"><span>Email login</span><button class="copy" data-copy-target="quick-code">Копировать</button></div>
<pre id="quick-code"><code>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"}'</code></pre>
</div>
<div class="callout"><strong>HTTPS в тестовом полигоне.</strong> Сертификаты выпущены локальным CA OVE. Добавьте CA в доверенные на устройстве; не отключайте проверку TLS в production-клиенте.</div>
</section>
<section class="doc-section" id="auth">
<div class="section-label">02 — Security</div>
<h2>Три схемы авторизации</h2>
<div class="auth-grid">
<article><span>01</span><h3>Messenger Bearer</h3><p>Пользовательский или bot token передается как <code>Authorization: Bearer &lt;token&gt;</code>.</p></article>
<article><span>02</span><h3>Gateway HMAC</h3><p>Каждый merchant-запрос подписывается ключом, timestamp и одноразовым UUID nonce.</p></article>
<article><span>03</span><h3>Operator Bearer</h3><p><code>/admin/v1/*</code> принимает отдельный секрет <code>GATEWAY_ADMIN_TOKEN</code>.</p></article>
</div>
<h3>Каноническая строка Gateway</h3>
<pre><code>timestamp + "\n" +
nonce + "\n" +
HTTP_METHOD + "\n" +
path_and_query + "\n" +
hex(sha256(raw_request_body))</code></pre>
<p>Результат подпишите HMAC-SHA256 и отправьте lowercase hex в <code>X-Api-Signature</code>. Также обязательны <code>X-Api-Key</code>, <code>X-Api-Timestamp</code>, <code>X-Api-Nonce</code>; для изменяющих запросов — <code>Idempotency-Key</code>.</p>
</section>
<section class="doc-section" id="messenger">
<div class="section-label">03 — Messenger</div>
<h2>Клиенты, сообщения и real-time</h2>
<p>HTTP API покрывает аккаунты, чаты, E2E-ключи, файлы, голосовые сессии, Dastars и очередь обновлений. Native Android транспорт MST5 работает на <code>ms.ove.rs:8080</code>: после защищённого handshake он передаёт CBOR-команды и мультиплексированные ответы внутри зашифрованного потока.</p>
<div class="facts">
<div><b>Updates</b><span>long polling + ACK</span></div>
<div><b>Voice</b><span>WebSocket ticket</span></div>
<div><b>E2E</b><span>не зависит от username</span></div>
<div><b>Files</b><span>multipart upload</span></div>
</div>
</section>
<section class="doc-section" id="bots">
<div class="section-label">04 — Bots</div>
<h2>Bot API</h2>
<p>Создайте бота через проверенного <code>botfather</code> или <code>POST /bots</code>. Bot token использует ту же Bearer-схему. Обновления хранятся до подтверждения через <code>POST /updates/ack</code>.</p>
<div class="two-col">
<div><h3>Кнопки</h3><p><code>url</code>, <code>callback</code> и <code>pay_dsr</code>; не более 12 кнопок, текст до 64 символов.</p></div>
<div><h3>Реакции</h3><p>Бот получает callback для обычных и платных реакций. Пакет платной реакции может содержать <code>amount &gt; 1</code>.</p></div>
</div>
</section>
<section class="doc-section" id="oauth">
<div class="section-label">05 — Identity</div>
<h2>OAuth 2.0 Device Flow + OIDC</h2>
<p>Сервис запрашивает device code у Messenger, показывает QR или user code, а пользователь подтверждает вход в Android-клиенте. Токен обменивается только после решения пользователя.</p>
<div class="flow"><span>1 · device_authorization</span><b></b><span>2 · QR / code</span><b></b><span>3 · approve</span><b></b><span>4 · token</span></div>
</section>
<section class="doc-section" id="gateway">
<div class="section-label">06 — Payments</div>
<h2>Crypto Gateway</h2>
<p>Создавайте invoice и withdrawal, сверяйте баланс и принимайте подписанные callbacks. Денежные величины всегда передаются строками в атомарных единицах — без float.</p>
<div class="asset-table" role="table" aria-label="Поддерживаемые активы">
<div class="thead"><span>Asset</span><span>Network</span><span>Decimals</span></div>
<div><b>BTC</b><span>bitcoin</span><code>8</code></div>
<div><b>LTC</b><span>litecoin</span><code>8</code></div>
<div><b>TON</b><span>ton</span><code>9</code></div>
<div><b>TRX</b><span>tron</span><code>6</code></div>
<div><b>USDT</b><span>ton / tron</span><code>6</code></div>
</div>
<div class="callout"><strong>Hosted checkout.</strong> URL из ответа invoice открывается как <code>/pay/{token}</code>; JSON-статус доступен в <code>/pay/{token}/status</code>. Таймер оплаты синхронизирован с серверным временем.</div>
</section>
<section class="doc-section" id="webhooks">
<div class="section-label">07 — Events</div>
<h2>Подпись callbacks</h2>
<pre><code>signature = hex(HMAC-SHA256(
webhook_secret,
timestamp + "." + event_id + "." + raw_body
))</code></pre>
<p>Проверяйте <code>X-Gateway-Timestamp</code>, <code>X-Gateway-Event-Id</code> и <code>X-Gateway-Signature</code>. Доставка at-least-once повторяется до 24 часов — дедуплицируйте события по <code>event_id</code>.</p>
</section>
<section class="doc-section" id="donations">
<div class="section-label">08 — Donations</div>
<h2>Страница доната и OBS Browser Source</h2>
<p>Стример входит через Messenger, получает публичный адрес <code>do.ove.rs/u/{slug}</code> и секретную ссылку виджета. Доноры авторизоваться не обязаны: Donation Service создаёт invoice в Crypto Gateway и показывает его hosted checkout.</p>
<div class="flow"><span>1 · public donation</span><b></b><span>2 · Gateway checkout</span><b></b><span>3 · signed webhook</span><b></b><span>4 · OBS SSE alert</span></div>
<div class="callout"><strong>Вывод средств.</strong> Каждый запрос резервирует доступный баланс и отдельно подтверждается свайпом в Messenger. В окне подтверждения показаны актив, сумма, сеть и сокращённый адрес.</div>
</section>
<section class="doc-section endpoints-section" id="endpoints">
<div class="section-label">09 — Reference</div>
<h2>Все endpoints</h2>
<div class="endpoint-toolbar">
<div id="service-filters" class="filters"></div>
<span id="endpoint-count"></span>
</div>
<div id="endpoint-list" class="endpoint-list" aria-live="polite"></div>
<div id="empty" class="empty" hidden>Ничего не найдено. Попробуйте другой запрос.</div>
</section>
<section class="doc-section" id="errors">
<div class="section-label">10 — Errors</div>
<h2>Ошибки и повторные запросы</h2>
<div class="error-grid">
<div><code>400</code><span>Неверные поля или состояние</span></div>
<div><code>401</code><span>Нет или неверна авторизация</span></div>
<div><code>403</code><span>Недостаточно прав</span></div>
<div><code>404</code><span>Объект не найден</span></div>
<div><code>409</code><span>Конфликт / повтор</span></div>
<div><code>429</code><span>Превышен лимит</span></div>
<div><code>5xx</code><span>Временная ошибка сервиса</span></div>
</div>
<p>Для безопасного повтора merchant POST используйте тот же <code>Idempotency-Key</code>. Не повторяйте запрос с новым ключом, пока результат предыдущего неизвестен.</p>
</section>
<footer>
<span>OVE API · public contract</span>
<span>Internal wallet gRPC и системные bot nodes не публикуются.</span>
</footer>
</main>
<template id="endpoint-template">
<article class="endpoint">
<button class="endpoint-summary" type="button" aria-expanded="false">
<span class="method"></span>
<code class="path"></code>
<span class="endpoint-title"></span>
<span class="auth-badge"></span>
<span class="chevron"></span>
</button>
<div class="endpoint-body">
<p class="endpoint-description"></p>
<div class="endpoint-meta"></div>
<div class="endpoint-example"></div>
</div>
</article>
</template>
</body>
</html>
+16
View File
@@ -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.
+660
View File
@@ -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"
]
}
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Allow: /
+137
View File
@@ -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} }