This commit is contained in:
2026-07-13 00:17:55 -03:00
parent 09c1f57a34
commit 2776eba034
9 changed files with 192 additions and 225 deletions
+6 -6
View File
@@ -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
View File
@@ -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);
+65 -35
View File
@@ -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>
<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>
<!-- 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>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>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 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>
</div>
<div class="form-actions">
<button class="btn" id="botConfigSaveBtn">Salvar configuração</button>
<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>
+1 -33
View File
@@ -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"
@@ -26,8 +26,6 @@ func botStoreReady(w http.ResponseWriter, store *Store) bool {
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"`
@@ -42,12 +40,10 @@ type botConfigDTO struct {
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"`
// 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"`
}
@@ -66,8 +62,6 @@ func handleBotConfig(store *Store) http.HandlerFunc {
}
botWriteJSON(w, botConfigDTO{
Enabled: cfg.Enabled,
TelegramMode: cfg.TelegramMode,
TelegramWebhookURL: cfg.TelegramWebhookURL,
MPConfirmMode: cfg.MPConfirmMode,
MPPollInterval: cfg.MPPollInterval,
PixExpirationMinutes: cfg.PixExpirationMinutes,
@@ -82,7 +76,6 @@ func handleBotConfig(store *Store) http.HandlerFunc {
XrayPublicHost: cfg.XrayPublicHost,
HasTelegramToken: cfg.TelegramToken != "",
HasMPAccessToken: cfg.MPAccessToken != "",
HasTelegramWebhookSecret: cfg.TelegramWebhookSecret != "",
HasMPWebhookSecret: cfg.MPWebhookSecret != "",
})
case http.MethodPost:
@@ -94,9 +87,6 @@ func handleBotConfig(store *Store) http.HandlerFunc {
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),
@@ -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)
}
+7 -22
View File
@@ -14,12 +14,11 @@ 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
@@ -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
@@ -62,9 +57,6 @@ 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,
@@ -85,9 +77,6 @@ func LoadBotConfig(ctx context.Context, store *Store) (*BotConfig, error) {
}
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
View File
@@ -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()
}
+6
View File
@@ -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 == "" {
-21
View File
@@ -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, ">", "&gt;")
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
}
+1 -2
View File
@@ -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))