From 4b2003c752015d7615876a152ea9c5bde866a675 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Sat, 16 May 2026 21:54:17 -0500 Subject: [PATCH] ip --- pkg/models/telegram_portal_token.go | 10 ++++ resources/views/layouts/portal.html | 8 +-- resources/views/telegram.html | 23 ++++++- rest/controllers/portal_controller.go | 79 +++++++++++++++++++++++++ rest/controllers/telegram_controller.go | 61 +++++++++++++++++++ rest/routes/portal.go | 1 + rest/routes/user.go | 2 + 7 files changed, 179 insertions(+), 5 deletions(-) diff --git a/pkg/models/telegram_portal_token.go b/pkg/models/telegram_portal_token.go index c548a6e..0e546dd 100644 --- a/pkg/models/telegram_portal_token.go +++ b/pkg/models/telegram_portal_token.go @@ -48,3 +48,13 @@ func FindTelegramPortalToken(token string) (*TelegramPortalToken, error) { func DeleteTelegramPortalToken(portalUserID uint) { app.Http.Database.DB.Unscoped().Where("portal_user_id = ?", portalUserID).Delete(&TelegramPortalToken{}) } + +// GetTelegramPortalTokenByUser devuelve el token vigente de un usuario del portal. +func GetTelegramPortalTokenByUser(portalUserID uint) (*TelegramPortalToken, error) { + var t TelegramPortalToken + err := app.Http.Database.DB.Where("portal_user_id = ?", portalUserID).First(&t).Error + if err != nil { + return nil, err + } + return &t, nil +} diff --git a/resources/views/layouts/portal.html b/resources/views/layouts/portal.html index 5277e34..0d74824 100644 --- a/resources/views/layouts/portal.html +++ b/resources/views/layouts/portal.html @@ -489,19 +489,19 @@ function miCuenta() { this.tg.verificando = true; this.tg.verMsg = ''; try { - const r = await axios.get('/portal/mi-perfil/telegram-status'); - if (r.data.linked) { + const r = await axios.post('/portal/mi-perfil/telegram-validar'); + if (r.data.ok) { this.tg.linked = true; this.tg.chat_id = r.data.chat_id; this.tg.token = ''; this.tg.verMsg = '✅ ¡Telegram vinculado correctamente!'; this.tg.verOk = true; } else { - this.tg.verMsg = 'Aún no se detectó el mensaje. Asegúrate de enviar el código al bot.'; + this.tg.verMsg = r.data.error || 'No se encontró el mensaje. Envía /vincular ' + this.tg.token + ' al bot.'; this.tg.verOk = false; } } catch(e) { - this.tg.verMsg = 'Error al verificar. Intenta de nuevo.'; + this.tg.verMsg = e.response?.data?.error || 'Error al verificar. Intenta de nuevo.'; this.tg.verOk = false; } finally { this.tg.verificando = false; } }, diff --git a/resources/views/telegram.html b/resources/views/telegram.html index 22bcb2d..d6ea856 100644 --- a/resources/views/telegram.html +++ b/resources/views/telegram.html @@ -43,12 +43,18 @@ class="text-xs px-2 py-0.5 rounded-full whitespace-nowrap">

-
+
+
@@ -218,6 +227,7 @@ function telegramApp() { deleteModal: false, deleteId: null, form: { nombre: '', bot_token: '', chat_id: '', notas: '', activo: true }, testLoading: {}, testResult: {}, testOk: {}, + webhookLoading: {}, webhookResult: {}, webhookOk: {}, sendForm: { config_ids: [], titulo: '', mensaje: '' }, sendLoading: false, sendResult: '', sendOk: true, logs: [], logPage: 1, logTotalPages: 1, logTotal: 0, @@ -279,6 +289,17 @@ function telegramApp() { this.$set(this.testResult, cfg.ID, d.ok ? '✅ Mensaje enviado correctamente.' : ('❌ ' + (d.error || 'Error'))); this.$set(this.testLoading, cfg.ID, false); }, + async setWebhook(cfg) { + this.$set(this.webhookLoading, cfg.ID, true); + this.$set(this.webhookResult, cfg.ID, ''); + const res = await fetch(`/app/telegram/${cfg.ID}/set-portal-webhook`, { method: 'POST' }); + const d = await res.json(); + this.$set(this.webhookOk, cfg.ID, d.ok); + this.$set(this.webhookResult, cfg.ID, d.ok + ? '✅ Webhook registrado: ' + (d.webhook_url || '') + : '❌ Error: ' + (d.telegram?.description || d.error || 'Error desconocido')); + this.$set(this.webhookLoading, cfg.ID, false); + }, async sendMessage() { this.sendResult = ''; diff --git a/rest/controllers/portal_controller.go b/rest/controllers/portal_controller.go index d8e0c62..0fd20e3 100644 --- a/rest/controllers/portal_controller.go +++ b/rest/controllers/portal_controller.go @@ -1,7 +1,10 @@ package controllers import ( + "encoding/json" "fmt" + "log" + "net/http" "net/url" "os" "path/filepath" @@ -620,6 +623,7 @@ func TelegramPortalWebhook(c *fiber.Ctx) error { chatID := update.Message.Chat.ID text := strings.TrimSpace(update.Message.Text) + log.Printf("[TelegramPortalWebhook] chatID=%d text=%q", chatID, text) if chatID == 0 || text == "" { return c.SendStatus(fiber.StatusOK) } @@ -666,3 +670,78 @@ func portalTelegramReply(chatID int64, text string) { } } +// PortalTelegramValidar llama getUpdates en todos los bots activos, busca el mensaje +// "/vincular TOKEN" del usuario y vincula el chat_id si coincide. +// POST /portal/mi-perfil/telegram-validar +func PortalTelegramValidar(c *fiber.Ctx) error { + u := middlewares.PortalUserFromLocals(c) + if u == nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "No autenticado"}) + } + + // Recuperar el token vigente del usuario + tkn, err := models.GetTelegramPortalTokenByUser(u.ID) + if err != nil || tkn == nil { + return c.Status(400).JSON(fiber.Map{"error": "No tienes un código activo. Genera uno primero."}) + } + + configs, _ := models.GetAllTelegramConfigs() + for _, cfg := range configs { + if !cfg.Activo || cfg.BotToken == "" { + continue + } + chatID, found := searchTokenInUpdates(cfg.BotToken, tkn.Token) + if !found { + continue + } + // Vincular + if err := models.UpdatePortalUserTelegramChatID(u.ID, fmt.Sprintf("%d", chatID)); err != nil { + return c.Status(500).JSON(fiber.Map{"error": "No se pudo vincular. Intenta de nuevo."}) + } + models.DeleteTelegramPortalToken(u.ID) + svc := &services.TelegramService{BotToken: cfg.BotToken} + _ = svc.SendMessage(chatID, "✅ ¡Tu Telegram ha sido vinculado al portal correctamente!\n\nRecibirás notificaciones importantes por este medio.") + log.Printf("[PortalTelegramValidar] usuario=%d chatID=%d vinculado", u.ID, chatID) + return c.JSON(fiber.Map{"ok": true, "chat_id": fmt.Sprintf("%d", chatID)}) + } + + return c.Status(200).JSON(fiber.Map{"ok": false, "error": "No se encontró el mensaje. Asegúrate de enviar /vincular " + tkn.Token + " al bot."}) +} + +// searchTokenInUpdates llama getUpdates y busca un mensaje que contenga el token. +// Retorna el chat_id y true si lo encuentra. +func searchTokenInUpdates(botToken, token string) (int64, bool) { + apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/getUpdates?limit=100", botToken) + resp, err := http.Get(apiURL) //nolint:noctx + if err != nil { + log.Printf("[searchTokenInUpdates] error getUpdates: %v", err) + return 0, false + } + defer resp.Body.Close() + + var result struct { + OK bool `json:"ok"` + Result []struct { + Message struct { + Chat struct { + ID int64 `json:"id"` + } `json:"chat"` + Text string `json:"text"` + } `json:"message"` + } `json:"result"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil || !result.OK { + return 0, false + } + + upperToken := strings.ToUpper(token) + for _, upd := range result.Result { + txt := strings.ToUpper(strings.TrimSpace(upd.Message.Text)) + if strings.Contains(txt, upperToken) && upd.Message.Chat.ID != 0 { + log.Printf("[searchTokenInUpdates] token=%s chatID=%d", token, upd.Message.Chat.ID) + return upd.Message.Chat.ID, true + } + } + return 0, false +} + diff --git a/rest/controllers/telegram_controller.go b/rest/controllers/telegram_controller.go index 4a958ec..642d221 100755 --- a/rest/controllers/telegram_controller.go +++ b/rest/controllers/telegram_controller.go @@ -4,12 +4,15 @@ import ( "bytes" "encoding/json" "fmt" + "io" + "log" "math" "net/http" "strconv" "strings" "github.com/gofiber/fiber/v2" + "github.com/sujit-baniya/fiber-boilerplate/app" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) @@ -204,3 +207,61 @@ func sendTelegramMessage(botToken, chatID, text string) error { } return nil } + +// ─── Webhook portal (set + info) ───────────────────────────────────────────── + +// SetPortalWebhook registra el webhook del portal en la API de Telegram para el bot indicado. +// POST /app/telegram/:id/set-portal-webhook +func SetPortalWebhook(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 32) + if err != nil { + return c.Status(400).JSON(fiber.Map{"error": "ID inválido"}) + } + cfg, err := models.GetTelegramConfigByID(uint(id)) + if err != nil { + return c.Status(404).JSON(fiber.Map{"error": "Configuración no encontrada"}) + } + + // Construir URL base desde la configuración de la app + baseURL := app.Http.Server.Url + if len(baseURL) > 0 && baseURL[len(baseURL)-1] == '/' { + baseURL = baseURL[:len(baseURL)-1] + } + webhookURL := baseURL + "/webhooks/telegram-portal" + + apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/setWebhook", cfg.BotToken) + payload, _ := json.Marshal(map[string]string{"url": webhookURL}) + resp, err := http.Post(apiURL, "application/json", bytes.NewReader(payload)) //nolint:noctx + if err != nil { + return c.Status(502).JSON(fiber.Map{"ok": false, "error": err.Error()}) + } + defer resp.Body.Close() + raw, _ := io.ReadAll(resp.Body) + + var tgResp map[string]interface{} + _ = json.Unmarshal(raw, &tgResp) + log.Printf("[SetPortalWebhook] bot=%d url=%s resp=%s", id, webhookURL, string(raw)) + return c.JSON(fiber.Map{"ok": tgResp["ok"], "webhook_url": webhookURL, "telegram": tgResp}) +} + +// GetPortalWebhookInfo consulta el estado actual del webhook en Telegram. +// GET /app/telegram/:id/webhook-info +func GetPortalWebhookInfo(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 32) + if err != nil { + return c.Status(400).JSON(fiber.Map{"error": "ID inválido"}) + } + cfg, err := models.GetTelegramConfigByID(uint(id)) + if err != nil { + return c.Status(404).JSON(fiber.Map{"error": "Configuración no encontrada"}) + } + apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/getWebhookInfo", cfg.BotToken) + resp, err := http.Get(apiURL) //nolint:noctx + if err != nil { + return c.Status(502).JSON(fiber.Map{"ok": false, "error": err.Error()}) + } + defer resp.Body.Close() + var result map[string]interface{} + _ = json.NewDecoder(resp.Body).Decode(&result) + return c.JSON(result) +} diff --git a/rest/routes/portal.go b/rest/routes/portal.go index 96d614d..ac8fd1f 100644 --- a/rest/routes/portal.go +++ b/rest/routes/portal.go @@ -51,4 +51,5 @@ func PortalRoutes(app fiber.Router) { // Telegram: vinculación guiada portal.Post("/mi-perfil/telegram-init", controllers.PortalTelegramInit) portal.Get("/mi-perfil/telegram-status", controllers.PortalTelegramStatus) + portal.Post("/mi-perfil/telegram-validar", controllers.PortalTelegramValidar) } diff --git a/rest/routes/user.go b/rest/routes/user.go index bd300e5..5ebc99a 100755 --- a/rest/routes/user.go +++ b/rest/routes/user.go @@ -224,6 +224,8 @@ func UserRoutes(app fiber.Router) { protected.Post("/telegram/:id/test", controllers.TestTelegramConfig) protected.Post("/telegram/send", controllers.SendTelegramNotification) protected.Get("/telegram/logs", controllers.GetTelegramLogs) + protected.Post("/telegram/:id/set-portal-webhook", controllers.SetPortalWebhook) + protected.Get("/telegram/:id/webhook-info", controllers.GetPortalWebhookInfo) // ─── Portal de Clientes ──────────────────────────────────────────────────── protected.Get("/proyectos", middlewares.MenuMiddleware, controllers.ProyectosIndex)