Bot TG
This commit is contained in:
@@ -1260,9 +1260,10 @@ Public status lookup for SSH users and Xray/V2Ray UUIDs (CORS `*`). See the **Pu
|
|||||||
### Como configurar
|
### Como configurar
|
||||||
1. Abra o painel → aba **Bot / Vendas**.
|
1. Abra o painel → aba **Bot / Vendas**.
|
||||||
2. Em **Configuração**: marque *Bot ativo*, cole o **Token do Telegram** (via @BotFather) e o **Access Token do Mercado Pago**. Clique **Testar conexão** e depois **Salvar**.
|
2. Em **Configuração**: marque *Bot ativo*, cole o **Token do Telegram** (via @BotFather) e o **Access Token do Mercado Pago**. Clique **Testar conexão** e depois **Salvar**.
|
||||||
3. **Webhook × Polling** (o painel deixa você escolher):
|
3. **Confirmação do pagamento — Webhook × Polling** (o painel deixa você escolher):
|
||||||
- **Telegram**: `polling` (padrão, não precisa de domínio/HTTPS) ou `webhook` (informe a URL pública `https://SEU_DOMINIO/api/telegram/webhook`).
|
- **Polling** (padrão): o bot consulta o status do PIX a cada intervalo. Não precisa de domínio/HTTPS.
|
||||||
- **Mercado Pago**: `polling` (o bot consulta o status a cada intervalo) ou `webhook` (configure no painel do Mercado Pago a URL `https://SEU_DOMINIO/api/mp/webhook`).
|
- **Webhook**: configure no painel do Mercado Pago a URL `https://SEU_DOMINIO/api/mp/webhook`. O painel mostra a URL exata quando você seleciona esse modo.
|
||||||
|
- O **Telegram** usa long-polling automático — não requer domínio, webhook nem configuração extra.
|
||||||
4. Em **Planos**: crie planos SSH e/ou Xray (dias, conexões, preço em R$, e — para Xray — o *inbound* e protocolo). Para revendedores, defina o *custo em créditos*.
|
4. Em **Planos**: crie planos SSH e/ou Xray (dias, conexões, preço em R$, e — para Xray — o *inbound* e protocolo). Para revendedores, defina o *custo em créditos*.
|
||||||
5. Em **Pacotes de Crédito**: defina os valores de recarga dos revendedores.
|
5. Em **Pacotes de Crédito**: defina os valores de recarga dos revendedores.
|
||||||
6. Em **Clientes do Bot**: promova um usuário a **revendedor** (vinculando-o a uma conta de revendedor em *Revendedores*), ajuste créditos ou bloqueie.
|
6. Em **Clientes do Bot**: promova um usuário a **revendedor** (vinculando-o a uma conta de revendedor em *Revendedores*), ajuste créditos ou bloqueie.
|
||||||
@@ -1279,7 +1280,6 @@ Os tokens (Telegram, Mercado Pago e segredos de webhook) são gravados **criptog
|
|||||||
- `GET/POST /api/bot/transactions` — lista pagamentos; POST com `action` = `refund` \| `reprocess`.
|
- `GET/POST /api/bot/transactions` — lista pagamentos; POST com `action` = `refund` \| `reprocess`.
|
||||||
- `GET/POST /api/bot/settings` — textos do bot (chave/valor).
|
- `GET/POST /api/bot/settings` — textos do bot (chave/valor).
|
||||||
- `POST /api/bot/test` — testa token do Telegram e do Mercado Pago.
|
- `POST /api/bot/test` — testa token do Telegram e do Mercado Pago.
|
||||||
- `POST /api/mp/webhook` — **público**, chamado pelo Mercado Pago (valida `x-signature` se houver segredo; sempre reconfirma o pagamento na API antes de liberar).
|
- `POST /api/mp/webhook` — **público**, chamado pelo Mercado Pago quando o modo de confirmação é *webhook* (valida `x-signature` se houver segredo; sempre reconfirma o pagamento na API antes de liberar). Ignorado em modo *polling*.
|
||||||
- `POST /api/telegram/webhook` — **público**, chamado pelo Telegram (valida o header `X-Telegram-Bot-Api-Secret-Token`).
|
|
||||||
|
|
||||||
**EN-US:** The panel ships an in-process Telegram bot that sells **SSH and Xray** accounts via **PIX (Mercado Pago)**, with free trial, renewal, and a **credit-based reseller system** — all managed from the superadmin **Bot / Vendas** tab. Bot secrets are stored **AES-256-GCM encrypted** in PostgreSQL; the master key lives outside the DB (`BOT_MASTER_KEY` env or a `0600` `/opt/sshpanel/bot_master.key` auto-generated on first use — back it up). Both Telegram delivery and Mercado Pago confirmation can be toggled between **polling** (default, no public HTTPS needed) and **webhook** in the panel.
|
**EN-US:** The panel ships an in-process Telegram bot that sells **SSH and Xray** accounts via **PIX (Mercado Pago)**, with free trial, renewal, and a **credit-based reseller system** — all managed from the superadmin **Bot / Vendas** tab. Bot secrets are stored **AES-256-GCM encrypted** in PostgreSQL; the master key lives outside the DB (`BOT_MASTER_KEY` env or a `0600` `/opt/sshpanel/bot_master.key` auto-generated on first use — back it up). Telegram runs on long-polling (no domain needed); only **Mercado Pago confirmation** is toggleable between **polling** (default) and **webhook** in the panel.
|
||||||
|
|||||||
+15
-10
@@ -32,8 +32,6 @@ async function loadBotConfig() {
|
|||||||
const set = (id, v) => { const e = document.getElementById(id); if (e) e.value = v ?? ""; };
|
const set = (id, v) => { const e = document.getElementById(id); if (e) e.value = v ?? ""; };
|
||||||
const chk = (id, v) => { const e = document.getElementById(id); if (e) e.checked = !!v; };
|
const chk = (id, v) => { const e = document.getElementById(id); if (e) e.checked = !!v; };
|
||||||
chk("botEnabled", c.enabled);
|
chk("botEnabled", c.enabled);
|
||||||
set("botTelegramMode", c.telegram_mode);
|
|
||||||
set("botTelegramWebhookURL", c.telegram_webhook_url);
|
|
||||||
set("botMPConfirmMode", c.mp_confirm_mode);
|
set("botMPConfirmMode", c.mp_confirm_mode);
|
||||||
set("botMPPollInterval", c.mp_poll_interval);
|
set("botMPPollInterval", c.mp_poll_interval);
|
||||||
set("botPixExp", c.pix_expiration_minutes);
|
set("botPixExp", c.pix_expiration_minutes);
|
||||||
@@ -45,14 +43,23 @@ async function loadBotConfig() {
|
|||||||
set("botAdminIDs", (c.admin_telegram_ids || []).join(","));
|
set("botAdminIDs", (c.admin_telegram_ids || []).join(","));
|
||||||
set("botPublicHost", c.public_host);
|
set("botPublicHost", c.public_host);
|
||||||
set("botXrayPublicHost", c.xray_public_host);
|
set("botXrayPublicHost", c.xray_public_host);
|
||||||
document.getElementById("botHasTgToken").textContent = c.has_telegram_token ? "✓ configurado" : "não definido";
|
const hint = (id, ok) => { const e = document.getElementById(id); if (e) e.textContent = ok ? "✓ configurado" : "não definido"; };
|
||||||
document.getElementById("botHasTgSecret").textContent = c.has_telegram_webhook_secret ? "✓ configurado" : "não definido";
|
hint("botHasTgToken", c.has_telegram_token);
|
||||||
document.getElementById("botHasMpToken").textContent = c.has_mp_access_token ? "✓ configurado" : "não definido";
|
hint("botHasMpToken", c.has_mp_access_token);
|
||||||
document.getElementById("botHasMpSecret").textContent = c.has_mp_webhook_secret ? "✓ configurado" : "não definido";
|
hint("botHasMpSecret", c.has_mp_webhook_secret);
|
||||||
|
botToggleMPWebhookBox();
|
||||||
botStatus("botConfigStatus", "Carregado.");
|
botStatus("botConfigStatus", "Carregado.");
|
||||||
} catch (e) { if (e.message !== "auth") botStatus("botConfigStatus", "Erro ao carregar.", false); }
|
} catch (e) { if (e.message !== "auth") botStatus("botConfigStatus", "Erro ao carregar.", false); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function botToggleMPWebhookBox() {
|
||||||
|
const mode = document.getElementById("botMPConfirmMode")?.value;
|
||||||
|
const box = document.getElementById("botMPWebhookBox");
|
||||||
|
if (box) box.style.display = mode === "webhook" ? "" : "none";
|
||||||
|
const url = document.getElementById("botMPWebhookURL");
|
||||||
|
if (url) url.textContent = location.origin + "/api/mp/webhook";
|
||||||
|
}
|
||||||
|
|
||||||
async function saveBotConfig() {
|
async function saveBotConfig() {
|
||||||
const val = id => (document.getElementById(id)?.value || "").trim();
|
const val = id => (document.getElementById(id)?.value || "").trim();
|
||||||
const num = id => parseInt(document.getElementById(id)?.value || "0", 10) || 0;
|
const num = id => parseInt(document.getElementById(id)?.value || "0", 10) || 0;
|
||||||
@@ -61,9 +68,6 @@ async function saveBotConfig() {
|
|||||||
const body = {
|
const body = {
|
||||||
enabled: chk("botEnabled"),
|
enabled: chk("botEnabled"),
|
||||||
telegram_token: val("botTelegramToken"),
|
telegram_token: val("botTelegramToken"),
|
||||||
telegram_mode: val("botTelegramMode"),
|
|
||||||
telegram_webhook_url: val("botTelegramWebhookURL"),
|
|
||||||
telegram_webhook_secret: val("botTelegramWebhookSecret"),
|
|
||||||
mp_access_token: val("botMPToken"),
|
mp_access_token: val("botMPToken"),
|
||||||
mp_confirm_mode: val("botMPConfirmMode"),
|
mp_confirm_mode: val("botMPConfirmMode"),
|
||||||
mp_webhook_secret: val("botMPWebhookSecret"),
|
mp_webhook_secret: val("botMPWebhookSecret"),
|
||||||
@@ -80,7 +84,7 @@ async function saveBotConfig() {
|
|||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await api("/api/bot/config", { method: "POST", body: JSON.stringify(body) });
|
await api("/api/bot/config", { method: "POST", body: JSON.stringify(body) });
|
||||||
["botTelegramToken", "botTelegramWebhookSecret", "botMPToken", "botMPWebhookSecret"].forEach(id => { const e = document.getElementById(id); if (e) e.value = ""; });
|
["botTelegramToken", "botMPToken", "botMPWebhookSecret"].forEach(id => { const e = document.getElementById(id); if (e) e.value = ""; });
|
||||||
botStatus("botConfigStatus", "Configuração salva e bot reiniciado.");
|
botStatus("botConfigStatus", "Configuração salva e bot reiniciado.");
|
||||||
loadBotConfig();
|
loadBotConfig();
|
||||||
} catch (e) { if (e.message !== "auth") botStatus("botConfigStatus", "Erro ao salvar.", false); }
|
} catch (e) { if (e.message !== "auth") botStatus("botConfigStatus", "Erro ao salvar.", false); }
|
||||||
@@ -309,6 +313,7 @@ async function saveBotSettings() {
|
|||||||
document.getElementById("botConfigSaveBtn")?.addEventListener("click", saveBotConfig);
|
document.getElementById("botConfigSaveBtn")?.addEventListener("click", saveBotConfig);
|
||||||
document.getElementById("botConfigReloadBtn")?.addEventListener("click", loadBotConfig);
|
document.getElementById("botConfigReloadBtn")?.addEventListener("click", loadBotConfig);
|
||||||
document.getElementById("botTestBtn")?.addEventListener("click", testBot);
|
document.getElementById("botTestBtn")?.addEventListener("click", testBot);
|
||||||
|
document.getElementById("botMPConfirmMode")?.addEventListener("change", botToggleMPWebhookBox);
|
||||||
document.getElementById("botReloadPlansBtn")?.addEventListener("click", loadBotPlans);
|
document.getElementById("botReloadPlansBtn")?.addEventListener("click", loadBotPlans);
|
||||||
document.getElementById("botNewPlanBtn")?.addEventListener("click", botClearPlanForm);
|
document.getElementById("botNewPlanBtn")?.addEventListener("click", botClearPlanForm);
|
||||||
document.getElementById("botCancelPlanBtn")?.addEventListener("click", botClearPlanForm);
|
document.getElementById("botCancelPlanBtn")?.addEventListener("click", botClearPlanForm);
|
||||||
|
|||||||
+67
-37
@@ -16,7 +16,7 @@
|
|||||||
setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500);
|
setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="assets/app.css?v=20260711updatecheck1"/>
|
<link rel="stylesheet" href="assets/app.css?v=20260713bot2"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
@@ -952,39 +952,69 @@
|
|||||||
<!-- ═══════════ Bot / Vendas Tab (superadmin only) ═══════════ -->
|
<!-- ═══════════ Bot / Vendas Tab (superadmin only) ═══════════ -->
|
||||||
<div class="tab-pane" id="tab-bot">
|
<div class="tab-pane" id="tab-bot">
|
||||||
|
|
||||||
<!-- Config -->
|
<!-- Config: sticky action bar -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-hdr">
|
<div class="card-hdr">
|
||||||
<div class="card-title">🤖 Configuração do Bot</div>
|
<div class="card-title">🤖 Configuração do Bot</div>
|
||||||
<div style="display:flex;gap:5px;">
|
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
|
||||||
|
<span id="botConfigStatus" class="hint">Pronto.</span>
|
||||||
<button class="btn btn-ghost btn-sm" id="botTestBtn">Testar conexão</button>
|
<button class="btn btn-ghost btn-sm" id="botTestBtn">Testar conexão</button>
|
||||||
<button class="btn btn-ghost btn-sm" id="botConfigReloadBtn">Recarregar</button>
|
<button class="btn btn-ghost btn-sm" id="botConfigReloadBtn">Recarregar</button>
|
||||||
|
<button class="btn btn-sm" id="botConfigSaveBtn">Salvar configuração</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-grid">
|
<p class="hint" style="margin:2px 0 0;">1) Cole os tokens abaixo e clique <b>Testar</b> · 2) Crie os <b>Planos</b> · 3) Promova revendedores em <b>Clientes</b>. O Telegram funciona por long-polling — não precisa de domínio.</p>
|
||||||
<div class="field"><label>Bot ativo</label><input id="botEnabled" type="checkbox" style="width:16px;height:16px;margin-top:10px;"/></div>
|
</div>
|
||||||
<div class="field"><label>Token do Telegram <span class="hint" id="botHasTgToken"></span></label><input id="botTelegramToken" type="password" autocomplete="new-password" placeholder="(em branco = manter)"/></div>
|
|
||||||
<div class="field"><label>Modo do Telegram</label><select id="botTelegramMode"><option value="polling">polling (recomendado)</option><option value="webhook">webhook</option></select></div>
|
<div class="grid2">
|
||||||
<div class="field"><label>Telegram Webhook URL</label><input id="botTelegramWebhookURL" placeholder="https://seu.dominio/api/telegram/webhook"/></div>
|
<!-- Telegram -->
|
||||||
<div class="field"><label>Telegram Webhook Secret <span class="hint" id="botHasTgSecret"></span></label><input id="botTelegramWebhookSecret" type="password" autocomplete="new-password" placeholder="(em branco = manter)"/></div>
|
<div class="card">
|
||||||
<div class="field"><label>Mercado Pago Access Token <span class="hint" id="botHasMpToken"></span></label><input id="botMPToken" type="password" autocomplete="new-password" placeholder="(em branco = manter)"/></div>
|
<div class="card-hdr"><div class="card-title">✈️ Telegram</div></div>
|
||||||
<div class="field"><label>Confirmação do pagamento</label><select id="botMPConfirmMode"><option value="polling">polling (recomendado)</option><option value="webhook">webhook</option></select></div>
|
<label style="display:flex;align-items:center;gap:8px;margin-bottom:12px;cursor:pointer;">
|
||||||
<div class="field"><label>MP Webhook Secret <span class="hint" id="botHasMpSecret"></span></label><input id="botMPWebhookSecret" type="password" autocomplete="new-password" placeholder="(em branco = manter)"/></div>
|
<input id="botEnabled" type="checkbox" style="width:16px;height:16px;"/> <span>Bot ativo</span>
|
||||||
<div class="field"><label>Intervalo de verificação (polling)</label><input id="botMPPollInterval" placeholder="20s"/></div>
|
</label>
|
||||||
<div class="field"><label>Expiração do PIX (min)</label><input id="botPixExp" type="number" min="1" placeholder="30"/></div>
|
<div class="field"><label>Token do bot <span class="hint" id="botHasTgToken"></span></label><input id="botTelegramToken" type="password" autocomplete="new-password" placeholder="cole o token do @BotFather (em branco = manter)"/></div>
|
||||||
<div class="field"><label>Teste grátis ativo</label><input id="botTrialEnabled" type="checkbox" style="width:16px;height:16px;margin-top:10px;"/></div>
|
<div class="field"><label>IDs de admin <span class="hint">separados por vírgula</span></label><input id="botAdminIDs" placeholder="111111111,222222222"/></div>
|
||||||
<div class="field"><label>Teste: horas</label><input id="botTrialHours" type="number" min="1" placeholder="1"/></div>
|
|
||||||
<div class="field"><label>Teste: conexões</label><input id="botTrialMaxConns" type="number" min="1" placeholder="1"/></div>
|
|
||||||
<div class="field"><label>Teste: tipo</label><select id="botTrialKind"><option value="ssh">SSH</option><option value="xray">Xray</option></select></div>
|
|
||||||
<div class="field"><label>Teste: inbound Xray</label><input id="botTrialInbound" placeholder="tag do inbound (se Xray)"/></div>
|
|
||||||
<div class="field"><label>IDs admin no Telegram <span class="hint">(vírgula)</span></label><input id="botAdminIDs" placeholder="111111111,222222222"/></div>
|
|
||||||
<div class="field"><label>Host público SSH</label><input id="botPublicHost" placeholder="seu.dominio ou IP"/></div>
|
|
||||||
<div class="field"><label>Host público Xray</label><input id="botXrayPublicHost" placeholder="seu.dominio (para links vless/vmess)"/></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
|
||||||
<button class="btn" id="botConfigSaveBtn">Salvar configuração</button>
|
<!-- Mercado Pago -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-hdr"><div class="card-title">💠 Mercado Pago (PIX)</div></div>
|
||||||
|
<div class="field"><label>Access Token <span class="hint" id="botHasMpToken"></span></label><input id="botMPToken" type="password" autocomplete="new-password" placeholder="APP_USR-... (em branco = manter)"/></div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="field"><label>Confirmação</label><select id="botMPConfirmMode"><option value="polling">Polling (sem domínio)</option><option value="webhook">Webhook</option></select></div>
|
||||||
|
<div class="field"><label>Intervalo do polling</label><input id="botMPPollInterval" placeholder="20s"/></div>
|
||||||
|
<div class="field"><label>Expiração do PIX (min)</label><input id="botPixExp" type="number" min="1" placeholder="30"/></div>
|
||||||
|
</div>
|
||||||
|
<div id="botMPWebhookBox" style="display:none;margin-top:6px;">
|
||||||
|
<div class="field"><label>Webhook Secret <span class="hint" id="botHasMpSecret"></span></label><input id="botMPWebhookSecret" type="password" autocomplete="new-password" placeholder="(em branco = manter)"/></div>
|
||||||
|
<p class="hint" style="margin:4px 0 0;">No Mercado Pago, cadastre a URL de notificação:<br><code id="botMPWebhookURL">https://SEU_HOST/api/mp/webhook</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid2">
|
||||||
|
<!-- Trial -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-hdr"><div class="card-title">⏳ Teste Grátis</div></div>
|
||||||
|
<label style="display:flex;align-items:center;gap:8px;margin-bottom:12px;cursor:pointer;">
|
||||||
|
<input id="botTrialEnabled" type="checkbox" style="width:16px;height:16px;"/> <span>Oferecer teste grátis</span>
|
||||||
|
</label>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="field"><label>Duração (horas)</label><input id="botTrialHours" type="number" min="1" placeholder="1"/></div>
|
||||||
|
<div class="field"><label>Conexões</label><input id="botTrialMaxConns" type="number" min="1" placeholder="1"/></div>
|
||||||
|
<div class="field"><label>Tipo</label><select id="botTrialKind"><option value="ssh">SSH</option><option value="xray">Xray</option></select></div>
|
||||||
|
<div class="field"><label>Inbound (se Xray)</label><input id="botTrialInbound" list="botInboundList" placeholder="tag"/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delivery host -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-hdr"><div class="card-title">🌐 Host de entrega</div></div>
|
||||||
|
<div class="field"><label>Host/IP para SSH</label><input id="botPublicHost" placeholder="seu.dominio ou IP público"/></div>
|
||||||
|
<div class="field"><label>Host para links Xray</label><input id="botXrayPublicHost" placeholder="seu.dominio (vless/vmess)"/></div>
|
||||||
|
<p class="hint" style="margin:4px 0 0;">Enviado ao cliente nas credenciais após o pagamento.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="statusbar"><span id="botConfigStatus">Ready.</span></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid2">
|
<div class="grid2">
|
||||||
@@ -1433,17 +1463,17 @@
|
|||||||
<!-- app.js was split into ordered modules for maintainability. They are plain
|
<!-- app.js was split into ordered modules for maintainability. They are plain
|
||||||
classic scripts sharing one global scope; `defer` preserves execution order,
|
classic scripts sharing one global scope; `defer` preserves execution order,
|
||||||
so behavior is identical to the old single file. Keep this load order. -->
|
so behavior is identical to the old single file. Keep this load order. -->
|
||||||
<script defer src="assets/js/01-core.js?v=20260711updatecheck1"></script>
|
<script defer src="assets/js/01-core.js?v=20260713bot2"></script>
|
||||||
<script defer src="assets/js/02-shell.js?v=20260711updatecheck1"></script>
|
<script defer src="assets/js/02-shell.js?v=20260713bot2"></script>
|
||||||
<script defer src="assets/js/03-ssh-users.js?v=20260711updatecheck1"></script>
|
<script defer src="assets/js/03-ssh-users.js?v=20260713bot2"></script>
|
||||||
<script defer src="assets/js/04-xray.js?v=20260711updatecheck1"></script>
|
<script defer src="assets/js/04-xray.js?v=20260713bot2"></script>
|
||||||
<script defer src="assets/js/05-resellers.js?v=20260711updatecheck1"></script>
|
<script defer src="assets/js/05-resellers.js?v=20260713bot2"></script>
|
||||||
<script defer src="assets/js/06-servers.js?v=20260711updatecheck1"></script>
|
<script defer src="assets/js/06-servers.js?v=20260713bot2"></script>
|
||||||
<script defer src="assets/js/07-stats-logs.js?v=20260711updatecheck1"></script>
|
<script defer src="assets/js/07-stats-logs.js?v=20260713bot2"></script>
|
||||||
<script defer src="assets/js/08-server-config.js?v=20260711updatecheck1"></script>
|
<script defer src="assets/js/08-server-config.js?v=20260713bot2"></script>
|
||||||
<script defer src="assets/js/09-xray-wizard.js?v=20260711updatecheck1"></script>
|
<script defer src="assets/js/09-xray-wizard.js?v=20260713bot2"></script>
|
||||||
<script defer src="assets/js/11-update-status.js?v=20260711updatecheck1"></script>
|
<script defer src="assets/js/11-update-status.js?v=20260713bot2"></script>
|
||||||
<script defer src="assets/js/12-bot.js?v=20260711updatecheck1"></script>
|
<script defer src="assets/js/12-bot.js?v=20260713bot2"></script>
|
||||||
<script defer src="assets/js/10-boot.js?v=20260711updatecheck1"></script>
|
<script defer src="assets/js/10-boot.js?v=20260713bot2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+52
-84
@@ -1,6 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
// bot_api.go — /api/bot/* admin endpoints (superadmin) + Telegram webhook route.
|
// bot_api.go — /api/bot/* admin endpoints (superadmin).
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -25,30 +25,26 @@ func botStoreReady(w http.ResponseWriter, store *Store) bool {
|
|||||||
// ---------- Config ----------
|
// ---------- Config ----------
|
||||||
|
|
||||||
type botConfigDTO struct {
|
type botConfigDTO struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
TelegramMode string `json:"telegram_mode"`
|
MPConfirmMode string `json:"mp_confirm_mode"`
|
||||||
TelegramWebhookURL string `json:"telegram_webhook_url"`
|
MPPollInterval string `json:"mp_poll_interval"`
|
||||||
MPConfirmMode string `json:"mp_confirm_mode"`
|
PixExpirationMinutes int `json:"pix_expiration_minutes"`
|
||||||
MPPollInterval string `json:"mp_poll_interval"`
|
TrialEnabled bool `json:"trial_enabled"`
|
||||||
PixExpirationMinutes int `json:"pix_expiration_minutes"`
|
TrialHours int `json:"trial_hours"`
|
||||||
TrialEnabled bool `json:"trial_enabled"`
|
TrialMaxConnections int `json:"trial_max_connections"`
|
||||||
TrialHours int `json:"trial_hours"`
|
TrialKind string `json:"trial_kind"`
|
||||||
TrialMaxConnections int `json:"trial_max_connections"`
|
TrialInboundTag string `json:"trial_inbound_tag"`
|
||||||
TrialKind string `json:"trial_kind"`
|
AdminTelegramIDs []int64 `json:"admin_telegram_ids"`
|
||||||
TrialInboundTag string `json:"trial_inbound_tag"`
|
Currency string `json:"currency"`
|
||||||
AdminTelegramIDs []int64 `json:"admin_telegram_ids"`
|
PublicHost string `json:"public_host"`
|
||||||
Currency string `json:"currency"`
|
XrayPublicHost string `json:"xray_public_host"`
|
||||||
PublicHost string `json:"public_host"`
|
HasTelegramToken bool `json:"has_telegram_token"`
|
||||||
XrayPublicHost string `json:"xray_public_host"`
|
HasMPAccessToken bool `json:"has_mp_access_token"`
|
||||||
HasTelegramToken bool `json:"has_telegram_token"`
|
HasMPWebhookSecret bool `json:"has_mp_webhook_secret"`
|
||||||
HasMPAccessToken bool `json:"has_mp_access_token"`
|
|
||||||
HasTelegramWebhookSecret bool `json:"has_telegram_webhook_secret"`
|
|
||||||
HasMPWebhookSecret bool `json:"has_mp_webhook_secret"`
|
|
||||||
// Write-only secret fields (empty on GET; empty on POST = keep existing).
|
// Write-only secret fields (empty on GET; empty on POST = keep existing).
|
||||||
TelegramToken string `json:"telegram_token"`
|
TelegramToken string `json:"telegram_token"`
|
||||||
MPAccessToken string `json:"mp_access_token"`
|
MPAccessToken string `json:"mp_access_token"`
|
||||||
TelegramWebhookSecret string `json:"telegram_webhook_secret"`
|
MPWebhookSecret string `json:"mp_webhook_secret"`
|
||||||
MPWebhookSecret string `json:"mp_webhook_secret"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleBotConfig(store *Store) http.HandlerFunc {
|
func handleBotConfig(store *Store) http.HandlerFunc {
|
||||||
@@ -65,25 +61,22 @@ func handleBotConfig(store *Store) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
botWriteJSON(w, botConfigDTO{
|
botWriteJSON(w, botConfigDTO{
|
||||||
Enabled: cfg.Enabled,
|
Enabled: cfg.Enabled,
|
||||||
TelegramMode: cfg.TelegramMode,
|
MPConfirmMode: cfg.MPConfirmMode,
|
||||||
TelegramWebhookURL: cfg.TelegramWebhookURL,
|
MPPollInterval: cfg.MPPollInterval,
|
||||||
MPConfirmMode: cfg.MPConfirmMode,
|
PixExpirationMinutes: cfg.PixExpirationMinutes,
|
||||||
MPPollInterval: cfg.MPPollInterval,
|
TrialEnabled: cfg.TrialEnabled,
|
||||||
PixExpirationMinutes: cfg.PixExpirationMinutes,
|
TrialHours: cfg.TrialHours,
|
||||||
TrialEnabled: cfg.TrialEnabled,
|
TrialMaxConnections: cfg.TrialMaxConnections,
|
||||||
TrialHours: cfg.TrialHours,
|
TrialKind: cfg.TrialKind,
|
||||||
TrialMaxConnections: cfg.TrialMaxConnections,
|
TrialInboundTag: cfg.TrialInboundTag,
|
||||||
TrialKind: cfg.TrialKind,
|
AdminTelegramIDs: cfg.AdminTelegramIDs,
|
||||||
TrialInboundTag: cfg.TrialInboundTag,
|
Currency: cfg.Currency,
|
||||||
AdminTelegramIDs: cfg.AdminTelegramIDs,
|
PublicHost: cfg.PublicHost,
|
||||||
Currency: cfg.Currency,
|
XrayPublicHost: cfg.XrayPublicHost,
|
||||||
PublicHost: cfg.PublicHost,
|
HasTelegramToken: cfg.TelegramToken != "",
|
||||||
XrayPublicHost: cfg.XrayPublicHost,
|
HasMPAccessToken: cfg.MPAccessToken != "",
|
||||||
HasTelegramToken: cfg.TelegramToken != "",
|
HasMPWebhookSecret: cfg.MPWebhookSecret != "",
|
||||||
HasMPAccessToken: cfg.MPAccessToken != "",
|
|
||||||
HasTelegramWebhookSecret: cfg.TelegramWebhookSecret != "",
|
|
||||||
HasMPWebhookSecret: cfg.MPWebhookSecret != "",
|
|
||||||
})
|
})
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
var dto botConfigDTO
|
var dto botConfigDTO
|
||||||
@@ -92,25 +85,22 @@ func handleBotConfig(store *Store) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
cfg := &BotConfig{
|
cfg := &BotConfig{
|
||||||
Enabled: dto.Enabled,
|
Enabled: dto.Enabled,
|
||||||
TelegramToken: strings.TrimSpace(dto.TelegramToken),
|
TelegramToken: strings.TrimSpace(dto.TelegramToken),
|
||||||
TelegramMode: dto.TelegramMode,
|
MPAccessToken: strings.TrimSpace(dto.MPAccessToken),
|
||||||
TelegramWebhookURL: strings.TrimSpace(dto.TelegramWebhookURL),
|
MPConfirmMode: dto.MPConfirmMode,
|
||||||
TelegramWebhookSecret: strings.TrimSpace(dto.TelegramWebhookSecret),
|
MPWebhookSecret: strings.TrimSpace(dto.MPWebhookSecret),
|
||||||
MPAccessToken: strings.TrimSpace(dto.MPAccessToken),
|
MPPollInterval: dto.MPPollInterval,
|
||||||
MPConfirmMode: dto.MPConfirmMode,
|
PixExpirationMinutes: dto.PixExpirationMinutes,
|
||||||
MPWebhookSecret: strings.TrimSpace(dto.MPWebhookSecret),
|
TrialEnabled: dto.TrialEnabled,
|
||||||
MPPollInterval: dto.MPPollInterval,
|
TrialHours: dto.TrialHours,
|
||||||
PixExpirationMinutes: dto.PixExpirationMinutes,
|
TrialMaxConnections: dto.TrialMaxConnections,
|
||||||
TrialEnabled: dto.TrialEnabled,
|
TrialKind: dto.TrialKind,
|
||||||
TrialHours: dto.TrialHours,
|
TrialInboundTag: strings.TrimSpace(dto.TrialInboundTag),
|
||||||
TrialMaxConnections: dto.TrialMaxConnections,
|
AdminTelegramIDs: dto.AdminTelegramIDs,
|
||||||
TrialKind: dto.TrialKind,
|
Currency: dto.Currency,
|
||||||
TrialInboundTag: strings.TrimSpace(dto.TrialInboundTag),
|
PublicHost: strings.TrimSpace(dto.PublicHost),
|
||||||
AdminTelegramIDs: dto.AdminTelegramIDs,
|
XrayPublicHost: strings.TrimSpace(dto.XrayPublicHost),
|
||||||
Currency: dto.Currency,
|
|
||||||
PublicHost: strings.TrimSpace(dto.PublicHost),
|
|
||||||
XrayPublicHost: strings.TrimSpace(dto.XrayPublicHost),
|
|
||||||
}
|
}
|
||||||
if err := SaveBotConfig(ctx, store, cfg); err != nil {
|
if err := SaveBotConfig(ctx, store, cfg); err != nil {
|
||||||
http.Error(w, "save config: "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "save config: "+err.Error(), http.StatusInternalServerError)
|
||||||
@@ -421,25 +411,3 @@ func handleBotTest(store *Store) http.HandlerFunc {
|
|||||||
botWriteJSON(w, out)
|
botWriteJSON(w, out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Telegram webhook (public) ----------
|
|
||||||
|
|
||||||
func handleTelegramWebhook(w http.ResponseWriter, r *http.Request) {
|
|
||||||
b := currentBot()
|
|
||||||
if b == nil {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if b.cfg.TelegramWebhookSecret != "" &&
|
|
||||||
r.Header.Get("X-Telegram-Bot-Api-Secret-Token") != b.cfg.TelegramWebhookSecret {
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
up, err := parseWebhookUpdate(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go b.handleUpdate(up)
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|||||||
+39
-54
@@ -14,26 +14,25 @@ var errInsufficientCredits = errors.New("insufficient credits")
|
|||||||
func botItoa(n int) string { return strconv.Itoa(n) }
|
func botItoa(n int) string { return strconv.Itoa(n) }
|
||||||
|
|
||||||
// BotConfig is the decrypted, ready-to-use bot configuration.
|
// BotConfig is the decrypted, ready-to-use bot configuration.
|
||||||
|
// Telegram always uses long-polling (no webhook). The webhook/polling toggle
|
||||||
|
// applies only to Mercado Pago payment confirmation (MPConfirmMode).
|
||||||
type BotConfig struct {
|
type BotConfig struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
TelegramToken string
|
TelegramToken string
|
||||||
TelegramMode string // polling | webhook
|
MPAccessToken string
|
||||||
TelegramWebhookURL string
|
MPConfirmMode string // webhook | polling
|
||||||
TelegramWebhookSecret string
|
MPWebhookSecret string
|
||||||
MPAccessToken string
|
MPPollInterval string
|
||||||
MPConfirmMode string // webhook | polling
|
PixExpirationMinutes int
|
||||||
MPWebhookSecret string
|
TrialEnabled bool
|
||||||
MPPollInterval string
|
TrialHours int
|
||||||
PixExpirationMinutes int
|
TrialMaxConnections int
|
||||||
TrialEnabled bool
|
TrialKind string // ssh | xray
|
||||||
TrialHours int
|
TrialInboundTag string
|
||||||
TrialMaxConnections int
|
AdminTelegramIDs []int64
|
||||||
TrialKind string // ssh | xray
|
Currency string
|
||||||
TrialInboundTag string
|
PublicHost string // SSH connection host shown to buyers
|
||||||
AdminTelegramIDs []int64
|
XrayPublicHost string // host used to build vless/vmess links
|
||||||
Currency string
|
|
||||||
PublicHost string // SSH connection host shown to buyers
|
|
||||||
XrayPublicHost string // host used to build vless/vmess links
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadBotConfig reads the config row and decrypts secrets into a BotConfig.
|
// LoadBotConfig reads the config row and decrypts secrets into a BotConfig.
|
||||||
@@ -46,10 +45,6 @@ func LoadBotConfig(ctx context.Context, store *Store) (*BotConfig, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tgSec, err := decryptSecret(r.TelegramWebhookSecretEnc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mp, err := decryptSecret(r.MPAccessTokenEnc)
|
mp, err := decryptSecret(r.MPAccessTokenEnc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -60,34 +55,28 @@ func LoadBotConfig(ctx context.Context, store *Store) (*BotConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg := &BotConfig{
|
cfg := &BotConfig{
|
||||||
Enabled: r.Enabled,
|
Enabled: r.Enabled,
|
||||||
TelegramToken: tok,
|
TelegramToken: tok,
|
||||||
TelegramMode: r.TelegramMode,
|
MPAccessToken: mp,
|
||||||
TelegramWebhookURL: r.TelegramWebhookURL,
|
MPConfirmMode: r.MPConfirmMode,
|
||||||
TelegramWebhookSecret: tgSec,
|
MPWebhookSecret: mpSec,
|
||||||
MPAccessToken: mp,
|
MPPollInterval: r.MPPollInterval,
|
||||||
MPConfirmMode: r.MPConfirmMode,
|
PixExpirationMinutes: r.PixExpirationMinutes,
|
||||||
MPWebhookSecret: mpSec,
|
TrialEnabled: r.TrialEnabled,
|
||||||
MPPollInterval: r.MPPollInterval,
|
TrialHours: r.TrialHours,
|
||||||
PixExpirationMinutes: r.PixExpirationMinutes,
|
TrialMaxConnections: r.TrialMaxConnections,
|
||||||
TrialEnabled: r.TrialEnabled,
|
TrialKind: r.TrialKind,
|
||||||
TrialHours: r.TrialHours,
|
TrialInboundTag: r.TrialInboundTag,
|
||||||
TrialMaxConnections: r.TrialMaxConnections,
|
AdminTelegramIDs: r.AdminTelegramIDs,
|
||||||
TrialKind: r.TrialKind,
|
Currency: r.Currency,
|
||||||
TrialInboundTag: r.TrialInboundTag,
|
PublicHost: r.PublicHost,
|
||||||
AdminTelegramIDs: r.AdminTelegramIDs,
|
XrayPublicHost: r.XrayPublicHost,
|
||||||
Currency: r.Currency,
|
|
||||||
PublicHost: r.PublicHost,
|
|
||||||
XrayPublicHost: r.XrayPublicHost,
|
|
||||||
}
|
}
|
||||||
cfg.applyDefaults()
|
cfg.applyDefaults()
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *BotConfig) applyDefaults() {
|
func (c *BotConfig) applyDefaults() {
|
||||||
if c.TelegramMode == "" {
|
|
||||||
c.TelegramMode = "polling"
|
|
||||||
}
|
|
||||||
if c.MPConfirmMode == "" {
|
if c.MPConfirmMode == "" {
|
||||||
c.MPConfirmMode = "polling"
|
c.MPConfirmMode = "polling"
|
||||||
}
|
}
|
||||||
@@ -126,8 +115,8 @@ func SaveBotConfig(ctx context.Context, store *Store, cfg *BotConfig) error {
|
|||||||
cfg.applyDefaults()
|
cfg.applyDefaults()
|
||||||
row := &botConfigRow{
|
row := &botConfigRow{
|
||||||
Enabled: cfg.Enabled,
|
Enabled: cfg.Enabled,
|
||||||
TelegramMode: cfg.TelegramMode,
|
TelegramMode: "polling", // Telegram is always long-polling
|
||||||
TelegramWebhookURL: cfg.TelegramWebhookURL,
|
TelegramWebhookURL: "",
|
||||||
MPConfirmMode: cfg.MPConfirmMode,
|
MPConfirmMode: cfg.MPConfirmMode,
|
||||||
MPPollInterval: cfg.MPPollInterval,
|
MPPollInterval: cfg.MPPollInterval,
|
||||||
PixExpirationMinutes: cfg.PixExpirationMinutes,
|
PixExpirationMinutes: cfg.PixExpirationMinutes,
|
||||||
@@ -141,18 +130,13 @@ func SaveBotConfig(ctx context.Context, store *Store, cfg *BotConfig) error {
|
|||||||
PublicHost: cfg.PublicHost,
|
PublicHost: cfg.PublicHost,
|
||||||
XrayPublicHost: cfg.XrayPublicHost,
|
XrayPublicHost: cfg.XrayPublicHost,
|
||||||
}
|
}
|
||||||
var tokEnc, tgSecEnc, mpEnc, mpSecEnc []byte
|
var tokEnc, mpEnc, mpSecEnc []byte
|
||||||
var err error
|
var err error
|
||||||
if cfg.TelegramToken != "" {
|
if cfg.TelegramToken != "" {
|
||||||
if tokEnc, err = encryptSecret(cfg.TelegramToken); err != nil {
|
if tokEnc, err = encryptSecret(cfg.TelegramToken); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cfg.TelegramWebhookSecret != "" {
|
|
||||||
if tgSecEnc, err = encryptSecret(cfg.TelegramWebhookSecret); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if cfg.MPAccessToken != "" {
|
if cfg.MPAccessToken != "" {
|
||||||
if mpEnc, err = encryptSecret(cfg.MPAccessToken); err != nil {
|
if mpEnc, err = encryptSecret(cfg.MPAccessToken); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -163,5 +147,6 @@ func SaveBotConfig(ctx context.Context, store *Store, cfg *BotConfig) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return store.saveBotConfigRow(ctx, row, tokEnc, tgSecEnc, mpEnc, mpSecEnc)
|
// tgSecEnc is always nil now (no Telegram webhook secret).
|
||||||
|
return store.saveBotConfigRow(ctx, row, tokEnc, nil, mpEnc, mpSecEnc)
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-11
@@ -81,18 +81,13 @@ func startBotService(store *Store) {
|
|||||||
func reloadBotService(store *Store) { startBotService(store) }
|
func reloadBotService(store *Store) { startBotService(store) }
|
||||||
|
|
||||||
func (b *Bot) start() {
|
func (b *Bot) start() {
|
||||||
log.Printf("[bot] starting (telegram_mode=%s mp_confirm=%s)", b.cfg.TelegramMode, b.cfg.MPConfirmMode)
|
log.Printf("[bot] starting (telegram=long-polling, mp_confirm=%s)", b.cfg.MPConfirmMode)
|
||||||
if b.cfg.TelegramMode == "webhook" && b.cfg.TelegramWebhookURL != "" {
|
// Telegram uses long-polling only. Clearing any stale webhook + polling both
|
||||||
if err := b.tg.setWebhook(b.ctx, b.cfg.TelegramWebhookURL, b.cfg.TelegramWebhookSecret); err != nil {
|
// hit the network, so run off the goroutine that holds botMgrMu.
|
||||||
log.Printf("[bot] setWebhook failed, falling back to polling: %v", err)
|
go func() {
|
||||||
go b.runPolling()
|
|
||||||
} else {
|
|
||||||
log.Printf("[bot] webhook registered at %s", b.cfg.TelegramWebhookURL)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_ = b.tg.deleteWebhook(b.ctx)
|
_ = b.tg.deleteWebhook(b.ctx)
|
||||||
go b.runPolling()
|
b.runPolling()
|
||||||
}
|
}()
|
||||||
if b.mp != nil && b.cfg.MPConfirmMode == "polling" {
|
if b.mp != nil && b.cfg.MPConfirmMode == "polling" {
|
||||||
go b.runPaymentPoller()
|
go b.runPaymentPoller()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,6 +162,12 @@ func handleMPWebhook(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK) // bot disabled; acknowledge to stop retries
|
w.WriteHeader(http.StatusOK) // bot disabled; acknowledge to stop retries
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Only honor webhooks when confirmation mode is "webhook". In polling mode
|
||||||
|
// the poller drives fulfillment; ignore unsolicited posts to this endpoint.
|
||||||
|
if b.cfg.MPConfirmMode != "webhook" {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
// Extract the payment id from body or query.
|
// Extract the payment id from body or query.
|
||||||
dataID := r.URL.Query().Get("data.id")
|
dataID := r.URL.Query().Get("data.id")
|
||||||
if dataID == "" {
|
if dataID == "" {
|
||||||
|
|||||||
@@ -206,18 +206,6 @@ func (c *tgClient) sendPhotoBytes(ctx context.Context, chatID int64, photo []byt
|
|||||||
return m.MessageID, nil
|
return m.MessageID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *tgClient) setWebhook(ctx context.Context, webhookURL, secret string) error {
|
|
||||||
payload := map[string]interface{}{
|
|
||||||
"url": webhookURL,
|
|
||||||
"allowed_updates": []string{"message", "callback_query"},
|
|
||||||
}
|
|
||||||
if secret != "" {
|
|
||||||
payload["secret_token"] = secret
|
|
||||||
}
|
|
||||||
_, err := c.call(ctx, "setWebhook", payload)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *tgClient) deleteWebhook(ctx context.Context) error {
|
func (c *tgClient) deleteWebhook(ctx context.Context) error {
|
||||||
_, err := c.call(ctx, "deleteWebhook", map[string]interface{}{"drop_pending_updates": false})
|
_, err := c.call(ctx, "deleteWebhook", map[string]interface{}{"drop_pending_updates": false})
|
||||||
return err
|
return err
|
||||||
@@ -242,12 +230,3 @@ func htmlEscape(s string) string {
|
|||||||
s = strings.ReplaceAll(s, ">", ">")
|
s = strings.ReplaceAll(s, ">", ">")
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseWebhookUpdate decodes a Telegram webhook POST body.
|
|
||||||
func parseWebhookUpdate(r io.Reader) (*tgUpdate, error) {
|
|
||||||
var u tgUpdate
|
|
||||||
if err := json.NewDecoder(io.LimitReader(r, 4<<20)).Decode(&u); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &u, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1620,9 +1620,8 @@ func startAdminAPI(store *Store, addr string, adminDir string) {
|
|||||||
mux.Handle("/api/bot/settings", saSession(http.HandlerFunc(handleBotSettings(store))))
|
mux.Handle("/api/bot/settings", saSession(http.HandlerFunc(handleBotSettings(store))))
|
||||||
mux.Handle("/api/bot/test", saSession(http.HandlerFunc(handleBotTest(store))))
|
mux.Handle("/api/bot/test", saSession(http.HandlerFunc(handleBotTest(store))))
|
||||||
|
|
||||||
// Public: payment + Telegram webhooks (secured by signature/secret inside).
|
// Public: Mercado Pago payment webhook (validated by signature inside).
|
||||||
mux.Handle("/api/mp/webhook", http.HandlerFunc(handleMPWebhook))
|
mux.Handle("/api/mp/webhook", http.HandlerFunc(handleMPWebhook))
|
||||||
mux.Handle("/api/telegram/webhook", http.HandlerFunc(handleTelegramWebhook))
|
|
||||||
|
|
||||||
// Public: user/UUID check — no auth, CORS *.
|
// Public: user/UUID check — no auth, CORS *.
|
||||||
mux.Handle("/check", http.HandlerFunc(handleCheck))
|
mux.Handle("/check", http.HandlerFunc(handleCheck))
|
||||||
|
|||||||
Reference in New Issue
Block a user