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
|
||||
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**.
|
||||
3. **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`).
|
||||
- **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`).
|
||||
3. **Confirmação do pagamento — Webhook × Polling** (o painel deixa você escolher):
|
||||
- **Polling** (padrão): o bot consulta o status do PIX a cada intervalo. Não precisa de domínio/HTTPS.
|
||||
- **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*.
|
||||
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.
|
||||
@@ -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/settings` — textos do bot (chave/valor).
|
||||
- `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/telegram/webhook` — **público**, chamado pelo Telegram (valida o header `X-Telegram-Bot-Api-Secret-Token`).
|
||||
- `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*.
|
||||
|
||||
**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 chk = (id, v) => { const e = document.getElementById(id); if (e) e.checked = !!v; };
|
||||
chk("botEnabled", c.enabled);
|
||||
set("botTelegramMode", c.telegram_mode);
|
||||
set("botTelegramWebhookURL", c.telegram_webhook_url);
|
||||
set("botMPConfirmMode", c.mp_confirm_mode);
|
||||
set("botMPPollInterval", c.mp_poll_interval);
|
||||
set("botPixExp", c.pix_expiration_minutes);
|
||||
@@ -45,14 +43,23 @@ async function loadBotConfig() {
|
||||
set("botAdminIDs", (c.admin_telegram_ids || []).join(","));
|
||||
set("botPublicHost", c.public_host);
|
||||
set("botXrayPublicHost", c.xray_public_host);
|
||||
document.getElementById("botHasTgToken").textContent = c.has_telegram_token ? "✓ configurado" : "não definido";
|
||||
document.getElementById("botHasTgSecret").textContent = c.has_telegram_webhook_secret ? "✓ configurado" : "não definido";
|
||||
document.getElementById("botHasMpToken").textContent = c.has_mp_access_token ? "✓ configurado" : "não definido";
|
||||
document.getElementById("botHasMpSecret").textContent = c.has_mp_webhook_secret ? "✓ configurado" : "não definido";
|
||||
const hint = (id, ok) => { const e = document.getElementById(id); if (e) e.textContent = ok ? "✓ configurado" : "não definido"; };
|
||||
hint("botHasTgToken", c.has_telegram_token);
|
||||
hint("botHasMpToken", c.has_mp_access_token);
|
||||
hint("botHasMpSecret", c.has_mp_webhook_secret);
|
||||
botToggleMPWebhookBox();
|
||||
botStatus("botConfigStatus", "Carregado.");
|
||||
} 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() {
|
||||
const val = id => (document.getElementById(id)?.value || "").trim();
|
||||
const num = id => parseInt(document.getElementById(id)?.value || "0", 10) || 0;
|
||||
@@ -61,9 +68,6 @@ async function saveBotConfig() {
|
||||
const body = {
|
||||
enabled: chk("botEnabled"),
|
||||
telegram_token: val("botTelegramToken"),
|
||||
telegram_mode: val("botTelegramMode"),
|
||||
telegram_webhook_url: val("botTelegramWebhookURL"),
|
||||
telegram_webhook_secret: val("botTelegramWebhookSecret"),
|
||||
mp_access_token: val("botMPToken"),
|
||||
mp_confirm_mode: val("botMPConfirmMode"),
|
||||
mp_webhook_secret: val("botMPWebhookSecret"),
|
||||
@@ -80,7 +84,7 @@ async function saveBotConfig() {
|
||||
};
|
||||
try {
|
||||
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.");
|
||||
loadBotConfig();
|
||||
} 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("botConfigReloadBtn")?.addEventListener("click", loadBotConfig);
|
||||
document.getElementById("botTestBtn")?.addEventListener("click", testBot);
|
||||
document.getElementById("botMPConfirmMode")?.addEventListener("change", botToggleMPWebhookBox);
|
||||
document.getElementById("botReloadPlansBtn")?.addEventListener("click", loadBotPlans);
|
||||
document.getElementById("botNewPlanBtn")?.addEventListener("click", botClearPlanForm);
|
||||
document.getElementById("botCancelPlanBtn")?.addEventListener("click", botClearPlanForm);
|
||||
|
||||
+67
-37
@@ -16,7 +16,7 @@
|
||||
setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500);
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="assets/app.css?v=20260711updatecheck1"/>
|
||||
<link rel="stylesheet" href="assets/app.css?v=20260713bot2"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
@@ -952,39 +952,69 @@
|
||||
<!-- ═══════════ Bot / Vendas Tab (superadmin only) ═══════════ -->
|
||||
<div class="tab-pane" id="tab-bot">
|
||||
|
||||
<!-- Config -->
|
||||
<!-- Config: sticky action bar -->
|
||||
<div class="card">
|
||||
<div class="card-hdr">
|
||||
<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="botConfigReloadBtn">Recarregar</button>
|
||||
<button class="btn btn-sm" id="botConfigSaveBtn">Salvar configuração</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="field"><label>Bot ativo</label><input id="botEnabled" type="checkbox" style="width:16px;height:16px;margin-top:10px;"/></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="field"><label>Telegram Webhook URL</label><input id="botTelegramWebhookURL" placeholder="https://seu.dominio/api/telegram/webhook"/></div>
|
||||
<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="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="field"><label>Confirmação do pagamento</label><select id="botMPConfirmMode"><option value="polling">polling (recomendado)</option><option value="webhook">webhook</option></select></div>
|
||||
<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>
|
||||
<div class="field"><label>Intervalo de verificação (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 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>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>
|
||||
<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>
|
||||
|
||||
<div class="grid2">
|
||||
<!-- Telegram -->
|
||||
<div class="card">
|
||||
<div class="card-hdr"><div class="card-title">✈️ Telegram</div></div>
|
||||
<label style="display:flex;align-items:center;gap:8px;margin-bottom:12px;cursor:pointer;">
|
||||
<input id="botEnabled" type="checkbox" style="width:16px;height:16px;"/> <span>Bot ativo</span>
|
||||
</label>
|
||||
<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>IDs de admin <span class="hint">separados por vírgula</span></label><input id="botAdminIDs" placeholder="111111111,222222222"/></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 class="statusbar"><span id="botConfigStatus">Ready.</span></div>
|
||||
</div>
|
||||
|
||||
<div class="grid2">
|
||||
@@ -1433,17 +1463,17 @@
|
||||
<!-- app.js was split into ordered modules for maintainability. They are plain
|
||||
classic scripts sharing one global scope; `defer` preserves execution 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/02-shell.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/03-ssh-users.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/04-xray.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/05-resellers.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/06-servers.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/07-stats-logs.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/08-server-config.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/09-xray-wizard.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/11-update-status.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/12-bot.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/10-boot.js?v=20260711updatecheck1"></script>
|
||||
<script defer src="assets/js/01-core.js?v=20260713bot2"></script>
|
||||
<script defer src="assets/js/02-shell.js?v=20260713bot2"></script>
|
||||
<script defer src="assets/js/03-ssh-users.js?v=20260713bot2"></script>
|
||||
<script defer src="assets/js/04-xray.js?v=20260713bot2"></script>
|
||||
<script defer src="assets/js/05-resellers.js?v=20260713bot2"></script>
|
||||
<script defer src="assets/js/06-servers.js?v=20260713bot2"></script>
|
||||
<script defer src="assets/js/07-stats-logs.js?v=20260713bot2"></script>
|
||||
<script defer src="assets/js/08-server-config.js?v=20260713bot2"></script>
|
||||
<script defer src="assets/js/09-xray-wizard.js?v=20260713bot2"></script>
|
||||
<script defer src="assets/js/11-update-status.js?v=20260713bot2"></script>
|
||||
<script defer src="assets/js/12-bot.js?v=20260713bot2"></script>
|
||||
<script defer src="assets/js/10-boot.js?v=20260713bot2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+52
-84
@@ -1,6 +1,6 @@
|
||||
package main
|
||||
|
||||
// bot_api.go — /api/bot/* admin endpoints (superadmin) + Telegram webhook route.
|
||||
// bot_api.go — /api/bot/* admin endpoints (superadmin).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -25,30 +25,26 @@ func botStoreReady(w http.ResponseWriter, store *Store) bool {
|
||||
// ---------- Config ----------
|
||||
|
||||
type botConfigDTO struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
TelegramMode string `json:"telegram_mode"`
|
||||
TelegramWebhookURL string `json:"telegram_webhook_url"`
|
||||
MPConfirmMode string `json:"mp_confirm_mode"`
|
||||
MPPollInterval string `json:"mp_poll_interval"`
|
||||
PixExpirationMinutes int `json:"pix_expiration_minutes"`
|
||||
TrialEnabled bool `json:"trial_enabled"`
|
||||
TrialHours int `json:"trial_hours"`
|
||||
TrialMaxConnections int `json:"trial_max_connections"`
|
||||
TrialKind string `json:"trial_kind"`
|
||||
TrialInboundTag string `json:"trial_inbound_tag"`
|
||||
AdminTelegramIDs []int64 `json:"admin_telegram_ids"`
|
||||
Currency string `json:"currency"`
|
||||
PublicHost string `json:"public_host"`
|
||||
XrayPublicHost string `json:"xray_public_host"`
|
||||
HasTelegramToken bool `json:"has_telegram_token"`
|
||||
HasMPAccessToken bool `json:"has_mp_access_token"`
|
||||
HasTelegramWebhookSecret bool `json:"has_telegram_webhook_secret"`
|
||||
HasMPWebhookSecret bool `json:"has_mp_webhook_secret"`
|
||||
Enabled bool `json:"enabled"`
|
||||
MPConfirmMode string `json:"mp_confirm_mode"`
|
||||
MPPollInterval string `json:"mp_poll_interval"`
|
||||
PixExpirationMinutes int `json:"pix_expiration_minutes"`
|
||||
TrialEnabled bool `json:"trial_enabled"`
|
||||
TrialHours int `json:"trial_hours"`
|
||||
TrialMaxConnections int `json:"trial_max_connections"`
|
||||
TrialKind string `json:"trial_kind"`
|
||||
TrialInboundTag string `json:"trial_inbound_tag"`
|
||||
AdminTelegramIDs []int64 `json:"admin_telegram_ids"`
|
||||
Currency string `json:"currency"`
|
||||
PublicHost string `json:"public_host"`
|
||||
XrayPublicHost string `json:"xray_public_host"`
|
||||
HasTelegramToken bool `json:"has_telegram_token"`
|
||||
HasMPAccessToken bool `json:"has_mp_access_token"`
|
||||
HasMPWebhookSecret bool `json:"has_mp_webhook_secret"`
|
||||
// Write-only secret fields (empty on GET; empty on POST = keep existing).
|
||||
TelegramToken string `json:"telegram_token"`
|
||||
MPAccessToken string `json:"mp_access_token"`
|
||||
TelegramWebhookSecret string `json:"telegram_webhook_secret"`
|
||||
MPWebhookSecret string `json:"mp_webhook_secret"`
|
||||
TelegramToken string `json:"telegram_token"`
|
||||
MPAccessToken string `json:"mp_access_token"`
|
||||
MPWebhookSecret string `json:"mp_webhook_secret"`
|
||||
}
|
||||
|
||||
func handleBotConfig(store *Store) http.HandlerFunc {
|
||||
@@ -65,25 +61,22 @@ func handleBotConfig(store *Store) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
botWriteJSON(w, botConfigDTO{
|
||||
Enabled: cfg.Enabled,
|
||||
TelegramMode: cfg.TelegramMode,
|
||||
TelegramWebhookURL: cfg.TelegramWebhookURL,
|
||||
MPConfirmMode: cfg.MPConfirmMode,
|
||||
MPPollInterval: cfg.MPPollInterval,
|
||||
PixExpirationMinutes: cfg.PixExpirationMinutes,
|
||||
TrialEnabled: cfg.TrialEnabled,
|
||||
TrialHours: cfg.TrialHours,
|
||||
TrialMaxConnections: cfg.TrialMaxConnections,
|
||||
TrialKind: cfg.TrialKind,
|
||||
TrialInboundTag: cfg.TrialInboundTag,
|
||||
AdminTelegramIDs: cfg.AdminTelegramIDs,
|
||||
Currency: cfg.Currency,
|
||||
PublicHost: cfg.PublicHost,
|
||||
XrayPublicHost: cfg.XrayPublicHost,
|
||||
HasTelegramToken: cfg.TelegramToken != "",
|
||||
HasMPAccessToken: cfg.MPAccessToken != "",
|
||||
HasTelegramWebhookSecret: cfg.TelegramWebhookSecret != "",
|
||||
HasMPWebhookSecret: cfg.MPWebhookSecret != "",
|
||||
Enabled: cfg.Enabled,
|
||||
MPConfirmMode: cfg.MPConfirmMode,
|
||||
MPPollInterval: cfg.MPPollInterval,
|
||||
PixExpirationMinutes: cfg.PixExpirationMinutes,
|
||||
TrialEnabled: cfg.TrialEnabled,
|
||||
TrialHours: cfg.TrialHours,
|
||||
TrialMaxConnections: cfg.TrialMaxConnections,
|
||||
TrialKind: cfg.TrialKind,
|
||||
TrialInboundTag: cfg.TrialInboundTag,
|
||||
AdminTelegramIDs: cfg.AdminTelegramIDs,
|
||||
Currency: cfg.Currency,
|
||||
PublicHost: cfg.PublicHost,
|
||||
XrayPublicHost: cfg.XrayPublicHost,
|
||||
HasTelegramToken: cfg.TelegramToken != "",
|
||||
HasMPAccessToken: cfg.MPAccessToken != "",
|
||||
HasMPWebhookSecret: cfg.MPWebhookSecret != "",
|
||||
})
|
||||
case http.MethodPost:
|
||||
var dto botConfigDTO
|
||||
@@ -92,25 +85,22 @@ func handleBotConfig(store *Store) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
cfg := &BotConfig{
|
||||
Enabled: dto.Enabled,
|
||||
TelegramToken: strings.TrimSpace(dto.TelegramToken),
|
||||
TelegramMode: dto.TelegramMode,
|
||||
TelegramWebhookURL: strings.TrimSpace(dto.TelegramWebhookURL),
|
||||
TelegramWebhookSecret: strings.TrimSpace(dto.TelegramWebhookSecret),
|
||||
MPAccessToken: strings.TrimSpace(dto.MPAccessToken),
|
||||
MPConfirmMode: dto.MPConfirmMode,
|
||||
MPWebhookSecret: strings.TrimSpace(dto.MPWebhookSecret),
|
||||
MPPollInterval: dto.MPPollInterval,
|
||||
PixExpirationMinutes: dto.PixExpirationMinutes,
|
||||
TrialEnabled: dto.TrialEnabled,
|
||||
TrialHours: dto.TrialHours,
|
||||
TrialMaxConnections: dto.TrialMaxConnections,
|
||||
TrialKind: dto.TrialKind,
|
||||
TrialInboundTag: strings.TrimSpace(dto.TrialInboundTag),
|
||||
AdminTelegramIDs: dto.AdminTelegramIDs,
|
||||
Currency: dto.Currency,
|
||||
PublicHost: strings.TrimSpace(dto.PublicHost),
|
||||
XrayPublicHost: strings.TrimSpace(dto.XrayPublicHost),
|
||||
Enabled: dto.Enabled,
|
||||
TelegramToken: strings.TrimSpace(dto.TelegramToken),
|
||||
MPAccessToken: strings.TrimSpace(dto.MPAccessToken),
|
||||
MPConfirmMode: dto.MPConfirmMode,
|
||||
MPWebhookSecret: strings.TrimSpace(dto.MPWebhookSecret),
|
||||
MPPollInterval: dto.MPPollInterval,
|
||||
PixExpirationMinutes: dto.PixExpirationMinutes,
|
||||
TrialEnabled: dto.TrialEnabled,
|
||||
TrialHours: dto.TrialHours,
|
||||
TrialMaxConnections: dto.TrialMaxConnections,
|
||||
TrialKind: dto.TrialKind,
|
||||
TrialInboundTag: strings.TrimSpace(dto.TrialInboundTag),
|
||||
AdminTelegramIDs: dto.AdminTelegramIDs,
|
||||
Currency: dto.Currency,
|
||||
PublicHost: strings.TrimSpace(dto.PublicHost),
|
||||
XrayPublicHost: strings.TrimSpace(dto.XrayPublicHost),
|
||||
}
|
||||
if err := SaveBotConfig(ctx, store, cfg); err != nil {
|
||||
http.Error(w, "save config: "+err.Error(), http.StatusInternalServerError)
|
||||
@@ -421,25 +411,3 @@ func handleBotTest(store *Store) http.HandlerFunc {
|
||||
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) }
|
||||
|
||||
// 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 {
|
||||
Enabled bool
|
||||
TelegramToken string
|
||||
TelegramMode string // polling | webhook
|
||||
TelegramWebhookURL string
|
||||
TelegramWebhookSecret string
|
||||
MPAccessToken string
|
||||
MPConfirmMode string // webhook | polling
|
||||
MPWebhookSecret string
|
||||
MPPollInterval string
|
||||
PixExpirationMinutes int
|
||||
TrialEnabled bool
|
||||
TrialHours int
|
||||
TrialMaxConnections int
|
||||
TrialKind string // ssh | xray
|
||||
TrialInboundTag string
|
||||
AdminTelegramIDs []int64
|
||||
Currency string
|
||||
PublicHost string // SSH connection host shown to buyers
|
||||
XrayPublicHost string // host used to build vless/vmess links
|
||||
Enabled bool
|
||||
TelegramToken string
|
||||
MPAccessToken string
|
||||
MPConfirmMode string // webhook | polling
|
||||
MPWebhookSecret string
|
||||
MPPollInterval string
|
||||
PixExpirationMinutes int
|
||||
TrialEnabled bool
|
||||
TrialHours int
|
||||
TrialMaxConnections int
|
||||
TrialKind string // ssh | xray
|
||||
TrialInboundTag string
|
||||
AdminTelegramIDs []int64
|
||||
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.
|
||||
@@ -46,10 +45,6 @@ func LoadBotConfig(ctx context.Context, store *Store) (*BotConfig, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tgSec, err := decryptSecret(r.TelegramWebhookSecretEnc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mp, err := decryptSecret(r.MPAccessTokenEnc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -60,34 +55,28 @@ func LoadBotConfig(ctx context.Context, store *Store) (*BotConfig, error) {
|
||||
}
|
||||
|
||||
cfg := &BotConfig{
|
||||
Enabled: r.Enabled,
|
||||
TelegramToken: tok,
|
||||
TelegramMode: r.TelegramMode,
|
||||
TelegramWebhookURL: r.TelegramWebhookURL,
|
||||
TelegramWebhookSecret: tgSec,
|
||||
MPAccessToken: mp,
|
||||
MPConfirmMode: r.MPConfirmMode,
|
||||
MPWebhookSecret: mpSec,
|
||||
MPPollInterval: r.MPPollInterval,
|
||||
PixExpirationMinutes: r.PixExpirationMinutes,
|
||||
TrialEnabled: r.TrialEnabled,
|
||||
TrialHours: r.TrialHours,
|
||||
TrialMaxConnections: r.TrialMaxConnections,
|
||||
TrialKind: r.TrialKind,
|
||||
TrialInboundTag: r.TrialInboundTag,
|
||||
AdminTelegramIDs: r.AdminTelegramIDs,
|
||||
Currency: r.Currency,
|
||||
PublicHost: r.PublicHost,
|
||||
XrayPublicHost: r.XrayPublicHost,
|
||||
Enabled: r.Enabled,
|
||||
TelegramToken: tok,
|
||||
MPAccessToken: mp,
|
||||
MPConfirmMode: r.MPConfirmMode,
|
||||
MPWebhookSecret: mpSec,
|
||||
MPPollInterval: r.MPPollInterval,
|
||||
PixExpirationMinutes: r.PixExpirationMinutes,
|
||||
TrialEnabled: r.TrialEnabled,
|
||||
TrialHours: r.TrialHours,
|
||||
TrialMaxConnections: r.TrialMaxConnections,
|
||||
TrialKind: r.TrialKind,
|
||||
TrialInboundTag: r.TrialInboundTag,
|
||||
AdminTelegramIDs: r.AdminTelegramIDs,
|
||||
Currency: r.Currency,
|
||||
PublicHost: r.PublicHost,
|
||||
XrayPublicHost: r.XrayPublicHost,
|
||||
}
|
||||
cfg.applyDefaults()
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (c *BotConfig) applyDefaults() {
|
||||
if c.TelegramMode == "" {
|
||||
c.TelegramMode = "polling"
|
||||
}
|
||||
if c.MPConfirmMode == "" {
|
||||
c.MPConfirmMode = "polling"
|
||||
}
|
||||
@@ -126,8 +115,8 @@ func SaveBotConfig(ctx context.Context, store *Store, cfg *BotConfig) error {
|
||||
cfg.applyDefaults()
|
||||
row := &botConfigRow{
|
||||
Enabled: cfg.Enabled,
|
||||
TelegramMode: cfg.TelegramMode,
|
||||
TelegramWebhookURL: cfg.TelegramWebhookURL,
|
||||
TelegramMode: "polling", // Telegram is always long-polling
|
||||
TelegramWebhookURL: "",
|
||||
MPConfirmMode: cfg.MPConfirmMode,
|
||||
MPPollInterval: cfg.MPPollInterval,
|
||||
PixExpirationMinutes: cfg.PixExpirationMinutes,
|
||||
@@ -141,18 +130,13 @@ func SaveBotConfig(ctx context.Context, store *Store, cfg *BotConfig) error {
|
||||
PublicHost: cfg.PublicHost,
|
||||
XrayPublicHost: cfg.XrayPublicHost,
|
||||
}
|
||||
var tokEnc, tgSecEnc, mpEnc, mpSecEnc []byte
|
||||
var tokEnc, mpEnc, mpSecEnc []byte
|
||||
var err error
|
||||
if cfg.TelegramToken != "" {
|
||||
if tokEnc, err = encryptSecret(cfg.TelegramToken); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if cfg.TelegramWebhookSecret != "" {
|
||||
if tgSecEnc, err = encryptSecret(cfg.TelegramWebhookSecret); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if cfg.MPAccessToken != "" {
|
||||
if mpEnc, err = encryptSecret(cfg.MPAccessToken); err != nil {
|
||||
return err
|
||||
@@ -163,5 +147,6 @@ func SaveBotConfig(ctx context.Context, store *Store, cfg *BotConfig) error {
|
||||
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 (b *Bot) start() {
|
||||
log.Printf("[bot] starting (telegram_mode=%s mp_confirm=%s)", b.cfg.TelegramMode, b.cfg.MPConfirmMode)
|
||||
if b.cfg.TelegramMode == "webhook" && b.cfg.TelegramWebhookURL != "" {
|
||||
if err := b.tg.setWebhook(b.ctx, b.cfg.TelegramWebhookURL, b.cfg.TelegramWebhookSecret); err != nil {
|
||||
log.Printf("[bot] setWebhook failed, falling back to polling: %v", err)
|
||||
go b.runPolling()
|
||||
} else {
|
||||
log.Printf("[bot] webhook registered at %s", b.cfg.TelegramWebhookURL)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[bot] starting (telegram=long-polling, mp_confirm=%s)", b.cfg.MPConfirmMode)
|
||||
// Telegram uses long-polling only. Clearing any stale webhook + polling both
|
||||
// hit the network, so run off the goroutine that holds botMgrMu.
|
||||
go func() {
|
||||
_ = b.tg.deleteWebhook(b.ctx)
|
||||
go b.runPolling()
|
||||
}
|
||||
b.runPolling()
|
||||
}()
|
||||
if b.mp != nil && b.cfg.MPConfirmMode == "polling" {
|
||||
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
|
||||
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.
|
||||
dataID := r.URL.Query().Get("data.id")
|
||||
if dataID == "" {
|
||||
|
||||
@@ -206,18 +206,6 @@ func (c *tgClient) sendPhotoBytes(ctx context.Context, chatID int64, photo []byt
|
||||
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 {
|
||||
_, err := c.call(ctx, "deleteWebhook", map[string]interface{}{"drop_pending_updates": false})
|
||||
return err
|
||||
@@ -242,12 +230,3 @@ func htmlEscape(s string) string {
|
||||
s = strings.ReplaceAll(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/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/telegram/webhook", http.HandlerFunc(handleTelegramWebhook))
|
||||
|
||||
// Public: user/UUID check — no auth, CORS *.
|
||||
mux.Handle("/check", http.HandlerFunc(handleCheck))
|
||||
|
||||
Reference in New Issue
Block a user