diff --git a/README.md b/README.md index 30b3850..561a451 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/admin/assets/js/12-bot.js b/admin/assets/js/12-bot.js index 926d887..8d930e2 100644 --- a/admin/assets/js/12-bot.js +++ b/admin/assets/js/12-bot.js @@ -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); diff --git a/admin/index.html b/admin/index.html index 88909ba..14b970c 100644 --- a/admin/index.html +++ b/admin/index.html @@ -16,7 +16,7 @@ setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500); })(); - +
@@ -952,39 +952,69 @@
- +
🤖 Configuração do Bot
-
+
+ Pronto. +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+

1) Cole os tokens abaixo e clique Testar · 2) Crie os Planos · 3) Promova revendedores em Clientes. O Telegram funciona por long-polling — não precisa de domínio.

+
+ +
+ +
+
✈️ Telegram
+ +
+
-
- + + +
+
💠 Mercado Pago (PIX)
+
+
+
+
+
+
+ +
+
+ +
+ +
+
⏳ Teste Grátis
+ +
+
+
+
+
+
+
+ + +
+
🌐 Host de entrega
+
+
+

Enviado ao cliente nas credenciais após o pagamento.

-
Ready.
@@ -1433,17 +1463,17 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/bot_api.go b/bot_api.go index a34a2cb..9dc8251 100644 --- a/bot_api.go +++ b/bot_api.go @@ -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) -} diff --git a/bot_config.go b/bot_config.go index 0440013..d299c65 100644 --- a/bot_config.go +++ b/bot_config.go @@ -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) } diff --git a/bot_core.go b/bot_core.go index c7c6f3c..e61b3e9 100644 --- a/bot_core.go +++ b/bot_core.go @@ -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() } diff --git a/bot_mercadopago.go b/bot_mercadopago.go index d58a36d..5386d6d 100644 --- a/bot_mercadopago.go +++ b/bot_mercadopago.go @@ -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 == "" { diff --git a/bot_telegram.go b/bot_telegram.go index b8c03cd..f965e50 100644 --- a/bot_telegram.go +++ b/bot_telegram.go @@ -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 -} diff --git a/main.go b/main.go index 57813f1..c132f52 100644 --- a/main.go +++ b/main.go @@ -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))