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)